diff --git a/Cargo.lock b/Cargo.lock index 22165fa4..5b4e184a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,38 +1,48 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e" + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "adler32" -version = "1.0.4" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "ahash" -version = "0.3.2" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0989268a37e128d4d7a8028f1c60099430113fdbc70419010601ce51a228e4fe" -dependencies = [ - "const-random", -] +checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" [[package]] name = "aho-corasick" -version = "0.7.8" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743ad5a418686aad3b87fd14c43badd828cf26e214a00f92a384291cf22e1811" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] [[package]] name = "andrew" -version = "0.2.1" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7f09f89872c2b6b29e319377b1fbe91c6f5947df19a25596e121cf19a7b35e" +checksum = "8c4afb09dd642feec8408e33f92f3ffc4052946f6b20f32fb99c1f58cd4fa7cf" dependencies = [ "bitflags", - "line_drawing", - "rusttype 0.7.9", + "rusttype", "walkdir", "xdg", "xml-rs", @@ -46,51 +56,33 @@ checksum = "000444226fcff248f2bc4c7625be32c63caccfecc2723a2b9f78a7487a49c407" [[package]] name = "anyhow" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7825f6833612eb2414095684fcf6c635becf3ce97fe48cf6421321e93bfbd53c" - -[[package]] -name = "approx" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" -dependencies = [ - "num-traits", -] - -[[package]] -name = "arrayvec" -version = "0.4.12" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" -dependencies = [ - "nodrop", -] +checksum = "62e1f47f7dc0422027a4e370dd4548d4d66b26782e513e98dca1e689e058a80e" [[package]] name = "arrayvec" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "atom" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c86699c3f02778ec07158376991c8f783dd1f2f95c579ffaf0738dc984b2fe2" +checksum = "c9ff149ed9780025acfdb36862d35b28856bb693ceb451259a7164442f22fdc3" [[package]] name = "autocfg" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block" @@ -100,142 +92,76 @@ checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "bracket-algorithm-traits" -version = "0.8.1" -dependencies = [ - "bracket-geometry 0.8.1", - "smallvec 1.4.2", -] - -[[package]] -name = "bracket-algorithm-traits" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec021d386ba4c891ee752738869465aa679b5902c6bcc93ca55532d9a33ab73" -dependencies = [ - "bracket-geometry 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "smallvec 1.4.2", -] - -[[package]] -name = "bracket-color" -version = "0.8.1" +checksum = "af23ee41725dd41febe5f614851db8e5d44c07254dd91838b5ddf817060e2e73" dependencies = [ - "byteorder", - "lazy_static", - "parking_lot 0.10.2", - "serde", + "bracket-geometry", + "smallvec", ] [[package]] name = "bracket-color" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4064968b35447f0c378010bbf5470ede41d07101bdf3b1fdf29dca5441f91b" +checksum = "3c1d1b160817fb74eebedccd678055cd688d1a73dc1a14519fa30ff4c9a5bdee" dependencies = [ "byteorder", "lazy_static", - "parking_lot 0.10.2", - "serde", -] - -[[package]] -name = "bracket-geometry" -version = "0.8.1" -dependencies = [ + "parking_lot", "serde", - "ultraviolet", ] [[package]] name = "bracket-geometry" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ec613acfe65a809f03d57e2cc8ab97f9c396ddb9929e784bc19b87dd5ccf358" +checksum = "4db22c32c68bd9330ab982f8ff7ffe7b10541d16ea7d7d51aac074499850402b" dependencies = [ "serde", "ultraviolet", ] -[[package]] -name = "bracket-lib" -version = "0.8.1" -dependencies = [ - "bracket-algorithm-traits 0.8.1", - "bracket-color 0.8.1", - "bracket-geometry 0.8.1", - "bracket-noise 0.8.1", - "bracket-pathfinding 0.8.1", - "bracket-random 0.8.0", - "bracket-terminal 0.8.1", -] - [[package]] name = "bracket-lib" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0fb005e908ce2e553277e554c32ab06d8b12bea38660da98f2e77e662dea617" dependencies = [ - "bracket-algorithm-traits 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "bracket-color 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "bracket-geometry 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "bracket-noise 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "bracket-pathfinding 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "bracket-random 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "bracket-terminal 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "bracket-noise" -version = "0.8.1" -dependencies = [ - "bracket-random 0.8.0", + "bracket-algorithm-traits", + "bracket-color", + "bracket-geometry", + "bracket-noise", + "bracket-pathfinding", + "bracket-random", + "bracket-terminal", ] [[package]] name = "bracket-noise" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b1235bcf925e1ed541da32902abe9ea2313ed388cd6b39038d5bb4b39f611f" -dependencies = [ - "bracket-random 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "bracket-pathfinding" -version = "0.8.1" +checksum = "60de9564f6a658c770666a6cf6ccf837f0669f5906f67de7f089bebe09b46723" dependencies = [ - "bracket-algorithm-traits 0.8.1", - "bracket-geometry 0.8.1", - "smallvec 1.4.2", + "bracket-random", ] [[package]] name = "bracket-pathfinding" -version = "0.8.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd1eeaabc58017f4708c451924db6140fd148ab64f4cb6d433fb0c193cd6733" -dependencies = [ - "bracket-algorithm-traits 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "bracket-geometry 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "smallvec 1.4.2", -] - -[[package]] -name = "bracket-random" -version = "0.8.0" +checksum = "6b713222141585b5e5cc6f0be1a0a473e1e339aa1300af91929ce6b1b2ed529c" dependencies = [ - "lazy_static", - "rand", - "rand_xorshift", - "regex", - "serde", + "bracket-algorithm-traits", + "bracket-geometry", + "smallvec", ] [[package]] name = "bracket-random" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac06539968b64bd20c9f7e1620e3a090737ecc54b850f9527a27f961284c8b97" +checksum = "66b5b977a40a6be337b2baff6911051966c61bd3987836c13e96f518ec4ba312" dependencies = [ "lazy_static", "rand", @@ -246,35 +172,13 @@ dependencies = [ [[package]] name = "bracket-terminal" -version = "0.8.1" -dependencies = [ - "bracket-color 0.8.1", - "bracket-geometry 0.8.1", - "byteorder", - "console_error_panic_hook", - "flate2", - "glow", - "glutin", - "image", - "lazy_static", - "object-pool 0.5.3", - "parking_lot 0.10.2", - "rand", - "ultraviolet", - "wasm-bindgen", - "wasm-timer", - "web-sys", - "winit", -] - -[[package]] -name = "bracket-terminal" -version = "0.8.1" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2336a503041d18d4335394f8216214593c25e072a9bbb9ae768fe6368071b86b" +checksum = "460594df0b3364cae6ce5a5de4d787b293d20df0deffed4a942c10b1e3d50b1d" dependencies = [ - "bracket-color 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "bracket-geometry 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow", + "bracket-color", + "bracket-geometry", "byteorder", "console_error_panic_hook", "flate2", @@ -282,8 +186,8 @@ dependencies = [ "glutin", "image", "lazy_static", - "object-pool 0.4.4", - "parking_lot 0.10.2", + "object-pool", + "parking_lot", "rand", "ultraviolet", "wasm-bindgen", @@ -294,47 +198,37 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.2.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f359dc14ff8911330a51ef78022d376f25ed00248912803b58f00cb1c27f742" +checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" [[package]] name = "bytemuck" -version = "1.2.0" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37fa13df2292ecb479ec23aa06f4507928bef07839be9ef15281411076629431" +checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" [[package]] name = "byteorder" -version = "1.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" - -[[package]] -name = "c2-chacha" -version = "0.2.3" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" -dependencies = [ - "ppv-lite86", -] +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "calloop" -version = "0.4.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aa2097be53a00de9e8fc349fea6d76221f398f5c4fa550d420669906962d160" +checksum = "0b036167e76041694579972c28cf4877b4f92da222560ddb49008937b6a6727c" dependencies = [ - "mio", - "mio-extras", - "nix", + "log", + "nix 0.18.0", ] [[package]] name = "cc" -version = "1.0.50" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" [[package]] name = "cfg-if" @@ -342,6 +236,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "cgl" version = "0.3.2" @@ -355,14 +255,14 @@ dependencies = [ name = "chapter-01-hellorust" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", ] [[package]] name = "chapter-02-helloecs" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "specs", "specs-derive", ] @@ -371,7 +271,7 @@ dependencies = [ name = "chapter-03-walkmap" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "specs", "specs-derive", ] @@ -380,7 +280,7 @@ dependencies = [ name = "chapter-04-newmap" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "specs", "specs-derive", ] @@ -389,7 +289,7 @@ dependencies = [ name = "chapter-05-fov" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "specs", "specs-derive", ] @@ -398,7 +298,7 @@ dependencies = [ name = "chapter-06-monsters" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "specs", "specs-derive", ] @@ -407,7 +307,7 @@ dependencies = [ name = "chapter-07-damage" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "specs", "specs-derive", ] @@ -416,7 +316,7 @@ dependencies = [ name = "chapter-08-ui" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "specs", "specs-derive", ] @@ -425,7 +325,7 @@ dependencies = [ name = "chapter-09-items" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "specs", "specs-derive", ] @@ -434,7 +334,7 @@ dependencies = [ name = "chapter-10-ranged" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "specs", "specs-derive", ] @@ -443,7 +343,7 @@ dependencies = [ name = "chapter-11-loadsave" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -454,7 +354,7 @@ dependencies = [ name = "chapter-12-delvingdeeper" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -465,7 +365,7 @@ dependencies = [ name = "chapter-13-difficulty" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -476,7 +376,7 @@ dependencies = [ name = "chapter-14-gear" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -487,7 +387,7 @@ dependencies = [ name = "chapter-16-nicewalls" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -498,7 +398,7 @@ dependencies = [ name = "chapter-17-blood" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -509,7 +409,7 @@ dependencies = [ name = "chapter-18-particles" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -520,7 +420,7 @@ dependencies = [ name = "chapter-19-food" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -531,7 +431,7 @@ dependencies = [ name = "chapter-20-magicmapping" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -542,7 +442,7 @@ dependencies = [ name = "chapter-21-rexmenu" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -553,7 +453,7 @@ dependencies = [ name = "chapter-22-simpletraps" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -564,7 +464,7 @@ dependencies = [ name = "chapter-23-generic-map" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -575,7 +475,7 @@ dependencies = [ name = "chapter-24-map-testing" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -586,7 +486,7 @@ dependencies = [ name = "chapter-25-bsproom-dungeons" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -597,7 +497,7 @@ dependencies = [ name = "chapter-26-bsp-interiors" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -608,7 +508,7 @@ dependencies = [ name = "chapter-27-cellular-automata" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -619,7 +519,7 @@ dependencies = [ name = "chapter-28-drunkards-walk" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -630,7 +530,7 @@ dependencies = [ name = "chapter-29-mazes" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -641,7 +541,7 @@ dependencies = [ name = "chapter-30-dla" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -652,7 +552,7 @@ dependencies = [ name = "chapter-31-symmetry" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -663,7 +563,7 @@ dependencies = [ name = "chapter-32-voronoi" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -674,7 +574,7 @@ dependencies = [ name = "chapter-33-wfc" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -685,7 +585,7 @@ dependencies = [ name = "chapter-34-vaults" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -696,7 +596,7 @@ dependencies = [ name = "chapter-35-vaults2" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -707,7 +607,7 @@ dependencies = [ name = "chapter-36-layers" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -718,7 +618,7 @@ dependencies = [ name = "chapter-37-layers2" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -729,7 +629,7 @@ dependencies = [ name = "chapter-38-rooms" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -740,7 +640,7 @@ dependencies = [ name = "chapter-39-halls" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -751,7 +651,7 @@ dependencies = [ name = "chapter-40-doors" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -762,7 +662,7 @@ dependencies = [ name = "chapter-41-camera" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -774,7 +674,7 @@ name = "chapter-45-raws1" version = "0.1.0" dependencies = [ "lazy_static", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -786,7 +686,7 @@ name = "chapter-46-raws2" version = "0.1.0" dependencies = [ "lazy_static", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -798,7 +698,7 @@ name = "chapter-47-town1" version = "0.1.0" dependencies = [ "lazy_static", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -810,7 +710,7 @@ name = "chapter-48-town2" version = "0.1.0" dependencies = [ "lazy_static", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -822,7 +722,7 @@ name = "chapter-49-town3" version = "0.1.0" dependencies = [ "lazy_static", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -834,7 +734,7 @@ name = "chapter-50-stats" version = "0.1.0" dependencies = [ "lazy_static", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -847,7 +747,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -860,7 +760,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -873,7 +773,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -886,7 +786,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -899,7 +799,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -912,7 +812,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -925,7 +825,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -938,7 +838,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -951,7 +851,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -964,7 +864,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -977,7 +877,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -990,7 +890,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -1003,7 +903,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -1016,7 +916,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -1029,7 +929,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -1042,7 +942,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -1055,7 +955,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -1068,7 +968,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -1081,7 +981,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -1094,7 +994,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -1107,7 +1007,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -1120,7 +1020,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -1133,7 +1033,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -1146,7 +1046,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "serde", "serde_json", "specs", @@ -1159,7 +1059,20 @@ version = "0.1.0" dependencies = [ "lazy_static", "regex", - "rltk 0.8.1", + "rltk", + "serde", + "serde_json", + "specs", + "specs-derive", +] + +[[package]] +name = "chapter-75-darkplaza" +version = "0.1.0" +dependencies = [ + "lazy_static", + "regex", + "rltk", "serde", "serde_json", "specs", @@ -1167,167 +1080,202 @@ dependencies = [ ] [[package]] -name = "cloudabi" -version = "0.0.3" +name = "cocoa" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +checksum = "c54201c07dcf3a5ca33fececb8042aed767ee4bfd5a0235a8ceabcda956044b2" dependencies = [ "bitflags", + "block", + "cocoa-foundation", + "core-foundation 0.9.2", + "core-graphics 0.22.3", + "foreign-types", + "libc", + "objc", ] [[package]] name = "cocoa" -version = "0.19.1" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29f7768b2d1be17b96158e3285951d366b40211320fb30826a76cb7a0da6400" +checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832" dependencies = [ "bitflags", "block", - "core-foundation", - "core-graphics", + "cocoa-foundation", + "core-foundation 0.9.2", + "core-graphics 0.22.3", "foreign-types", "libc", "objc", ] [[package]] -name = "console_error_panic_hook" -version = "0.1.6" +name = "cocoa-foundation" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211" +checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" dependencies = [ - "cfg-if", - "wasm-bindgen", + "bitflags", + "block", + "core-foundation 0.9.2", + "core-graphics-types", + "foreign-types", + "libc", + "objc", ] [[package]] -name = "const-random" -version = "0.1.8" +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f1af9ac737b2dd2d577701e59fd09ba34822f6f2ebdb30a7647405d9e55e16a" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ - "const-random-macro", - "proc-macro-hack", + "cfg-if 1.0.0", + "wasm-bindgen", ] [[package]] -name = "const-random-macro" -version = "0.1.8" +name = "core-foundation" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" dependencies = [ - "getrandom", - "proc-macro-hack", + "core-foundation-sys 0.7.0", + "libc", ] [[package]] name = "core-foundation" -version = "0.6.4" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d" +checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.3", "libc", ] [[package]] name = "core-foundation-sys" -version = "0.6.2" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "core-graphics" -version = "0.17.3" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56790968ab1c8a1202a102e6de05fc6e1ec87da99e4e93e9a7d13efbfc1e95a9" +checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.7.0", "foreign-types", "libc", ] [[package]] -name = "core-video-sys" -version = "0.1.3" +name = "core-graphics" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc065219542086f72d1e9f7aadbbab0989e980263695d129d502082d063a9d0" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ - "cfg-if", - "core-foundation-sys", - "core-graphics", + "bitflags", + "core-foundation 0.9.2", + "core-graphics-types", + "foreign-types", "libc", - "objc", ] [[package]] -name = "crc32fast" -version = "1.2.0" +name = "core-graphics-types" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" dependencies = [ - "cfg-if", + "bitflags", + "core-foundation 0.9.2", + "foreign-types", + "libc", ] [[package]] -name = "crossbeam" -version = "0.7.3" +name = "core-video-sys" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e" +checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" dependencies = [ - "cfg-if", - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "core-graphics 0.19.2", + "libc", + "objc", +] + +[[package]] +name = "crc32fast" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3825b1e8580894917dc4468cb634a1b4e9745fddc854edad72d9c04644c0319f" +dependencies = [ + "cfg-if 1.0.0", ] [[package]] name = "crossbeam-channel" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" dependencies = [ - "crossbeam-utils", - "maybe-uninit", + "cfg-if 1.0.0", + "crossbeam-utils 0.8.5", ] [[package]] name = "crossbeam-deque" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" dependencies = [ + "cfg-if 1.0.0", "crossbeam-epoch", - "crossbeam-utils", - "maybe-uninit", + "crossbeam-utils 0.8.5", ] [[package]] name = "crossbeam-epoch" -version = "0.8.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", + "cfg-if 1.0.0", + "crossbeam-utils 0.8.5", "lazy_static", - "maybe-uninit", "memoffset", "scopeguard", ] [[package]] name = "crossbeam-queue" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db" +checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" dependencies = [ - "cfg-if", - "crossbeam-utils", + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "maybe-uninit", ] [[package]] @@ -1337,74 +1285,165 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" dependencies = [ "autocfg", - "cfg-if", + "cfg-if 0.1.10", "lazy_static", ] [[package]] -name = "deflate" -version = "0.8.4" +name = "crossbeam-utils" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e5d2a2273fed52a7f947ee55b092c4057025d7a3e04e5ecdbd25d6c3fb1bd7" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" dependencies = [ - "adler32", - "byteorder", + "cfg-if 1.0.0", + "lazy_static", ] [[package]] -name = "dispatch" -version = "0.2.0" +name = "darling" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] [[package]] -name = "dlib" -version = "0.4.1" +name = "darling_core" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e51249a9d823a4cb79e3eca6dcd756153e8ed0157b6c04775d04bf1b13b76a" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" dependencies = [ - "libloading", + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", ] [[package]] -name = "downcast-rs" -version = "1.1.1" +name = "darling_macro" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ba6eb47c2131e784a38b726eb54c1e1484904f013e576a25354d0124161af6" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] [[package]] -name = "either" -version = "1.5.3" +name = "deflate" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] [[package]] -name = "flate2" -version = "1.0.14" +name = "derivative" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "cfg-if", - "crc32fast", - "libc", - "miniz_oxide", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "foreign-types" -version = "0.3.2" +name = "dirs" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" dependencies = [ - "foreign-types-shared", + "dirs-sys", ] [[package]] -name = "foreign-types-shared" -version = "0.1.1" +name = "dirs-sys" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b11f15d1e3268f140f68d390637d5e76d849782d971ae7063e0da69fe9709a76" +dependencies = [ + "libloading 0.6.7", +] + +[[package]] +name = "dlib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +dependencies = [ + "libloading 0.7.2", +] + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "flate2" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide 0.4.4", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "fuchsia-zircon" version = "0.3.3" @@ -1423,27 +1462,26 @@ checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" [[package]] name = "futures" -version = "0.1.29" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b980f2816d6ee8673b6517b52cb0e808a180efc92e5c19d02cdda79066703ef" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" [[package]] name = "getrandom" -version = "0.1.14" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "wasi", - "wasm-bindgen", ] [[package]] name = "gl_generator" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca98bbde17256e02d17336a6bdb5a50f7d0ccacee502e191d3e3d0ec2f96f84a" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" dependencies = [ "khronos_api", "log", @@ -1452,11 +1490,10 @@ dependencies = [ [[package]] name = "glow" -version = "0.4.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31aed196700daf16e1241d819ff4a4855a78ee0cddb051948d50b9213deec82f" +checksum = "945be163fdb893227410c8b44c2412dade922585b262d1daa6a7e96135217d4c" dependencies = [ - "gl_generator", "js-sys", "slotmap", "wasm-bindgen", @@ -1465,39 +1502,39 @@ dependencies = [ [[package]] name = "glutin" -version = "0.24.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611023dea5047f3e9047aecb9e361852dcfd0881129daf5d110106ca2b14f3f3" +checksum = "1ae1cbb9176b9151c4ce03f012e3cd1c6c18c4be79edeaeb3d99f5d8085c5fa3" dependencies = [ "android_glue", "cgl", - "cocoa", - "core-foundation", - "core-graphics", + "cocoa 0.23.0", + "core-foundation 0.9.2", "glutin_egl_sys", "glutin_emscripten_sys", "glutin_gles2_sys", "glutin_glx_sys", "glutin_wgl_sys", "lazy_static", - "libloading", + "libloading 0.6.7", "log", "objc", "osmesa-sys", - "parking_lot 0.10.2", + "parking_lot", "wayland-client", - "winapi 0.3.8", + "wayland-egl", + "winapi 0.3.9", "winit", ] [[package]] name = "glutin_egl_sys" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772edef3b28b8ad41e4ea202748e65eefe8e5ffd1f4535f1219793dbb20b3d4c" +checksum = "2abb6aa55523480c4adc5a56bbaa249992e2dddb2fc63dc96e04a3355364c211" dependencies = [ "gl_generator", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -1508,9 +1545,9 @@ checksum = "80de4146df76e8a6c32b03007bc764ff3249dcaeb4f675d68a06caf1bac363f1" [[package]] name = "glutin_gles2_sys" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e853d96bebcb8e53e445225c3009758c6f5960d44f2543245f6f07b567dae0" +checksum = "e8094e708b730a7c8a1954f4f8a31880af00eb8a1c5b5bf85d28a0a3c6d69103" dependencies = [ "gl_generator", "objc", @@ -1518,9 +1555,9 @@ dependencies = [ [[package]] name = "glutin_glx_sys" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c243de74d6cf5ea100c788826d2fb9319de315485dd4b310811a663b3809c3" +checksum = "7e393c8fc02b807459410429150e9c4faffdb312d59b8c038566173c81991351" dependencies = [ "gl_generator", "x11-dl", @@ -1528,37 +1565,28 @@ dependencies = [ [[package]] name = "glutin_wgl_sys" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93dba7ee3a0feeac0f437141ff25e71ce2066bcf1a706acab1559ffff94eb6a" +checksum = "3da5951a1569dbab865c6f2a863efafff193a93caf05538d193e9e3816d21696" dependencies = [ "gl_generator", ] [[package]] name = "hashbrown" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728e7d31e63d53c436094370f1e6fa249f60a4bb318cc5dfbbbe0aa2bc5a29d7" +checksum = "96282e96bfcd3da0d3aa9938bedf1e50df3269b6db08b4876d2da0bb1a0841cf" dependencies = [ "ahash", "autocfg", ] -[[package]] -name = "heck" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "hermit-abi" -version = "0.1.8" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] @@ -1573,14 +1601,21 @@ dependencies = [ "rayon", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "image" -version = "0.23.3" +version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfc5483f8d5afd3653b38a196c52294dcb239c3e1a5bade1990353ea13bcf387" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" dependencies = [ "bytemuck", "byteorder", + "color_quant", "jpeg-decoder", "num-iter", "num-rational", @@ -1588,21 +1623,15 @@ dependencies = [ "png", ] -[[package]] -name = "inflate" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" -dependencies = [ - "adler32", -] - [[package]] name = "instant" -version = "0.1.2" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c346c299e3fe8ef94dc10c2c0253d858a69aac1245157a3bf4125915d528caf" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", "web-sys", ] @@ -1617,24 +1646,27 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.5" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jpeg-decoder" -version = "0.1.18" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0256f0aec7352539102a9efbcb75543227b7ab1117e0f95450023af730128451" -dependencies = [ - "byteorder", -] +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" [[package]] name = "js-sys" -version = "0.3.35" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7889c7c36282151f6bf465be4700359318aef36baa951462382eae49e9577cf9" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" dependencies = [ "wasm-bindgen", ] @@ -1663,64 +1695,52 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lazycell" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f" - -[[package]] -name = "lexical-core" -version = "0.4.6" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304bccb228c4b020f3a4835d247df0a02a7c4686098d4167762cfbbe4c5cb14" -dependencies = [ - "arrayvec 0.4.12", - "cfg-if", - "rustc_version", - "ryu", - "static_assertions", -] +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.67" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" +checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" [[package]] name = "libloading" -version = "0.5.2" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" +checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" dependencies = [ - "cc", - "winapi 0.3.8", + "cfg-if 1.0.0", + "winapi 0.3.9", ] [[package]] -name = "line_drawing" -version = "0.7.0" +name = "libloading" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc7ad3d82c845bdb5dde34ffdcc7a5fb4d2996e1e1ee0f19c33bc80e15196b9" +checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52" dependencies = [ - "num-traits", + "cfg-if 1.0.0", + "winapi 0.3.9", ] [[package]] name = "lock_api" -version = "0.3.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" dependencies = [ "scopeguard", ] [[package]] name = "log" -version = "0.4.8" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -1740,45 +1760,60 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "memchr" -version = "2.3.3" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] -name = "memmap" -version = "0.7.0" +name = "memmap2" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" +checksum = "d9b70ca2a6103ac8b665dc150b142ef0e4e89df640c9e6cf295d189c3caebe5a" dependencies = [ "libc", - "winapi 0.3.8", ] [[package]] name = "memoffset" -version = "0.5.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75189eb85871ea5c2e2c15abbdd541185f63b408415e5051f5cac122d8c774b9" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" dependencies = [ - "rustc_version", + "autocfg", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" dependencies = [ "adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "mio" -version = "0.6.21" +version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "fuchsia-zircon", "fuchsia-zircon-sys", "iovec", @@ -1805,9 +1840,9 @@ dependencies = [ [[package]] name = "miow" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" dependencies = [ "kernel32-sys", "net2", @@ -1821,41 +1856,91 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a785740271256c230f57462d3b83e52f998433a7062fc18f96d5999474a9f915" +[[package]] +name = "ndk" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb167c1febed0a496639034d0c76b3b74263636045db5489eee52143c246e73" +dependencies = [ + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-glue" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf399b8b7a39c6fb153c4ec32c72fd5fe789df24a647f229c239aa7adb15241" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" +dependencies = [ + "darling", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ndk-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" + [[package]] name = "net2" -version = "0.2.33" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "libc", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] name = "nix" -version = "0.14.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce" +checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" dependencies = [ "bitflags", "cc", - "cfg-if", + "cfg-if 0.1.10", "libc", - "void", ] [[package]] -name = "nodrop" -version = "0.1.14" +name = "nix" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", +] [[package]] name = "nom" -version = "4.2.3" +version = "5.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" dependencies = [ "memchr", "version_check", @@ -1863,20 +1948,20 @@ dependencies = [ [[package]] name = "nom" -version = "5.1.0" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c433f4d505fe6ce7ff78523d2fa13a0b9f2690e181fc26168bcbe5ccc5d14e07" +checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" dependencies = [ - "lexical-core", "memchr", + "minimal-lexical", "version_check", ] [[package]] name = "num-integer" -version = "0.1.42" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ "autocfg", "num-traits", @@ -1884,9 +1969,9 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.40" +version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" dependencies = [ "autocfg", "num-integer", @@ -1895,9 +1980,9 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.2.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da4dc79f9e6c81bef96148c8f6b8e72ad4541caa4a24373e900a36da07de03a3" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" dependencies = [ "autocfg", "num-integer", @@ -1906,61 +1991,69 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.11" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" dependencies = [ "hermit-abi", "libc", ] [[package]] -name = "objc" -version = "0.2.7" +name = "num_enum" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +checksum = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4" dependencies = [ - "malloc_buf", + "derivative", + "num_enum_derive", ] [[package]] -name = "object-pool" -version = "0.4.4" +name = "num_enum_derive" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af8d6b57bfd185264f6b81e1a6c65a4f2cf430bbf05454058f1ab5070d72cd69" +checksum = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d" dependencies = [ - "crossbeam", - "parking_lot 0.9.0", - "serde", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "object-pool" -version = "0.5.3" +name = "objc" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57280719d7b44758cab397e55d4a1a194d4a62575ceea8c794841742b9636e6c" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ - "parking_lot 0.10.2", + "malloc_buf", ] [[package]] -name = "ordered-float" -version = "1.0.2" +name = "object-pool" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18869315e81473c951eb56ad5558bbc56978562d3ecfb87abb7a1e944cea4518" +checksum = "ee9a3e7196d09ec86002b939f1576e8e446d58def8fd48fe578e2c72d5328d68" dependencies = [ - "num-traits", + "parking_lot", ] +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + [[package]] name = "osmesa-sys" version = "0.1.2" @@ -1971,53 +2064,37 @@ dependencies = [ ] [[package]] -name = "parking_lot" -version = "0.9.0" +name = "owned_ttf_parser" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" +checksum = "9f923fb806c46266c02ab4a5b239735c144bdeda724a50ed058e5226f594cde3" dependencies = [ - "lock_api", - "parking_lot_core 0.6.2", - "rustc_version", + "ttf-parser", ] [[package]] name = "parking_lot" -version = "0.10.2" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ + "instant", "lock_api", - "parking_lot_core 0.7.2", + "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" -dependencies = [ - "cfg-if", - "cloudabi", - "libc", - "redox_syscall", - "rustc_version", - "smallvec 0.6.13", - "winapi 0.3.8", -] - -[[package]] -name = "parking_lot_core" -version = "0.7.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ - "cfg-if", - "cloudabi", + "cfg-if 1.0.0", + "instant", "libc", "redox_syscall", - "smallvec 1.4.2", - "winapi 0.3.8", + "smallvec", + "winapi 0.3.9", ] [[package]] @@ -2028,82 +2105,61 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pkg-config" -version = "0.3.17" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" +checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" [[package]] name = "png" -version = "0.16.2" +version = "0.16.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "910f09135b1ed14bb16be445a8c23ddf0777eca485fbfc7cee00d81fecab158a" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" dependencies = [ "bitflags", "crc32fast", "deflate", - "inflate", + "miniz_oxide 0.3.7", ] [[package]] name = "ppv-lite86" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" - -[[package]] -name = "proc-macro-hack" -version = "0.5.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5" -dependencies = [ - "proc-macro2 1.0.24", - "quote 1.0.2", - "syn", -] +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" [[package]] -name = "proc-macro2" -version = "0.4.30" +name = "proc-macro-crate" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" dependencies = [ - "unicode-xid 0.1.0", + "toml", ] [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" dependencies = [ - "unicode-xid 0.2.0", + "unicode-xid", ] [[package]] name = "quote" -version = "0.6.13" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" dependencies = [ - "proc-macro2 0.4.30", -] - -[[package]] -name = "quote" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" -dependencies = [ - "proc-macro2 1.0.24", + "proc-macro2", ] [[package]] name = "rand" -version = "0.7.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ - "getrandom", "libc", "rand_chacha", "rand_core", @@ -2112,37 +2168,37 @@ dependencies = [ [[package]] name = "rand_chacha" -version = "0.2.1" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ - "c2-chacha", + "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" -version = "0.5.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ "getrandom", ] [[package]] name = "rand_hc" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" dependencies = [ "rand_core", ] [[package]] name = "rand_xorshift" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77d416b86801d23dde1aa643023b775c3a462efc0ed96443add11546cdf1dca8" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ "rand_core", "serde", @@ -2159,10 +2215,11 @@ dependencies = [ [[package]] name = "rayon" -version = "1.3.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098" +checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" dependencies = [ + "autocfg", "crossbeam-deque", "either", "rayon-core", @@ -2170,47 +2227,52 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.7.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9" +checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" dependencies = [ + "crossbeam-channel", "crossbeam-deque", - "crossbeam-queue", - "crossbeam-utils", + "crossbeam-utils 0.8.5", "lazy_static", "num_cpus", ] [[package]] name = "redox_syscall" -version = "0.1.56" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] [[package]] name = "regex" -version = "1.3.6" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6946991529684867e47d86474e3a6d0c0ab9b82d5821e314b1ede31fa3a4b3" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ "aho-corasick", "memchr", "regex-syntax", - "thread_local", ] [[package]] name = "regex-syntax" -version = "0.6.17" +version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" - -[[package]] -name = "rltk" -version = "0.8.1" -dependencies = [ - "bracket-lib 0.8.1", -] +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "rltk" @@ -2218,54 +2280,43 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c83076f8174384edf79d3f4a91934b8c0e1feed6eed943608d54af790f0a7dd" dependencies = [ - "bracket-lib 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bracket-lib", ] [[package]] name = "rust_roguelike_tutorial" version = "0.1.0" dependencies = [ - "rltk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rltk", "specs", "specs-derive", ] [[package]] -name = "rustc_version" -version = "0.2.3" +name = "rusttype" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +checksum = "dc7c727aded0be18c5b80c1640eae0ac8e396abf6fa8477d96cb37d18ee5ec59" dependencies = [ - "semver", + "ab_glyph_rasterizer", + "owned_ttf_parser", ] [[package]] -name = "rusttype" -version = "0.7.9" +name = "ryu" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310942406a39981bed7e12b09182a221a29e0990f3e7e0c971f131922ed135d5" -dependencies = [ - "rusttype 0.8.2", -] +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" [[package]] -name = "rusttype" -version = "0.8.2" +name = "safe_arch" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14a911032fb5791ccbeec9f28fdcb9bf0983b81f227bafdfd227c658d0731c8a" +checksum = "c1ff3d6d9696af502cc3110dacce942840fb06ff4514cad92236ecc455f2ce05" dependencies = [ - "approx", - "arrayvec 0.5.1", - "ordered-float", - "stb_truetype", + "bytemuck", ] -[[package]] -name = "ryu" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" - [[package]] name = "same-file" version = "1.0.6" @@ -2276,25 +2327,16 @@ dependencies = [ ] [[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "semver" -version = "0.9.0" +name = "scoped-tls" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" [[package]] -name = "semver-parser" -version = "0.7.0" +name = "scopeguard" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "send_wrapper" @@ -2304,29 +2346,29 @@ checksum = "a0eddf2e8f50ced781f288c19f18621fa72a3779e3cb58dbf23b07469b0abeb4" [[package]] name = "serde" -version = "1.0.117" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.117" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ - "proc-macro2 1.0.24", - "quote 1.0.2", + "proc-macro2", + "quote", "syn", ] [[package]] name = "serde_json" -version = "1.0.48" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25" +checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19" dependencies = [ "itoa", "ryu", @@ -2349,11 +2391,11 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5f08237e667ac94ad20f8878b5943d91a93ccb231428446c57c21c57779016d" dependencies = [ - "arrayvec 0.5.1", + "arrayvec", "hashbrown", "mopa", "rayon", - "smallvec 1.4.2", + "smallvec", "tynm", ] @@ -2365,53 +2407,44 @@ checksum = "b5752e017e03af9d735b4b069f53b7a7fd90fefafa04d8bd0c25581b0bff437f" [[package]] name = "slab" -version = "0.4.2" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "slotmap" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fd553261805f128e2900bf69ab3d034260bc338caf7f0ee54dbf035c85acd" - -[[package]] -name = "smallvec" -version = "0.6.13" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7b0758c52e15a8b5e3691eae6cc559f08eee9406e548a4477ba4e67770a82b6" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" dependencies = [ - "maybe-uninit", + "version_check", ] [[package]] name = "smallvec" -version = "1.4.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "smithay-client-toolkit" -version = "0.6.6" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "421c8dc7acf5cb205b88160f8b4cc2c5cfabe210e43b2f80f009f4c1ef910f1d" +checksum = "4750c76fd5d3ac95fa3ed80fe667d6a3d8590a960e5b575b98eea93339a80b80" dependencies = [ "andrew", "bitflags", - "dlib", + "calloop", + "dlib 0.4.2", "lazy_static", - "memmap", - "nix", + "log", + "memmap2", + "nix 0.18.0", "wayland-client", + "wayland-cursor", "wayland-protocols", ] -[[package]] -name = "sourcefile" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3" - [[package]] name = "specs" version = "0.16.1" @@ -2435,44 +2468,46 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e23e09360f3d2190fec4222cd9e19d3158d5da948c0d1ea362df617dd103511" dependencies = [ - "proc-macro2 1.0.24", - "quote 1.0.2", + "proc-macro2", + "quote", "syn", ] [[package]] -name = "static_assertions" -version = "0.3.4" +name = "strsim" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] -name = "stb_truetype" -version = "0.3.1" +name = "syn" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f77b6b07e862c66a9f3e62a07588fee67cd90a9135a2b942409f195507b4fb51" +checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" dependencies = [ - "byteorder", + "proc-macro2", + "quote", + "unicode-xid", ] [[package]] -name = "syn" -version = "1.0.48" +name = "thiserror" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ - "proc-macro2 1.0.24", - "quote 1.0.2", - "unicode-xid 0.2.0", + "thiserror-impl", ] [[package]] -name = "thread_local" -version = "1.0.1" +name = "thiserror-impl" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ - "lazy_static", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2481,7 +2516,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.7.2", "futures", ] @@ -2491,12 +2526,27 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.7.2", "futures", "slab", "tokio-executor", ] +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "ttf-parser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e5d7cd7ab3e47dda6e56542f4bbf3824c15234958c6e1bd6aaa347e93499fdc" + [[package]] name = "tuple_utils" version = "0.3.0" @@ -2505,112 +2555,94 @@ checksum = "44834418e2c5b16f47bedf35c28e148db099187dd5feee6367fb2525863af4f1" [[package]] name = "tynm" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e87d8ff35b1a0aea28758ec2e5959f9e5d826cebf2349a3a7fad43b3e78de28" +checksum = "a4df2caa2dc9c3d1f7641ba981f4cd40ab229775aa7aeb834c9ab2850d50623d" dependencies = [ - "nom 5.1.0", + "nom 5.1.2", ] [[package]] name = "ultraviolet" -version = "0.4.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "432f35260aff6ee992f3027a1166eaebd7c6ebc0844188b0e545f7fa2121daf7" +checksum = "16b9e3507eba17043af05c8a72fce3ec2c24b58945f45732e71dbc6646d904a7" dependencies = [ "wide", ] -[[package]] -name = "unicode-segmentation" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" - [[package]] name = "unicode-xid" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" - -[[package]] -name = "unicode-xid" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "version_check" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" - -[[package]] -name = "void" -version = "1.0.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" [[package]] name = "walkdir" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" dependencies = [ "same-file", - "winapi 0.3.8", + "winapi 0.3.9", "winapi-util", ] [[package]] name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" +version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.58" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5205e9afdf42282b192e2310a5b463a6d1c1d774e30dc3c791ac37ab42d2616c" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.58" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11cdb95816290b525b32587d76419facd99662a07e59d3cdb560488a819d9a45" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" dependencies = [ "bumpalo", "lazy_static", "log", - "proc-macro2 1.0.24", - "quote 1.0.2", + "proc-macro2", + "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.58" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "574094772ce6921576fb6f2e3f7497b8a76273b6db092be18fc48a082de09dc3" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" dependencies = [ - "quote 1.0.2", + "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.58" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e85031354f25eaebe78bb7db1c3d86140312a911a106b2e29f9cc440ce3e7668" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" dependencies = [ - "proc-macro2 1.0.24", - "quote 1.0.2", + "proc-macro2", + "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", @@ -2618,25 +2650,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.58" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e7e61fc929f4c0dddb748b102ebf9f632e2b8d739f2016542b4de2965a9601" - -[[package]] -name = "wasm-bindgen-webidl" -version = "0.2.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef012a0d93fc0432df126a8eaf547b2dce25a8ce9212e1d3cbeef5c11157975d" -dependencies = [ - "anyhow", - "heck", - "log", - "proc-macro2 1.0.24", - "quote 1.0.2", - "syn", - "wasm-bindgen-backend", - "weedle", -] +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" [[package]] name = "wasm-timer" @@ -2654,16 +2670,15 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.23.6" +version = "0.28.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1080ebe0efabcf12aef2132152f616038f2d7dcbbccf7b2d8c5270fe14bcda" +checksum = "e3ab332350e502f159382201394a78e3cc12d0f04db863429260164ea40e0355" dependencies = [ "bitflags", - "calloop", "downcast-rs", "libc", - "mio", - "nix", + "nix 0.20.0", + "scoped-tls", "wayland-commons", "wayland-scanner", "wayland-sys", @@ -2671,19 +2686,42 @@ dependencies = [ [[package]] name = "wayland-commons" -version = "0.23.6" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21817947c7011bbd0a27e11b17b337bfd022e8544b071a2641232047966fbda" +dependencies = [ + "nix 0.20.0", + "once_cell", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-cursor" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be610084edd1586d45e7bdd275fe345c7c1873598caa464c4fb835dee70fa65a" +dependencies = [ + "nix 0.20.0", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-egl" +version = "0.28.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb66b0d1a27c39bbce712b6372131c6e25149f03ffb0cd017cf8f7de8d66dbdb" +checksum = "99ba1ab1e18756b23982d36f08856d521d7df45015f404a2d7c4f0b2d2f66956" dependencies = [ - "nix", + "wayland-client", "wayland-sys", ] [[package]] name = "wayland-protocols" -version = "0.23.6" +version = "0.28.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cc286643656742777d55dc8e70d144fa4699e426ca8e9d4ef454f4bf15ffcf9" +checksum = "286620ea4d803bacf61fa087a4242ee316693099ee5a140796aaba02b29f861f" dependencies = [ "bitflags", "wayland-client", @@ -2693,54 +2731,44 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.23.6" +version = "0.28.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93b02247366f395b9258054f964fe293ddd019c3237afba9be2ccbe9e1651c3d" +checksum = "ce923eb2deb61de332d1f356ec7b6bf37094dc5573952e1c8936db03b54c03f1" dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", + "proc-macro2", + "quote", "xml-rs", ] [[package]] name = "wayland-sys" -version = "0.23.6" +version = "0.28.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d94e89a86e6d6d7c7c9b19ebf48a03afaac4af6bc22ae570e9a24124b75358f4" +checksum = "d841fca9aed7febf9bed2e9796c49bf58d4152ceda8ac949ebe00868d8f0feb8" dependencies = [ - "dlib", + "dlib 0.5.0", "lazy_static", + "pkg-config", ] [[package]] name = "web-sys" -version = "0.3.35" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf97caf6aa8c2b1dac90faf0db529d9d63c93846cca4911856f78a83cebf53b" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" dependencies = [ - "anyhow", "js-sys", - "sourcefile", "wasm-bindgen", - "wasm-bindgen-webidl", -] - -[[package]] -name = "weedle" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164" -dependencies = [ - "nom 4.2.3", ] [[package]] name = "wide" -version = "0.4.6" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5825a3b8b18b67af1c24165f8ae1feb8f691222736d14099c3483effed2ea729" +checksum = "46bbe7c604a27ca0b05c5503221e76da628225b568e6f1280b42dbad3b72d89b" dependencies = [ "bytemuck", + "safe_arch", ] [[package]] @@ -2751,9 +2779,9 @@ checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" [[package]] name = "winapi" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", @@ -2773,11 +2801,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ccfbf554c6ad11084fb7517daca16cfdcaccbdadba4fc336f032a8b12c2ad80" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -2788,15 +2816,14 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winit" -version = "0.22.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e9092b71b48ad6a0d98835a786308d10760cc09369d02e4a166608327f1f26" +checksum = "da4eda6fce0eb84bd0a33e3c8794eb902e1033d0a1d5a31bc4f19b1b4bbff597" dependencies = [ - "android_glue", "bitflags", - "cocoa", - "core-foundation", - "core-graphics", + "cocoa 0.24.0", + "core-foundation 0.9.2", + "core-graphics 0.22.3", "core-video-sys", "dispatch", "instant", @@ -2805,15 +2832,18 @@ dependencies = [ "log", "mio", "mio-extras", + "ndk", + "ndk-glue", + "ndk-sys", "objc", - "parking_lot 0.10.2", + "parking_lot", "percent-encoding", "raw-window-handle", "smithay-client-toolkit", "wasm-bindgen", "wayland-client", "web-sys", - "winapi 0.3.8", + "winapi 0.3.9", "x11-dl", ] @@ -2829,24 +2859,35 @@ dependencies = [ [[package]] name = "x11-dl" -version = "2.18.5" +version = "2.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf981e3a5b3301209754218f962052d4d9ee97e478f4d26d4a6eced34c1fef8" +checksum = "ea26926b4ce81a6f5d9d0f3a0bc401e5a37c6ae14a1bfaa8ff6099ca80038c59" dependencies = [ "lazy_static", "libc", - "maybe-uninit", "pkg-config", ] +[[package]] +name = "xcursor" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" +dependencies = [ + "nom 7.1.0", +] + [[package]] name = "xdg" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" +checksum = "3a23fe958c70412687039c86f578938b4a0bb50ec788e96bce4d6ab00ddd5803" +dependencies = [ + "dirs", +] [[package]] name = "xml-rs" -version = "0.8.0" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "541b12c998c5b56aa2b4e6f18f03664eef9a4fd0a246a55594efae6cc2d964b5" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" diff --git a/Cargo.toml b/Cargo.toml index b5572467..762c3ad5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,8 @@ members = [ "chapter-71-logging", "chapter-72-textlayers", "chapter-73-systems", - "chapter-74-darkcity" + "chapter-74-darkcity", + "chapter-75-darkplaza", ] [profile.dev] diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 1b0d21cc..d8a9c434 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -78,5 +78,6 @@ - [Text Layers](./chapter_72.md) - [Systems/Dispatch](./chapter_73.md) - [Dark Elf City 1](./chapter_74.md) + - [Dark Elf Plaza](./chapter_75.md) - [Contributors](./contributors.md) - [Licensing](./license.md) diff --git a/book/src/beta-webBanner-old.jpg b/book/src/beta-webBanner-old.jpg new file mode 100644 index 00000000..9a98bcdd Binary files /dev/null and b/book/src/beta-webBanner-old.jpg differ diff --git a/book/src/c75-altar.jpg b/book/src/c75-altar.jpg new file mode 100644 index 00000000..cd55e327 Binary files /dev/null and b/book/src/c75-altar.jpg differ diff --git a/book/src/c75-edgeroads.jpg b/book/src/c75-edgeroads.jpg new file mode 100644 index 00000000..889d255c Binary files /dev/null and b/book/src/c75-edgeroads.jpg differ diff --git a/book/src/c75-emptymap.jpg b/book/src/c75-emptymap.jpg new file mode 100644 index 00000000..8b06c878 Binary files /dev/null and b/book/src/c75-emptymap.jpg differ diff --git a/book/src/c75-pools.jpg b/book/src/c75-pools.jpg new file mode 100644 index 00000000..99639c60 Binary files /dev/null and b/book/src/c75-pools.jpg differ diff --git a/book/src/c75-solidrock.jpg b/book/src/c75-solidrock.jpg new file mode 100644 index 00000000..bacb6cbb Binary files /dev/null and b/book/src/c75-solidrock.jpg differ diff --git a/book/src/c75-vokoth.jpg b/book/src/c75-vokoth.jpg new file mode 100644 index 00000000..dcc35b46 Binary files /dev/null and b/book/src/c75-vokoth.jpg differ diff --git a/book/src/chapter_74.md b/book/src/chapter_74.md index 697ae38e..5e1d7d45 100644 --- a/book/src/chapter_74.md +++ b/book/src/chapter_74.md @@ -21,7 +21,7 @@ The next level of the game is a dark elven city. The design document is a bit sp ## Generating a basic city -The `level_builder` function in `map_builder/mod.rs` controls which map algorithm is called for a given level. Add a placeholder entry for a new map type: +The `level_builder` function in `map_builders/mod.rs` controls which map algorithm is called for a given level. Add a placeholder entry for a new map type: ```rust pub fn level_builder(new_depth: i32, width: i32, height: i32) -> BuilderChain { diff --git a/book/src/chapter_75.md b/book/src/chapter_75.md new file mode 100644 index 00000000..ad330617 --- /dev/null +++ b/book/src/chapter_75.md @@ -0,0 +1,653 @@ +# One Night in the Plaza + +--- + +***About this tutorial*** + +*This tutorial is free and open source, and all code uses the MIT license - so you are free to do with it as you like. My hope is that you will enjoy the tutorial, and make great games!* + +*If you enjoy this and would like me to keep writing, please consider supporting [my Patreon](https://www.patreon.com/blackfuture).* + +[![Hands-On Rust](./beta-webBanner.jpg)](https://pragprog.com/titles/hwrust/hands-on-rust/) + +--- + +The city level was deliberately messy: the hero is fighting through cramped, sprawling a Dark Elf under-city - facing different noble houses' troops who were also intent upon killing one another. It makes for fast-paced, tight combat. The last part of the city is the plaza - which is meant to offer more of a contrast. A park in the city holds a portal to the Abyss, and only the most affluent/influential dark elves can build here. So despite being underground, it's more of an outdoor city type of feeling. + +So let's think a bit about what makes up the plaza level: + +* A decent sized park, defended by some tough baddies. We can add something demonic here for the first time, since we're right next to a portal to their home. +* Some larger buildings. +* Statues, fountains and similar niceties. + +Continuing to think about dark elves, they aren't really known for their civic planning. They are, at heart, a Chaotic species. So we want to avoid the feeling that they really planned out their city, and meticulously built it to make sense. In fact, not making sense adds to the surreality. + +## Generating the Plaza + +Just like we have for other level builders, we need to add a placeholder builder for level `11`. Open `map_builders/mod.rs` and add a call to `dark_elf_plaza` for level 11: + +```rust +pub fn level_builder(new_depth: i32, width: i32, height: i32) -> BuilderChain { + rltk::console::log(format!("Depth: {}", new_depth)); + match new_depth { + 1 => town_builder(new_depth, width, height), + 2 => forest_builder(new_depth, width, height), + 3 => limestone_cavern_builder(new_depth, width, height), + 4 => limestone_deep_cavern_builder(new_depth, width, height), + 5 => limestone_transition_builder(new_depth, width, height), + 6 => dwarf_fort_builder(new_depth, width, height), + 7 => mushroom_entrance(new_depth, width, height), + 8 => mushroom_builder(new_depth, width, height), + 9 => mushroom_exit(new_depth, width, height), + 10 => dark_elf_city(new_depth, width, height), + 11 => dark_elf_plaza(new_depth, width, height), + _ => random_builder(new_depth, width, height) + } +} +``` + +Now open `map_builders/dark_elves.rs` and create the new map builder function---`dark_elf_plaza`. We'll start with generating a BSP interior map; that'll change, but it's good to get something compiling: + +```rust +pub fn dark_elf_plaza(new_depth: i32, width: i32, height: i32) -> BuilderChain { + println!("Dark elf plaza builder"); + let mut chain = BuilderChain::new(new_depth, width, height, "Dark Elven Plaza"); + chain.start_with(BspInteriorBuilder::new()); + chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER)); + chain.with(CullUnreachable::new()); + chain.with(AreaEndingPosition::new(XEnd::RIGHT, YEnd::CENTER)); + chain.with(VoronoiSpawning::new()); + chain +} +``` + +### Deliberately Poor City Planning + +Now that we have the exact same map as the previous level, let's build a generator to create the plaza. We'll get started by making a boring, empty map - just to validate that our map builder is working. At the end of `dark_elves.rs`, paste in the following: + +```rust +// Plaza Builder +use super::{InitialMapBuilder, BuilderMap, TileType }; + +pub struct PlazaMapBuilder {} + +impl InitialMapBuilder for PlazaMapBuilder { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.empty_map(build_data); + } +} + +impl PlazaMapBuilder { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(PlazaMapBuilder{}) + } + + fn empty_map(&mut self, build_data : &mut BuilderMap) { + build_data.map.tiles.iter_mut().for_each(|t| *t = TileType::Floor); + } +} +``` + +You also need to go into the `dark_elf_plaza` and change the initial builder to use it: + +```rust +pub fn dark_elf_plaza(new_depth: i32, width: i32, height: i32) -> BuilderChain { + println!("Dark elf plaza builder"); + let mut chain = BuilderChain::new(new_depth, width, height, "Dark Elven Plaza"); + chain.start_with(PlazaMapBuilder::new()); + chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER)); + chain.with(CullUnreachable::new()); + chain.with(AreaEndingPosition::new(XEnd::RIGHT, YEnd::CENTER)); + chain.with(VoronoiSpawning::new()); + chain +} +``` + +If you run the game now and teleport down to the last level, the "plaza" is a giant open space full of people killing both you and one another. I found it quite entertaining, but it's not what we want. + +![](./c75-emptymap.jpg) + +The plaza needs to be divided into zones, which contain plaza content. That's similar to what we did for Voronoi maps, but we aren't looking to create cellular walls - just areas in which to place content. Let's start by making a basic Voronoi cell area. Extend your map builder to call a new function named `spawn_zones`: + +```rust +impl InitialMapBuilder for PlazaMapBuilder { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.empty_map(build_data); + self.spawn_zones(build_data); + } +} +``` + +We'll start by taking our previous Voronoi code, and making it always have 32 seeds and use Pythagoras for distance: + +```rust +fn spawn_zones(&mut self, build_data : &mut BuilderMap) { + let mut voronoi_seeds : Vec<(usize, rltk::Point)> = Vec::new(); + + while voronoi_seeds.len() < 32 { + let vx = crate::rng::roll_dice(1, build_data.map.width-1); + let vy = crate::rng::roll_dice(1, build_data.map.height-1); + let vidx = build_data.map.xy_idx(vx, vy); + let candidate = (vidx, rltk::Point::new(vx, vy)); + if !voronoi_seeds.contains(&candidate) { + voronoi_seeds.push(candidate); + } + } + + let mut voronoi_distance = vec![(0, 0.0f32) ; 32]; + let mut voronoi_membership : Vec = vec![0 ; build_data.map.width as usize * build_data.map.height as usize]; + for (i, vid) in voronoi_membership.iter_mut().enumerate() { + let x = i as i32 % build_data.map.width; + let y = i as i32 / build_data.map.width; + + for (seed, pos) in voronoi_seeds.iter().enumerate() { + let distance = rltk::DistanceAlg::PythagorasSquared.distance2d( + rltk::Point::new(x, y), + pos.1 + ); + voronoi_distance[seed] = (seed, distance); + } + + voronoi_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap()); + + *vid = voronoi_distance[0].0 as i32; + } + + // Spawning code will go here +} +``` + +At the end of the new `spawn_zones` function, we have an array named `voronoi_membership` that categorizes every tile into one of 32 zones. The zones are guaranteed to be contiguous. Let's write some quick code to count the sizes of each zone to validate our work: + +```rust +// Make a list of zone sizes and cull empty ones +let mut zone_sizes : Vec<(i32, usize)> = Vec::with_capacity(32); +for zone in 0..32 { + let num_tiles = voronoi_membership.iter().filter(|z| **z == zone).count(); + if num_tiles > 0 { + zone_sizes.push((zone, num_tiles)); + } +} +println!("{:?}", zone_sizes); +``` + +This will give different results every time, but will give a good idea of how many zones we've created and how large they are. Here's the output from a quick test run: + +``` +[(0, 88), (1, 60), (2, 143), (3, 261), (4, 192), (5, 165), (6, 271), (7, 68), (8, 151), (9, 78), (10, 45), (11, 154), (12, 132), (13, 88), (14, 162), (15, 49), (16, 138), (17, 57), (18, 206), (19, 117), (20, 168), (21, 67), (22, 153), (23, 119), (24, 41), (25, 48), (26, 78), (27, 118), (28, 197), (29, 129), (30, 163), (31, 94)] +``` + +So we know that the zone creation works: there are 32 zones, none of which are excessively tiny - although some are quite large. Let's sort the list by size, descending: + +```rust +zone_sizes.sort_by(|a,b| b.1.cmp(&a.1)); +``` + +This yields a weighted "importance" map: the big zones are first, the smaller zones last. We'll use this to spawn content in order of importance. The big "portal park" is guaranteed to be the largest area. Here's the start of our creation system: + +```rust +// Start making zonal terrain +zone_sizes.iter().enumerate().for_each(|(i, (zone, _))| { + match i { + 0 => self.portal_park(build_data, &voronoi_membership, *zone), + _ => {} + } +}); +``` + +The placeholder signature for `portal_park` is as follows: + +```rust +fn portal_park(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32], zone: i32) { +} +``` + +We'll use this pattern to gradually populate the plaza. For now, we'll skip the portal park and add some other features first. + +### Solid Rock + +Let's start with the simplest: we're going to turn some of the smaller zones into solid rock. These might be areas the elves haven't mined yet, or - more likely - they left them in place to hold the cavern up. We're going to use a feature we haven't touched before: a "match guard". You can make `match` work for "greater than" as follows: + +```rust +// Start making zonal terrain +zone_sizes.iter().enumerate().for_each(|(i, (zone, _))| { + match i { + 0 => self.portal_park(build_data, &voronoi_membership, *zone), + i if i > 20 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::Wall), + _ => {} + } +}); +``` + +The actual `fill_zone` function is quite simple: it finds tiles in the zone and turns them into walls: + +```rust +fn fill_zone(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32], zone: i32, tile_type: TileType) { + voronoi_membership + .iter() + .enumerate() + .filter(|(_, tile_zone)| **tile_zone == zone) + .for_each(|(idx, _)| build_data.map.tiles[idx] = tile_type); + } +``` + +This already injects a little life into our map: + +![](./c75-solidrock.jpg) + +### Pools + +Caves tend to be dank, wet places. The dark elves probably enjoy a few pools - plazas are known for magnificent pools! Let's extend the "default" matching to sometimes create zone pools: + +```rust +// Start making zonal terrain +zone_sizes.iter().enumerate().for_each(|(i, (zone, _))| { + match i { + 0 => self.portal_park(build_data, &voronoi_membership, *zone), + i if i > 20 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::Wall), + _ => { + let roll = crate::rng::roll_dice(1, 6); + match roll { + 1 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::DeepWater), + 2 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::ShallowWater), + _ => {} + } + } + } +}); +``` +See how if we aren't matching anything else, we roll a dice? If it comes up 1 or 2, we add a pool of varying depth. Actually adding the pool is just like adding solid rock - but we add water instead. + +The addition of some water features continues to bring the zone to life: + +![](./c75-pools.jpg) + +### Stalactite Parks + +Stalactites (and presumably their twin, stalagmites) can be a beautiful feature of real caves. They are a natural candidate for inclusion in a dark elf park. It would be nice to have a bit of color in the city, so let's surround them with grass. They are a carefully cultivated park, offering privacy for whatever dark elves do in their spare time (you don't want to know...). + +Add it to the "unknown" zone options: + +```rust +// Start making zonal terrain +zone_sizes.iter().enumerate().for_each(|(i, (zone, _))| { + match i { + 0 => self.portal_park(build_data, &voronoi_membership, *zone), + i if i > 20 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::Wall), + _ => { + let roll = crate::rng::roll_dice(1, 6); + match roll { + 1 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::DeepWater), + 2 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::ShallowWater), + 3 => self.stalactite_display(build_data, &voronoi_membership, *zone), + _ => {} + } + } + } +}); +``` + +And use a similar function to `fill_zone` to populate each tile in the zone with either grass or a stalactite: + +```rust +fn stalactite_display(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32], zone: i32) { + voronoi_membership + .iter() + .enumerate() + .filter(|(_, tile_zone)| **tile_zone == zone) + .for_each(|(idx, _)| { + build_data.map.tiles[idx] = match crate::rng::roll_dice(1,10) { + 1 => TileType::Stalactite, + 2 => TileType::Stalagmite, + _ => TileType::Grass, + }; + }); + } +``` + +### Parks & Sacrifice Areas + +A few areas of vegetative cover, with seating adds to the park feel. These should be larger areas - that's the dominant theme of the zone. I don't really picture dark elves sitting around listening to a nice concert---so let's go with an altar in the middle, complete with bloodstains. Notice how we're using an "or" statement in our `match` to match both the 2nd and 3rd largest zones: + +```rust +// Start making zonal terrain +zone_sizes.iter().enumerate().for_each(|(i, (zone, _))| { + match i { + 0 => self.portal_park(build_data, &voronoi_membership, *zone), + 1 | 2 => self.park(build_data, &voronoi_membership, *zone), + i if i > 20 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::Wall), + _ => { + let roll = crate::rng::roll_dice(1, 6); + match roll { + 1 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::DeepWater), + 2 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::ShallowWater), + 3 => self.stalactite_display(build_data, &voronoi_membership, *zone), + _ => {} + } + } + } +}); +``` + +Actually populating the park is slightly more convoluted: + +```rust +fn park(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32], zone: i32, seeds: &[(usize, rltk::Point)]) { + let zone_tiles : Vec = voronoi_membership + .iter() + .enumerate() + .filter(|(_, tile_zone)| **tile_zone == zone) + .map(|(idx, _)| idx) + .collect(); + + // Start all grass + zone_tiles.iter().for_each(|idx| build_data.map.tiles[*idx] = TileType::Grass); + + // Add a stone area in the middle + let center = seeds[zone as usize].1; + for y in center.y-2 ..= center.y+2 { + for x in center.x-2 ..= center.x+2 { + let idx = build_data.map.xy_idx(x, y); + build_data.map.tiles[idx] = TileType::Road; + if crate::rng::roll_dice(1,6) > 2 { + build_data.map.bloodstains.insert(idx); + } + } + } + + // With an altar at the center + build_data.spawn_list.push(( + build_data.map.xy_idx(center.x, center.y), + "Altar".to_string() + )); + + // And chairs for spectators + zone_tiles.iter().for_each(|idx| { + if build_data.map.tiles[*idx] == TileType::Grass && crate::rng::roll_dice(1, 6)==1 { + build_data.spawn_list.push(( + *idx, + "Chair".to_string() + )); + } + }); +} +``` + +We start by collecting a list of available tiles. Then we cover them all in nice grass. Find the center point of the Voronoi zone (it'll be the seed that generated it), and cover that area with road. Spawn an altar in the middle, some random blood stains and a bunch of chairs. It's all spawning we've done before - but pulled together to make a (not entirely pleasant) theme park. + +The park areas look sufficiently chaotic: + +![](./c75-altar.jpg) + +### Adding Walkways + +At this point, there's no guaranty that you can actually traverse the map. It's entirely possible that water and walls will coincide in just the wrong way to block your progress. That's not a good thing! Let's use the system we encountered when we created the first Voronoi builder to identify edges between voronoi zones---and replace the edge tiles with roads. This ensures that there's a pathway between zones, as well as giving a nice honeycomb effect across the map. + +Start by adding a call to the end of `spawn_zones` that calls the road builder: + +```rust +// Clear the path +self.make_roads(build_data, &voronoi_membership); +``` + +Now we actually have to build some roads. Most of this code is the same as the voronoi edge detection. Instead of placing floors inside the zone, we're placing roads on the edge: + +```rust +fn make_roads(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32]) { + for y in 1..build_data.map.height-1 { + for x in 1..build_data.map.width-1 { + let mut neighbors = 0; + let my_idx = build_data.map.xy_idx(x, y); + let my_seed = voronoi_membership[my_idx]; + if voronoi_membership[build_data.map.xy_idx(x-1, y)] != my_seed { neighbors += 1; } + if voronoi_membership[build_data.map.xy_idx(x+1, y)] != my_seed { neighbors += 1; } + if voronoi_membership[build_data.map.xy_idx(x, y-1)] != my_seed { neighbors += 1; } + if voronoi_membership[build_data.map.xy_idx(x, y+1)] != my_seed { neighbors += 1; } + + if neighbors > 1 { + build_data.map.tiles[my_idx] = TileType::Road; + } + } + } +} +``` + +With this in place, the map is passable. Roads delineate the edges, without looking too square: + +![](./c75-edgeroads.jpg) + +### Cleaning up the Spawns + +Currently, the map is *very* chaotic --- and quite likely to kill you very fast. There's big open areas, chock full of baddies, traps (why would you build a trap in a park?), and items strewn around. Chaos is good, but there's such a thing as too much randomness. We'd like to have the map make some sense---in a random sort of way. + +Let's start by completely removing the random entity spawner from the builder chain: + +```rust +pub fn dark_elf_plaza(new_depth: i32, width: i32, height: i32) -> BuilderChain { + println!("Dark elf plaza builder"); + let mut chain = BuilderChain::new(new_depth, width, height, "Dark Elven Plaza"); + chain.start_with(PlazaMapBuilder::new()); + chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER)); + chain.with(CullUnreachable::new()); + chain.with(AreaEndingPosition::new(XEnd::RIGHT, YEnd::CENTER)); + chain +} +``` + +This gives you an enemy-free map, albeit one that still has some chairs and altars. This is a "theme park" map - so we're going to retain some control over what spawns in a given area. It will be light on assistance to the player---we're nearly at the end, so hopefully they stocked up! + +Let's start by putting some monsters in the park/altar areas. One dark elf family or the other is there, leading to clusters of enemies. Now find the `park` function, and we'll extend the "add chairs" section: + +```rust +// And chairs for spectators, and the spectators themselves +let available_enemies = match crate::rng::roll_dice(1, 3) { + 1 => vec![ + "Arbat Dark Elf", + "Arbat Dark Elf Leader", + "Arbat Orc Slave", + ], + 2 => vec![ + "Barbo Dark Elf", + "Barbo Goblin Archer", + ], + _ => vec![ + "Cirro Dark Elf", + "Cirro Dark Priestess", + "Cirro Spider", + ] +}; + +zone_tiles.iter().for_each(|idx| { + if build_data.map.tiles[*idx] == TileType::Grass { + match crate::rng::roll_dice(1, 10) { + 1 => build_data.spawn_list.push(( + *idx, + "Chair".to_string() + )), + 2 => { + let to_spawn = crate::rng::range(0, available_enemies.len() as i32); + build_data.spawn_list.push(( + *idx, + available_enemies[to_spawn as usize].to_string() + )); + } + _ => {} + } + } +}); +``` + +We're doing a couple of new things here. We're randomly assigning an owner to the park - A, B or C groups of Dark Elves. Then we make a list of available spawns for each, and spawn a few in that park. This ensures that the park *starts* as owned by one faction. Since they can often see one another, carnage will commence - but at least it's themed carnage. + +We're going to leave the stalactite galleries and pools empty of enemies. They are just window dressing, and provide a quiet area to hide/rest (see? We're not being totally unfair!). + +### The Portal Park + +Now that we've got the basic shape of the map down, it's time to focus on the park. The first thing to do is to stop the exit from spawning randomly. Change the basic map builder to not include exit placement: + +```rust +pub fn dark_elf_plaza(new_depth: i32, width: i32, height: i32) -> BuilderChain { + println!("Dark elf plaza builder"); + let mut chain = BuilderChain::new(new_depth, width, height, "Dark Elven Plaza"); + chain.start_with(PlazaMapBuilder::new()); + chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER)); + chain.with(CullUnreachable::new()); + chain +} +``` + +That leaves you with no exit at all. We want to place it in the middle of Portal Park. Let's extend the function signature to include the voronoi seeds, and use the seed point to place the exit---just like we did for other parks: + +```rust +fn portal_park(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32], zone: i32, seeds: &[(usize, rltk::Point)]) { + let center = seeds[zone as usize].1; + let idx = build_data.map.xy_idx(center.x, center.y); + build_data.map.tiles[idx] = TileType::DownStairs; +} +``` + +Now, let's make the portal park stand out a bit by covering it in gravel: + +```rust +fn portal_park(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32], zone: i32, seeds: &[(usize, rltk::Point)]) { + let zone_tiles : Vec = voronoi_membership + .iter() + .enumerate() + .filter(|(_, tile_zone)| **tile_zone == zone) + .map(|(idx, _)| idx) + .collect(); + + // Start all gravel + zone_tiles.iter().for_each(|idx| build_data.map.tiles[*idx] = TileType::Gravel); + + // Add the exit + let center = seeds[zone as usize].1; + let idx = build_data.map.xy_idx(center.x, center.y); + build_data.map.tiles[idx] = TileType::DownStairs; +} +``` + +Next, we'll add some altars around the exit: + +```rust +// Add some altars around the exit +let altars = [ + build_data.map.xy_idx(center.x - 2, center.y), + build_data.map.xy_idx(center.x + 2, center.y), + build_data.map.xy_idx(center.x, center.y - 2), + build_data.map.xy_idx(center.x, center.y + 2), +]; +altars.iter().for_each(|idx| build_data.spawn_list.push((*idx, "Altar".to_string()))); +``` + +This gives a pretty good start at the exit to Abyss. You have the exit in the right place, creepy altars and a clearly marked approach. It's also devoid of risk (other than the elves killing one another all over the map). + +Let's make the exit a little more challenging by adding a boss fight to the exit. It's the last big push before Abyss, so it's a natural spot for it. I randomly generated a demon name, and decided to name the boss "Vokoth". Let's spawn it one tile adjacent to th exit: + +```rust +let demon_spawn = build_data.map.xy_idx(center.x+1, center.y+1); +build_data.spawn_list.push((demon_spawn, "Vokoth".to_string())); +``` + +This won't do anything at all until we define Vokoth! We want a tough baddie. Let's take a quick trip down memory lane in `spawns.json` and remind ourselves how we defined the black dragon: + +```json +{ + "name" : "Black Dragon", + "renderable": { + "glyph" : "D", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1, + "x_size" : 2, + "y_size" : 2 + }, + "blocks_tile" : true, + "vision_range" : 12, + "movement" : "static", + "attributes" : { + "might" : 13, + "fitness" : 13 + }, + "skills" : { + "Melee" : 18, + "Defense" : 16 + }, + "natural" : { + "armor_class" : 17, + "attacks" : [ + { "name" : "bite", "hit_bonus" : 4, "damage" : "1d10+2" }, + { "name" : "left_claw", "hit_bonus" : 2, "damage" : "1d10" }, + { "name" : "right_claw", "hit_bonus" : 2, "damage" : "1d10" } + ] + }, + "loot_table" : "Wyrms", + "faction" : "Wyrm", + "level" : 6, + "gold" : "20d10", + "abilities" : [ + { "spell" : "Acid Breath", "chance" : 0.2, "range" : 8.0, "min_range" : 2.0 } + ] +}, +``` + +That's a really tough monster, and makes for a good template for the Abyssal demon. Let's clone it (copy/paste time!) and build an entry for Vokoth: + +```json +{ + "name" : "Vokoth", + "renderable": { + "glyph" : "&", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1, + "x_size" : 2, + "y_size" : 2 + }, + "blocks_tile" : true, + "vision_range" : 6, + "movement" : "static", + "attributes" : { + "might" : 13, + "fitness" : 13 + }, + "skills" : { + "Melee" : 18, + "Defense" : 16 + }, + "natural" : { + "armor_class" : 17, + "attacks" : [ + { "name" : "whip", "hit_bonus" : 4, "damage" : "1d10+2" } + ] + }, + "loot_table" : "Wyrms", + "faction" : "Wyrm", + "level" : 8, + "gold" : "20d10", + "abilities" : [] +} +``` + +Now if you play the game, you'll find yourself facing a nasty demon monster at the exit to Abyss. + +![](./c75-vokoth.jpg) + +## Wrap-Up + +We now have the second-to-last section done! You can battle your way down to the Dark Elf Plaza, and find the gateway to Abyss - but only if you can evade a hulking demon and a horde of elves---with very little in the way of help offered. Next up, we'll begin to build the Abyss. + +--- + +**The source code for this chapter may be found [here](https://github.com/thebracket/rustrogueliketutorial/tree/master/chapter-75-darkplaza)** + + +[Run this chapter's example with web assembly, in your browser (WebGL2 required)](https://bfnightly.bracketproductions.com/rustbook/wasm/chapter-75-darkplaza) +--- + +Copyright (C) 2019, Herbert Wolverson. + +--- \ No newline at end of file diff --git a/chapter-74-darkcity/Cargo.toml b/chapter-74-darkcity/Cargo.toml index 917cfede..43b38b4c 100644 --- a/chapter-74-darkcity/Cargo.toml +++ b/chapter-74-darkcity/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rltk = { path="c:/users/herbe/documents/learnrust/rltk_rs/rltk", features = ["serde"] } +rltk = { version = "0.8.0", features = ["serde"] } specs = { version = "0.16.1", features = ["serde"] } specs-derive = "0.4.1" serde= { version = "^1.0.44", features = ["derive"] } diff --git a/chapter-75-darkplaza/Cargo.toml b/chapter-75-darkplaza/Cargo.toml new file mode 100644 index 00000000..85249e27 --- /dev/null +++ b/chapter-75-darkplaza/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "chapter-75-darkplaza" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rltk = { version = "0.8.0", features = ["serde"] } +specs = { version = "0.16.1", features = ["serde"] } +specs-derive = "0.4.1" +serde= { version = "^1.0.44", features = ["derive"] } +serde_json = "^1.0.44" +lazy_static = "1.4.0" +regex = "1.3.6" diff --git a/chapter-75-darkplaza/raws/spawns.json b/chapter-75-darkplaza/raws/spawns.json new file mode 100644 index 00000000..03157ad4 --- /dev/null +++ b/chapter-75-darkplaza/raws/spawns.json @@ -0,0 +1,2868 @@ +{ +"spawn_table" : [ + { "name" : "Goblin", "weight" : 10, "min_depth" : 3, "max_depth" : 4 }, + { "name" : "Goblin Archer", "weight" : 10, "min_depth" : 3, "max_depth" : 4 }, + { "name" : "Orc", "weight" : 1, "min_depth" : 4, "max_depth" : 100 }, + { "name" : "Beginner's Magic", "weight" : 6, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Venom 101", "weight" : 3, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Arachnophilia 101", "weight" : 3, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Strength Potion", "weight" : 2, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Poison Potion", "weight" : 3, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Slow Potion", "weight" : 3, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Haste Potion", "weight" : 3, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Web Scroll", "weight" : 2, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Rod of Venom", "weight" : 2, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Health Potion", "weight" : 15, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Mana Potion", "weight" : 7, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Fireball Scroll", "weight" : 2, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Confusion Scroll", "weight" : 2, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Magic Missile Scroll", "weight" : 4, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Town Portal Scroll", "weight" : 4, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Remove Curse Scroll", "weight" : 4, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Identify Scroll", "weight" : 4, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Rod of Fireballs", "weight" : 1, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Gauntlets of Ogre Power", "weight" : 1, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Dagger", "weight" : 3, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Shield", "weight" : 3, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Shortbow", "weight" : 2, "min_depth" : 3, "max_depth" : 100 }, + { "name" : "Longsword", "weight" : 2, "min_depth" : 3, "max_depth" : 100 }, + { "name" : "Tower Shield", "weight" : 1, "min_depth" : 3, "max_depth" : 100 }, + { "name" : "Leather Armor", "weight" : 1, "min_depth" : 2, "max_depth" : 100 }, + { "name" : "Leather Boots", "weight" : 1, "min_depth" : 2, "max_depth" : 100 }, + { "name" : "Chainmail Armor", "weight" : 1, "min_depth" : 4, "max_depth" : 100 }, + { "name" : "Cloth Cap", "weight" : 5, "min_depth" : 4, "max_depth" : 100 }, + { "name" : "Leather Cap", "weight" : 4, "min_depth" : 4, "max_depth" : 100 }, + { "name" : "Chain Coif", "weight" : 3, "min_depth" : 4, "max_depth" : 100 }, + { "name" : "Steel Helm", "weight" : 2, "min_depth" : 4, "max_depth" : 100 }, + { "name" : "Cloth Pants", "weight" : 6, "min_depth" : 1, "max_depth" : 100 }, + { "name" : "Leather Pants", "weight" : 5, "min_depth" : 1, "max_depth" : 100 }, + { "name" : "Chain Leggings", "weight" : 4, "min_depth" : 1, "max_depth" : 100 }, + { "name" : "Steel Greaves", "weight" : 3, "min_depth" : 5, "max_depth" : 100 }, + { "name" : "Leather Boots", "weight" : 5, "min_depth" : 1, "max_depth" : 100 }, + { "name" : "Chain Boots", "weight" : 4, "min_depth" : 3, "max_depth" : 100 }, + { "name" : "Steel Boots", "weight" : 2, "min_depth" : 5, "max_depth" : 100 }, + { "name" : "Cloth Gloves", "weight" : 6, "min_depth" : 1, "max_depth" : 100 }, + { "name" : "Leather Gloves", "weight" : 5, "min_depth" : 1, "max_depth" : 100 }, + { "name" : "Chain Gloves", "weight" : 3, "min_depth" : 1, "max_depth" : 100 }, + { "name" : "Steel Gloves", "weight" : 2, "min_depth" : 5, "max_depth" : 100 }, + { "name" : "Rations", "weight" : 10, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Magic Mapping Scroll", "weight" : 2, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Bear Trap", "weight" : 5, "min_depth" : 0, "max_depth" : 100 }, + { "name" : "Battleaxe", "weight" : 1, "min_depth" : 2, "max_depth" : 100 }, + { "name" : "Kobold", "weight" : 15, "min_depth" : 3, "max_depth" : 3 }, + { "name" : "Rat", "weight" : 15, "min_depth" : 2, "max_depth" : 2 }, + { "name" : "Mangy Wolf", "weight" : 13, "min_depth" : 2, "max_depth" : 2 }, + { "name" : "Bandit", "weight" : 9, "min_depth" : 2, "max_depth" : 3 }, + { "name" : "Bandit Archer", "weight" : 9, "min_depth" : 2, "max_depth" : 3 }, + { "name" : "Bat", "weight" : 15, "min_depth" : 3, "max_depth" : 3 }, + { "name" : "Large Spider", "weight" : 3, "min_depth" : 3, "max_depth" : 3 }, + { "name" : "Gelatinous Cube", "weight" : 3, "min_depth" : 3, "max_depth" : 3 }, + { "name" : "Dragon Wyrmling", "weight" : 1, "min_depth" : 5, "max_depth" : 6 }, + { "name" : "Lizardman", "weight" : 10, "min_depth" : 5, "max_depth" : 7 }, + { "name" : "Giant Lizard", "weight" : 4, "min_depth" : 5, "max_depth" : 7 }, + { "name" : "Rock Golem", "weight" : 4, "min_depth" : 5, "max_depth" : 7 }, + { "name" : "Firecap Mushroom", "weight" : 10, "min_depth" : 7, "max_depth" : 9 }, + { "name" : "Sporecap Mushroom", "weight" : 10, "min_depth" : 7, "max_depth" : 9 }, + { "name" : "Deathcap Mushroom", "weight" : 7, "min_depth" : 7, "max_depth" : 9 }, + { "name" : "Fungus Man", "weight" : 8, "min_depth" : 7, "max_depth" : 9 }, + { "name" : "Spore Zombie", "weight" : 7, "min_depth" : 7, "max_depth" : 9 }, + { "name" : "Fungal Beast", "weight" : 9, "min_depth" : 7, "max_depth" : 9 }, + { "name" : "Stonefall Trap", "weight" : 4, "min_depth" : 5, "max_depth" : 6 }, + { "name" : "Landmine", "weight" : 1, "min_depth" : 5, "max_depth" : 6 }, + { "name" : "Breastplate", "weight" : 7, "min_depth" : 5, "max_depth" : 7 }, + { "name" : "War Axe", "weight" : 7, "min_depth" : 5, "max_depth" : 7 }, + { "name" : "Dwarf-Steel Shirt", "weight" : 1, "min_depth" : 5, "max_depth" : 7 }, + { "name" : "Hand Crossbow", "weight" : 2, "min_depth" : 9, "max_depth" : 11 }, + { "name" : "Dark Elf", "weight": 10, "min_depth": 10, "max_depth": 11 }, + { "name" : "Arbat Dark Elf", "weight": 10, "min_depth": 10, "max_depth": 11 }, + { "name" : "Arbat Dark Elf Leader", "weight": 7, "min_depth": 10, "max_depth": 11 }, + { "name" : "Arbat Orc Slave", "weight": 14, "min_depth": 10, "max_depth": 11 }, + { "name" : "Barbo Dark Elf", "weight": 9, "min_depth": 10, "max_depth": 11 }, + { "name" : "Barbo Goblin Archer", "weight": 13, "min_depth": 10, "max_depth": 11 }, + { "name" : "Cirro Dark Elf", "weight": 7, "min_depth": 10, "max_depth": 11 }, + { "name" : "Cirro Dark Priestess", "weight": 6, "min_depth": 10, "max_depth": 11 }, + { "name" : "Cirro Spider", "weight": 10, "min_depth": 10, "max_depth": 11 } +], + +"loot_tables" : [ + { "name" : "Animal", + "drops" : [ + { "name" : "Hide", "weight" : 10 }, + { "name" : "Meat", "weight" : 10 } + ] + }, + { "name" : "Wyrms", + "drops" : [ + { "name" : "Dragon Scale", "weight" : 10 }, + { "name" : "Meat", "weight" : 10 } + ] + } +], + +"faction_table" : [ + { "name" : "Player", "responses": { }}, + { "name" : "Mindless", "responses": { "Default" : "attack" } }, + { "name" : "Townsfolk", "responses" : { "Default" : "flee", "Player" : "ignore", "Townsfolk" : "ignore" } }, + { "name" : "Bandits", "responses" : { "Default" : "attack", "Bandits" : "ignore" } }, + { "name" : "Cave Goblins", "responses" : { "Default" : "attack", "Cave Goblins" : "ignore" } }, + { "name" : "Carnivores", "responses" : { "Default" : "attack", "Carnivores" : "ignore" } }, + { "name" : "Herbivores", "responses" : { "Default" : "flee", "Herbivores" : "ignore" } }, + { "name" : "Hungry Rodents", "responses": { "Default" : "attack", "Hungry Rodents" : "ignore" }}, + { "name" : "Wyrm", "responses": { "Default" : "attack", "Wyrm" : "ignore", "Fungi" : "ignore" }}, + { "name" : "Dwarven Remnant", "responses": { "Default" : "attack", "Player" : "ignore", "Dwarven Remnant" : "ignore" }}, + { "name" : "Fungi", "responses": { "Default" : "attack", "Fungi" : "ignore", "Wyrm" : "ignore" }}, + { "name" : "DarkElf", "responses" : { "Default" : "attack", "DarkElf" : "ignore" } }, + { "name" : "DarkElfA", "responses" : { "Default" : "attack", "DarkElfA" : "ignore", "DarkElfB" : "attack", "DarkElfC" : "attack" } }, + { "name" : "DarkElfB", "responses" : { "Default" : "attack", "DarkElfB" : "ignore", "DarkElfA" : "attack", "DarkElfC" : "attack" } }, + { "name" : "DarkElfC", "responses" : { "Default" : "attack", "DarkElfC" : "ignore", "DarkElfA" : "attack", "DarkElfB" : "attack" } } +], + +"items" : [ + { + "name" : "Beginner's Magic", + "renderable": { + "glyph" : "¶", + "fg" : "#FF00FF", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { "teach_spell" : "Zap" } + }, + "weight_lbs" : 0.5, + "base_value" : 50.0, + "vendor_category" : "alchemy" + }, + + { + "name" : "Arachnophilia 101", + "renderable": { + "glyph" : "¶", + "fg" : "#FF00FF", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { "teach_spell" : "Web" } + }, + "weight_lbs" : 0.5, + "base_value" : 50.0, + "vendor_category" : "alchemy" + }, + + { + "name" : "Venom 101", + "renderable": { + "glyph" : "¶", + "fg" : "#FF00FF", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { "teach_spell" : "Venom" } + }, + "weight_lbs" : 0.5, + "base_value" : 50.0, + "vendor_category" : "alchemy" + }, + + { + "name" : "Poison Potion", + "renderable": { + "glyph" : "!", + "fg" : "#FF00FF", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { "damage_over_time" : "2" } + }, + "weight_lbs" : 0.5, + "base_value" : 50.0, + "vendor_category" : "alchemy", + "magic" : { "class" : "common", "naming" : "potion" } + }, + + { + "name" : "Slow Potion", + "renderable": { + "glyph" : "!", + "fg" : "#FF00FF", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { "slow" : "2.0" } + }, + "weight_lbs" : 0.5, + "base_value" : 50.0, + "vendor_category" : "alchemy", + "magic" : { "class" : "common", "naming" : "potion" } + }, + + { + "name" : "Haste Potion", + "renderable": { + "glyph" : "!", + "fg" : "#FF00FF", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { "slow" : "-2.0" } + }, + "weight_lbs" : 0.5, + "base_value" : 100.0, + "vendor_category" : "alchemy", + "magic" : { "class" : "common", "naming" : "potion" } + }, + + { + "name" : "Health Potion", + "renderable": { + "glyph" : "!", + "fg" : "#FF00FF", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { "provides_healing" : "8" } + }, + "weight_lbs" : 0.5, + "base_value" : 50.0, + "vendor_category" : "alchemy", + "magic" : { "class" : "common", "naming" : "potion" } + }, + + { + "name" : "Mana Potion", + "renderable": { + "glyph" : "!", + "fg" : "#FF00FF", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { "provides_mana" : "4" } + }, + "weight_lbs" : 0.5, + "base_value" : 50.0, + "vendor_category" : "alchemy", + "magic" : { "class" : "common", "naming" : "potion" } + }, + + { + "name" : "Strength Potion", + "renderable": { + "glyph" : "!", + "fg" : "#FF00FF", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { "particle" : "!;#FF0000;200.0" } + }, + "weight_lbs" : 0.5, + "base_value" : 50.0, + "vendor_category" : "alchemy", + "magic" : { "class" : "common", "naming" : "potion" }, + "attributes" : { "might" : 5 } + }, + + { + "name" : "Magic Missile Scroll", + "renderable": { + "glyph" : ")", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { + "ranged" : "6", + "damage" : "20", + "particle_line" : "▓;#00FFFF;200.0" + } + }, + "weight_lbs" : 0.5, + "base_value" : 50.0, + "vendor_category" : "alchemy", + "magic" : { "class" : "common", "naming" : "scroll" } + }, + + { + "name" : "Web Scroll", + "renderable": { + "glyph" : ")", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { + "ranged" : "6", + "slow" : "10.0", + "area_of_effect" : "3", + "particle_line" : "☼;#FFFFFF;200.0" + } + }, + "weight_lbs" : 0.5, + "base_value" : 500.0, + "vendor_category" : "alchemy", + "magic" : { "class" : "common", "naming" : "scroll" } + }, + + { + "name" : "Fireball Scroll", + "renderable": { + "glyph" : ")", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { + "ranged" : "6", + "damage" : "20", + "area_of_effect" : "3", + "particle" : "▓;#FFA500;200.0" + } + }, + "weight_lbs" : 0.5, + "base_value" : 100.0, + "vendor_category" : "alchemy", + "magic" : { "class" : "common", "naming" : "scroll" } + }, + + { + "name" : "Confusion Scroll", + "renderable": { + "glyph" : ")", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { + "ranged" : "6", + "confusion" : "4" + } + }, + "weight_lbs" : 0.5, + "base_value" : 75.0, + "vendor_category" : "alchemy", + "magic" : { "class" : "common", "naming" : "scroll" } + }, + + { + "name" : "Magic Mapping Scroll", + "renderable": { + "glyph" : ")", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { + "magic_mapping" : "" + } + }, + "weight_lbs" : 0.5, + "base_value" : 50.0, + "vendor_category" : "alchemy", + "magic" : { "class" : "common", "naming" : "scroll" } + }, + + { + "name" : "Town Portal Scroll", + "renderable": { + "glyph" : ")", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { + "town_portal" : "" + } + }, + "weight_lbs" : 0.5, + "base_value" : 20.0, + "vendor_category" : "alchemy", + "magic" : { "class" : "common", "naming" : "scroll" } + }, + + { + "name" : "Remove Curse Scroll", + "renderable": { + "glyph" : ")", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { + "remove_curse" : "" + } + }, + "weight_lbs" : 0.5, + "base_value" : 50.0, + "vendor_category" : "alchemy", + "magic" : { "class" : "common", "naming" : "scroll" } + }, + + { + "name" : "Identify Scroll", + "renderable": { + "glyph" : ")", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { + "identify" : "" + } + }, + "weight_lbs" : 0.5, + "base_value" : 50.0, + "vendor_category" : "alchemy", + "magic" : { "class" : "common", "naming" : "scroll" } + }, + + { + "name" : "Rations", + "renderable": { + "glyph" : "%", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { + "food" : "" + } + }, + "weight_lbs" : 2.0, + "base_value" : 0.5, + "vendor_category" : "food" + }, + + { + "name" : "Meat", + "renderable": { + "glyph" : "%", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { + "food" : "" + } + }, + "weight_lbs" : 2.0, + "base_value" : 0.5, + "vendor_category" : "food" + }, + + { + "name" : "Hide", + "renderable": { + "glyph" : "ß", + "fg" : "#A52A2A", + "bg" : "#000000", + "order" : 2 + }, + "weight_lbs" : 2.0, + "base_value" : 5.0 + }, + + { + "name" : "Dragon Scale", + "renderable": { + "glyph" : "ß", + "fg" : "#FFD700", + "bg" : "#000000", + "order" : 2 + }, + "weight_lbs" : 2.0, + "base_value" : 75.0 + }, + + { + "name" : "Dried Sausage", + "renderable": { + "glyph" : "%", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { + "food" : "" + } + }, + "weight_lbs" : 2.0, + "base_value" : 0.5 + }, + + { + "name" : "Beer", + "renderable": { + "glyph" : "!", + "fg" : "#FF00FF", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { "provides_healing" : "4" } + }, + "weight_lbs" : 2.0, + "base_value" : 0.5, + "vendor_category" : "food" + }, + + { + "name" : "Rusty Longsword", + "renderable": { + "glyph" : "/", + "fg" : "#BB77BB", + "bg" : "#000000", + "order" : 2 + }, + "weapon" : { + "range" : "melee", + "attribute" : "might", + "base_damage" : "1d8-1", + "hit_bonus" : -1 + }, + "weight_lbs" : 3.0, + "base_value" : 10.0, + "initiative_penalty" : 2, + "vendor_category" : "junk" + }, + + { + "name" : "Dagger", + "renderable": { + "glyph" : "/", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 2 + }, + "weapon" : { + "range" : "melee", + "attribute" : "Quickness", + "base_damage" : "1d4", + "hit_bonus" : 0 + }, + "weight_lbs" : 1.0, + "base_value" : 2.0, + "initiative_penalty" : 0, + "vendor_category" : "weapon", + "template_magic" : { + "unidentified_name" : "Unidentified Dagger", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Shortbow", + "renderable": { + "glyph" : ")", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 2 + }, + "weapon" : { + "range" : "4", + "attribute" : "Quickness", + "base_damage" : "1d4", + "hit_bonus" : 0 + }, + "weight_lbs" : 2.0, + "base_value" : 5.0, + "initiative_penalty" : 1, + "vendor_category" : "weapon", + "template_magic" : { + "unidentified_name" : "Unidentified Shortbow", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Hand Crossbow", + "renderable": { + "glyph" : ")", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 2 + }, + "weapon" : { + "range" : "6", + "attribute" : "Quickness", + "base_damage" : "1d6", + "hit_bonus" : 0 + }, + "weight_lbs" : 2.0, + "base_value" : 5.0, + "initiative_penalty" : 1, + "vendor_category" : "weapon", + "template_magic" : { + "unidentified_name" : "Unidentified Hand Crossbow", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Shortsword", + "renderable": { + "glyph" : "/", + "fg" : "#FFAAFF", + "bg" : "#000000", + "order" : 2 + }, + "weapon" : { + "range" : "melee", + "attribute" : "might", + "base_damage" : "1d6", + "hit_bonus" : 0 + }, + "weight_lbs" : 2.0, + "base_value" : 10.0, + "initiative_penalty" : 1, + "vendor_category" : "weapon", + "template_magic" : { + "unidentified_name" : "Unidentified Shortsword", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Longsword", + "renderable": { + "glyph" : "/", + "fg" : "#FFAAFF", + "bg" : "#000000", + "order" : 2 + }, + "weapon" : { + "range" : "melee", + "attribute" : "might", + "base_damage" : "1d8", + "hit_bonus" : 0 + }, + "weight_lbs" : 3.0, + "base_value" : 15.0, + "initiative_penalty" : 2, + "vendor_category" : "weapon", + "template_magic" : { + "unidentified_name" : "Unidentified Longsword", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Scimitar", + "renderable": { + "glyph" : "/", + "fg" : "#FFAAFF", + "bg" : "#000000", + "order" : 2 + }, + "weapon" : { + "range" : "melee", + "attribute" : "might", + "base_damage" : "1d6+2", + "hit_bonus" : 1 + }, + "weight_lbs" : 2.5, + "base_value" : 25.0, + "initiative_penalty" : 1, + "vendor_category" : "weapon", + "template_magic" : { + "unidentified_name" : "Unidentified Scimitar", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Battleaxe", + "renderable": { + "glyph" : "¶", + "fg" : "#FF55FF", + "bg" : "#000000", + "order" : 2 + }, + "weapon" : { + "range" : "melee", + "attribute" : "might", + "base_damage" : "1d8", + "hit_bonus" : 0 + }, + "weight_lbs" : 4.0, + "base_value" : 10.0, + "initiative_penalty" : 2, + "vendor_category" : "weapon", + "template_magic" : { + "unidentified_name" : "Unidentified Battleaxe", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "War Axe", + "renderable": { + "glyph" : "¶", + "fg" : "#FF55FF", + "bg" : "#000000", + "order" : 2 + }, + "weapon" : { + "range" : "melee", + "attribute" : "might", + "base_damage" : "1d12", + "hit_bonus" : 0 + }, + "weight_lbs" : 4.0, + "base_value" : 100.0, + "initiative_penalty" : 2, + "vendor_category" : "weapon", + "template_magic" : { + "unidentified_name" : "Unidentified War Axe", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Shield", + "renderable": { + "glyph" : "[", + "fg" : "#00AAFF", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Shield", + "armor_class" : 1.0 + }, + "weight_lbs" : 5.0, + "base_value" : 3.0, + "initiative_penalty" : 0.5, + "vendor_category" : "armor", + "template_magic" : { + "unidentified_name" : "Unidentified Shield", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Tower Shield", + "renderable": { + "glyph" : "[", + "fg" : "#00FFFF", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Shield", + "armor_class" : 2.0 + }, + "weight_lbs" : 45.0, + "base_value" : 30.0, + "initiative_penalty" : 1.0, + "vendor_category" : "armor", + "template_magic" : { + "unidentified_name" : "Unidentified Tower Shield", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Stained Tunic", + "renderable": { + "glyph" : "[", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Torso", + "armor_class" : 0.1 + }, + "weight_lbs" : 1.0, + "base_value" : 1.0, + "initiative_penalty" : 0.1, + "vendor_category" : "junk" + }, + + { + "name" : "Torn Trousers", + "renderable": { + "glyph" : "[", + "fg" : "#00FFFF", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Legs", + "armor_class" : 0.1 + }, + "weight_lbs" : 1.0, + "base_value" : 1.0, + "initiative_penalty" : 0.1, + "vendor_category" : "junk" + }, + + { + "name" : "Old Boots", + "renderable": { + "glyph" : "[", + "fg" : "#FF9999", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Feet", + "armor_class" : 0.1 + }, + "weight_lbs" : 1.0, + "base_value" : 1.0, + "initiative_penalty" : 0.1, + "vendor_category" : "junk" + }, + + { + "name" : "Cudgel", + "renderable": { + "glyph" : "/", + "fg" : "#A52A2A", + "bg" : "#000000", + "order" : 2 + }, + "weapon" : { + "range" : "melee", + "attribute" : "Quickness", + "base_damage" : "1d4", + "hit_bonus" : 0 + }, + "weight_lbs" : 2.0, + "base_value" : 0.1, + "initiative_penalty" : 2.0, + "vendor_category" : "junk", + "template_magic" : { + "unidentified_name" : "Unidentified Cudgel", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Cloth Tunic", + "renderable": { + "glyph" : "[", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Torso", + "armor_class" : 0.1 + }, + "weight_lbs" : 1.0, + "base_value" : 1.0, + "initiative_penalty" : 0.1, + "vendor_category" : "clothes" + }, + + { + "name" : "Cloth Pants", + "renderable": { + "glyph" : "[", + "fg" : "#00FFFF", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Legs", + "armor_class" : 0.1 + }, + "weight_lbs" : 1.0, + "base_value" : 1.0, + "initiative_penalty" : 0.1, + "vendor_category" : "clothes" + }, + + { + "name" : "Leather Pants", + "renderable": { + "glyph" : "[", + "fg" : "#00FFFF", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Legs", + "armor_class" : 0.2 + }, + "weight_lbs" : 5.0, + "base_value" : 25.0, + "initiative_penalty" : 0.2, + "vendor_category" : "clothes", + "template_magic" : { + "unidentified_name" : "Unidentified Leather Pants", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Chain Leggings", + "renderable": { + "glyph" : "[", + "fg" : "#00FFFF", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Legs", + "armor_class" : 0.3 + }, + "weight_lbs" : 10.0, + "base_value" : 50.0, + "initiative_penalty" : 0.3, + "vendor_category" : "clothes", + "template_magic" : { + "unidentified_name" : "Unidentified Chain Leggings", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Drow Leggings", + "renderable": { + "glyph" : "[", + "fg" : "#00FFFF", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Legs", + "armor_class" : 0.4 + }, + "weight_lbs" : 10.0, + "base_value" : 50.0, + "initiative_penalty" : 0.1, + "vendor_category" : "clothes", + "template_magic" : { + "unidentified_name" : "Unidentified Drow Leggings", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Steel Greaves", + "renderable": { + "glyph" : "[", + "fg" : "#00FFFF", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Legs", + "armor_class" : 0.5 + }, + "weight_lbs" : 20.0, + "base_value" : 100.0, + "initiative_penalty" : 0.5, + "vendor_category" : "clothes", + "template_magic" : { + "unidentified_name" : "Unidentified Steel Greaves", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Slippers", + "renderable": { + "glyph" : "[", + "fg" : "#FF9999", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Feet", + "armor_class" : 0.1 + }, + "weight_lbs" : 1.0, + "base_value" : 1.0, + "initiative_penalty" : 0.1, + "vendor_category" : "clothes" + }, + + { + "name" : "Leather Armor", + "renderable": { + "glyph" : "[", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Torso", + "armor_class" : 1.0 + }, + "weight_lbs" : 15.0, + "base_value" : 10.0, + "initiative_penalty" : 0.5, + "vendor_category" : "clothes", + "template_magic" : { + "unidentified_name" : "Unidentified Leather Armor", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Chainmail Armor", + "renderable": { + "glyph" : "[", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Torso", + "armor_class" : 2.0 + }, + "weight_lbs" : 20.0, + "base_value" : 50.0, + "initiative_penalty" : 1.0, + "vendor_category" : "armor", + "template_magic" : { + "unidentified_name" : "Unidentified Chainmail Armor", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Drow Chain", + "renderable": { + "glyph" : "[", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Torso", + "armor_class" : 3.0 + }, + "weight_lbs" : 5.0, + "base_value" : 50.0, + "initiative_penalty" : 0.0, + "vendor_category" : "armor", + "template_magic" : { + "unidentified_name" : "Unidentified Drow Chain", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Breastplate", + "renderable": { + "glyph" : "[", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Torso", + "armor_class" : 3.0 + }, + "weight_lbs" : 25.0, + "base_value" : 100.0, + "initiative_penalty" : 2.0, + "vendor_category" : "armor", + "template_magic" : { + "unidentified_name" : "Unidentified Breastplate", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Dwarf-Steel Shirt", + "renderable": { + "glyph" : "[", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Torso", + "armor_class" : 3.0 + }, + "weight_lbs" : 5.0, + "base_value" : 500.0, + "initiative_penalty" : 0.0, + "vendor_category" : "armor", + "template_magic" : { + "unidentified_name" : "Unidentified Dwarf-Steel Shirt", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Cloth Cap", + "renderable": { + "glyph" : "[", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Head", + "armor_class" : 0.2 + }, + "weight_lbs" : 0.25, + "base_value" : 5.0, + "initiative_penalty" : 0.1, + "vendor_category" : "armor" + }, + + { + "name" : "Leather Cap", + "renderable": { + "glyph" : "[", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Head", + "armor_class" : 0.4 + }, + "weight_lbs" : 0.5, + "base_value" : 10.0, + "initiative_penalty" : 0.2, + "vendor_category" : "armor", + "template_magic" : { + "unidentified_name" : "Unidentified Leather Cap", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Chain Coif", + "renderable": { + "glyph" : "[", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Head", + "armor_class" : 1.0 + }, + "weight_lbs" : 5.0, + "base_value" : 20.0, + "initiative_penalty" : 0.5, + "vendor_category" : "armor", + "template_magic" : { + "unidentified_name" : "Unidentified Chain Coif", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Steel Helm", + "renderable": { + "glyph" : "[", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Head", + "armor_class" : 2.0 + }, + "weight_lbs" : 15.0, + "base_value" : 100.0, + "initiative_penalty" : 1.0, + "vendor_category" : "armor", + "template_magic" : { + "unidentified_name" : "Unidentified Steel Helm", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Leather Boots", + "renderable": { + "glyph" : "[", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Feet", + "armor_class" : 0.2 + }, + "weight_lbs" : 2.0, + "base_value" : 5.0, + "initiative_penalty" : 0.25, + "vendor_category" : "clothes", + "template_magic" : { + "unidentified_name" : "Unidentified Leather Boots", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Chain Boots", + "renderable": { + "glyph" : "[", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Feet", + "armor_class" : 0.3 + }, + "weight_lbs" : 3.0, + "base_value" : 10.0, + "initiative_penalty" : 0.25, + "vendor_category" : "armor", + "template_magic" : { + "unidentified_name" : "Unidentified Chain Boots", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Drow Boots", + "renderable": { + "glyph" : "[", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Feet", + "armor_class" : 0.4 + }, + "weight_lbs" : 2.0, + "base_value" : 10.0, + "initiative_penalty" : 0.1, + "vendor_category" : "armor", + "template_magic" : { + "unidentified_name" : "Unidentified Drow Boots", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Steel Boots", + "renderable": { + "glyph" : "[", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Feet", + "armor_class" : 0.5 + }, + "weight_lbs" : 5.0, + "base_value" : 10.0, + "initiative_penalty" : 0.4, + "vendor_category" : "armor", + "template_magic" : { + "unidentified_name" : "Unidentified Steel Boots", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Cloth Gloves", + "renderable": { + "glyph" : "[", + "fg" : "#FF9999", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Hands", + "armor_class" : 0.1 + }, + "weight_lbs" : 0.5, + "base_value" : 1.0, + "initiative_penalty" : 0.1, + "vendor_category" : "clothes" + }, + + { + "name" : "Leather Gloves", + "renderable": { + "glyph" : "[", + "fg" : "#FF9999", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Hands", + "armor_class" : 0.2 + }, + "weight_lbs" : 1.0, + "base_value" : 1.0, + "initiative_penalty" : 0.1, + "vendor_category" : "clothes", + "template_magic" : { + "unidentified_name" : "Unidentified Leather Gloves", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Chain Gloves", + "renderable": { + "glyph" : "[", + "fg" : "#FF9999", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Hands", + "armor_class" : 0.3 + }, + "weight_lbs" : 2.0, + "base_value" : 10.0, + "initiative_penalty" : 0.2, + "vendor_category" : "clothes", + "template_magic" : { + "unidentified_name" : "Unidentified Chain Gloves", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Steel Gloves", + "renderable": { + "glyph" : "[", + "fg" : "#FF9999", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Hands", + "armor_class" : 0.5 + }, + "weight_lbs" : 5.0, + "base_value" : 10.0, + "initiative_penalty" : 0.3, + "vendor_category" : "clothes", + "template_magic" : { + "unidentified_name" : "Unidentified Gauntlets", + "bonus_min" : 1, + "bonus_max" : 5, + "include_cursed" : true + } + }, + + { + "name" : "Gauntlets of Ogre Power", + "renderable": { + "glyph" : "[", + "fg" : "#00FF00", + "bg" : "#000000", + "order" : 2 + }, + "wearable" : { + "slot" : "Hands", + "armor_class" : 0.1 + }, + "weight_lbs" : 1.0, + "base_value" : 300.0, + "initiative_penalty" : 0.0, + "vendor_category" : "armor", + "magic" : { "class" : "common", "naming" : "Unidentified Gauntlets" }, + "attributes" : { "might" : 5 } + }, + + { + "name" : "Rod of Fireballs", + "renderable": { + "glyph" : "/", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { + "ranged" : "6", + "damage" : "20", + "area_of_effect" : "3", + "particle" : "▓;#FFA500;200.0" + }, + "charges" : 5 + }, + "weight_lbs" : 0.5, + "base_value" : 500.0, + "vendor_category" : "alchemy", + "magic" : { "class" : "common", "naming" : "Unidentified Rod" } + }, + + { + "name" : "Rod of Venom", + "renderable": { + "glyph" : "/", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 2 + }, + "consumable" : { + "effects" : { + "ranged" : "6", + "damage_over_time" : "1", + "particle_line" : "▓;#00FF00;200.0" + }, + "charges" : 5 + }, + "weight_lbs" : 0.5, + "base_value" : 500.0, + "vendor_category" : "alchemy", + "magic" : { "class" : "common", "naming" : "Unidentified Rod" } + } +], + +"mobs" : [ + { + "name" : "Barkeep", + "renderable": { + "glyph" : "☻", + "fg" : "#EE82EE", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "static", + "attributes" : { + "intelligence" : 13 + }, + "skills" : { + "Melee" : 2 + }, + "equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ], + "faction" : "Townsfolk", + "gold" : "2d6", + "vendor" : [ "food" ] + }, + + { + "name" : "Shady Salesman", + "renderable": { + "glyph" : "h", + "fg" : "#EE82EE", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "static", + "attributes" : {}, + "equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ], + "faction" : "Townsfolk", + "gold" : "2d6", + "vendor" : [ "junk" ] + }, + + { + "name" : "Patron", + "renderable": { + "glyph" : "☻", + "fg" : "#AAAAAA", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "random", + "quips" : [ "Quiet down, it's too early!", "Oh my, I drank too much.", "Still saving the world, eh?" ], + "attributes" : {}, + "equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ], + "faction" : "Townsfolk", + "gold" : "1d4" + }, + + { + "name" : "Priest", + "renderable": { + "glyph" : "☻", + "fg" : "#EE82EE", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "static", + "attributes" : {}, + "equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ], + "faction" : "Townsfolk", + "gold" : "2d6" + }, + + { + "name" : "Parishioner", + "renderable": { + "glyph" : "☻", + "fg" : "#AAAAAA", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "random", + "quips" : [ "Great to see a new face here!", "I hear there's going to be a good sermon on tea", "Want some cake?" ], + "attributes" : {}, + "equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ], + "faction" : "Townsfolk", + "gold" : "1d4" + }, + + { + "name" : "Blacksmith", + "renderable": { + "glyph" : "☻", + "fg" : "#EE82EE", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "static", + "attributes" : {}, + "equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ], + "faction" : "Townsfolk", + "gold" : "2d6", + "vendor" : [ "armor", "weapon" ] + }, + + { + "name" : "Clothier", + "renderable": { + "glyph" : "☻", + "fg" : "#EE82EE", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "static", + "attributes" : {}, + "equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ], + "faction" : "Townsfolk", + "gold" : "2d6", + "vendor" : [ "clothes" ] + }, + + { + "name" : "Alchemist", + "renderable": { + "glyph" : "☻", + "fg" : "#EE82EE", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "static", + "attributes" : {}, + "equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ], + "faction" : "Townsfolk", + "gold" : "2d6", + "vendor" : [ "alchemy" ] + }, + + { + "name" : "Mom", + "renderable": { + "glyph" : "☻", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "static", + "quips" : [ "Hello, dear", "Off saving the world again?", "Be careful in the dungeon!", "Your father would be so proud, were he here." ], + "attributes" : {}, + "equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ], + "faction" : "Townsfolk", + "gold" : "2d6" + }, + + { + "name" : "Peasant", + "renderable": { + "glyph" : "☻", + "fg" : "#999999", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "random_waypoint", + "quips" : [ "Why are you in my house?" ], + "attributes" : {}, + "equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ], + "faction" : "Townsfolk", + "gold" : "1d2" + }, + + { + "name" : "Dock Worker", + "renderable": { + "glyph" : "☻", + "fg" : "#999999", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "random_waypoint", + "quips" : [ "Lovely day, eh?", "Nice weather", "Hello" ], + "attributes" : {}, + "equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ], + "faction" : "Townsfolk", + "gold" : "1d2" + }, + + { + "name" : "Fisher", + "renderable": { + "glyph" : "☻", + "fg" : "#999999", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "random_waypoint", + "quips" : [ "They're biting today!", "I caught something, but it wasn't a fish!", "Looks like rain" ], + "attributes" : {}, + "equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ], + "faction" : "Townsfolk", + "gold" : "1d2" + }, + + { + "name" : "Wannabe Pirate", + "renderable": { + "glyph" : "☻", + "fg" : "#aa9999", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "random_waypoint", + "quips" : [ "Arrr", "Grog!", "Booze!" ], + "attributes" : {}, + "equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ], + "faction" : "Townsfolk", + "gold" : "2d6" + }, + + { + "name" : "Drunk", + "renderable": { + "glyph" : "☻", + "fg" : "#aa9999", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "random", + "quips" : [ "Hic", "Need... more... booze!", "Spare a copper?" ], + "attributes" : {}, + "equipped" : [ "Cudgel", "Cloth Tunic", "Cloth Pants", "Slippers" ], + "faction" : "Townsfolk", + "gold" : "1d2" + }, + + { + "name" : "Rat", + "renderable": { + "glyph" : "r", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "static", + "attributes" : { + "might" : 7, + "fitness" : 3 + }, + "skills" : { + "Melee" : -1, + "Defense" : -1 + }, + "natural" : { + "armor_class" : 11, + "attacks" : [ + { "name" : "bite", "hit_bonus" : 0, "damage" : "1d4" } + ] + }, + "faction" : "Hungry Rodents" + }, + + { + "name" : "Mangy Wolf", + "renderable": { + "glyph" : "w", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "static", + "attributes" : { + "might" : 3, + "fitness" : 3 + }, + "skills" : { + "Melee" : -1, + "Defense" : -1 + }, + "natural" : { + "armor_class" : 12, + "attacks" : [ + { "name" : "bite", "hit_bonus" : 0, "damage" : "1d6" } + ] + }, + "loot_table" : "Animal", + "faction" : "Carnivores" + }, + + { + "name" : "Fox", + "renderable": { + "glyph" : "f", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "static", + "attributes" : { + "might" : 3, + "fitness" : 3 + }, + "skills" : { + "Melee" : -1, + "Defense" : -1 + }, + "natural" : { + "armor_class" : 11, + "attacks" : [ + { "name" : "bite", "hit_bonus" : 0, "damage" : "1d4" } + ] + }, + "loot_table" : "Animal", + "faction" : "Carnivores" + }, + + { + "name" : "Deer", + "renderable": { + "glyph" : "d", + "fg" : "#FFFF00", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "random", + "attributes" : { + "might" : 3, + "fitness" : 3 + }, + "skills" : { + "Melee" : -1, + "Defense" : -1 + }, + "natural" : { + "armor_class" : 11, + "attacks" : [ + { "name" : "bite", "hit_bonus" : 0, "damage" : "1d4" } + ] + }, + "loot_table" : "Animal", + "faction" : "Herbivores" + }, + + { + "name" : "Bandit", + "renderable": { + "glyph" : "☻", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 6, + "movement" : "random_waypoint", + "quips" : [ "Stand and deliver!", "Alright, hand it over" ], + "attributes" : {}, + "equipped" : [ "Dagger", "Shield", "Leather Armor", "Leather Boots" ], + "light" : { + "range" : 6, + "color" : "#FFFF55" + }, + "faction" : "Bandits", + "gold" : "1d6" + }, + + { + "name" : "Bandit Archer", + "renderable": { + "glyph" : "☻", + "fg" : "#FF5500", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 6, + "movement" : "random_waypoint", + "quips" : [ "Stand and deliver!", "Alright, hand it over" ], + "attributes" : {}, + "equipped" : [ "Shortbow", "Leather Armor", "Leather Boots" ], + "light" : { + "range" : 6, + "color" : "#FFFF55" + }, + "faction" : "Bandits", + "gold" : "1d6" + }, + + { + "name" : "Dark Elf", + "renderable": { + "glyph" : "e", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "random_waypoint", + "attributes" : {}, + "equipped" : [ "Hand Crossbow", "Scimitar", "Buckler", "Drow Chain", "Drow Leggings", "Drow Boots" ], + "faction" : "DarkElf", + "gold" : "3d6", + "level" : 6 + }, + + { + "name" : "Arbat Dark Elf", + "renderable": { + "glyph" : "e", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "random_waypoint", + "attributes" : {}, + "equipped" : [ "Scimitar +1", "Buckler", "Drow Chain", "Drow Leggings", "Drow Boots" ], + "faction" : "DarkElfA", + "gold" : "3d6", + "level" : 6 + }, + + { + "name" : "Arbat Dark Elf Leader", + "renderable": { + "glyph" : "E", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "random_waypoint", + "attributes" : {}, + "equipped" : [ "Scimitar +2", "Buckler +1", "Drow Chain", "Drow Leggings", "Drow Boots" ], + "faction" : "DarkElfA", + "gold" : "3d6", + "level" : 7 + }, + + { + "name" : "Arbat Orc Slave", + "renderable": { + "glyph" : "o", + "fg" : "#FFAAAA", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "static", + "attributes" : {}, + "faction" : "DarkElfA", + "gold" : "1d8" + }, + + { + "name" : "Barbo Dark Elf", + "renderable": { + "glyph" : "e", + "fg" : "#FF9900", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "random_waypoint", + "attributes" : {}, + "equipped" : [ "Hand Crossbow +1", "Dagger", "Buckler", "Drow Chain", "Drow Leggings", "Drow Boots" ], + "faction" : "DarkElfB", + "gold" : "3d6", + "level" : 6 + }, + + { + "name" : "Barbo Goblin Archer", + "renderable": { + "glyph" : "g", + "fg" : "#FF9900", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "static", + "attributes" : {}, + "faction" : "Cave Goblins", + "gold" : "1d6", + "equipped" : [ "Shortbow", "Leather Armor", "Leather Boots" ] + }, + + { + "name" : "Cirro Dark Elf", + "renderable": { + "glyph" : "e", + "fg" : "#FF00FF", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "random_waypoint", + "attributes" : {}, + "equipped" : [ "Hand Crossbow", "Scimitar", "Buckler", "Drow Chain", "Drow Leggings", "Drow Boots" ], + "faction" : "DarkElfC", + "gold" : "3d6", + "level" : 7 + }, + + { + "name" : "Cirro Dark Priestess", + "renderable": { + "glyph" : "E", + "fg" : "#FF00FF", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "random_waypoint", + "attributes" : {}, + "equipped" : [ "Hand Crossbow", "Scimitar", "Buckler", "Drow Chain", "Drow Leggings", "Drow Boots" ], + "faction" : "DarkElfC", + "gold" : "3d6", + "level" : 8, + "abilities" : [ + { "spell" : "Web", "chance" : 0.2, "range" : 6.0, "min_range" : 3.0 } + ] + }, + + { + "name" : "Cirro Spider", + "level" : 3, + "attributes" : {}, + "renderable": { + "glyph" : "s", + "fg" : "#FF00FF", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 6, + "movement" : "static", + "natural" : { + "armor_class" : 12, + "attacks" : [ + { "name" : "bite", "hit_bonus" : 1, "damage" : "1d12" } + ] + }, + "abilities" : [ + { "spell" : "Web", "chance" : 0.2, "range" : 6.0, "min_range" : 3.0 } + ], + "faction" : "DarkElfC" + }, + + { + "name" : "Orc", + "renderable": { + "glyph" : "o", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "static", + "attributes" : {}, + "faction" : "Cave Goblins", + "gold" : "1d8" + }, + + { + "name" : "Orc Leader", + "renderable": { + "glyph" : "O", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "static", + "attributes" : {}, + "faction" : "Cave Goblins", + "gold" : "3d8", + "equipped" : [ "Battleaxe", "Tower Shield", "Leather Armor", "Leather Boots" ], + "level" : 2 + }, + + { + "name" : "Goblin", + "renderable": { + "glyph" : "g", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "static", + "attributes" : {}, + "faction" : "Cave Goblins", + "gold" : "1d6" + }, + + { + "name" : "Goblin Archer", + "renderable": { + "glyph" : "g", + "fg" : "#FFFF00", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "static", + "attributes" : {}, + "faction" : "Cave Goblins", + "gold" : "1d6", + "equipped" : [ "Shortbow", "Leather Armor", "Leather Boots" ] + }, + + { + "name" : "Kobold", + "renderable": { + "glyph" : "k", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "static", + "attributes" : {}, + "faction" : "Cave Goblins", + "gold" : "1d4" + }, + + { + "name" : "Bat", + "renderable": { + "glyph" : "b", + "fg" : "#995555", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 6, + "movement" : "random", + "attributes" : { + "might" : 3, + "fitness" : 3 + }, + "skills" : { + "Melee" : -1, + "Defense" : -1 + }, + "natural" : { + "armor_class" : 11, + "attacks" : [ + { "name" : "bite", "hit_bonus" : 0, "damage" : "1d4" } + ] + }, + "faction" : "Herbivores" + }, + + { + "name" : "Large Spider", + "level" : 2, + "attributes" : {}, + "renderable": { + "glyph" : "s", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 6, + "movement" : "static", + "natural" : { + "armor_class" : 12, + "attacks" : [ + { "name" : "bite", "hit_bonus" : 1, "damage" : "1d12" } + ] + }, + "abilities" : [ + { "spell" : "Web", "chance" : 0.2, "range" : 6.0, "min_range" : 3.0 } + ], + "faction" : "Carnivores" + }, + + { + "name" : "Gelatinous Cube", + "level" : 2, + "attributes" : {}, + "renderable": { + "glyph" : "▄", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "static", + "natural" : { + "armor_class" : 12, + "attacks" : [ + { "name" : "engulf", "hit_bonus" : 0, "damage" : "1d8" } + ] + }, + "light" : { + "range" : 4, + "color" : "#550000" + } + }, + + { + "name" : "Dragon Wyrmling", + "renderable": { + "glyph" : "d", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 12, + "movement" : "random_waypoint", + "attributes" : { + "might" : 3, + "fitness" : 3 + }, + "skills" : { + "Melee" : 15, + "Defense" : 14 + }, + "natural" : { + "armor_class" : 15, + "attacks" : [ + { "name" : "bite", "hit_bonus" : 4, "damage" : "1d10+2" } + ] + }, + "loot_table" : "Wyrms", + "faction" : "Wyrm", + "level" : 3, + "gold" : "3d6" + }, + + { + "name" : "Black Dragon", + "renderable": { + "glyph" : "D", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1, + "x_size" : 2, + "y_size" : 2 + }, + "blocks_tile" : true, + "vision_range" : 12, + "movement" : "static", + "attributes" : { + "might" : 13, + "fitness" : 13 + }, + "skills" : { + "Melee" : 18, + "Defense" : 16 + }, + "natural" : { + "armor_class" : 17, + "attacks" : [ + { "name" : "bite", "hit_bonus" : 4, "damage" : "1d10+2" }, + { "name" : "left_claw", "hit_bonus" : 2, "damage" : "1d10" }, + { "name" : "right_claw", "hit_bonus" : 2, "damage" : "1d10" } + ] + }, + "loot_table" : "Wyrms", + "faction" : "Wyrm", + "level" : 6, + "gold" : "20d10", + "abilities" : [ + { "spell" : "Acid Breath", "chance" : 0.2, "range" : 8.0, "min_range" : 2.0 } + ] + }, + + { + "name" : "Lizardman", + "renderable": { + "glyph" : "l", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "random_waypoint", + "attributes" : {}, + "faction" : "Wyrm", + "gold" : "1d12", + "level" : 2 + }, + + { + "name" : "Giant Lizard", + "renderable": { + "glyph" : "l", + "fg" : "#FFFF00", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 4, + "movement" : "random", + "attributes" : {}, + "faction" : "Wyrm", + "level" : 2, + "loot_table" : "Animal" + }, + + { + "name" : "Rock Golem", + "renderable": { + "glyph" : "g", + "fg" : "#AAAAAA", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 6, + "movement" : "random_waypoint", + "attributes" : {}, + "faction" : "Dwarven Remnant", + "level" : 3 + }, + + { + "name" : "Firecap Mushroom", + "renderable": { + "glyph" : "♠", + "fg" : "#FFAA50", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 3, + "movement" : "static", + "attributes" : {}, + "faction" : "Fungi", + "level" : 1, + "abilities" : [ + { "spell" : "Explode", "chance" : 1.0, "range" : 3.0, "min_range" : 0.0 } + ], + "on_death" : [ + { "spell" : "Explode", "chance" : 1.0, "range" : 0.0, "min_range" : 0.0 } + ] + }, + + { + "name" : "Sporecap Mushroom", + "renderable": { + "glyph" : "♠", + "fg" : "#00AAFF", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 3, + "movement" : "static", + "attributes" : {}, + "faction" : "Fungi", + "level" : 1, + "abilities" : [ + { "spell" : "ConfusionCloud", "chance" : 1.0, "range" : 3.0, "min_range" : 0.0 } + ], + "on_death" : [ + { "spell" : "ConfusionCloud", "chance" : 1.0, "range" : 0.0, "min_range" : 0.0 } + ] + }, + + { + "name" : "Deathcap Mushroom", + "renderable": { + "glyph" : "♠", + "fg" : "#55FF55", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 3, + "movement" : "static", + "attributes" : {}, + "faction" : "Fungi", + "level" : 1, + "abilities" : [ + { "spell" : "PoisonCloud", "chance" : 1.0, "range" : 3.0, "min_range" : 0.0 } + ], + "on_death" : [ + { "spell" : "PoisonCloud", "chance" : 1.0, "range" : 0.0, "min_range" : 0.0 } + ] + }, + + { + "name" : "Fungus Man", + "renderable": { + "glyph" : "f", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "random_waypoint", + "attributes" : {}, + "faction" : "Fungi", + "gold" : "2d8", + "level" : 4, + "loot_table" : "Animal" + }, + + { + "name" : "Spore Zombie", + "renderable": { + "glyph" : "z", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 8, + "movement" : "random_waypoint", + "attributes" : {}, + "faction" : "Fungi", + "gold" : "2d8", + "level" : 5 + }, + + { + "name" : "Fungal Beast", + "renderable": { + "glyph" : "F", + "fg" : "#995555", + "bg" : "#000000", + "order" : 1 + }, + "blocks_tile" : true, + "vision_range" : 6, + "movement" : "random", + "attributes" : {}, + "natural" : { + "armor_class" : 11, + "attacks" : [ + { "name" : "bite", "hit_bonus" : 0, "damage" : "1d4" } + ] + }, + "faction" : "Fungi" + }, + + { + "name" : "Vokoth", + "renderable": { + "glyph" : "&", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 1, + "x_size" : 2, + "y_size" : 2 + }, + "blocks_tile" : true, + "vision_range" : 6, + "movement" : "static", + "attributes" : { + "might" : 13, + "fitness" : 13 + }, + "skills" : { + "Melee" : 18, + "Defense" : 16 + }, + "natural" : { + "armor_class" : 17, + "attacks" : [ + { "name" : "whip", "hit_bonus" : 4, "damage" : "1d10+2" } + ] + }, + "loot_table" : "Wyrms", + "faction" : "Wyrm", + "level" : 8, + "gold" : "20d10", + "abilities" : [] + } +], + +"props" : [ + { + "name" : "Bear Trap", + "renderable": { + "glyph" : "^", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : true, + "entry_trigger" : { + "effects" : { + "damage" : "6", + "single_activation" : "1" + } + } + }, + + { + "name" : "Stonefall Trap", + "renderable": { + "glyph" : "^", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : true, + "entry_trigger" : { + "effects" : { + "damage" : "12", + "single_activation" : "1" + } + } + }, + + { + "name" : "Landmine", + "renderable": { + "glyph" : "^", + "fg" : "#FF0000", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : true, + "entry_trigger" : { + "effects" : { + "damage" : "18", + "single_activation" : "1", + "area_of_effect" : "3", + "particle" : "▓;#FFA500;200.0" + } + } + }, + + { + "name" : "Door", + "renderable": { + "glyph" : "+", + "fg" : "#805A46", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false, + "blocks_tile" : true, + "blocks_visibility" : true, + "door_open" : true + }, + + { + "name" : "Keg", + "renderable": { + "glyph" : "φ", + "fg" : "#AAAAAA", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false + }, + + { + "name" : "Table", + "renderable": { + "glyph" : "╦", + "fg" : "#AAAAAA", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false + }, + + { + "name" : "Chair", + "renderable": { + "glyph" : "└", + "fg" : "#AAAAAA", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false + }, + + { + "name" : "Altar", + "renderable": { + "glyph" : "╫", + "fg" : "#5555FF", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false, + "entry_trigger" : { + "effects" : { + "provides_healing" : "100" + } + } + }, + + { + "name" : "Candle", + "renderable": { + "glyph" : "Ä", + "fg" : "#FFA500", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false + }, + + { + "name" : "Anvil", + "renderable": { + "glyph" : "╔", + "fg" : "#AAAAAA", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false + }, + + { + "name" : "Water Trough", + "renderable": { + "glyph" : "•", + "fg" : "#5555FF", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false + }, + + { + "name" : "Weapon Rack", + "renderable": { + "glyph" : "π", + "fg" : "#FFD700", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false + }, + + { + "name" : "Armor Stand", + "renderable": { + "glyph" : "⌠", + "fg" : "#FFFFFF", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false + }, + + { + "name" : "Chemistry Set", + "renderable": { + "glyph" : "δ", + "fg" : "#00FFFF", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false + }, + + { + "name" : "Dead Thing", + "renderable": { + "glyph" : "☻", + "fg" : "#AA0000", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false + }, + + { + "name" : "Cabinet", + "renderable": { + "glyph" : "∩", + "fg" : "#805A46", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false + }, + + { + "name" : "Bed", + "renderable": { + "glyph" : "8", + "fg" : "#805A46", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false + }, + + { + "name" : "Loom", + "renderable": { + "glyph" : "≡", + "fg" : "#805A46", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false + }, + + { + "name" : "Hide Rack", + "renderable": { + "glyph" : "π", + "fg" : "#805A46", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false + }, + + { + "name" : "Watch Fire", + "renderable": { + "glyph" : "☼", + "fg" : "#FFFF55", + "bg" : "#000000", + "order" : 2 + }, + "hidden" : false, + "light" : { + "range" : 6, + "color" : "#FFFF55" + }, + "entry_trigger" : { + "effects" : { + "damage" : "6" + } + } + } +], + +"spells" : [ + { + "name" : "Zap", + "mana_cost" : 1, + "effects" : { + "ranged" : "6", + "damage" : "5", + "particle_line" : "▓;#00FFFF;400.0" + } + }, + + { + "name" : "Web", + "mana_cost" : 2, + "effects" : { + "ranged" : "6", + "slow" : "10", + "area_of_effect" : "3", + "particle_line" : "☼;#FFFFFF;400.0" + } + }, + + { + "name" : "Venom", + "mana_cost" : 2, + "effects" : { + "ranged" : "6", + "damage_over_time" : "4", + "particle_line" : "▓;#00FF00;400.0" + } + }, + + { + "name" : "Acid Breath", + "mana_cost" : 2, + "effects" : { + "ranged" : "6", + "damage" : "10", + "area_of_effect" : "3", + "particle" : "☼;#00FF00;400.0" + } + }, + + { + "name" : "Explode", + "mana_cost" : 1, + "effects" : { + "ranged" : "3", + "damage" : "20", + "area_of_effect" : "3", + "particle" : "▒;#FFAA50;400.0", + "single_activation" : "1", + "target_self" : "1" + } + }, + + { + "name" : "ConfusionCloud", + "mana_cost" : 1, + "effects" : { + "ranged" : "3", + "confusion" : "4", + "area_of_effect" : "3", + "particle" : "?;#FFFF00;400.0", + "single_activation" : "1", + "target_self" : "1" + } + }, + + { + "name" : "PoisonCloud", + "mana_cost" : 1, + "effects" : { + "ranged" : "3", + "damage_over_time" : "4", + "area_of_effect" : "3", + "particle" : "*;#00FF00;400.0", + "single_activation" : "1", + "target_self" : "1" + } + } +], + +"weapon_traits" : [ + { + "name" : "Venomous", + "effects" : { "damage_over_time" : "2" } + }, + { + "name" : "Dazzling", + "effects" : { "confusion" : "2" } + } +] +} \ No newline at end of file diff --git a/chapter-75-darkplaza/src/components.rs b/chapter-75-darkplaza/src/components.rs new file mode 100644 index 00000000..42ba281b --- /dev/null +++ b/chapter-75-darkplaza/src/components.rs @@ -0,0 +1,494 @@ +use specs::prelude::*; +use specs_derive::*; +use rltk::{RGB, Point}; +use serde::{Serialize, Deserialize}; +use specs::saveload::{Marker, ConvertSaveload}; +use specs::error::NoError; +use std::collections::HashMap; + +#[derive(Component, ConvertSaveload, Clone)] +pub struct Position { + pub x: i32, + pub y: i32, +} + +#[derive(Component, ConvertSaveload, Clone)] +pub struct TileSize { + pub x: i32, + pub y: i32, +} + +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct OtherLevelPosition { + pub x: i32, + pub y: i32, + pub depth: i32 +} + +#[derive(Component, ConvertSaveload, Clone)] +pub struct Renderable { + pub glyph: rltk::FontCharType, + pub fg: RGB, + pub bg: RGB, + pub render_order : i32 +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Player {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Target {} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct KnownSpell { + pub display_name : String, + pub mana_cost : i32 +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct KnownSpells { + pub spells : Vec +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SpecialAbility { + pub spell : String, + pub chance : f32, + pub range : f32, + pub min_range : f32 +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct SpecialAbilities { + pub abilities : Vec +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct OnDeath { + pub abilities : Vec +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct AlwaysTargetsSelf {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct SpellTemplate { + pub mana_cost : i32 +} + +#[derive(Component, ConvertSaveload, Clone)] +pub struct Viewshed { + pub visible_tiles : Vec, + pub range : i32, + pub dirty : bool +} + +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct LightSource { + pub color : RGB, + pub range: i32 +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Initiative { + pub current : i32 +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Vendor { + pub categories : Vec +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct MyTurn {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Faction { + pub name : String +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct ApplyMove { + pub dest_idx : usize +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct ApplyTeleport { + pub dest_x : i32, + pub dest_y : i32, + pub dest_depth : i32 +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct WantsToApproach { + pub idx : i32 +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct WantsToFlee { + pub indices : Vec +} + +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)] +pub enum Movement { + Static, + Random, + RandomWaypoint{ path : Option> } +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct MoveMode { + pub mode : Movement +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct Name { + pub name : String +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct ObfuscatedName { + pub name : String +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct IdentifiedItem { + pub name : String +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct BlocksTile {} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Pool { + pub max: i32, + pub current: i32 +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Pools { + pub hit_points : Pool, + pub mana : Pool, + pub xp : i32, + pub level : i32, + pub total_weight : f32, + pub total_initiative_penalty : f32, + pub gold : f32, + pub god_mode : bool +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Attribute { + pub base : i32, + pub modifiers : i32, + pub bonus : i32 +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Attributes { + pub might : Attribute, + pub fitness : Attribute, + pub quickness : Attribute, + pub intelligence : Attribute +} + +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)] +pub enum Skill { Melee, Defense, Magic } + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Skills { + pub skills : HashMap +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct WantsToMelee { + pub target : Entity +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct WantsToShoot { + pub target : Entity +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct Chasing { + pub target : Entity +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct LootTable { + pub table : String +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct EquipmentChanged {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Item { + pub initiative_penalty : f32, + pub weight_lbs : f32, + pub base_value : f32 +} + +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)] +pub enum MagicItemClass { Common, Rare, Legendary } + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct MagicItem { + pub class : MagicItemClass +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct AttributeBonus { + pub might : Option, + pub fitness : Option, + pub quickness : Option, + pub intelligence : Option +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct CursedItem {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Consumable { + pub max_charges : i32, + pub charges : i32 +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct ProvidesRemoveCurse {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct ProvidesIdentification {} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct Ranged { + pub range : i32 +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct InflictsDamage { + pub damage : i32 +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct AreaOfEffect { + pub radius : i32 +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Confusion {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Slow { + pub initiative_penalty : f32 +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct DamageOverTime { + pub damage : i32 +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Duration { + pub turns : i32 +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct StatusEffect { + pub target : Entity +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct ProvidesHealing { + pub heal_amount : i32 +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct ProvidesMana { + pub mana_amount : i32 +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct TeachesSpell { + pub spell : String +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct BlocksVisibility {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Door { + pub open: bool +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct InBackpack { + pub owner : Entity +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct WantsToPickupItem { + pub collected_by : Entity, + pub item : Entity +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct WantsToUseItem { + pub item : Entity, + pub target : Option +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct WantsToCastSpell { + pub spell : Entity, + pub target : Option +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct WantsToDropItem { + pub item : Entity +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct WantsToRemoveItem { + pub item : Entity +} + +#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] +pub enum EquipmentSlot { Melee, Shield, Head, Torso, Legs, Feet, Hands } + +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct Equippable { + pub slot : EquipmentSlot +} + +#[derive(Component, ConvertSaveload, Clone)] +pub struct Equipped { + pub owner : Entity, + pub slot : EquipmentSlot +} + +#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] +pub enum WeaponAttribute { Might, Quickness } + +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct Weapon { + pub range : Option, + pub attribute : WeaponAttribute, + pub damage_n_dice : i32, + pub damage_die_type : i32, + pub damage_bonus : i32, + pub hit_bonus : i32, + pub proc_chance : Option, + pub proc_target : Option, +} + +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct Wearable { + pub armor_class : f32, + pub slot : EquipmentSlot +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct NaturalAttack { + pub name : String, + pub damage_n_dice : i32, + pub damage_die_type : i32, + pub damage_bonus : i32, + pub hit_bonus : i32 +} + +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct NaturalAttackDefense { + pub armor_class : Option, + pub attacks : Vec +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct ParticleAnimation { + pub step_time : f32, + pub path : Vec, + pub current_step : usize, + pub timer : f32 +} + +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct ParticleLifetime { + pub lifetime_ms : f32, + pub animation : Option +} + +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct SpawnParticleLine { + pub glyph : rltk::FontCharType, + pub color : RGB, + pub lifetime_ms : f32 +} + +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct SpawnParticleBurst { + pub glyph : rltk::FontCharType, + pub color : RGB, + pub lifetime_ms : f32 +} + +#[derive(Serialize, Deserialize, Copy, Clone, PartialEq)] +pub enum HungerState { WellFed, Normal, Hungry, Starving } + +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct HungerClock { + pub state : HungerState, + pub duration : i32 +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct ProvidesFood {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct MagicMapper {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct TownPortal {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct TeleportTo { + pub x: i32, + pub y: i32, + pub depth: i32, + pub player_only : bool +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Hidden {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct EntryTrigger {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct EntityMoved {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct SingleActivation {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Quips { + pub available : Vec +} + +// Serialization helper code. We need to implement ConvertSaveLoad for each type that contains an +// Entity. + +pub struct SerializeMe; + +// Special component that exists to help serialize the game data +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct SerializationHelper { + pub map : super::map::Map, +} +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct DMSerializationHelper { + pub map : super::map::MasterDungeonMap, + pub log : Vec>, + pub events : HashMap +} diff --git a/chapter-75-darkplaza/src/damage_system.rs b/chapter-75-darkplaza/src/damage_system.rs new file mode 100644 index 00000000..70280329 --- /dev/null +++ b/chapter-75-darkplaza/src/damage_system.rs @@ -0,0 +1,129 @@ +use specs::prelude::*; +use super::{Pools, Player, Name, RunState, Position, + InBackpack, Equipped, LootTable}; + +pub fn delete_the_dead(ecs : &mut World) { + let mut dead : Vec = Vec::new(); + // Using a scope to make the borrow checker happy + { + let combat_stats = ecs.read_storage::(); + let players = ecs.read_storage::(); + let names = ecs.read_storage::(); + let entities = ecs.entities(); + for (entity, stats) in (&entities, &combat_stats).join() { + if stats.hit_points.current < 1 { + let player = players.get(entity); + match player { + None => { + let victim_name = names.get(entity); + if let Some(victim_name) = victim_name { + crate::gamelog::Logger::new() + .color(rltk::RED) + .append(&victim_name.name) + .append("is dead!") + .log(); + } + dead.push(entity) + } + Some(_) => { + let mut runstate = ecs.write_resource::(); + *runstate = RunState::GameOver; + } + } + } + } + } + + // Drop everything held by dead people + let mut to_spawn : Vec<(String, Position)> = Vec::new(); + { // To avoid keeping hold of borrowed entries, use a scope + let mut to_drop : Vec<(Entity, Position)> = Vec::new(); + let entities = ecs.entities(); + let mut equipped = ecs.write_storage::(); + let mut carried = ecs.write_storage::(); + let mut positions = ecs.write_storage::(); + let loot_tables = ecs.read_storage::(); + for victim in dead.iter() { + let pos = positions.get(*victim); + for (entity, equipped) in (&entities, &equipped).join() { + if equipped.owner == *victim { + // Drop their stuff + if let Some(pos) = pos { + to_drop.push((entity, pos.clone())); + } + } + } + for (entity, backpack) in (&entities, &carried).join() { + if backpack.owner == *victim { + // Drop their stuff + if let Some(pos) = pos { + to_drop.push((entity, pos.clone())); + } + } + } + + if let Some(table) = loot_tables.get(*victim) { + let drop_finder = crate::raws::get_item_drop( + &crate::raws::RAWS.lock().unwrap(), + &table.table + ); + if let Some(tag) = drop_finder { + if let Some(pos) = pos { + to_spawn.push((tag, pos.clone())); + } + } + } + } + + for drop in to_drop.iter() { + equipped.remove(drop.0); + carried.remove(drop.0); + positions.insert(drop.0, drop.1.clone()).expect("Unable to insert position"); + } + } + + { + for drop in to_spawn.iter() { + crate::raws::spawn_named_item( + &crate::raws::RAWS.lock().unwrap(), + ecs, + &drop.0, + crate::raws::SpawnType::AtPosition{x : drop.1.x, y: drop.1.y} + ); + } + } + + // Fire death events + use crate::effects::*; + use crate::Map; + use crate::components::{OnDeath, AreaOfEffect}; + for victim in dead.iter() { + let death_effects = ecs.read_storage::(); + if let Some(death_effect) = death_effects.get(*victim) { + for effect in death_effect.abilities.iter() { + if crate::rng::roll_dice(1,100) <= (effect.chance * 100.0) as i32 { + let map = ecs.fetch::(); + if let Some(pos) = ecs.read_storage::().get(*victim) { + let spell_entity = crate::raws::find_spell_entity(ecs, &effect.spell).unwrap(); + let tile_idx = map.xy_idx(pos.x, pos.y); + let target = + if let Some(aoe) = ecs.read_storage::().get(spell_entity) { + Targets::Tiles { tiles : aoe_tiles(&map, rltk::Point::new(pos.x, pos.y), aoe.radius) } + } else { + Targets::Tile{ tile_idx : tile_idx as i32 } + }; + add_effect( + None, + EffectType::SpellUse{ spell: crate::raws::find_spell_entity( ecs, &effect.spell ).unwrap() }, + target + ); + } + } + } + } + } + + for victim in dead { + ecs.delete_entity(victim).expect("Unable to delete"); + } +} diff --git a/chapter-75-darkplaza/src/effects/damage.rs b/chapter-75-darkplaza/src/effects/damage.rs new file mode 100644 index 00000000..e43c4c93 --- /dev/null +++ b/chapter-75-darkplaza/src/effects/damage.rs @@ -0,0 +1,240 @@ +use specs::prelude::*; +use super::*; +use crate::components::{Pools, Player, Attributes, Confusion, SerializeMe, Duration, StatusEffect, + Name, EquipmentChanged, Slow, DamageOverTime, Skills }; +use crate::map::Map; +use crate::gamesystem::{player_hp_at_level, mana_at_level}; +use specs::saveload::{MarkedBuilder, SimpleMarker}; + +pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) { + let mut pools = ecs.write_storage::(); + let player_entity = ecs.fetch::(); + if let Some(pool) = pools.get_mut(target) { + if !pool.god_mode { + if let Some(creator) = damage.creator { + if creator == target { + return; + } + } + if let EffectType::Damage{amount} = damage.effect_type { + pool.hit_points.current -= amount; + add_effect(None, EffectType::Bloodstain, Targets::Single{target}); + add_effect(None, + EffectType::Particle{ + glyph: rltk::to_cp437('‼'), + fg : rltk::RGB::named(rltk::ORANGE), + bg : rltk::RGB::named(rltk::BLACK), + lifespan: 200.0 + }, + Targets::Single{target} + ); + if target == *player_entity { + crate::gamelog::record_event("Damage Taken", amount); + } + if let Some(creator) = damage.creator { + if creator == *player_entity { + crate::gamelog::record_event("Damage Inflicted", amount); + } + } + + if pool.hit_points.current < 1 { + add_effect(damage.creator, EffectType::EntityDeath, Targets::Single{target}); + } + } + } + } +} + +pub fn bloodstain(ecs: &mut World, tile_idx : i32) { + let mut map = ecs.fetch_mut::(); + map.bloodstains.insert(tile_idx as usize); +} + +pub fn death(ecs: &mut World, effect: &EffectSpawner, target : Entity) { + let mut xp_gain = 0; + let mut gold_gain = 0.0f32; + + let mut pools = ecs.write_storage::(); + let mut attributes = ecs.write_storage::(); + + if let Some(pos) = entity_position(ecs, target) { + crate::spatial::remove_entity(target, pos as usize); + } + + if let Some(source) = effect.creator { + if ecs.read_storage::().get(source).is_some() { + if let Some(stats) = pools.get(target) { + xp_gain += stats.level * 100; + gold_gain += stats.gold; + } + + if xp_gain != 0 || gold_gain != 0.0 { + let mut player_stats = pools.get_mut(source).unwrap(); + let mut player_attributes = attributes.get_mut(source).unwrap(); + player_stats.xp += xp_gain; + player_stats.gold += gold_gain; + if player_stats.xp >= player_stats.level * 1000 { + // We've gone up a level! + player_stats.level += 1; + crate::gamelog::Logger::new() + .color(rltk::MAGENTA) + .append("Congratulations, you are now level") + .append(format!("{}", player_stats.level)) + .log(); + + // Improve a random attribute + let attr_to_boost = crate::rng::roll_dice(1, 4); + match attr_to_boost { + 1 => { + player_attributes.might.base += 1; + crate::gamelog::Logger::new().color(rltk::GREEN).append("You feel stronger!").log(); + } + 2 => { + player_attributes.fitness.base += 1; + crate::gamelog::Logger::new().color(rltk::GREEN).append("You feel healthier!").log(); + } + 3 => { + player_attributes.quickness.base += 1; + crate::gamelog::Logger::new().color(rltk::GREEN).append("You feel quicker!").log(); + } + _ => { + player_attributes.intelligence.base += 1; + crate::gamelog::Logger::new().color(rltk::GREEN).append("You feel smarter!").log(); + } + } + + // Improve all skills + let mut skills = ecs.write_storage::(); + let player_skills = skills.get_mut(*ecs.fetch::()).unwrap(); + for sk in player_skills.skills.iter_mut() { + *sk.1 += 1; + } + + ecs.write_storage::() + .insert( + *ecs.fetch::(), + EquipmentChanged{}) + .expect("Insert Failed"); + + player_stats.hit_points.max = player_hp_at_level( + player_attributes.fitness.base + player_attributes.fitness.modifiers, + player_stats.level + ); + player_stats.hit_points.current = player_stats.hit_points.max; + player_stats.mana.max = mana_at_level( + player_attributes.intelligence.base + player_attributes.intelligence.modifiers, + player_stats.level + ); + player_stats.mana.current = player_stats.mana.max; + + let player_pos = ecs.fetch::(); + let map = ecs.fetch::(); + for i in 0..10 { + if player_pos.y - i > 1 { + add_effect(None, + EffectType::Particle{ + glyph: rltk::to_cp437('░'), + fg : rltk::RGB::named(rltk::GOLD), + bg : rltk::RGB::named(rltk::BLACK), + lifespan: 400.0 + }, + Targets::Tile{ tile_idx : map.xy_idx(player_pos.x, player_pos.y - i) as i32 } + ); + } + } + } + } + } + } +} + +pub fn heal_damage(ecs: &mut World, heal: &EffectSpawner, target: Entity) { + let mut pools = ecs.write_storage::(); + if let Some(pool) = pools.get_mut(target) { + if let EffectType::Healing{amount} = heal.effect_type { + pool.hit_points.current = i32::min(pool.hit_points.max, pool.hit_points.current + amount); + add_effect(None, + EffectType::Particle{ + glyph: rltk::to_cp437('‼'), + fg : rltk::RGB::named(rltk::GREEN), + bg : rltk::RGB::named(rltk::BLACK), + lifespan: 200.0 + }, + Targets::Single{target} + ); + } + } +} + +pub fn restore_mana(ecs: &mut World, mana: &EffectSpawner, target: Entity) { + let mut pools = ecs.write_storage::(); + if let Some(pool) = pools.get_mut(target) { + if let EffectType::Mana{amount} = mana.effect_type { + pool.mana.current = i32::min(pool.mana.max, pool.mana.current + amount); + add_effect(None, + EffectType::Particle{ + glyph: rltk::to_cp437('‼'), + fg : rltk::RGB::named(rltk::BLUE), + bg : rltk::RGB::named(rltk::BLACK), + lifespan: 200.0 + }, + Targets::Single{target} + ); + } + } +} + +pub fn add_confusion(ecs: &mut World, effect: &EffectSpawner, target: Entity) { + if let EffectType::Confusion{turns} = &effect.effect_type { + ecs.create_entity() + .with(StatusEffect{ target }) + .with(Confusion{}) + .with(Duration{ turns : *turns}) + .with(Name{ name : "Confusion".to_string() }) + .marked::>() + .build(); + } +} + +pub fn attribute_effect(ecs: &mut World, effect: &EffectSpawner, target: Entity) { + if let EffectType::AttributeEffect{bonus, name, duration} = &effect.effect_type { + ecs.create_entity() + .with(StatusEffect{ target }) + .with(bonus.clone()) + .with(Duration { turns : *duration }) + .with(Name { name : name.clone() }) + .marked::>() + .build(); + ecs.write_storage::().insert(target, EquipmentChanged{}).expect("Insert failed"); + } +} + +pub fn slow(ecs: &mut World, effect: &EffectSpawner, target: Entity) { + if let EffectType::Slow{initiative_penalty} = &effect.effect_type { + ecs.create_entity() + .with(StatusEffect{ target }) + .with(Slow{ initiative_penalty : *initiative_penalty }) + .with(Duration{ turns : 5}) + .with( + if *initiative_penalty > 0.0 { + Name{ name : "Slowed".to_string() } + } else { + Name{ name : "Hasted".to_string() } + } + ) + .marked::>() + .build(); + } +} + +pub fn damage_over_time(ecs: &mut World, effect: &EffectSpawner, target: Entity) { + if let EffectType::DamageOverTime{damage} = &effect.effect_type { + ecs.create_entity() + .with(StatusEffect{ target }) + .with(DamageOverTime{ damage : *damage }) + .with(Duration{ turns : 5}) + .with(Name{ name : "Damage Over Time".to_string() }) + .marked::>() + .build(); + } +} diff --git a/chapter-75-darkplaza/src/effects/hunger.rs b/chapter-75-darkplaza/src/effects/hunger.rs new file mode 100644 index 00000000..03139ce3 --- /dev/null +++ b/chapter-75-darkplaza/src/effects/hunger.rs @@ -0,0 +1,10 @@ +use specs::prelude::*; +use super::*; +use crate::components::{HungerClock, HungerState}; + +pub fn well_fed(ecs: &mut World, _damage: &EffectSpawner, target: Entity) { + if let Some(hc) = ecs.write_storage::().get_mut(target) { + hc.state = HungerState::WellFed; + hc.duration = 20; + } +} diff --git a/chapter-75-darkplaza/src/effects/mod.rs b/chapter-75-darkplaza/src/effects/mod.rs new file mode 100644 index 00000000..50890c83 --- /dev/null +++ b/chapter-75-darkplaza/src/effects/mod.rs @@ -0,0 +1,145 @@ +use std::sync::Mutex; +use specs::prelude::*; +use std::collections::{HashSet, VecDeque}; +use crate::map::Map; +mod damage; +mod targeting; +pub use targeting::*; +mod particles; +mod triggers; +mod hunger; +mod movement; +use crate::components::AttributeBonus; +use rltk::Point; + +lazy_static! { + pub static ref EFFECT_QUEUE : Mutex> = Mutex::new(VecDeque::new()); +} + +#[derive(Debug)] +pub enum EffectType { + Damage { amount : i32 }, + Bloodstain, + Particle { glyph: rltk::FontCharType, fg : rltk::RGB, bg: rltk::RGB, lifespan: f32 }, + ParticleProjectile { glyph: rltk::FontCharType, fg : rltk::RGB, bg: rltk::RGB, lifespan: f32, speed: f32, path: Vec }, + EntityDeath, + ItemUse { item: Entity }, + SpellUse { spell: Entity }, + WellFed, + Healing { amount : i32 }, + Mana { amount : i32 }, + Confusion { turns : i32 }, + TriggerFire { trigger: Entity }, + TeleportTo { x:i32, y:i32, depth: i32, player_only : bool }, + AttributeEffect { bonus : AttributeBonus, name : String, duration : i32 }, + Slow { initiative_penalty : f32 }, + DamageOverTime { damage : i32 } +} + +#[derive(Clone, Debug)] +pub enum Targets { + Single { target : Entity }, + TargetList { targets: Vec }, + Tile { tile_idx : i32 }, + Tiles { tiles: Vec } +} + +#[derive(Debug)] +pub struct EffectSpawner { + pub creator : Option, + pub effect_type : EffectType, + pub targets : Targets, + dedupe : HashSet +} + +pub fn add_effect(creator : Option, effect_type: EffectType, targets : Targets) { + EFFECT_QUEUE + .lock() + .unwrap() + .push_back(EffectSpawner{ + creator, + effect_type, + targets, + dedupe : HashSet::new() + }); +} + +pub fn run_effects_queue(ecs : &mut World) { + loop { + let effect : Option = EFFECT_QUEUE.lock().unwrap().pop_front(); + if let Some(mut effect) = effect { + target_applicator(ecs, &mut effect); + } else { + break; + } + } +} + +fn target_applicator(ecs : &mut World, effect : &mut EffectSpawner) { + if let EffectType::ItemUse{item} = effect.effect_type { + triggers::item_trigger(effect.creator, item, &effect.targets, ecs); + } else if let EffectType::SpellUse{spell} = effect.effect_type { + triggers::spell_trigger(effect.creator, spell, &effect.targets, ecs); + } else if let EffectType::TriggerFire{trigger} = effect.effect_type { + triggers::trigger(effect.creator, trigger, &effect.targets, ecs); + } else { + match &effect.targets.clone() { + Targets::Tile{tile_idx} => affect_tile(ecs, effect, *tile_idx), + Targets::Tiles{tiles} => tiles.iter().for_each(|tile_idx| affect_tile(ecs, effect, *tile_idx)), + Targets::Single{target} => affect_entity(ecs, effect, *target), + Targets::TargetList{targets} => targets.iter().for_each(|entity| affect_entity(ecs, effect, *entity)), + } + } +} + +fn tile_effect_hits_entities(effect: &EffectType) -> bool { + match effect { + EffectType::Damage{..} => true, + EffectType::WellFed => true, + EffectType::Healing{..} => true, + EffectType::Mana{..} => true, + EffectType::Confusion{..} => true, + EffectType::TeleportTo{..} => true, + EffectType::AttributeEffect{..} => true, + EffectType::Slow{..} => true, + EffectType::DamageOverTime{..} => true, + _ => false + } +} + +fn affect_tile(ecs: &mut World, effect: &mut EffectSpawner, tile_idx : i32) { + if tile_effect_hits_entities(&effect.effect_type) { + let content = crate::spatial::get_tile_content_clone(tile_idx as usize); + content.iter().for_each(|entity| affect_entity(ecs, effect, *entity)); + } + + match &effect.effect_type { + EffectType::Bloodstain => damage::bloodstain(ecs, tile_idx), + EffectType::Particle{..} => particles::particle_to_tile(ecs, tile_idx, &effect), + EffectType::ParticleProjectile{..} => particles::projectile(ecs, tile_idx, &effect), + _ => {} + } +} + +fn affect_entity(ecs: &mut World, effect: &mut EffectSpawner, target: Entity) { + if effect.dedupe.contains(&target) { + return; + } + effect.dedupe.insert(target); + match &effect.effect_type { + EffectType::Damage{..} => damage::inflict_damage(ecs, effect, target), + EffectType::EntityDeath => damage::death(ecs, effect, target), + EffectType::Bloodstain{..} => if let Some(pos) = entity_position(ecs, target) { damage::bloodstain(ecs, pos) }, + EffectType::Particle{..} => if let Some(pos) = entity_position(ecs, target) { particles::particle_to_tile(ecs, pos, &effect) }, + EffectType::WellFed => hunger::well_fed(ecs, effect, target), + EffectType::Healing{..} => damage::heal_damage(ecs, effect, target), + EffectType::Mana{..} => damage::restore_mana(ecs, effect, target), + EffectType::Confusion{..} => damage::add_confusion(ecs, effect, target), + EffectType::TeleportTo{..} => movement::apply_teleport(ecs, effect, target), + EffectType::AttributeEffect{..} => damage::attribute_effect(ecs, effect, target), + EffectType::Slow{..} => damage::slow(ecs, effect, target), + EffectType::DamageOverTime{..} => damage::damage_over_time(ecs, effect, target), + _ => {} + } +} + diff --git a/chapter-75-darkplaza/src/effects/movement.rs b/chapter-75-darkplaza/src/effects/movement.rs new file mode 100644 index 00000000..63b734e6 --- /dev/null +++ b/chapter-75-darkplaza/src/effects/movement.rs @@ -0,0 +1,17 @@ +use specs::prelude::*; +use super::*; +use crate::components::{ApplyTeleport}; + +pub fn apply_teleport(ecs: &mut World, destination: &EffectSpawner, target: Entity) { + let player_entity = ecs.fetch::(); + if let EffectType::TeleportTo{x, y, depth, player_only} = &destination.effect_type { + if !player_only || target == *player_entity { + let mut apply_teleport = ecs.write_storage::(); + apply_teleport.insert(target, ApplyTeleport{ + dest_x : *x, + dest_y : *y, + dest_depth : *depth + }).expect("Unable to insert"); + } + } +} diff --git a/chapter-75-darkplaza/src/effects/particles.rs b/chapter-75-darkplaza/src/effects/particles.rs new file mode 100644 index 00000000..90944ed9 --- /dev/null +++ b/chapter-75-darkplaza/src/effects/particles.rs @@ -0,0 +1,44 @@ +use specs::prelude::*; +use super::*; +use crate::systems::particle_system::ParticleBuilder; +use crate::map::Map; +use crate::components::{ParticleAnimation, ParticleLifetime, Renderable, Position}; + +pub fn particle_to_tile(ecs: &mut World, tile_idx : i32, effect: &EffectSpawner) { + if let EffectType::Particle{ glyph, fg, bg, lifespan } = effect.effect_type { + let map = ecs.fetch::(); + let mut particle_builder = ecs.fetch_mut::(); + particle_builder.request( + tile_idx % map.width, + tile_idx / map.width, + fg, + bg, + glyph, + lifespan + ); + } +} + +pub fn projectile(ecs: &mut World, tile_idx : i32, effect: &EffectSpawner) { + if let EffectType::ParticleProjectile{ glyph, fg, bg, + lifespan: _, speed, path } = &effect.effect_type + { + let map = ecs.fetch::(); + let x = tile_idx % map.width; + let y = tile_idx / map.width; + std::mem::drop(map); + ecs.create_entity() + .with(Position{ x, y }) + .with(Renderable{ fg: *fg, bg: *bg, glyph: *glyph, render_order: 0 }) + .with(ParticleLifetime{ + lifetime_ms: path.len() as f32 * speed, + animation: Some(ParticleAnimation{ + step_time: *speed, + path: path.to_vec(), + current_step: 0, + timer: 0.0 + }) + }) + .build(); + } +} diff --git a/chapter-75-darkplaza/src/effects/targeting.rs b/chapter-75-darkplaza/src/effects/targeting.rs new file mode 100644 index 00000000..bb85e302 --- /dev/null +++ b/chapter-75-darkplaza/src/effects/targeting.rs @@ -0,0 +1,55 @@ +use specs::prelude::*; +use crate::components::{Position, InBackpack, Equipped}; +use crate::map::Map; + +pub fn entity_position(ecs: &World, target: Entity) -> Option { + if let Some(pos) = ecs.read_storage::().get(target) { + let map = ecs.fetch::(); + return Some(map.xy_idx(pos.x, pos.y) as i32); + } + None +} + +pub fn aoe_tiles(map: &Map, target: rltk::Point, radius: i32) -> Vec { + let mut blast_tiles = rltk::field_of_view(target, radius, &*map); + blast_tiles.retain(|p| p.x > 0 && p.x < map.width-1 && p.y > 0 && p.y < map.height-1 ); + let mut result = Vec::new(); + for t in blast_tiles.iter() { + result.push(map.xy_idx(t.x, t.y) as i32); + } + result +} + +pub fn find_item_position(ecs: &World, target: Entity, creator: Option) -> Option { + let positions = ecs.read_storage::(); + let map = ecs.fetch::(); + + // Easy - it has a position + if let Some(pos) = positions.get(target) { + return Some(map.xy_idx(pos.x, pos.y) as i32); + } + + // Maybe it is carried? + if let Some(carried) = ecs.read_storage::().get(target) { + if let Some(pos) = positions.get(carried.owner) { + return Some(map.xy_idx(pos.x, pos.y) as i32); + } + } + + // Maybe it is equipped? + if let Some(equipped) = ecs.read_storage::().get(target) { + if let Some(pos) = positions.get(equipped.owner) { + return Some(map.xy_idx(pos.x, pos.y) as i32); + } + } + + // Maybe the creator has a position? + if let Some(creator) = creator { + if let Some(pos) = positions.get(creator) { + return Some(map.xy_idx(pos.x, pos.y) as i32); + } + } + + // No idea - give up + None +} diff --git a/chapter-75-darkplaza/src/effects/triggers.rs b/chapter-75-darkplaza/src/effects/triggers.rs new file mode 100644 index 00000000..f9d812d1 --- /dev/null +++ b/chapter-75-darkplaza/src/effects/triggers.rs @@ -0,0 +1,270 @@ +use super::*; +use crate::components::*; +use crate::RunState; + +pub fn item_trigger(creator : Option, item: Entity, targets : &Targets, ecs: &mut World) { + // Check charges + if let Some(c) = ecs.write_storage::().get_mut(item) { + if c.charges < 1 { + // Cancel + crate::gamelog::Logger::new() + .item_name(&ecs.read_storage::().get(item).unwrap().name) + .append("is out of charges!") + .log(); + return; + } else { + c.charges -= 1; + } + } + + // Use the item via the generic system + let did_something = event_trigger(creator, item, targets, ecs); + + // If it was a consumable, then it gets deleted + if did_something { + if let Some(c) = ecs.read_storage::().get(item) { + rltk::console::log(format!("{}", c.max_charges)); + if c.max_charges < 2 { + ecs.entities().delete(item).expect("Delete Failed"); + } + } + } +} + +pub fn spell_trigger(creator : Option, spell: Entity, targets : &Targets, ecs: &mut World) { + let mut targeting = targets.clone(); + let mut self_destruct = false; + if let Some(template) = ecs.read_storage::().get(spell) { + let mut pools = ecs.write_storage::(); + if let Some(caster) = creator { + if let Some(pool) = pools.get_mut(caster) { + if template.mana_cost <= pool.mana.current { + pool.mana.current -= template.mana_cost; + } + } + + // Handle self-targeting override + if ecs.read_storage::().get(spell).is_some() { + if let Some(pos) = ecs.read_storage::().get(caster) { + let map = ecs.fetch::(); + targeting = if let Some(aoe) = ecs.read_storage::().get(spell) { + Targets::Tiles { tiles : aoe_tiles(&map, rltk::Point::new(pos.x, pos.y), aoe.radius) } + } else { + Targets::Tile{ tile_idx : map.xy_idx(pos.x, pos.y) as i32 } + } + } + } + } + if let Some(_destruct) = ecs.read_storage::().get(spell) { + self_destruct = true; + } + } + event_trigger(creator, spell, &targeting, ecs); + if self_destruct && creator.is_some() { + ecs.entities().delete(creator.unwrap()).expect("Unable to delete owner"); + } +} + +pub fn trigger(creator : Option, trigger: Entity, targets : &Targets, ecs: &mut World) { + // The triggering item is no longer hidden + ecs.write_storage::().remove(trigger); + + // Use the item via the generic system + let did_something = event_trigger(creator, trigger, targets, ecs); + + // If it was a single activation, then it gets deleted + if did_something && ecs.read_storage::().get(trigger).is_some() { + ecs.entities().delete(trigger).expect("Delete Failed"); + } +} + +#[allow(clippy::cognitive_complexity)] +fn event_trigger(creator : Option, entity: Entity, targets : &Targets, ecs: &mut World) -> bool { + let mut did_something = false; + + // Simple particle spawn + if let Some(part) = ecs.read_storage::().get(entity) { + add_effect( + creator, + EffectType::Particle{ + glyph : part.glyph, + fg : part.color, + bg : rltk::RGB::named(rltk::BLACK), + lifespan : part.lifetime_ms + }, + targets.clone() + ); + } + + // Line particle spawn + if let Some(part) = ecs.read_storage::().get(entity) { + if let Some(start_pos) = targeting::find_item_position(ecs, entity, creator) { + match targets { + Targets::Tile{tile_idx} => spawn_line_particles(ecs, start_pos, *tile_idx, part), + Targets::Tiles{tiles} => tiles.iter().for_each(|tile_idx| spawn_line_particles(ecs, start_pos, *tile_idx, part)), + Targets::Single{ target } => { + if let Some(end_pos) = entity_position(ecs, *target) { + spawn_line_particles(ecs, start_pos, end_pos, part); + } + } + Targets::TargetList{ targets } => { + targets.iter().for_each(|target| { + if let Some(end_pos) = entity_position(ecs, *target) { + spawn_line_particles(ecs, start_pos, end_pos, part); + } + }); + } + } + } + } + + // Providing food + if ecs.read_storage::().get(entity).is_some() { + add_effect(creator, EffectType::WellFed, targets.clone()); + let names = ecs.read_storage::(); + crate::gamelog::Logger::new() + .append("You eat the") + .item_name(&names.get(entity).unwrap().name) + .log(); + did_something = true; + } + + // Magic mapper + if ecs.read_storage::().get(entity).is_some() { + let mut runstate = ecs.fetch_mut::(); + crate::gamelog::Logger::new().append("The map is revealed to you!").log(); + *runstate = RunState::MagicMapReveal{ row : 0}; + did_something = true; + } + + // Remove Curse + if ecs.read_storage::().get(entity).is_some() { + let mut runstate = ecs.fetch_mut::(); + *runstate = RunState::ShowRemoveCurse; + did_something = true; + } + + // Identify Item + if ecs.read_storage::().get(entity).is_some() { + let mut runstate = ecs.fetch_mut::(); + *runstate = RunState::ShowIdentify; + did_something = true; + } + + // Town Portal + if ecs.read_storage::().get(entity).is_some() { + let map = ecs.fetch::(); + if map.depth == 1 { + crate::gamelog::Logger::new().append("You are already in town, so the scroll does nothing.").log(); + } else { + crate::gamelog::Logger::new().append("You are telported back to town!").log(); + let mut runstate = ecs.fetch_mut::(); + *runstate = RunState::TownPortal; + did_something = true; + } + } + + // Healing + if let Some(heal) = ecs.read_storage::().get(entity) { + add_effect(creator, EffectType::Healing{amount: heal.heal_amount}, targets.clone()); + did_something = true; + } + + // Mana + if let Some(mana) = ecs.read_storage::().get(entity) { + add_effect(creator, EffectType::Mana{amount: mana.mana_amount}, targets.clone()); + did_something = true; + } + + // Damage + if let Some(damage) = ecs.read_storage::().get(entity) { + add_effect(creator, EffectType::Damage{ amount: damage.damage }, targets.clone()); + did_something = true; + } + + // Confusion + if let Some(_confusion) = ecs.read_storage::().get(entity) { + if let Some(duration) = ecs.read_storage::().get(entity) { + add_effect(creator, EffectType::Confusion{ turns : duration.turns }, targets.clone()); + did_something = true; + } + } + + // Teleport + if let Some(teleport) = ecs.read_storage::().get(entity) { + add_effect( + creator, + EffectType::TeleportTo{ + x : teleport.x, + y : teleport.y, + depth: teleport.depth, + player_only: teleport.player_only + }, + targets.clone() + ); + did_something = true; + } + + // Attribute Modifiers + if let Some(attr) = ecs.read_storage::().get(entity) { + add_effect( + creator, + EffectType::AttributeEffect{ + bonus : attr.clone(), + duration : 10, + name : ecs.read_storage::().get(entity).unwrap().name.clone() + }, + targets.clone() + ); + did_something = true; + } + + // Learn spells + if let Some(spell) = ecs.read_storage::().get(entity) { + if let Some(known) = ecs.write_storage::().get_mut(creator.unwrap()) { + if let Some(spell_entity) = crate::raws::find_spell_entity(ecs, &spell.spell) { + if let Some(spell_info) = ecs.read_storage::().get(spell_entity) { + let mut already_known = false; + known.spells.iter().for_each(|s| if s.display_name == spell.spell { already_known = true }); + if !already_known { + known.spells.push(KnownSpell{ display_name: spell.spell.clone(), mana_cost : spell_info.mana_cost }); + } + } + } + } + did_something = true; + } + + // Slow + if let Some(slow) = ecs.read_storage::().get(entity) { + add_effect(creator, EffectType::Slow{ initiative_penalty : slow.initiative_penalty }, targets.clone()); + did_something = true; + } + + // Damage Over Time + if let Some(damage) = ecs.read_storage::().get(entity) { + add_effect(creator, EffectType::DamageOverTime{ damage : damage.damage }, targets.clone()); + did_something = true; + } + + did_something +} + +fn spawn_line_particles(ecs:&World, start: i32, end: i32, part: &SpawnParticleLine) { + let map = ecs.fetch::(); + let start_pt = rltk::Point::new(start % map.width, end / map.width); + let end_pt = rltk::Point::new(end % map.width, end / map.width); + let line = rltk::line2d(rltk::LineAlg::Bresenham, start_pt, end_pt); + for pt in line.iter() { + add_effect( + None, + EffectType::Particle{ + glyph : part.glyph, + fg : part.color, + bg : rltk::RGB::named(rltk::BLACK), + lifespan : part.lifetime_ms + }, + Targets::Tile{ tile_idx : map.xy_idx(pt.x, pt.y) as i32} + ); + } +} diff --git a/chapter-75-darkplaza/src/gamelog/builder.rs b/chapter-75-darkplaza/src/gamelog/builder.rs new file mode 100644 index 00000000..f0691aca --- /dev/null +++ b/chapter-75-darkplaza/src/gamelog/builder.rs @@ -0,0 +1,65 @@ +use rltk::prelude::*; +use super::{LogFragment, append_entry}; + +pub struct Logger { + current_color : RGB, + fragments : Vec +} + +impl Logger { + pub fn new() -> Self { + Logger{ + current_color : RGB::named(rltk::WHITE), + fragments : Vec::new() + } + } + + pub fn color(mut self, color: (u8, u8, u8)) -> Self { + self.current_color = RGB::named(color); + self + } + + pub fn append(mut self, text : T) -> Self { + self.fragments.push( + LogFragment{ + color : self.current_color, + text : text.to_string() + } + ); + self + } + + pub fn log(self) { + append_entry(self.fragments) + } + + pub fn npc_name(mut self, text : T) -> Self { + self.fragments.push( + LogFragment{ + color : RGB::named(rltk::YELLOW), + text : text.to_string() + } + ); + self + } + + pub fn item_name(mut self, text : T) -> Self { + self.fragments.push( + LogFragment{ + color : RGB::named(rltk::CYAN), + text : text.to_string() + } + ); + self + } + + pub fn damage(mut self, damage: i32) -> Self { + self.fragments.push( + LogFragment{ + color : RGB::named(rltk::RED), + text : format!("{}", damage).to_string() + } + ); + self + } +} \ No newline at end of file diff --git a/chapter-75-darkplaza/src/gamelog/events.rs b/chapter-75-darkplaza/src/gamelog/events.rs new file mode 100644 index 00000000..b20efdff --- /dev/null +++ b/chapter-75-darkplaza/src/gamelog/events.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; +use std::sync::Mutex; + +lazy_static! { + static ref EVENTS : Mutex> = Mutex::new(HashMap::new()); +} + +pub fn clear_events() { + EVENTS.lock().unwrap().clear(); +} + +pub fn record_event(event: T, n : i32) { + let event_name = event.to_string(); + let mut events_lock = EVENTS.lock(); + let events = events_lock.as_mut().unwrap(); + if let Some(e) = events.get_mut(&event_name) { + *e += n; + } else { + events.insert(event_name, n); + } +} + +pub fn get_event_count(event: T) -> i32 { + let event_name = event.to_string(); + let events_lock = EVENTS.lock(); + let events = events_lock.unwrap(); + if let Some(e) = events.get(&event_name) { + *e + } else { + 0 + } +} + +pub fn clone_events() -> HashMap { + EVENTS.lock().unwrap().clone() +} + +pub fn load_events(events : HashMap) { + EVENTS.lock().unwrap().clear(); + events.iter().for_each(|(k,v)| { + EVENTS.lock().unwrap().insert(k.to_string(), *v); + }); +} \ No newline at end of file diff --git a/chapter-75-darkplaza/src/gamelog/logstore.rs b/chapter-75-darkplaza/src/gamelog/logstore.rs new file mode 100644 index 00000000..e7b62b0e --- /dev/null +++ b/chapter-75-darkplaza/src/gamelog/logstore.rs @@ -0,0 +1,38 @@ +use std::sync::Mutex; +use super::LogFragment; +use rltk::prelude::*; + +lazy_static! { + static ref LOG : Mutex>> = Mutex::new(Vec::new()); +} + +pub fn append_entry(fragments : Vec) { + LOG.lock().unwrap().push(fragments); +} + +pub fn clear_log() { + LOG.lock().unwrap().clear(); +} + +pub fn print_log(console: &mut Box, pos: Point) { + let mut y = pos.y; + let mut x = pos.x; + LOG.lock().unwrap().iter().rev().take(6).for_each(|log| { + log.iter().for_each(|frag| { + console.print_color(x, y, frag.color.to_rgba(1.0), RGBA::named(rltk::BLACK), &frag.text); + x += frag.text.len() as i32; + x += 1; + }); + y += 1; + x = pos.x; + }); +} + +pub fn clone_log() -> Vec> { + LOG.lock().unwrap().clone() +} + +pub fn restore_log(log : &mut Vec>) { + LOG.lock().unwrap().clear(); + LOG.lock().unwrap().append(log); +} \ No newline at end of file diff --git a/chapter-75-darkplaza/src/gamelog/mod.rs b/chapter-75-darkplaza/src/gamelog/mod.rs new file mode 100644 index 00000000..ed01bcd7 --- /dev/null +++ b/chapter-75-darkplaza/src/gamelog/mod.rs @@ -0,0 +1,15 @@ +use rltk::prelude::*; +mod builder; +pub use builder::*; +mod logstore; +use logstore::*; +pub use logstore::{clear_log, clone_log, restore_log, print_log}; +use serde::{Serialize, Deserialize}; +mod events; +pub use events::*; + +#[derive(Serialize, Deserialize, Clone)] +pub struct LogFragment { + pub color : RGB, + pub text : String +} diff --git a/chapter-75-darkplaza/src/gamesystem.rs b/chapter-75-darkplaza/src/gamesystem.rs new file mode 100644 index 00000000..920f1a44 --- /dev/null +++ b/chapter-75-darkplaza/src/gamesystem.rs @@ -0,0 +1,37 @@ +use super::{Skill, Skills}; + +pub fn attr_bonus(value: i32) -> i32 { + (value-10)/2 // See: https://roll20.net/compendium/dnd5e/Ability%20Scores#content +} + +pub fn player_hp_per_level(fitness: i32) -> i32 { + 15 + attr_bonus(fitness) +} + +pub fn player_hp_at_level(fitness:i32, level:i32) -> i32 { + 15 + (player_hp_per_level(fitness) * level) +} + +pub fn npc_hp(fitness: i32, level: i32) -> i32 { + let mut total = 1; + for _i in 0..level { + total += i32::max(1, 8 + attr_bonus(fitness)); + } + total +} + +pub fn mana_per_level(intelligence: i32) -> i32 { + i32::max(1, 4 + attr_bonus(intelligence)) +} + +pub fn mana_at_level(intelligence: i32, level: i32) -> i32 { + mana_per_level(intelligence) * level +} + +pub fn skill_bonus(skill : Skill, skills: &Skills) -> i32 { + if skills.skills.contains_key(&skill) { + skills.skills[&skill] + } else { + -4 + } +} diff --git a/chapter-75-darkplaza/src/gui/cheat_menu.rs b/chapter-75-darkplaza/src/gui/cheat_menu.rs new file mode 100644 index 00000000..5a2d17ac --- /dev/null +++ b/chapter-75-darkplaza/src/gui/cheat_menu.rs @@ -0,0 +1,42 @@ +use rltk::prelude::*; +use crate::State; +use super::{menu_option, menu_box}; + +#[derive(PartialEq, Copy, Clone)] +pub enum CheatMenuResult { NoResponse, Cancel, TeleportToExit, Heal, Reveal, GodMode } + +pub fn show_cheat_mode(_gs : &mut State, ctx : &mut Rltk) -> CheatMenuResult { + let mut draw_batch = DrawBatch::new(); + let count = 4; + let mut y = (25 - (count / 2)) as i32; + menu_box(&mut draw_batch, 15, y, (count+3) as i32, "Cheating!"); + draw_batch.print_color( + Point::new(18, y+count as i32+1), + "ESCAPE to cancel", + ColorPair::new(RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK)) + ); + + menu_option(&mut draw_batch, 17, y, rltk::to_cp437('T'), "Teleport to next level"); + y += 1; + menu_option(&mut draw_batch, 17, y, rltk::to_cp437('H'), "Heal all wounds"); + y += 1; + menu_option(&mut draw_batch, 17, y, rltk::to_cp437('R'), "Reveal the map"); + y += 1; + menu_option(&mut draw_batch, 17, y, rltk::to_cp437('G'), "God Mode (No Death)"); + + draw_batch.submit(6000); + + match ctx.key { + None => CheatMenuResult::NoResponse, + Some(key) => { + match key { + VirtualKeyCode::T => CheatMenuResult::TeleportToExit, + VirtualKeyCode::H => CheatMenuResult::Heal, + VirtualKeyCode::R => CheatMenuResult::Reveal, + VirtualKeyCode::G => CheatMenuResult::GodMode, + VirtualKeyCode::Escape => CheatMenuResult::Cancel, + _ => CheatMenuResult::NoResponse + } + } + } +} diff --git a/chapter-75-darkplaza/src/gui/drop_item_menu.rs b/chapter-75-darkplaza/src/gui/drop_item_menu.rs new file mode 100644 index 00000000..912c894b --- /dev/null +++ b/chapter-75-darkplaza/src/gui/drop_item_menu.rs @@ -0,0 +1,29 @@ +use rltk::prelude::*; +use specs::prelude::*; +use crate::{State, InBackpack}; +use super::{get_item_display_name, ItemMenuResult, item_result_menu}; + +pub fn drop_item_menu(gs : &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option) { + let mut draw_batch = DrawBatch::new(); + + let player_entity = gs.ecs.fetch::(); + let backpack = gs.ecs.read_storage::(); + let entities = gs.ecs.entities(); + + let mut items : Vec<(Entity, String)> = Vec::new(); + (&entities, &backpack).join() + .filter(|item| item.1.owner == *player_entity ) + .for_each(|item| { + items.push((item.0, get_item_display_name(&gs.ecs, item.0))) + }); + + let result = item_result_menu( + &mut draw_batch, + "Drop which item?", + items.len(), + &items, + ctx.key + ); + draw_batch.submit(6000); + result +} \ No newline at end of file diff --git a/chapter-75-darkplaza/src/gui/game_over_menu.rs b/chapter-75-darkplaza/src/gui/game_over_menu.rs new file mode 100644 index 00000000..deb34d72 --- /dev/null +++ b/chapter-75-darkplaza/src/gui/game_over_menu.rs @@ -0,0 +1,51 @@ +use rltk::prelude::*; + +#[derive(PartialEq, Copy, Clone)] +pub enum GameOverResult { NoSelection, QuitToMenu } + +pub fn game_over(ctx : &mut Rltk) -> GameOverResult { + let mut draw_batch = DrawBatch::new(); + draw_batch.print_color_centered( + 15, + "Your journey has ended!", + ColorPair::new(RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK)) + ); + draw_batch.print_color_centered( + 17, + "One day, we'll tell you all about how you did.", + ColorPair::new(RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)) + ); + draw_batch.print_color_centered( + 18, + "That day, sadly, is not in this chapter..", + ColorPair::new(RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)) + ); + + draw_batch.print_color_centered( + 19, + &format!("You lived for {} turns.", crate::gamelog::get_event_count("Turn")), + ColorPair::new(RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)) + ); + draw_batch.print_color_centered( + 20, + &format!("You suffered {} points of damage.", crate::gamelog::get_event_count("Damage Taken")), + ColorPair::new(RGB::named(rltk::RED), RGB::named(rltk::BLACK)) + ); + draw_batch.print_color_centered( + 21, + &format!("You inflicted {} points of damage.", crate::gamelog::get_event_count("Damage Inflicted")), + ColorPair::new(RGB::named(rltk::RED), RGB::named(rltk::BLACK))); + + draw_batch.print_color_centered( + 23, + "Press any key to return to the menu.", + ColorPair::new(RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK)) + ); + + draw_batch.submit(6000); + + match ctx.key { + None => GameOverResult::NoSelection, + Some(_) => GameOverResult::QuitToMenu + } +} diff --git a/chapter-75-darkplaza/src/gui/hud.rs b/chapter-75-darkplaza/src/gui/hud.rs new file mode 100644 index 00000000..8615f6e3 --- /dev/null +++ b/chapter-75-darkplaza/src/gui/hud.rs @@ -0,0 +1,278 @@ +use rltk::prelude::*; +use specs::prelude::*; +use crate::{Pools, Map, Name, InBackpack, + Equipped, HungerClock, HungerState, Attributes, Attribute, Consumable, + StatusEffect, Duration, KnownSpells, Weapon, gamelog }; +use super::{draw_tooltips, get_item_display_name, get_item_color}; + +fn draw_attribute(name : &str, attribute : &Attribute, y : i32, draw_batch: &mut DrawBatch) { + let black = RGB::named(rltk::BLACK); + let attr_gray : RGB = RGB::from_hex("#CCCCCC").expect("Oops"); + draw_batch.print_color(Point::new(50, y), name, ColorPair::new(attr_gray, black)); + let color : RGB = + if attribute.modifiers < 0 { RGB::from_f32(1.0, 0.0, 0.0) } + else if attribute.modifiers == 0 { RGB::named(rltk::WHITE) } + else { RGB::from_f32(0.0, 1.0, 0.0) }; + draw_batch.print_color(Point::new(67, y), &format!("{}", attribute.base + attribute.modifiers), ColorPair::new(color, black)); + draw_batch.print_color(Point::new(73, y), &format!("{}", attribute.bonus), ColorPair::new(color, black)); + if attribute.bonus > 0 { + draw_batch.set(Point::new(72, y), ColorPair::new(color, black), to_cp437('+')); + } +} + +fn box_framework(draw_batch : &mut DrawBatch) { + let box_gray : RGB = RGB::from_hex("#999999").expect("Oops"); + let black = RGB::named(rltk::BLACK); + + draw_batch.draw_hollow_box(Rect::with_size(0, 0, 79, 59), ColorPair::new(box_gray, black)); // Overall box + draw_batch.draw_hollow_box(Rect::with_size(0, 0, 49, 45), ColorPair::new(box_gray, black)); // Map box + draw_batch.draw_hollow_box(Rect::with_size(0, 45, 79, 14), ColorPair::new(box_gray, black)); // Log box + draw_batch.draw_hollow_box(Rect::with_size(49, 0, 30, 8), ColorPair::new(box_gray, black)); // Top-right panel + + // Draw box connectors + draw_batch.set(Point::new(0, 45), ColorPair::new(box_gray, black), to_cp437('├')); + draw_batch.set(Point::new(49, 8), ColorPair::new(box_gray, black), to_cp437('├')); + draw_batch.set(Point::new(49, 0), ColorPair::new(box_gray, black), to_cp437('┬')); + draw_batch.set(Point::new(49, 45), ColorPair::new(box_gray, black), to_cp437('┴')); + draw_batch.set(Point::new(79, 8), ColorPair::new(box_gray, black), to_cp437('┤')); + draw_batch.set(Point::new(79, 45), ColorPair::new(box_gray, black), to_cp437('┤')); +} + +pub fn map_label(ecs: &World, draw_batch: &mut DrawBatch) { + let box_gray : RGB = RGB::from_hex("#999999").expect("Oops"); + let black = RGB::named(rltk::BLACK); + let white = RGB::named(rltk::WHITE); + + let map = ecs.fetch::(); + let name_length = map.name.len() + 2; + let x_pos = (22 - (name_length / 2)) as i32; + draw_batch.set(Point::new(x_pos, 0), ColorPair::new(box_gray, black), to_cp437('┤')); + draw_batch.set(Point::new(x_pos + name_length as i32 - 1, 0), ColorPair::new(box_gray, black), to_cp437('├')); + draw_batch.print_color(Point::new(x_pos+1, 0), &map.name, ColorPair::new(white, black)); +} + +fn draw_stats(ecs: &World, draw_batch: &mut DrawBatch, player_entity: &Entity) { + let black = RGB::named(rltk::BLACK); + let white = RGB::named(rltk::WHITE); + let pools = ecs.read_storage::(); + let player_pools = pools.get(*player_entity).unwrap(); + let health = format!("Health: {}/{}", player_pools.hit_points.current, player_pools.hit_points.max); + let mana = format!("Mana: {}/{}", player_pools.mana.current, player_pools.mana.max); + let xp = format!("Level: {}", player_pools.level); + draw_batch.print_color(Point::new(50, 1), &health, ColorPair::new(white, black)); + draw_batch.print_color(Point::new(50, 2), &mana, ColorPair::new(white, black)); + draw_batch.print_color(Point::new(50, 3), &xp, ColorPair::new(white, black)); + draw_batch.bar_horizontal( + Point::new(64, 1), + 14, + player_pools.hit_points.current, + player_pools.hit_points.max, + ColorPair::new(RGB::named(rltk::RED), RGB::named(rltk::BLACK)) + ); + draw_batch.bar_horizontal( + Point::new(64, 2), + 14, + player_pools.mana.current, + player_pools.mana.max, + ColorPair::new(RGB::named(rltk::BLUE), RGB::named(rltk::BLACK)) + ); + let xp_level_start = (player_pools.level-1) * 1000; + draw_batch.bar_horizontal( + Point::new(64, 3), + 14, + player_pools.xp - xp_level_start, + 1000, + ColorPair::new(RGB::named(rltk::GOLD), RGB::named(rltk::BLACK)) + ); +} + +fn draw_attributes(ecs: &World, draw_batch: &mut DrawBatch, player_entity: &Entity) { + let attributes = ecs.read_storage::(); + let attr = attributes.get(*player_entity).unwrap(); + draw_attribute("Might:", &attr.might, 4, draw_batch); + draw_attribute("Quickness:", &attr.quickness, 5, draw_batch); + draw_attribute("Fitness:", &attr.fitness, 6, draw_batch); + draw_attribute("Intelligence:", &attr.intelligence, 7, draw_batch); +} + +fn initiative_weight(ecs: &World, draw_batch: &mut DrawBatch, player_entity: &Entity) { + let attributes = ecs.read_storage::(); + let attr = attributes.get(*player_entity).unwrap(); + let black = RGB::named(rltk::BLACK); + let white = RGB::named(rltk::WHITE); + let pools = ecs.read_storage::(); + let player_pools = pools.get(*player_entity).unwrap(); + draw_batch.print_color( + Point::new(50, 9), + &format!("{:.0} lbs ({} lbs max)", + player_pools.total_weight, + (attr.might.base + attr.might.modifiers) * 15 + ), + ColorPair::new(white, black) + ); + draw_batch.print_color( + Point::new(50,10), + &format!("Initiative Penalty: {:.0}", player_pools.total_initiative_penalty), + ColorPair::new(white, black) + ); + draw_batch.print_color( + Point::new(50,11), + &format!("Gold: {:.1}", player_pools.gold), + ColorPair::new(RGB::named(rltk::GOLD), black) + ); +} + +fn equipped(ecs: &World, draw_batch: &mut DrawBatch, player_entity: &Entity) -> i32 { + let black = RGB::named(rltk::BLACK); + let yellow = RGB::named(rltk::YELLOW); + let mut y = 13; + let entities = ecs.entities(); + let equipped = ecs.read_storage::(); + let weapon = ecs.read_storage::(); + for (entity, equipped_by) in (&entities, &equipped).join() { + if equipped_by.owner == *player_entity { + let name = get_item_display_name(ecs, entity); + draw_batch.print_color( + Point::new(50, y), + &name, + ColorPair::new(get_item_color(ecs, entity), black)); + y += 1; + + if let Some(weapon) = weapon.get(entity) { + let mut weapon_info = if weapon.damage_bonus < 0 { + format!("┤ {} ({}d{}{})", &name, weapon.damage_n_dice, weapon.damage_die_type, weapon.damage_bonus) + } else if weapon.damage_bonus == 0 { + format!("┤ {} ({}d{})", &name, weapon.damage_n_dice, weapon.damage_die_type) + } else { + format!("┤ {} ({}d{}+{})", &name, weapon.damage_n_dice, weapon.damage_die_type, weapon.damage_bonus) + }; + + if let Some(range) = weapon.range { + weapon_info += &format!(" (range: {}, F to fire, V cycle targets)", range); + } + weapon_info += " ├"; + draw_batch.print_color( + Point::new(3, 45), + &weapon_info, + ColorPair::new(yellow, black)); + } + } + } + y +} + +fn consumables(ecs: &World, draw_batch: &mut DrawBatch, player_entity: &Entity, mut y : i32) -> i32 { + y += 1; + let black = RGB::named(rltk::BLACK); + let yellow = RGB::named(rltk::YELLOW); + let entities = ecs.entities(); + let consumables = ecs.read_storage::(); + let backpack = ecs.read_storage::(); + let mut index = 1; + for (entity, carried_by, _consumable) in (&entities, &backpack, &consumables).join() { + if carried_by.owner == *player_entity && index < 10 { + draw_batch.print_color( + Point::new(50, y), + &format!("↑{}", index), + ColorPair::new(yellow, black) + ); + draw_batch.print_color( + Point::new(53, y), + &get_item_display_name(ecs, entity), + ColorPair::new(get_item_color(ecs, entity), black) + ); + y += 1; + index += 1; + } + } + y +} + +fn spells(ecs: &World, draw_batch: &mut DrawBatch, player_entity: &Entity, mut y : i32) -> i32 { + y += 1; + let black = RGB::named(rltk::BLACK); + let blue = RGB::named(rltk::CYAN); + let known_spells_storage = ecs.read_storage::(); + let known_spells = &known_spells_storage.get(*player_entity).unwrap().spells; + let mut index = 1; + for spell in known_spells.iter() { + draw_batch.print_color( + Point::new(50, y), + &format!("^{}", index), + ColorPair::new(blue, black) + ); + draw_batch.print_color( + Point::new(53, y), + &format!("{} ({})", &spell.display_name, spell.mana_cost), + ColorPair::new(blue, black) + ); + index += 1; + y += 1; + } + y +} + +fn status(ecs: &World, draw_batch: &mut DrawBatch, player_entity: &Entity) { + let mut y = 44; + let hunger = ecs.read_storage::(); + let hc = hunger.get(*player_entity).unwrap(); + match hc.state { + HungerState::WellFed => { + draw_batch.print_color( + Point::new(50, y), + "Well Fed", + ColorPair::new(RGB::named(rltk::GREEN), RGB::named(rltk::BLACK)) + ); + y -= 1; + } + HungerState::Normal => {} + HungerState::Hungry => { + draw_batch.print_color( + Point::new(50, y), + "Hungry", + ColorPair::new(RGB::named(rltk::ORANGE), RGB::named(rltk::BLACK)) + ); + y -= 1; + } + HungerState::Starving => { + draw_batch.print_color( + Point::new(50, y), + "Starving", + ColorPair::new(RGB::named(rltk::RED), RGB::named(rltk::BLACK)) + ); + y -= 1; + } + } + let statuses = ecs.read_storage::(); + let durations = ecs.read_storage::(); + let names = ecs.read_storage::(); + for (status, duration, name) in (&statuses, &durations, &names).join() { + if status.target == *player_entity { + draw_batch.print_color( + Point::new(50, y), + &format!("{} ({})", name.name, duration.turns), + ColorPair::new(RGB::named(rltk::RED), RGB::named(rltk::BLACK)), + ); + y -= 1; + } + } +} + +pub fn draw_ui(ecs: &World, ctx : &mut Rltk) { + let mut draw_batch = DrawBatch::new(); + let player_entity = ecs.fetch::(); + + box_framework(&mut draw_batch); + map_label(ecs, &mut draw_batch); + draw_stats(ecs, &mut draw_batch, &player_entity); + draw_attributes(ecs, &mut draw_batch, &player_entity); + initiative_weight(ecs, &mut draw_batch, &player_entity); + let mut y = equipped(ecs, &mut draw_batch, &player_entity); + y += consumables(ecs, &mut draw_batch, &player_entity, y); + spells(ecs, &mut draw_batch, &player_entity, y); + status(ecs, &mut draw_batch, &player_entity); + gamelog::print_log(&mut rltk::BACKEND_INTERNAL.lock().consoles[1].console, Point::new(1, 23)); + draw_tooltips(ecs, ctx); + + draw_batch.submit(5000); +} \ No newline at end of file diff --git a/chapter-75-darkplaza/src/gui/identify_menu.rs b/chapter-75-darkplaza/src/gui/identify_menu.rs new file mode 100644 index 00000000..05592818 --- /dev/null +++ b/chapter-75-darkplaza/src/gui/identify_menu.rs @@ -0,0 +1,56 @@ +use rltk::prelude::*; +use specs::prelude::*; +use crate::{Name, State, InBackpack, Equipped, MasterDungeonMap, Item, ObfuscatedName }; +use super::{get_item_display_name, item_result_menu, ItemMenuResult}; + +pub fn identify_menu(gs : &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option) { + let mut draw_batch = DrawBatch::new(); + + let player_entity = gs.ecs.fetch::(); + let equipped = gs.ecs.read_storage::(); + let backpack = gs.ecs.read_storage::(); + let entities = gs.ecs.entities(); + let item_components = gs.ecs.read_storage::(); + let names = gs.ecs.read_storage::(); + let dm = gs.ecs.fetch::(); + let obfuscated = gs.ecs.read_storage::(); + + let mut items : Vec<(Entity, String)> = Vec::new(); + (&entities, &item_components).join() + .filter(|(item_entity,_item)| { + let mut keep = false; + if let Some(bp) = backpack.get(*item_entity) { + if bp.owner == *player_entity { + if let Some(name) = names.get(*item_entity) { + if obfuscated.get(*item_entity).is_some() && !dm.identified_items.contains(&name.name) { + keep = true; + } + } + } + } + // It's equipped, so we know it's cursed + if let Some(equip) = equipped.get(*item_entity) { + if equip.owner == *player_entity { + if let Some(name) = names.get(*item_entity) { + if obfuscated.get(*item_entity).is_some() && !dm.identified_items.contains(&name.name) { + keep = true; + } + } + } + } + keep + }) + .for_each(|item| { + items.push((item.0, get_item_display_name(&gs.ecs, item.0))) + }); + + let result = item_result_menu( + &mut draw_batch, + "Inventory", + items.len(), + &items, + ctx.key + ); + draw_batch.submit(6000); + result +} \ No newline at end of file diff --git a/chapter-75-darkplaza/src/gui/inventory_menu.rs b/chapter-75-darkplaza/src/gui/inventory_menu.rs new file mode 100644 index 00000000..58d824f2 --- /dev/null +++ b/chapter-75-darkplaza/src/gui/inventory_menu.rs @@ -0,0 +1,32 @@ +use rltk::prelude::*; +use specs::prelude::*; +use crate::{State, InBackpack }; +use super::{get_item_display_name, item_result_menu}; + +#[derive(PartialEq, Copy, Clone)] +pub enum ItemMenuResult { Cancel, NoResponse, Selected } + +pub fn show_inventory(gs : &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option) { + let player_entity = gs.ecs.fetch::(); + let backpack = gs.ecs.read_storage::(); + let entities = gs.ecs.entities(); + + let mut draw_batch = DrawBatch::new(); + + let mut items : Vec<(Entity, String)> = Vec::new(); + (&entities, &backpack).join() + .filter(|item| item.1.owner == *player_entity ) + .for_each(|item| { + items.push((item.0, get_item_display_name(&gs.ecs, item.0))) + }); + + let result = item_result_menu( + &mut draw_batch, + "Inventory", + items.len(), + &items, + ctx.key + ); + draw_batch.submit(6000); + result +} diff --git a/chapter-75-darkplaza/src/gui/item_render.rs b/chapter-75-darkplaza/src/gui/item_render.rs new file mode 100644 index 00000000..44dd5554 --- /dev/null +++ b/chapter-75-darkplaza/src/gui/item_render.rs @@ -0,0 +1,49 @@ +use rltk::prelude::*; +use specs::prelude::*; +use crate::{Name, Consumable, MagicItem, MagicItemClass, ObfuscatedName, CursedItem }; + +pub fn get_item_color(ecs : &World, item : Entity) -> RGB { + let dm = ecs.fetch::(); + if let Some(name) = ecs.read_storage::().get(item) { + if ecs.read_storage::().get(item).is_some() && dm.identified_items.contains(&name.name) { + return RGB::from_f32(1.0, 0.0, 0.0); + } + } + + if let Some(magic) = ecs.read_storage::().get(item) { + match magic.class { + MagicItemClass::Common => return RGB::from_f32(0.5, 1.0, 0.5), + MagicItemClass::Rare => return RGB::from_f32(0.0, 1.0, 1.0), + MagicItemClass::Legendary => return RGB::from_f32(0.71, 0.15, 0.93) + } + } + RGB::from_f32(1.0, 1.0, 1.0) +} + +pub fn get_item_display_name(ecs: &World, item : Entity) -> String { + if let Some(name) = ecs.read_storage::().get(item) { + if ecs.read_storage::().get(item).is_some() { + let dm = ecs.fetch::(); + if dm.identified_items.contains(&name.name) { + if let Some(c) = ecs.read_storage::().get(item) { + if c.max_charges > 1 { + format!("{} ({})", name.name.clone(), c.charges).to_string() + } else { + name.name.clone() + } + } else { + name.name.clone() + } + } else if let Some(obfuscated) = ecs.read_storage::().get(item) { + obfuscated.name.clone() + } else { + "Unidentified magic item".to_string() + } + } else { + name.name.clone() + } + + } else { + "Nameless item (bug)".to_string() + } +} \ No newline at end of file diff --git a/chapter-75-darkplaza/src/gui/main_menu.rs b/chapter-75-darkplaza/src/gui/main_menu.rs new file mode 100644 index 00000000..e03b1675 --- /dev/null +++ b/chapter-75-darkplaza/src/gui/main_menu.rs @@ -0,0 +1,86 @@ +use rltk::prelude::*; +use crate::{State, RunState, rex_assets::RexAssets }; + +#[derive(PartialEq, Copy, Clone)] +pub enum MainMenuSelection { NewGame, LoadGame, Quit } + +#[derive(PartialEq, Copy, Clone)] +pub enum MainMenuResult { NoSelection{ selected : MainMenuSelection }, Selected{ selected: MainMenuSelection } } + +pub fn main_menu(gs : &mut State, ctx : &mut Rltk) -> MainMenuResult { + let mut draw_batch = DrawBatch::new(); + let save_exists = crate::saveload_system::does_save_exist(); + let runstate = gs.ecs.fetch::(); + let assets = gs.ecs.fetch::(); + ctx.render_xp_sprite(&assets.menu, 0, 0); + + draw_batch.draw_double_box(Rect::with_size(24, 18, 31, 10), ColorPair::new(RGB::named(rltk::WHEAT), RGB::named(rltk::BLACK))); + + draw_batch.print_color_centered(20, "Rust Roguelike Tutorial", ColorPair::new(RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK))); + draw_batch.print_color_centered(21, "by Herbert Wolverson", ColorPair::new(RGB::named(rltk::CYAN), RGB::named(rltk::BLACK))); + draw_batch.print_color_centered(22, "Use Up/Down Arrows and Enter", ColorPair::new(RGB::named(rltk::GRAY), RGB::named(rltk::BLACK))); + + let mut y = 24; + if let RunState::MainMenu{ menu_selection : selection } = *runstate { + if selection == MainMenuSelection::NewGame { + draw_batch.print_color_centered(y, "Begin New Game", ColorPair::new(RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK))); + } else { + draw_batch.print_color_centered(y, "Begin New Game", ColorPair::new(RGB::named(rltk::WHITE), RGB::named(rltk::BLACK))); + } + y += 1; + + if save_exists { + if selection == MainMenuSelection::LoadGame { + draw_batch.print_color_centered(y, "Load Game", ColorPair::new(RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK))); + } else { + draw_batch.print_color_centered(y, "Load Game", ColorPair::new(RGB::named(rltk::WHITE), RGB::named(rltk::BLACK))); + } + y += 1; + } + + if selection == MainMenuSelection::Quit { + draw_batch.print_color_centered(y, "Quit", ColorPair::new(RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK))); + } else { + draw_batch.print_color_centered(y, "Quit", ColorPair::new(RGB::named(rltk::WHITE), RGB::named(rltk::BLACK))); + } + + draw_batch.submit(6000); + + match ctx.key { + None => return MainMenuResult::NoSelection{ selected: selection }, + Some(key) => { + match key { + VirtualKeyCode::Escape => { return MainMenuResult::NoSelection{ selected: MainMenuSelection::Quit } } + VirtualKeyCode::Up => { + let mut newselection; + match selection { + MainMenuSelection::NewGame => newselection = MainMenuSelection::Quit, + MainMenuSelection::LoadGame => newselection = MainMenuSelection::NewGame, + MainMenuSelection::Quit => newselection = MainMenuSelection::LoadGame + } + if newselection == MainMenuSelection::LoadGame && !save_exists { + newselection = MainMenuSelection::NewGame; + } + return MainMenuResult::NoSelection{ selected: newselection } + } + VirtualKeyCode::Down => { + let mut newselection; + match selection { + MainMenuSelection::NewGame => newselection = MainMenuSelection::LoadGame, + MainMenuSelection::LoadGame => newselection = MainMenuSelection::Quit, + MainMenuSelection::Quit => newselection = MainMenuSelection::NewGame + } + if newselection == MainMenuSelection::LoadGame && !save_exists { + newselection = MainMenuSelection::Quit; + } + return MainMenuResult::NoSelection{ selected: newselection } + } + VirtualKeyCode::Return => return MainMenuResult::Selected{ selected : selection }, + _ => return MainMenuResult::NoSelection{ selected: selection } + } + } + } + } + + MainMenuResult::NoSelection { selected: MainMenuSelection::NewGame } +} diff --git a/chapter-75-darkplaza/src/gui/menus.rs b/chapter-75-darkplaza/src/gui/menus.rs new file mode 100644 index 00000000..bcb4686a --- /dev/null +++ b/chapter-75-darkplaza/src/gui/menus.rs @@ -0,0 +1,88 @@ +use rltk::prelude::*; +use specs::prelude::*; +use super::ItemMenuResult; + +pub fn menu_box(draw_batch: &mut DrawBatch, x: i32, y: i32, width: i32, title: T) { + draw_batch.draw_box( + Rect::with_size(x, y-2, 31, width), + ColorPair::new(RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)) + ); + draw_batch.print_color( + Point::new(18, y-2), + &title.to_string(), + ColorPair::new(RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK)) + ); +} + +pub fn menu_option(draw_batch: &mut DrawBatch, x: i32, y: i32, hotkey: rltk::FontCharType, text: T) { + draw_batch.set( + Point::new(x, y), + ColorPair::new(RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)), + rltk::to_cp437('(') + ); + draw_batch.set( + Point::new(x+1, y), + ColorPair::new(RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK)), + hotkey + ); + draw_batch.set( + Point::new(x+2, y), + ColorPair::new(RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)), + rltk::to_cp437(')') + ); + draw_batch.print_color( + Point::new(x+5, y), + &text.to_string(), + ColorPair::new(RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK)) + ); +} + +pub fn item_result_menu( + draw_batch: &mut DrawBatch, + title: S, + count: usize, + items: &[(Entity, String)], + key: Option +) -> (ItemMenuResult, Option) { + + let mut y = (25 - (count / 2)) as i32; + draw_batch.draw_box( + Rect::with_size(15, y-2, 31, (count+3) as i32), + ColorPair::new(RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)) + ); + draw_batch.print_color( + Point::new(18, y-2), + &title.to_string(), + ColorPair::new(RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK)) + ); + draw_batch.print_color( + Point::new(18, y+count as i32+1), + "ESCAPE to cancel", + ColorPair::new(RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK)) + ); + + let mut item_list : Vec = Vec::new(); + let mut j = 0; + for item in items { + menu_option(draw_batch, 17, y, 97+j as rltk::FontCharType, &item.1); + item_list.push(item.0); + y += 1; + j += 1; + } + + match key { + None => (ItemMenuResult::NoResponse, None), + Some(key) => { + match key { + VirtualKeyCode::Escape => { (ItemMenuResult::Cancel, None) } + _ => { + let selection = rltk::letter_to_option(key); + if selection > -1 && selection < count as i32 { + return (ItemMenuResult::Selected, Some(item_list[selection as usize])); + } + (ItemMenuResult::NoResponse, None) + } + } + } + } +} \ No newline at end of file diff --git a/chapter-75-darkplaza/src/gui/mod.rs b/chapter-75-darkplaza/src/gui/mod.rs new file mode 100644 index 00000000..6a1cf892 --- /dev/null +++ b/chapter-75-darkplaza/src/gui/mod.rs @@ -0,0 +1,29 @@ +mod item_render; +pub use item_render::*; +mod hud; +pub use hud::*; +mod tooltips; +pub use tooltips::*; +mod inventory_menu; +pub use inventory_menu::*; +mod drop_item_menu; +pub use drop_item_menu::*; +mod remove_item_menu; +pub use remove_item_menu::*; +mod remove_curse_menu; +pub use remove_curse_menu::*; +mod identify_menu; +pub use identify_menu::*; +mod ranged_target; +pub use ranged_target::*; +mod main_menu; +pub use main_menu::*; +mod game_over_menu; +pub use game_over_menu::*; +mod cheat_menu; +pub use cheat_menu::*; +mod vendor_menu; +pub use vendor_menu::*; +mod menus; +pub use menus::*; + diff --git a/chapter-75-darkplaza/src/gui/ranged_target.rs b/chapter-75-darkplaza/src/gui/ranged_target.rs new file mode 100644 index 00000000..3b311963 --- /dev/null +++ b/chapter-75-darkplaza/src/gui/ranged_target.rs @@ -0,0 +1,62 @@ +use rltk::prelude::*; +use specs::prelude::*; +use crate::{State, camera, Viewshed }; +use super::ItemMenuResult; + +pub fn ranged_target(gs : &mut State, ctx : &mut Rltk, range : i32) -> (ItemMenuResult, Option) { + let (min_x, max_x, min_y, max_y) = camera::get_screen_bounds(&gs.ecs, ctx); + let player_entity = gs.ecs.fetch::(); + let player_pos = gs.ecs.fetch::(); + let viewsheds = gs.ecs.read_storage::(); + + let mut draw_batch = DrawBatch::new(); + + draw_batch.print_color( + Point::new(5, 0), + "Select Target:", + ColorPair::new(RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK)) + ); + + // Highlight available target cells + let mut available_cells = Vec::new(); + let visible = viewsheds.get(*player_entity); + if let Some(visible) = visible { + // We have a viewshed + for idx in visible.visible_tiles.iter() { + let distance = rltk::DistanceAlg::Pythagoras.distance2d(*player_pos, *idx); + if distance <= range as f32 { + let screen_x = idx.x - min_x; + let screen_y = idx.y - min_y; + if screen_x > 1 && screen_x < (max_x - min_x)-1 && screen_y > 1 && screen_y < (max_y - min_y)-1 { + draw_batch.set_bg(Point::new(screen_x, screen_y), RGB::named(rltk::BLUE)); + available_cells.push(idx); + } + } + } + } else { + return (ItemMenuResult::Cancel, None); + } + + // Draw mouse cursor + let mouse_pos = ctx.mouse_pos(); + let mut mouse_map_pos = mouse_pos; + mouse_map_pos.0 += min_x - 1; + mouse_map_pos.1 += min_y - 1; + let mut valid_target = false; + for idx in available_cells.iter() { if idx.x == mouse_map_pos.0 && idx.y == mouse_map_pos.1 { valid_target = true; } } + if valid_target { + draw_batch.set_bg(Point::new(mouse_pos.0, mouse_pos.1), RGB::named(rltk::CYAN)); + if ctx.left_click { + return (ItemMenuResult::Selected, Some(Point::new(mouse_map_pos.0, mouse_map_pos.1))); + } + } else { + draw_batch.set_bg(Point::new(mouse_pos.0, mouse_pos.1), RGB::named(rltk::RED)); + if ctx.left_click { + return (ItemMenuResult::Cancel, None); + } + } + + draw_batch.submit(5000); + + (ItemMenuResult::NoResponse, None) +} \ No newline at end of file diff --git a/chapter-75-darkplaza/src/gui/remove_curse_menu.rs b/chapter-75-darkplaza/src/gui/remove_curse_menu.rs new file mode 100644 index 00000000..01206d6f --- /dev/null +++ b/chapter-75-darkplaza/src/gui/remove_curse_menu.rs @@ -0,0 +1,52 @@ +use rltk::prelude::*; +use specs::prelude::*; +use crate::{Name, State, InBackpack, Equipped, MasterDungeonMap, CursedItem, Item }; +use super::{get_item_display_name, item_result_menu, ItemMenuResult}; + +pub fn remove_curse_menu(gs : &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option) { + let player_entity = gs.ecs.fetch::(); + let equipped = gs.ecs.read_storage::(); + let backpack = gs.ecs.read_storage::(); + let entities = gs.ecs.entities(); + let item_components = gs.ecs.read_storage::(); + let cursed = gs.ecs.read_storage::(); + let names = gs.ecs.read_storage::(); + let dm = gs.ecs.fetch::(); + + let mut draw_batch = DrawBatch::new(); + + let mut items : Vec<(Entity, String)> = Vec::new(); + (&entities, &item_components, &cursed).join() + .filter(|(item_entity,_item,_cursed)| { + let mut keep = false; + if let Some(bp) = backpack.get(*item_entity) { + if bp.owner == *player_entity { + if let Some(name) = names.get(*item_entity) { + if dm.identified_items.contains(&name.name) { + keep = true; + } + } + } + } + // It's equipped, so we know it's cursed + if let Some(equip) = equipped.get(*item_entity) { + if equip.owner == *player_entity { + keep = true; + } + } + keep + }) + .for_each(|item| { + items.push((item.0, get_item_display_name(&gs.ecs, item.0))) + }); + + let result = item_result_menu( + &mut draw_batch, + "Inventory", + items.len(), + &items, + ctx.key + ); + draw_batch.submit(6000); + result +} diff --git a/chapter-75-darkplaza/src/gui/remove_item_menu.rs b/chapter-75-darkplaza/src/gui/remove_item_menu.rs new file mode 100644 index 00000000..3fd5ad00 --- /dev/null +++ b/chapter-75-darkplaza/src/gui/remove_item_menu.rs @@ -0,0 +1,29 @@ +use rltk::prelude::*; +use specs::prelude::*; +use crate::{State, Equipped }; +use super::{get_item_display_name, ItemMenuResult, item_result_menu}; + +pub fn remove_item_menu(gs : &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option) { + let mut draw_batch = DrawBatch::new(); + + let player_entity = gs.ecs.fetch::(); + let backpack = gs.ecs.read_storage::(); + let entities = gs.ecs.entities(); + + let mut items : Vec<(Entity, String)> = Vec::new(); + (&entities, &backpack).join() + .filter(|item| item.1.owner == *player_entity ) + .for_each(|item| { + items.push((item.0, get_item_display_name(&gs.ecs, item.0))) + }); + + let result = item_result_menu( + &mut draw_batch, + "Remove which item?", + items.len(), + &items, + ctx.key + ); + draw_batch.submit(6000); + result +} \ No newline at end of file diff --git a/chapter-75-darkplaza/src/gui/tooltips.rs b/chapter-75-darkplaza/src/gui/tooltips.rs new file mode 100644 index 00000000..dbc064b0 --- /dev/null +++ b/chapter-75-darkplaza/src/gui/tooltips.rs @@ -0,0 +1,151 @@ +use rltk::prelude::*; +use specs::prelude::*; +use crate::{Pools, Map, Name, Hidden, camera, Attributes, StatusEffect, Duration }; +use super::get_item_display_name; + +struct Tooltip { + lines : Vec +} + +impl Tooltip { + fn new() -> Tooltip { + Tooltip { lines : Vec::new() } + } + + fn add(&mut self, line : S) { + self.lines.push(line.to_string()); + } + + fn width(&self) -> i32 { + let mut max = 0; + for s in self.lines.iter() { + if s.len() > max { + max = s.len(); + } + } + max as i32 + 2i32 + } + + fn height(&self) -> i32 { self.lines.len() as i32 + 2i32 } + + fn render(&self, draw_batch : &mut DrawBatch, x : i32, y : i32) { + let box_gray : RGB = RGB::from_hex("#999999").expect("Oops"); + let light_gray : RGB = RGB::from_hex("#DDDDDD").expect("Oops"); + let white = RGB::named(rltk::WHITE); + let black = RGB::named(rltk::BLACK); + draw_batch.draw_box(Rect::with_size(x, y, self.width()-1, self.height()-1), ColorPair::new(white, box_gray)); + for (i,s) in self.lines.iter().enumerate() { + let col = if i == 0 { white } else { light_gray }; + draw_batch.print_color(Point::new(x+1, y+i as i32+1), &s, ColorPair::new(col, black)); + } + } +} + +pub fn draw_tooltips(ecs: &World, ctx : &mut Rltk) { + let mut draw_batch = DrawBatch::new(); + + let (min_x, _max_x, min_y, _max_y) = camera::get_screen_bounds(ecs, ctx); + let map = ecs.fetch::(); + let hidden = ecs.read_storage::(); + let attributes = ecs.read_storage::(); + let pools = ecs.read_storage::(); + + let mouse_pos = ctx.mouse_pos(); + let mut mouse_map_pos = mouse_pos; + mouse_map_pos.0 += min_x - 1; + mouse_map_pos.1 += min_y - 1; + if mouse_pos.0 < 1 || mouse_pos.0 > 49 || mouse_pos.1 < 1 || mouse_pos.1 > 40 { + return; + } + if mouse_map_pos.0 >= map.width-1 || mouse_map_pos.1 >= map.height-1 || mouse_map_pos.0 < 1 || mouse_map_pos.1 < 1 + { + return; + } + if !map.in_bounds(rltk::Point::new(mouse_map_pos.0, mouse_map_pos.1)) { return; } + let mouse_idx = map.xy_idx(mouse_map_pos.0, mouse_map_pos.1); + if !map.visible_tiles[mouse_idx] { return; } + + let mut tip_boxes : Vec = Vec::new(); + crate::spatial::for_each_tile_content(mouse_idx, |entity| { + if hidden.get(entity).is_some() { return; } + let mut tip = Tooltip::new(); + tip.add(get_item_display_name(ecs, entity)); + + // Comment on attributes + let attr = attributes.get(entity); + if let Some(attr) = attr { + let mut s = "".to_string(); + if attr.might.bonus < 0 { s += "Weak. " }; + if attr.might.bonus > 0 { s += "Strong. " }; + if attr.quickness.bonus < 0 { s += "Clumsy. " }; + if attr.quickness.bonus > 0 { s += "Agile. " }; + if attr.fitness.bonus < 0 { s += "Unheathy. " }; + if attr.fitness.bonus > 0 { s += "Healthy." }; + if attr.intelligence.bonus < 0 { s += "Unintelligent. "}; + if attr.intelligence.bonus > 0 { s += "Smart. "}; + if s.is_empty() { + s = "Quite Average".to_string(); + } + tip.add(s); + } + + // Comment on pools + let stat = pools.get(entity); + if let Some(stat) = stat { + tip.add(format!("Level: {}", stat.level)); + } + + // Status effects + let statuses = ecs.read_storage::(); + let durations = ecs.read_storage::(); + let names = ecs.read_storage::(); + for (status, duration, name) in (&statuses, &durations, &names).join() { + if status.target == entity { + tip.add(format!("{} ({})", name.name, duration.turns)); + } + } + + tip_boxes.push(tip); + }); + + if tip_boxes.is_empty() { return; } + + let box_gray : RGB = RGB::from_hex("#999999").expect("Oops"); + let white = RGB::named(rltk::WHITE); + + let arrow; + let arrow_x; + let arrow_y = mouse_pos.1; + if mouse_pos.0 < 40 { + // Render to the left + arrow = to_cp437('→'); + arrow_x = mouse_pos.0 - 1; + } else { + // Render to the right + arrow = to_cp437('←'); + arrow_x = mouse_pos.0 + 1; + } + draw_batch.set(Point::new(arrow_x, arrow_y), ColorPair::new(white, box_gray), arrow); + + let mut total_height = 0; + for tt in tip_boxes.iter() { + total_height += tt.height(); + } + + let mut y = mouse_pos.1 - (total_height / 2); + while y + (total_height/2) > 50 { + y -= 1; + } + + for tt in tip_boxes.iter() { + let x = if mouse_pos.0 < 40 { + mouse_pos.0 - (1 + tt.width()) + } else { + mouse_pos.0 + (1 + tt.width()) + }; + tt.render(&mut draw_batch, x, y); + y += tt.height(); + } + + draw_batch.submit(7000); +} diff --git a/chapter-75-darkplaza/src/gui/vendor_menu.rs b/chapter-75-darkplaza/src/gui/vendor_menu.rs new file mode 100644 index 00000000..0ed20040 --- /dev/null +++ b/chapter-75-darkplaza/src/gui/vendor_menu.rs @@ -0,0 +1,118 @@ +use rltk::prelude::*; +use specs::prelude::*; +use crate::{Name, State, InBackpack, VendorMode, Vendor, Item }; +use super::{get_item_display_name, get_item_color, menu_box}; + +#[derive(PartialEq, Copy, Clone)] +pub enum VendorResult { NoResponse, Cancel, Sell, BuyMode, SellMode, Buy } + +fn vendor_sell_menu(gs : &mut State, ctx : &mut Rltk, _vendor : Entity, _mode : VendorMode) -> (VendorResult, Option, Option, Option) { + let mut draw_batch = DrawBatch::new(); + let player_entity = gs.ecs.fetch::(); + let names = gs.ecs.read_storage::(); + let backpack = gs.ecs.read_storage::(); + let items = gs.ecs.read_storage::(); + let entities = gs.ecs.entities(); + + let inventory = (&backpack, &names).join().filter(|item| item.0.owner == *player_entity ); + let count = inventory.count(); + + let mut y = (25 - (count / 2)) as i32; + menu_box(&mut draw_batch, 15, y, (count+3) as i32, "Sell Which Item? (space to switch to buy mode)"); + draw_batch.print_color( + Point::new(18, y+count as i32+1), + "ESCAPE to cancel", + ColorPair::new(RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK)) + ); + + let mut equippable : Vec = Vec::new(); + let mut j = 0; + for (entity, _pack, item) in (&entities, &backpack, &items).join().filter(|item| item.1.owner == *player_entity ) { + draw_batch.set(Point::new(17, y), ColorPair::new(RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)), rltk::to_cp437('(')); + draw_batch.set(Point::new(18, y), ColorPair::new(RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK)), 97+j as rltk::FontCharType); + draw_batch.set(Point::new(19, y), ColorPair::new(RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)), rltk::to_cp437(')')); + + draw_batch.print_color( + Point::new(21, y), + &get_item_display_name(&gs.ecs, entity), + ColorPair::new(get_item_color(&gs.ecs, entity), RGB::from_f32(0.0, 0.0, 0.0)) + ); + draw_batch.print(Point::new(50, y), &format!("{:.1} gp", item.base_value * 0.8)); + equippable.push(entity); + y += 1; + j += 1; + } + + draw_batch.submit(6000); + + match ctx.key { + None => (VendorResult::NoResponse, None, None, None), + Some(key) => { + match key { + VirtualKeyCode::Space => { (VendorResult::BuyMode, None, None, None) } + VirtualKeyCode::Escape => { (VendorResult::Cancel, None, None, None) } + _ => { + let selection = rltk::letter_to_option(key); + if selection > -1 && selection < count as i32 { + return (VendorResult::Sell, Some(equippable[selection as usize]), None, None); + } + (VendorResult::NoResponse, None, None, None) + } + } + } + } +} + +fn vendor_buy_menu(gs : &mut State, ctx : &mut Rltk, vendor : Entity, _mode : VendorMode) -> (VendorResult, Option, Option, Option) { + use crate::raws::*; + let mut draw_batch = DrawBatch::new(); + + let vendors = gs.ecs.read_storage::(); + + let inventory = crate::raws::get_vendor_items(&vendors.get(vendor).unwrap().categories, &RAWS.lock().unwrap()); + let count = inventory.len(); + + let mut y = (25 - (count / 2)) as i32; + menu_box(&mut draw_batch, 15, y, (count+3) as i32, "Buy Which Item? (space to switch to sell mode)"); + draw_batch.print_color( + Point::new(18, y+count as i32+1), + "ESCAPE to cancel", + ColorPair::new(RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK)) + ); + + for (j,sale) in inventory.iter().enumerate() { + draw_batch.set(Point::new(17, y), ColorPair::new(RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)), rltk::to_cp437('(')); + draw_batch.set(Point::new(18, y), ColorPair::new(RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK)), 97+j as rltk::FontCharType); + draw_batch.set(Point::new(19, y), ColorPair::new(RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)), rltk::to_cp437(')')); + + draw_batch.print(Point::new(21, y), &sale.0); + draw_batch.print(Point::new(50, y), &format!("{:.1} gp", sale.1 * 1.2)); + y += 1; + } + + draw_batch.submit(6000); + + match ctx.key { + None => (VendorResult::NoResponse, None, None, None), + Some(key) => { + match key { + VirtualKeyCode::Space => { (VendorResult::SellMode, None, None, None) } + VirtualKeyCode::Escape => { (VendorResult::Cancel, None, None, None) } + _ => { + let selection = rltk::letter_to_option(key); + if selection > -1 && selection < count as i32 { + return (VendorResult::Buy, None, Some(inventory[selection as usize].0.clone()), Some(inventory[selection as usize].1)); + } + (VendorResult::NoResponse, None, None, None) + } + } + } + } +} + +pub fn show_vendor_menu(gs : &mut State, ctx : &mut Rltk, vendor : Entity, mode : VendorMode) -> (VendorResult, Option, Option, Option) { + match mode { + VendorMode::Buy => vendor_buy_menu(gs, ctx, vendor, mode), + VendorMode::Sell => vendor_sell_menu(gs, ctx, vendor, mode) + } +} diff --git a/chapter-75-darkplaza/src/main.rs b/chapter-75-darkplaza/src/main.rs new file mode 100644 index 00000000..2cc86ff9 --- /dev/null +++ b/chapter-75-darkplaza/src/main.rs @@ -0,0 +1,575 @@ +extern crate serde; +use rltk::{GameState, Rltk, Point}; +use specs::prelude::*; +use specs::saveload::{SimpleMarker, SimpleMarkerAllocator}; + +mod components; +pub use components::*; +mod map; +pub use map::*; +mod player; +use player::*; +mod rect; +pub use rect::Rect; +mod damage_system; +mod gui; +mod gamelog; +mod spawner; +pub mod saveload_system; +pub mod random_table; +pub mod rex_assets; +pub mod map_builders; +pub mod raws; +mod gamesystem; +pub use gamesystem::*; +pub mod effects; +#[macro_use] +extern crate lazy_static; +mod systems; +pub mod rng; +pub mod spatial; + +const SHOW_MAPGEN_VISUALIZER : bool = false; +const SHOW_FPS : bool = true; + +#[derive(PartialEq, Copy, Clone)] +pub enum VendorMode { Buy, Sell } + +#[derive(PartialEq, Copy, Clone)] +pub enum RunState { + AwaitingInput, + PreRun, + Ticking, + ShowInventory, + ShowDropItem, + ShowTargeting { range : i32, item : Entity}, + MainMenu { menu_selection : gui::MainMenuSelection }, + SaveGame, + NextLevel, + PreviousLevel, + TownPortal, + ShowRemoveItem, + GameOver, + MagicMapReveal { row : i32 }, + MapGeneration, + ShowCheatMenu, + ShowVendor { vendor: Entity, mode : VendorMode }, + TeleportingToOtherLevel { x: i32, y: i32, depth: i32 }, + ShowRemoveCurse, + ShowIdentify +} + +pub struct State { + pub ecs: World, + mapgen_next_state : Option, + mapgen_history : Vec, + mapgen_index : usize, + mapgen_timer : f32, + dispatcher : Box +} + +impl State { + fn run_systems(&mut self) { + self.dispatcher.run_now(&mut self.ecs); + self.ecs.maintain(); + } +} + +impl GameState for State { + #[allow(clippy::cognitive_complexity)] + fn tick(&mut self, ctx : &mut Rltk) { + let mut newrunstate; + { + let runstate = self.ecs.fetch::(); + newrunstate = *runstate; + } + + ctx.set_active_console(1); + ctx.cls(); + ctx.set_active_console(0); + ctx.cls(); + systems::particle_system::update_particles(&mut self.ecs, ctx); + + match newrunstate { + RunState::MainMenu{..} => {} + RunState::GameOver{..} => {} + _ => { + camera::render_camera(&self.ecs, ctx); + gui::draw_ui(&self.ecs, ctx); + } + } + + match newrunstate { + RunState::MapGeneration => { + if !SHOW_MAPGEN_VISUALIZER { + newrunstate = self.mapgen_next_state.unwrap(); + } else { + ctx.cls(); + if self.mapgen_index < self.mapgen_history.len() && self.mapgen_index < self.mapgen_history.len() { camera::render_debug_map(&self.mapgen_history[self.mapgen_index], ctx); } + + self.mapgen_timer += ctx.frame_time_ms; + if self.mapgen_timer > 250.0 { + self.mapgen_timer = 0.0; + self.mapgen_index += 1; + if self.mapgen_index >= self.mapgen_history.len() { + //self.mapgen_index -= 1; + newrunstate = self.mapgen_next_state.unwrap(); + } + } + } + } + RunState::PreRun => { + self.run_systems(); + self.ecs.maintain(); + newrunstate = RunState::AwaitingInput; + } + RunState::AwaitingInput => { + newrunstate = player_input(self, ctx); + if newrunstate != RunState::AwaitingInput { + crate::gamelog::record_event("Turn", 1); + } + } + RunState::Ticking => { + let mut should_change_target = false; + while newrunstate == RunState::Ticking { + self.run_systems(); + self.ecs.maintain(); + match *self.ecs.fetch::() { + RunState::AwaitingInput => { + newrunstate = RunState::AwaitingInput; + should_change_target = true; + } + RunState::MagicMapReveal{ .. } => newrunstate = RunState::MagicMapReveal{ row: 0 }, + RunState::TownPortal => newrunstate = RunState::TownPortal, + RunState::TeleportingToOtherLevel{ x, y, depth } => newrunstate = RunState::TeleportingToOtherLevel{ x, y, depth }, + RunState::ShowRemoveCurse => newrunstate = RunState::ShowRemoveCurse, + RunState::ShowIdentify => newrunstate = RunState::ShowIdentify, + _ => newrunstate = RunState::Ticking + } + } + if should_change_target { + player::end_turn_targeting(&mut self.ecs); + } + } + RunState::ShowInventory => { + let result = gui::show_inventory(self, ctx); + match result.0 { + gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput, + gui::ItemMenuResult::NoResponse => {} + gui::ItemMenuResult::Selected => { + let item_entity = result.1.unwrap(); + let is_ranged = self.ecs.read_storage::(); + let is_item_ranged = is_ranged.get(item_entity); + if let Some(is_item_ranged) = is_item_ranged { + newrunstate = RunState::ShowTargeting{ range: is_item_ranged.range, item: item_entity }; + } else { + let mut intent = self.ecs.write_storage::(); + intent.insert(*self.ecs.fetch::(), WantsToUseItem{ item: item_entity, target: None }).expect("Unable to insert intent"); + newrunstate = RunState::Ticking; + } + } + } + } + RunState::ShowCheatMenu => { + let result = gui::show_cheat_mode(self, ctx); + match result { + gui::CheatMenuResult::Cancel => newrunstate = RunState::AwaitingInput, + gui::CheatMenuResult::NoResponse => {} + gui::CheatMenuResult::TeleportToExit => { + self.goto_level(1); + self.mapgen_next_state = Some(RunState::PreRun); + newrunstate = RunState::MapGeneration; + } + gui::CheatMenuResult::Heal => { + let player = self.ecs.fetch::(); + let mut pools = self.ecs.write_storage::(); + let mut player_pools = pools.get_mut(*player).unwrap(); + player_pools.hit_points.current = player_pools.hit_points.max; + newrunstate = RunState::AwaitingInput; + } + gui::CheatMenuResult::Reveal => { + let mut map = self.ecs.fetch_mut::(); + for v in map.revealed_tiles.iter_mut() { + *v = true; + } + newrunstate = RunState::AwaitingInput; + } + gui::CheatMenuResult::GodMode => { + let player = self.ecs.fetch::(); + let mut pools = self.ecs.write_storage::(); + let mut player_pools = pools.get_mut(*player).unwrap(); + player_pools.god_mode = true; + newrunstate = RunState::AwaitingInput; + } + } + } + RunState::ShowDropItem => { + let result = gui::drop_item_menu(self, ctx); + match result.0 { + gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput, + gui::ItemMenuResult::NoResponse => {} + gui::ItemMenuResult::Selected => { + let item_entity = result.1.unwrap(); + let mut intent = self.ecs.write_storage::(); + intent.insert(*self.ecs.fetch::(), WantsToDropItem{ item: item_entity }).expect("Unable to insert intent"); + newrunstate = RunState::Ticking; + } + } + } + RunState::ShowRemoveItem => { + let result = gui::remove_item_menu(self, ctx); + match result.0 { + gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput, + gui::ItemMenuResult::NoResponse => {} + gui::ItemMenuResult::Selected => { + let item_entity = result.1.unwrap(); + let mut intent = self.ecs.write_storage::(); + intent.insert(*self.ecs.fetch::(), WantsToRemoveItem{ item: item_entity }).expect("Unable to insert intent"); + newrunstate = RunState::Ticking; + } + } + } + RunState::ShowRemoveCurse => { + let result = gui::remove_curse_menu(self, ctx); + match result.0 { + gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput, + gui::ItemMenuResult::NoResponse => {} + gui::ItemMenuResult::Selected => { + let item_entity = result.1.unwrap(); + self.ecs.write_storage::().remove(item_entity); + newrunstate = RunState::Ticking; + } + } + } + RunState::ShowIdentify => { + let result = gui::identify_menu(self, ctx); + match result.0 { + gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput, + gui::ItemMenuResult::NoResponse => {} + gui::ItemMenuResult::Selected => { + let item_entity = result.1.unwrap(); + if let Some(name) = self.ecs.read_storage::().get(item_entity) { + let mut dm = self.ecs.fetch_mut::(); + dm.identified_items.insert(name.name.clone()); + } + newrunstate = RunState::Ticking; + } + } + } + RunState::ShowTargeting{range, item} => { + let result = gui::ranged_target(self, ctx, range); + match result.0 { + gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput, + gui::ItemMenuResult::NoResponse => {} + gui::ItemMenuResult::Selected => { + if self.ecs.read_storage::().get(item).is_some() { + let mut intent = self.ecs.write_storage::(); + intent.insert(*self.ecs.fetch::(), WantsToCastSpell{ spell: item, target: result.1 }).expect("Unable to insert intent"); + newrunstate = RunState::Ticking; + } else { + let mut intent = self.ecs.write_storage::(); + intent.insert(*self.ecs.fetch::(), WantsToUseItem{ item, target: result.1 }).expect("Unable to insert intent"); + newrunstate = RunState::Ticking; + } + } + } + } + RunState::ShowVendor{vendor, mode} => { + use crate::raws::*; + let result = gui::show_vendor_menu(self, ctx, vendor, mode); + match result.0 { + gui::VendorResult::Cancel => newrunstate = RunState::AwaitingInput, + gui::VendorResult::NoResponse => {} + gui::VendorResult::Sell => { + let price = self.ecs.read_storage::().get(result.1.unwrap()).unwrap().base_value * 0.8; + self.ecs.write_storage::().get_mut(*self.ecs.fetch::()).unwrap().gold += price; + self.ecs.delete_entity(result.1.unwrap()).expect("Unable to delete"); + } + gui::VendorResult::Buy => { + let tag = result.2.unwrap(); + let price = result.3.unwrap(); + let mut pools = self.ecs.write_storage::(); + let player_entity = self.ecs.fetch::(); + let mut identified = self.ecs.write_storage::(); + identified.insert(*player_entity, IdentifiedItem{ name : tag.clone() }).expect("Unable to insert"); + std::mem::drop(identified); + let player_pools = pools.get_mut(*player_entity).unwrap(); + std::mem::drop(player_entity); + if player_pools.gold >= price { + player_pools.gold -= price; + std::mem::drop(pools); + let player_entity = *self.ecs.fetch::(); + crate::raws::spawn_named_item(&RAWS.lock().unwrap(), &mut self.ecs, &tag, SpawnType::Carried{ by: player_entity }); + } + } + gui::VendorResult::BuyMode => newrunstate = RunState::ShowVendor{ vendor, mode: VendorMode::Buy }, + gui::VendorResult::SellMode => newrunstate = RunState::ShowVendor{ vendor, mode: VendorMode::Sell } + } + } + RunState::MainMenu{ .. } => { + let result = gui::main_menu(self, ctx); + match result { + gui::MainMenuResult::NoSelection{ selected } => newrunstate = RunState::MainMenu{ menu_selection: selected }, + gui::MainMenuResult::Selected{ selected } => { + match selected { + gui::MainMenuSelection::NewGame => newrunstate = RunState::PreRun, + gui::MainMenuSelection::LoadGame => { + saveload_system::load_game(&mut self.ecs); + newrunstate = RunState::AwaitingInput; + saveload_system::delete_save(); + } + gui::MainMenuSelection::Quit => { ::std::process::exit(0); } + } + } + } + } + RunState::GameOver => { + let result = gui::game_over(ctx); + match result { + gui::GameOverResult::NoSelection => {} + gui::GameOverResult::QuitToMenu => { + self.game_over_cleanup(); + newrunstate = RunState::MapGeneration; + self.mapgen_next_state = Some(RunState::MainMenu{ menu_selection: gui::MainMenuSelection::NewGame }); + } + } + } + RunState::SaveGame => { + saveload_system::save_game(&mut self.ecs); + newrunstate = RunState::MainMenu{ menu_selection : gui::MainMenuSelection::LoadGame }; + } + RunState::NextLevel => { + self.goto_level(1); + self.mapgen_next_state = Some(RunState::PreRun); + newrunstate = RunState::MapGeneration; + } + RunState::PreviousLevel => { + self.goto_level(-1); + self.mapgen_next_state = Some(RunState::PreRun); + newrunstate = RunState::MapGeneration; + } + RunState::TownPortal => { + // Spawn the portal + spawner::spawn_town_portal(&mut self.ecs); + + // Transition + let map_depth = self.ecs.fetch::().depth; + let destination_offset = 0 - (map_depth-1); + self.goto_level(destination_offset); + self.mapgen_next_state = Some(RunState::PreRun); + newrunstate = RunState::MapGeneration; + } + RunState::TeleportingToOtherLevel{x, y, depth} => { + self.goto_level(depth-1); + let player_entity = self.ecs.fetch::(); + if let Some(pos) = self.ecs.write_storage::().get_mut(*player_entity) { + pos.x = x; + pos.y = y; + } + let mut ppos = self.ecs.fetch_mut::(); + ppos.x = x; + ppos.y = y; + self.mapgen_next_state = Some(RunState::PreRun); + newrunstate = RunState::MapGeneration; + } + RunState::MagicMapReveal{row} => { + let mut map = self.ecs.fetch_mut::(); + for x in 0..map.width { + let idx = map.xy_idx(x as i32,row); + map.revealed_tiles[idx] = true; + } + if row == map.height-1 { + newrunstate = RunState::Ticking; + } else { + newrunstate = RunState::MagicMapReveal{ row: row+1 }; + } + } + } + + { + let mut runwriter = self.ecs.write_resource::(); + *runwriter = newrunstate; + } + damage_system::delete_the_dead(&mut self.ecs); + + rltk::render_draw_buffer(ctx); + if SHOW_FPS { + ctx.print(1, 59, &format!("FPS: {}", ctx.fps)); + } + } +} + +impl State { + fn goto_level(&mut self, offset: i32) { + freeze_level_entities(&mut self.ecs); + + // Build a new map and place the player + let current_depth = self.ecs.fetch::().depth; + self.generate_world_map(current_depth + offset, offset); + + // Notify the player + gamelog::Logger::new().append("You change level.").log(); + } + + fn game_over_cleanup(&mut self) { + // Delete everything + let mut to_delete = Vec::new(); + for e in self.ecs.entities().join() { + to_delete.push(e); + } + for del in to_delete.iter() { + self.ecs.delete_entity(*del).expect("Deletion failed"); + } + + // Spawn a new player + { + let player_entity = spawner::player(&mut self.ecs, 0, 0); + let mut player_entity_writer = self.ecs.write_resource::(); + *player_entity_writer = player_entity; + } + + // Replace the world maps + self.ecs.insert(map::MasterDungeonMap::new()); + + // Build a new map and place the player + self.generate_world_map(1, 0); + } + + fn generate_world_map(&mut self, new_depth : i32, offset: i32) { + self.mapgen_index = 0; + self.mapgen_timer = 0.0; + self.mapgen_history.clear(); + let map_building_info = map::level_transition(&mut self.ecs, new_depth, offset); + if let Some(history) = map_building_info { + self.mapgen_history = history; + } else { + map::thaw_level_entities(&mut self.ecs); + } + + gamelog::clear_log(); + gamelog::Logger::new() + .append("Welcome to") + .color(rltk::CYAN) + .append("Rusty Roguelike") + .log(); + + gamelog::clear_events(); + } +} + +fn main() -> rltk::BError { + use rltk::RltkBuilder; + let mut context = RltkBuilder::simple(80, 60) + .unwrap() + .with_title("Roguelike Tutorial") + .with_font("vga8x16.png", 8, 16) + .with_sparse_console(80, 30, "vga8x16.png") + .with_vsync(false) + .build()?; + context.with_post_scanlines(true); + let mut gs = State { + ecs: World::new(), + mapgen_next_state : Some(RunState::MainMenu{ menu_selection: gui::MainMenuSelection::NewGame }), + mapgen_index : 0, + mapgen_history: Vec::new(), + mapgen_timer: 0.0, + dispatcher: systems::build() + }; + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::>(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.insert(SimpleMarkerAllocator::::new()); + + raws::load_raws(); + + gs.ecs.insert(map::MasterDungeonMap::new()); + gs.ecs.insert(Map::new(1, 64, 64, "New Map")); + gs.ecs.insert(Point::new(0, 0)); + let player_entity = spawner::player(&mut gs.ecs, 0, 0); + gs.ecs.insert(player_entity); + gs.ecs.insert(RunState::MapGeneration{} ); + gs.ecs.insert(systems::particle_system::ParticleBuilder::new()); + gs.ecs.insert(rex_assets::RexAssets::new()); + + gs.generate_world_map(1, 0); + + rltk::main_loop(context, gs) +} diff --git a/chapter-75-darkplaza/src/map/camera.rs b/chapter-75-darkplaza/src/map/camera.rs new file mode 100644 index 00000000..0832527b --- /dev/null +++ b/chapter-75-darkplaza/src/map/camera.rs @@ -0,0 +1,149 @@ +use specs::prelude::*; +use crate::{Map,Position,Renderable,Hidden,TileSize,Target}; +use rltk::prelude::*; +use crate::map::tile_glyph; + +pub fn get_screen_bounds(ecs: &World, _ctx: &mut Rltk) -> (i32, i32, i32, i32) { + let player_pos = ecs.fetch::(); + //let (x_chars, y_chars) = ctx.get_char_size(); + let (x_chars, y_chars) = (48, 44); + + let center_x = (x_chars / 2) as i32; + let center_y = (y_chars / 2) as i32; + + let min_x = player_pos.x - center_x; + let max_x = min_x + x_chars as i32; + let min_y = player_pos.y - center_y; + let max_y = min_y + y_chars as i32; + + (min_x, max_x, min_y, max_y) +} + +const SHOW_BOUNDARIES : bool = false; + +pub fn render_camera(ecs: &World, ctx : &mut Rltk) { + let mut draw_batch = DrawBatch::new(); + let map = ecs.fetch::(); + let (min_x, max_x, min_y, max_y) = get_screen_bounds(ecs, ctx); + + // Render the Map + + let map_width = map.width-1; + let map_height = map.height-1; + + for (y,ty) in (min_y .. max_y).enumerate() { + for (x,tx) in (min_x .. max_x).enumerate() { + if tx > 0 && tx < map_width && ty > 0 && ty < map_height { + let idx = map.xy_idx(tx, ty); + if map.revealed_tiles[idx] { + let (glyph, fg, bg) = tile_glyph(idx, &*map); + draw_batch.set( + Point::new(x+1, y+1), + ColorPair::new(fg, bg), + glyph + ); + } + } else if SHOW_BOUNDARIES { + draw_batch.set( + Point::new(x+1, y+1), + ColorPair::new(RGB::named(rltk::GRAY), RGB::named(rltk::BLACK)), + to_cp437('·') + ); + } + } + } + + // Render entities + let positions = ecs.read_storage::(); + let renderables = ecs.read_storage::(); + let hidden = ecs.read_storage::(); + let map = ecs.fetch::(); + let sizes = ecs.read_storage::(); + let entities = ecs.entities(); + let targets = ecs.read_storage::(); + + let mut data = (&positions, &renderables, &entities, !&hidden).join().collect::>(); + data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order) ); + for (pos, render, entity, _hidden) in data.iter() { + if let Some(size) = sizes.get(*entity) { + for cy in 0 .. size.y { + for cx in 0 .. size.x { + let tile_x = cx + pos.x; + let tile_y = cy + pos.y; + let idx = map.xy_idx(tile_x, tile_y); + if map.visible_tiles[idx] { + let entity_screen_x = (cx + pos.x) - min_x; + let entity_screen_y = (cy + pos.y) - min_y; + if entity_screen_x > 0 && entity_screen_x < map_width && entity_screen_y > 0 && entity_screen_y < map_height { + draw_batch.set( + Point::new(entity_screen_x + 1, entity_screen_y + 1), + ColorPair::new(render.fg, render.bg), + render.glyph + ); + } + } + } + } + } else { + let idx = map.xy_idx(pos.x, pos.y); + if map.visible_tiles[idx] { + let entity_screen_x = pos.x - min_x; + let entity_screen_y = pos.y - min_y; + if entity_screen_x > 0 && entity_screen_x < map_width && entity_screen_y > 0 && entity_screen_y < map_height { + draw_batch.set( + Point::new(entity_screen_x + 1, entity_screen_y + 1), + ColorPair::new(render.fg, render.bg), + render.glyph + ); + } + } + } + + if targets.get(*entity).is_some() { + let entity_screen_x = pos.x - min_x; + let entity_screen_y = pos.y - min_y; + draw_batch.set( + Point::new(entity_screen_x , entity_screen_y + 1), + ColorPair::new(RGB::named(rltk::RED), RGB::named(rltk::YELLOW)), + to_cp437('[') + ); + draw_batch.set( + Point::new(entity_screen_x +2, entity_screen_y + 1), + ColorPair::new(RGB::named(rltk::RED), RGB::named(rltk::YELLOW)), + to_cp437(']') + ); + } + } + + draw_batch.submit(0); +} + +pub fn render_debug_map(map : &Map, ctx : &mut Rltk) { + let player_pos = Point::new(map.width / 2, map.height / 2); + let (x_chars, y_chars) = ctx.get_char_size(); + + let center_x = (x_chars / 2) as i32; + let center_y = (y_chars / 2) as i32; + + let min_x = player_pos.x - center_x; + let max_x = min_x + x_chars as i32; + let min_y = player_pos.y - center_y; + let max_y = min_y + y_chars as i32; + + let map_width = map.width-1; + let map_height = map.height-1; + + for (y,ty) in (min_y .. max_y).enumerate() { + for (x,tx) in (min_x .. max_x).enumerate() { + if tx > 0 && tx < map_width && ty > 0 && ty < map_height { + let idx = map.xy_idx(tx, ty); + if map.revealed_tiles[idx] { + let (glyph, fg, bg) = tile_glyph(idx, &*map); + ctx.set(x as i32, y as i32, fg, bg, glyph); + } + } else if SHOW_BOUNDARIES { + ctx.set(x as i32, y as i32, RGB::named(rltk::GRAY), RGB::named(rltk::BLACK), rltk::to_cp437('·')); + } + } + } +} diff --git a/chapter-75-darkplaza/src/map/dungeon.rs b/chapter-75-darkplaza/src/map/dungeon.rs new file mode 100644 index 00000000..c89a797b --- /dev/null +++ b/chapter-75-darkplaza/src/map/dungeon.rs @@ -0,0 +1,254 @@ +use std::collections::{HashMap, HashSet}; +use serde::{Serialize, Deserialize}; +use super::{Map, TileType}; +use crate::components::{Position, Viewshed, OtherLevelPosition}; +use crate::map_builders::level_builder; +use specs::prelude::*; +use rltk::Point; + +#[derive(Default, Serialize, Deserialize, Clone)] +pub struct MasterDungeonMap { + maps : HashMap, + pub identified_items : HashSet, + pub scroll_mappings : HashMap, + pub potion_mappings : HashMap +} + +impl MasterDungeonMap { + pub fn new() -> MasterDungeonMap { + let mut dm = MasterDungeonMap{ + maps: HashMap::new() , + identified_items : HashSet::new(), + scroll_mappings : HashMap::new(), + potion_mappings : HashMap::new() + }; + + for scroll_tag in crate::raws::get_scroll_tags().iter() { + let masked_name = make_scroll_name(); + dm.scroll_mappings.insert(scroll_tag.to_string(), masked_name); + } + + let mut used_potion_names : HashSet = HashSet::new(); + for potion_tag in crate::raws::get_potion_tags().iter() { + let masked_name = make_potion_name(&mut used_potion_names); + dm.potion_mappings.insert(potion_tag.to_string(), masked_name); + } + + dm + } + + pub fn store_map(&mut self, map : &Map) { + self.maps.insert(map.depth, map.clone()); + } + + pub fn get_map(&self, depth : i32) -> Option { + if self.maps.contains_key(&depth) { + let mut result = self.maps[&depth].clone(); + Some(result) + } else { + None + } + } +} + +fn make_scroll_name() -> String { + let length = 4 + crate::rng::roll_dice(1, 4); + let mut name = "Scroll of ".to_string(); + + for i in 0..length { + if i % 2 == 0 { + name += match crate::rng::roll_dice(1, 5) { + 1 => "a", + 2 => "e", + 3 => "i", + 4 => "o", + _ => "u" + } + } else { + name += match crate::rng::roll_dice(1, 21) { + 1 => "b", + 2 => "c", + 3 => "d", + 4 => "f", + 5 => "g", + 6 => "h", + 7 => "j", + 8 => "k", + 9 => "l", + 10 => "m", + 11 => "n", + 12 => "p", + 13 => "q", + 14 => "r", + 15 => "s", + 16 => "t", + 17 => "v", + 18 => "w", + 19 => "x", + 20 => "y", + _ => "z" + } + } + } + + name +} + +const POTION_COLORS: &[&str] = &["Red", "Orange", "Yellow", "Green", "Brown", "Indigo", "Violet"]; +const POTION_ADJECTIVES : &[&str] = &["Swirling", "Effervescent", "Slimey", "Oiley", "Viscous", "Smelly", "Glowing"]; + +fn make_potion_name(used_names : &mut HashSet) -> String { + loop { + let mut name : String = POTION_ADJECTIVES[crate::rng::roll_dice(1, POTION_ADJECTIVES.len() as i32) as usize -1].to_string(); + name += " "; + name += POTION_COLORS[crate::rng::roll_dice(1, POTION_COLORS.len() as i32) as usize -1]; + name += " Potion"; + + if !used_names.contains(&name) { + used_names.insert(name.clone()); + return name; + } + } +} + +fn transition_to_new_map(ecs : &mut World, new_depth: i32) -> Vec { + let mut builder = level_builder(new_depth, 80, 50); + builder.build_map(); + if new_depth > 1 { + if let Some(pos) = &builder.build_data.starting_position { + let up_idx = builder.build_data.map.xy_idx(pos.x, pos.y); + builder.build_data.map.tiles[up_idx] = TileType::UpStairs; + } + } + let mapgen_history = builder.build_data.history.clone(); + let player_start; + { + let mut worldmap_resource = ecs.write_resource::(); + *worldmap_resource = builder.build_data.map.clone(); + player_start = builder.build_data.starting_position.as_mut().unwrap().clone(); + } + + // Spawn bad guys + builder.spawn_entities(ecs); + + // Place the player and update resources + let (player_x, player_y) = (player_start.x, player_start.y); + let mut player_position = ecs.write_resource::(); + *player_position = Point::new(player_x, player_y); + let mut position_components = ecs.write_storage::(); + let player_entity = ecs.fetch::(); + let player_pos_comp = position_components.get_mut(*player_entity); + if let Some(player_pos_comp) = player_pos_comp { + player_pos_comp.x = player_x; + player_pos_comp.y = player_y; + } + + // Mark the player's visibility as dirty + let mut viewshed_components = ecs.write_storage::(); + let vs = viewshed_components.get_mut(*player_entity); + if let Some(vs) = vs { + vs.dirty = true; + } + + // Store the newly minted map + let mut dungeon_master = ecs.write_resource::(); + dungeon_master.store_map(&builder.build_data.map); + + mapgen_history +} + +fn transition_to_existing_map(ecs: &mut World, new_depth: i32, offset: i32) { + let dungeon_master = ecs.read_resource::(); + let map = dungeon_master.get_map(new_depth).unwrap(); + let mut worldmap_resource = ecs.write_resource::(); + let player_entity = ecs.fetch::(); + + // Find the down stairs and place the player + let w = map.width; + let stair_type = if offset < 0 { TileType::DownStairs } else { TileType::UpStairs }; + for (idx, tt) in map.tiles.iter().enumerate() { + if *tt == stair_type { + let mut player_position = ecs.write_resource::(); + *player_position = Point::new(idx as i32 % w, idx as i32 / w); + let mut position_components = ecs.write_storage::(); + let player_pos_comp = position_components.get_mut(*player_entity); + if let Some(player_pos_comp) = player_pos_comp { + player_pos_comp.x = idx as i32 % w; + player_pos_comp.y = idx as i32 / w; + if new_depth == 1 { + player_pos_comp.x -= 1; + } + } + } + } + + *worldmap_resource = map; + + // Mark the player's visibility as dirty + let mut viewshed_components = ecs.write_storage::(); + let vs = viewshed_components.get_mut(*player_entity); + if let Some(vs) = vs { + vs.dirty = true; + } +} + +pub fn freeze_level_entities(ecs: &mut World) { + // Obtain ECS access + let entities = ecs.entities(); + let mut positions = ecs.write_storage::(); + let mut other_level_positions = ecs.write_storage::(); + let player_entity = ecs.fetch::(); + let map_depth = ecs.fetch::().depth; + + // Find positions and make OtherLevelPosition + let mut pos_to_delete : Vec = Vec::new(); + for (entity, pos) in (&entities, &positions).join() { + if entity != *player_entity { + other_level_positions.insert(entity, OtherLevelPosition{ x: pos.x, y: pos.y, depth: map_depth }).expect("Insert fail"); + pos_to_delete.push(entity); + } + } + + // Remove positions + for p in pos_to_delete.iter() { + positions.remove(*p); + } +} + +pub fn thaw_level_entities(ecs: &mut World) { + // Obtain ECS access + let entities = ecs.entities(); + let mut positions = ecs.write_storage::(); + let mut other_level_positions = ecs.write_storage::(); + let player_entity = ecs.fetch::(); + let map_depth = ecs.fetch::().depth; + + // Find OtherLevelPosition + let mut pos_to_delete : Vec = Vec::new(); + for (entity, pos) in (&entities, &other_level_positions).join() { + if entity != *player_entity && pos.depth == map_depth { + positions.insert(entity, Position{ x: pos.x, y: pos.y }).expect("Insert fail"); + pos_to_delete.push(entity); + } + } + + // Remove positions + for p in pos_to_delete.iter() { + other_level_positions.remove(*p); + } +} + +pub fn level_transition(ecs : &mut World, new_depth: i32, offset: i32) -> Option> { + // Obtain the master dungeon map + let dungeon_master = ecs.read_resource::(); + + // Do we already have a map? + if dungeon_master.get_map(new_depth).is_some() { + std::mem::drop(dungeon_master); + transition_to_existing_map(ecs, new_depth, offset); + None + } else { + std::mem::drop(dungeon_master); + Some(transition_to_new_map(ecs, new_depth)) + } +} diff --git a/chapter-75-darkplaza/src/map/mod.rs b/chapter-75-darkplaza/src/map/mod.rs new file mode 100644 index 00000000..1b8f06ff --- /dev/null +++ b/chapter-75-darkplaza/src/map/mod.rs @@ -0,0 +1,137 @@ +use rltk::{ BaseMap, Algorithm2D, Point }; +use specs::prelude::*; +use serde::{Serialize, Deserialize}; +use std::collections::HashSet; +mod tiletype; +pub use tiletype::{TileType, tile_walkable, tile_opaque, tile_cost}; +mod themes; +pub use themes::*; +mod dungeon; +pub use dungeon::{MasterDungeonMap, level_transition, freeze_level_entities, thaw_level_entities}; +pub mod camera; + +#[derive(Default, Serialize, Deserialize, Clone)] +pub struct Map { + pub tiles : Vec, + pub width : i32, + pub height : i32, + pub revealed_tiles : Vec, + pub visible_tiles : Vec, + pub depth : i32, + pub bloodstains : HashSet, + pub view_blocked : HashSet, + pub name : String, + pub outdoors : bool, + pub light : Vec, +} + +impl Map { + pub fn xy_idx(&self, x: i32, y: i32) -> usize { + (y as usize * self.width as usize) + x as usize + } + + fn is_exit_valid(&self, x:i32, y:i32) -> bool { + if x < 1 || x > self.width-1 || y < 1 || y > self.height-1 { return false; } + let idx = self.xy_idx(x, y); + !crate::spatial::is_blocked(idx) + } + + pub fn populate_blocked(&mut self) { + crate::spatial::populate_blocked_from_map(self); + } + + pub fn populate_blocked_multi(&mut self, width : i32, height : i32) { + self.populate_blocked(); + for y in 1 .. self.height-1 { + for x in 1 .. self.width - 1 { + let idx = self.xy_idx(x, y); + if !crate::spatial::is_blocked(idx) { + for cy in 0..height { + for cx in 0..width { + let tx = x + cx; + let ty = y + cy; + if tx < self.width-1 && ty < self.height-1 { + let tidx = self.xy_idx(tx, ty); + if crate::spatial::is_blocked(tidx) { + crate::spatial::set_blocked(idx, true); + } + } else { + crate::spatial::set_blocked(idx, true); + } + } + } + } + } + } + } + + pub fn clear_content_index(&mut self) { + crate::spatial::clear(); + } + + /// Generates an empty map, consisting entirely of solid walls + pub fn new(new_depth : i32, width: i32, height: i32, name: S) -> Map { + let map_tile_count = (width*height) as usize; + crate::spatial::set_size(map_tile_count); + Map{ + tiles : vec![TileType::Wall; map_tile_count], + width, + height, + revealed_tiles : vec![false; map_tile_count], + visible_tiles : vec![false; map_tile_count], + depth: new_depth, + bloodstains: HashSet::new(), + view_blocked : HashSet::new(), + name : name.to_string(), + outdoors : true, + light: vec![rltk::RGB::from_f32(0.0, 0.0, 0.0); map_tile_count] + } + } +} + +impl BaseMap for Map { + fn is_opaque(&self, idx:usize) -> bool { + if idx > 0 && idx < self.tiles.len() { + tile_opaque(self.tiles[idx]) || self.view_blocked.contains(&idx) + } else { + true + } + } + + fn get_available_exits(&self, idx:usize) -> rltk::SmallVec<[(usize, f32); 10]> { + const DIAGONAL_COST : f32 = 1.5; + let mut exits = rltk::SmallVec::new(); + let x = idx as i32 % self.width; + let y = idx as i32 / self.width; + let tt = self.tiles[idx as usize]; + let w = self.width as usize; + + // Cardinal directions + if self.is_exit_valid(x-1, y) { exits.push((idx-1, tile_cost(tt))) }; + if self.is_exit_valid(x+1, y) { exits.push((idx+1, tile_cost(tt))) }; + if self.is_exit_valid(x, y-1) { exits.push((idx-w, tile_cost(tt))) }; + if self.is_exit_valid(x, y+1) { exits.push((idx+w, tile_cost(tt))) }; + + // Diagonals + if self.is_exit_valid(x-1, y-1) { exits.push(((idx-w)-1, tile_cost(tt) * DIAGONAL_COST)); } + if self.is_exit_valid(x+1, y-1) { exits.push(((idx-w)+1, tile_cost(tt) * DIAGONAL_COST)); } + if self.is_exit_valid(x-1, y+1) { exits.push(((idx+w)-1, tile_cost(tt) * DIAGONAL_COST)); } + if self.is_exit_valid(x+1, y+1) { exits.push(((idx+w)+1, tile_cost(tt) * DIAGONAL_COST)); } + + exits + } + + fn get_pathing_distance(&self, idx1:usize, idx2:usize) -> f32 { + let w = self.width as usize; + let p1 = Point::new(idx1 % w, idx1 / w); + let p2 = Point::new(idx2 % w, idx2 / w); + rltk::DistanceAlg::Pythagoras.distance2d(p1, p2) + } +} + +impl Algorithm2D for Map { + fn dimensions(&self) -> Point { + Point::new(self.width, self.height) + } +} + diff --git a/chapter-75-darkplaza/src/map/themes.rs b/chapter-75-darkplaza/src/map/themes.rs new file mode 100644 index 00000000..5a2d0bc1 --- /dev/null +++ b/chapter-75-darkplaza/src/map/themes.rs @@ -0,0 +1,169 @@ +use super::{Map, TileType}; +use rltk::RGB; + +pub fn tile_glyph(idx: usize, map : &Map) -> (rltk::FontCharType, RGB, RGB) { + let (glyph, mut fg, mut bg) = match map.depth { + 9 => get_mushroom_glyph(idx, map), + 8 => get_mushroom_glyph(idx, map), + 7 => { + let x = idx as i32 % map.width; + if x > map.width-16 { + get_tile_glyph_default(idx, map) + } else { + get_mushroom_glyph(idx, map) + } + } + 5 => { + let x = idx as i32 % map.width; + if x < map.width/2 { + get_limestone_cavern_glyph(idx, map) + } else { + get_tile_glyph_default(idx, map) + } + } + 4 => get_limestone_cavern_glyph(idx, map), + 3 => get_limestone_cavern_glyph(idx, map), + 2 => get_forest_glyph(idx, map), + _ => get_tile_glyph_default(idx, map) + }; + + if map.bloodstains.contains(&idx) { bg = RGB::from_f32(0.75, 0., 0.); } + if !map.visible_tiles[idx] { + fg = fg.to_greyscale(); + bg = RGB::from_f32(0., 0., 0.); // Don't show stains out of visual range + } else if !map.outdoors { + fg = fg * map.light[idx]; + bg = bg * map.light[idx]; + } + + (glyph, fg, bg) +} + +fn get_forest_glyph(idx:usize, map: &Map) -> (rltk::FontCharType, RGB, RGB) { + let glyph; + let fg; + let bg = RGB::from_f32(0., 0., 0.); + + match map.tiles[idx] { + TileType::Wall => { glyph = rltk::to_cp437('♣'); fg = RGB::from_f32(0.0, 0.6, 0.0); } + TileType::Bridge => { glyph = rltk::to_cp437('.'); fg = RGB::named(rltk::CHOCOLATE); } + TileType::Road => { glyph = rltk::to_cp437('≡'); fg = RGB::named(rltk::YELLOW); } + TileType::Grass => { glyph = rltk::to_cp437('"'); fg = RGB::named(rltk::GREEN); } + TileType::ShallowWater => { glyph = rltk::to_cp437('~'); fg = RGB::named(rltk::CYAN); } + TileType::DeepWater => { glyph = rltk::to_cp437('~'); fg = RGB::named(rltk::BLUE); } + TileType::Gravel => { glyph = rltk::to_cp437(';'); fg = RGB::from_f32(0.5, 0.5, 0.5); } + TileType::DownStairs => { glyph = rltk::to_cp437('>'); fg = RGB::from_f32(0., 1.0, 1.0); } + TileType::UpStairs => { glyph = rltk::to_cp437('<'); fg = RGB::from_f32(0., 1.0, 1.0); } + _ => { glyph = rltk::to_cp437('"'); fg = RGB::from_f32(0.0, 0.5, 0.0); } + } + + (glyph, fg, bg) +} + +fn get_mushroom_glyph(idx:usize, map: &Map) -> (rltk::FontCharType, RGB, RGB) { + let glyph; + let fg; + let bg = RGB::from_f32(0., 0., 0.); + + match map.tiles[idx] { + TileType::Wall => { glyph = rltk::to_cp437('♠'); fg = RGB::from_f32(1.0, 0.0, 1.0); } + TileType::Bridge => { glyph = rltk::to_cp437('.'); fg = RGB::named(rltk::GREEN); } + TileType::Road => { glyph = rltk::to_cp437('≡'); fg = RGB::named(rltk::CHOCOLATE); } + TileType::Grass => { glyph = rltk::to_cp437('"'); fg = RGB::named(rltk::GREEN); } + TileType::ShallowWater => { glyph = rltk::to_cp437('~'); fg = RGB::named(rltk::CYAN); } + TileType::DeepWater => { glyph = rltk::to_cp437('~'); fg = RGB::named(rltk::BLUE); } + TileType::Gravel => { glyph = rltk::to_cp437(';'); fg = RGB::from_f32(0.5, 0.5, 0.5); } + TileType::DownStairs => { glyph = rltk::to_cp437('>'); fg = RGB::from_f32(0., 1.0, 1.0); } + TileType::UpStairs => { glyph = rltk::to_cp437('<'); fg = RGB::from_f32(0., 1.0, 1.0); } + _ => { glyph = rltk::to_cp437('"'); fg = RGB::from_f32(0.0, 0.6, 0.0); } + } + + (glyph, fg, bg) +} + +fn get_limestone_cavern_glyph(idx:usize, map: &Map) -> (rltk::FontCharType, RGB, RGB) { + let glyph; + let fg; + let bg = RGB::from_f32(0., 0., 0.); + + match map.tiles[idx] { + TileType::Wall => { glyph = rltk::to_cp437('▒'); fg = RGB::from_f32(0.7, 0.7, 0.7); } + TileType::Bridge => { glyph = rltk::to_cp437('.'); fg = RGB::named(rltk::CHOCOLATE); } + TileType::Road => { glyph = rltk::to_cp437('≡'); fg = RGB::named(rltk::YELLOW); } + TileType::Grass => { glyph = rltk::to_cp437('"'); fg = RGB::named(rltk::GREEN); } + TileType::ShallowWater => { glyph = rltk::to_cp437('░'); fg = RGB::named(rltk::CYAN); } + TileType::DeepWater => { glyph = rltk::to_cp437('▓'); fg = RGB::from_f32(0.2, 0.2, 1.0); } + TileType::Gravel => { glyph = rltk::to_cp437(';'); fg = RGB::from_f32(0.5, 0.5, 0.5); } + TileType::DownStairs => { glyph = rltk::to_cp437('>'); fg = RGB::from_f32(0., 1.0, 1.0); } + TileType::UpStairs => { glyph = rltk::to_cp437('<'); fg = RGB::from_f32(0., 1.0, 1.0); } + TileType::Stalactite => { glyph = rltk::to_cp437('╨'); fg = RGB::from_f32(0.7, 0.7, 0.7); } + TileType::Stalagmite => { glyph = rltk::to_cp437('╥'); fg = RGB::from_f32(0.7, 0.7, 0.7); } + _ => { glyph = rltk::to_cp437('\''); fg = RGB::from_f32(0.4, 0.4, 0.4); } + } + + (glyph, fg, bg) +} + +fn get_tile_glyph_default(idx: usize, map : &Map) -> (rltk::FontCharType, RGB, RGB) { + let glyph; + let fg; + let bg = RGB::from_f32(0., 0., 0.); + + match map.tiles[idx] { + TileType::Floor => { glyph = rltk::to_cp437('.'); fg = RGB::from_f32(0.0, 0.5, 0.5); } + TileType::WoodFloor => { glyph = rltk::to_cp437('░'); fg = RGB::named(rltk::CHOCOLATE); } + TileType::Wall => { + let x = idx as i32 % map.width; + let y = idx as i32 / map.width; + glyph = wall_glyph(&*map, x, y); + fg = RGB::from_f32(0., 1.0, 0.); + } + TileType::DownStairs => { glyph = rltk::to_cp437('>'); fg = RGB::from_f32(0., 1.0, 1.0); } + TileType::UpStairs => { glyph = rltk::to_cp437('<'); fg = RGB::from_f32(0., 1.0, 1.0); } + TileType::Bridge => { glyph = rltk::to_cp437('.'); fg = RGB::named(rltk::CHOCOLATE); } + TileType::Road => { glyph = rltk::to_cp437('≡'); fg = RGB::named(rltk::GRAY); } + TileType::Grass => { glyph = rltk::to_cp437('"'); fg = RGB::named(rltk::GREEN); } + TileType::ShallowWater => { glyph = rltk::to_cp437('~'); fg = RGB::named(rltk::CYAN); } + TileType::DeepWater => { glyph = rltk::to_cp437('~'); fg = RGB::named(rltk::BLUE); } + TileType::Gravel => { glyph = rltk::to_cp437(';'); fg = RGB::from_f32(0.5, 0.5, 0.5); } + TileType::Stalactite => { glyph = rltk::to_cp437('╨'); fg = RGB::from_f32(0.5, 0.5, 0.5); } + TileType::Stalagmite => { glyph = rltk::to_cp437('╥'); fg = RGB::from_f32(0.5, 0.5, 0.5); } + } + + (glyph, fg, bg) +} + +fn wall_glyph(map : &Map, x: i32, y:i32) -> rltk::FontCharType { + if x < 1 || x > map.width-2 || y < 1 || y > map.height-2 as i32 { return 35; } + let mut mask : u8 = 0; + + if is_revealed_and_wall(map, x, y - 1) { mask +=1; } + if is_revealed_and_wall(map, x, y + 1) { mask +=2; } + if is_revealed_and_wall(map, x - 1, y) { mask +=4; } + if is_revealed_and_wall(map, x + 1, y) { mask +=8; } + + match mask { + 0 => { 9 } // Pillar because we can't see neighbors + 1 => { 186 } // Wall only to the north + 2 => { 186 } // Wall only to the south + 3 => { 186 } // Wall to the north and south + 4 => { 205 } // Wall only to the west + 5 => { 188 } // Wall to the north and west + 6 => { 187 } // Wall to the south and west + 7 => { 185 } // Wall to the north, south and west + 8 => { 205 } // Wall only to the east + 9 => { 200 } // Wall to the north and east + 10 => { 201 } // Wall to the south and east + 11 => { 204 } // Wall to the north, south and east + 12 => { 205 } // Wall to the east and west + 13 => { 202 } // Wall to the east, west, and south + 14 => { 203 } // Wall to the east, west, and north + 15 => { 206 } // ╬ Wall on all sides + _ => { 35 } // We missed one? + } +} + +fn is_revealed_and_wall(map: &Map, x: i32, y: i32) -> bool { + let idx = map.xy_idx(x, y); + map.tiles[idx] == TileType::Wall && map.revealed_tiles[idx] +} diff --git a/chapter-75-darkplaza/src/map/tiletype.rs b/chapter-75-darkplaza/src/map/tiletype.rs new file mode 100644 index 00000000..ad3fdec6 --- /dev/null +++ b/chapter-75-darkplaza/src/map/tiletype.rs @@ -0,0 +1,44 @@ +use serde::{Serialize, Deserialize}; + +#[derive(PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)] +pub enum TileType { + Wall, + Stalactite, + Stalagmite, + Floor, + DownStairs, + Road, + Grass, + ShallowWater, + DeepWater, + WoodFloor, + Bridge, + Gravel, + UpStairs +} + +pub fn tile_walkable(tt : TileType) -> bool { + match tt { + TileType::Floor | TileType::DownStairs | TileType::Road | TileType::Grass | + TileType::ShallowWater | TileType::WoodFloor | TileType::Bridge | TileType::Gravel | + TileType::UpStairs + => true, + _ => false + } +} + +pub fn tile_opaque(tt : TileType) -> bool { + match tt { + TileType::Wall | TileType::Stalactite | TileType::Stalagmite => true, + _ => false + } +} + +pub fn tile_cost(tt : TileType) -> f32 { + match tt { + TileType::Road => 0.8, + TileType::Grass => 1.1, + TileType::ShallowWater => 1.2, + _ => 1.0 + } +} diff --git a/chapter-75-darkplaza/src/map_builders/area_ending_point.rs b/chapter-75-darkplaza/src/map_builders/area_ending_point.rs new file mode 100644 index 00000000..bbcaf2a8 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/area_ending_point.rs @@ -0,0 +1,68 @@ +use super::{MetaMapBuilder, BuilderMap, TileType}; +use crate::map; + +#[allow(dead_code)] +pub enum XEnd { LEFT, CENTER, RIGHT } + +#[allow(dead_code)] +pub enum YEnd{ TOP, CENTER, BOTTOM } + +pub struct AreaEndingPosition { + x : XEnd, + y : YEnd +} + +impl MetaMapBuilder for AreaEndingPosition { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl AreaEndingPosition { + #[allow(dead_code)] + pub fn new(x : XEnd, y : YEnd) -> Box { + Box::new(AreaEndingPosition{ + x, y + }) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + let seed_x; + let seed_y; + + match self.x { + XEnd::LEFT => seed_x = 1, + XEnd::CENTER => seed_x = build_data.map.width / 2, + XEnd::RIGHT => seed_x = build_data.map.width - 2 + } + + match self.y { + YEnd::TOP => seed_y = 1, + YEnd::CENTER => seed_y = build_data.map.height / 2, + YEnd::BOTTOM => seed_y = build_data.map.height - 2 + } + + let mut available_floors : Vec<(usize, f32)> = Vec::new(); + for (idx, tiletype) in build_data.map.tiles.iter().enumerate() { + if map::tile_walkable(*tiletype) { + available_floors.push( + ( + idx, + rltk::DistanceAlg::PythagorasSquared.distance2d( + rltk::Point::new(idx as i32 % build_data.map.width, idx as i32 / build_data.map.width), + rltk::Point::new(seed_x, seed_y) + ) + ) + ); + } + } + if available_floors.is_empty() { + panic!("No valid floors to start on"); + } + + available_floors.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap()); + + build_data.map.tiles[available_floors[0].0] = TileType::DownStairs; + build_data.take_snapshot(); + } +} diff --git a/chapter-75-darkplaza/src/map_builders/area_starting_points.rs b/chapter-75-darkplaza/src/map_builders/area_starting_points.rs new file mode 100644 index 00000000..9041b75c --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/area_starting_points.rs @@ -0,0 +1,70 @@ +use super::{MetaMapBuilder, BuilderMap, Position}; +use crate::map; + +#[allow(dead_code)] +pub enum XStart { LEFT, CENTER, RIGHT } + +#[allow(dead_code)] +pub enum YStart { TOP, CENTER, BOTTOM } + +pub struct AreaStartingPosition { + x : XStart, + y : YStart +} + +impl MetaMapBuilder for AreaStartingPosition { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl AreaStartingPosition { + #[allow(dead_code)] + pub fn new(x : XStart, y : YStart) -> Box { + Box::new(AreaStartingPosition{ + x, y + }) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + let seed_x; + let seed_y; + + match self.x { + XStart::LEFT => seed_x = 1, + XStart::CENTER => seed_x = build_data.map.width / 2, + XStart::RIGHT => seed_x = build_data.map.width - 2 + } + + match self.y { + YStart::TOP => seed_y = 1, + YStart::CENTER => seed_y = build_data.map.height / 2, + YStart::BOTTOM => seed_y = build_data.map.height - 2 + } + + let mut available_floors : Vec<(usize, f32)> = Vec::new(); + for (idx, tiletype) in build_data.map.tiles.iter().enumerate() { + if map::tile_walkable(*tiletype) { + available_floors.push( + ( + idx, + rltk::DistanceAlg::PythagorasSquared.distance2d( + rltk::Point::new(idx as i32 % build_data.map.width, idx as i32 / build_data.map.width), + rltk::Point::new(seed_x, seed_y) + ) + ) + ); + } + } + if available_floors.is_empty() { + panic!("No valid floors to start on"); + } + + available_floors.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap()); + + let start_x = available_floors[0].0 as i32 % build_data.map.width; + let start_y = available_floors[0].0 as i32 / build_data.map.width; + + build_data.starting_position = Some(Position{x : start_x, y: start_y}); + } +} diff --git a/chapter-75-darkplaza/src/map_builders/bsp_dungeon.rs b/chapter-75-darkplaza/src/map_builders/bsp_dungeon.rs new file mode 100644 index 00000000..06300679 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/bsp_dungeon.rs @@ -0,0 +1,112 @@ +use super::{InitialMapBuilder, BuilderMap, Rect, TileType}; + +pub struct BspDungeonBuilder { + rects: Vec, +} + +impl InitialMapBuilder for BspDungeonBuilder { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl BspDungeonBuilder { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(BspDungeonBuilder{ + rects: Vec::new(), + }) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + let mut rooms : Vec = Vec::new(); + self.rects.clear(); + self.rects.push( Rect::new(2, 2, build_data.map.width-5, build_data.map.height-5) ); // Start with a single map-sized rectangle + let first_room = self.rects[0]; + self.add_subrects(first_room); // Divide the first room + + // Up to 240 times, we get a random rectangle and divide it. If its possible to squeeze a + // room in there, we place it and add it to the rooms list. + let mut n_rooms = 0; + while n_rooms < 240 { + let rect = self.get_random_rect(); + let candidate = self.get_random_sub_rect(rect); + + if self.is_possible(candidate, &build_data, &rooms) { + //apply_room_to_map(&mut build_data.map, &candidate); + rooms.push(candidate); + self.add_subrects(rect); + } + + n_rooms += 1; + } + + build_data.rooms = Some(rooms); + } + + fn add_subrects(&mut self, rect : Rect) { + let width = i32::abs(rect.x1 - rect.x2); + let height = i32::abs(rect.y1 - rect.y2); + let half_width = i32::max(width / 2, 1); + let half_height = i32::max(height / 2, 1); + + self.rects.push(Rect::new( rect.x1, rect.y1, half_width, half_height )); + self.rects.push(Rect::new( rect.x1, rect.y1 + half_height, half_width, half_height )); + self.rects.push(Rect::new( rect.x1 + half_width, rect.y1, half_width, half_height )); + self.rects.push(Rect::new( rect.x1 + half_width, rect.y1 + half_height, half_width, half_height )); + } + + fn get_random_rect(&mut self) -> Rect { + if self.rects.len() == 1 { return self.rects[0]; } + let idx = (crate::rng::roll_dice(1, self.rects.len() as i32)-1) as usize; + self.rects[idx] + } + + fn get_random_sub_rect(&self, rect : Rect) -> Rect { + let mut result = rect; + let rect_width = i32::abs(rect.x1 - rect.x2); + let rect_height = i32::abs(rect.y1 - rect.y2); + + let w = i32::max(3, crate::rng::roll_dice(1, i32::min(rect_width, 20))-1) + 1; + let h = i32::max(3, crate::rng::roll_dice(1, i32::min(rect_height, 20))-1) + 1; + + result.x1 += crate::rng::roll_dice(1, 6)-1; + result.y1 += crate::rng::roll_dice(1, 6)-1; + result.x2 = result.x1 + w; + result.y2 = result.y1 + h; + + result + } + + fn is_possible(&self, rect : Rect, build_data : &BuilderMap, rooms: &[Rect]) -> bool { + let mut expanded = rect; + expanded.x1 -= 2; + expanded.x2 += 2; + expanded.y1 -= 2; + expanded.y2 += 2; + + let mut can_build = true; + + for r in rooms.iter() { + if r.intersect(&rect) { can_build = false; } + } + + for y in expanded.y1 ..= expanded.y2 { + for x in expanded.x1 ..= expanded.x2 { + if x > build_data.map.width-2 { can_build = false; } + if y > build_data.map.height-2 { can_build = false; } + if x < 1 { can_build = false; } + if y < 1 { can_build = false; } + if can_build { + let idx = build_data.map.xy_idx(x, y); + if build_data.map.tiles[idx] != TileType::Wall { + can_build = false; + } + } + } + } + + can_build + } +} diff --git a/chapter-75-darkplaza/src/map_builders/bsp_interior.rs b/chapter-75-darkplaza/src/map_builders/bsp_interior.rs new file mode 100644 index 00000000..bd16692e --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/bsp_interior.rs @@ -0,0 +1,94 @@ +use super::{InitialMapBuilder, BuilderMap, Rect, TileType, draw_corridor}; + +const MIN_ROOM_SIZE : i32 = 8; + +pub struct BspInteriorBuilder { + rects: Vec +} + +impl InitialMapBuilder for BspInteriorBuilder { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl BspInteriorBuilder { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(BspInteriorBuilder{ + rects: Vec::new() + }) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + let mut rooms : Vec = Vec::new(); + self.rects.clear(); + self.rects.push( Rect::new(1, 1, build_data.map.width-2, build_data.map.height-2) ); // Start with a single map-sized rectangle + let first_room = self.rects[0]; + self.add_subrects(first_room); // Divide the first room + + let rooms_copy = self.rects.clone(); + for r in rooms_copy.iter() { + let room = *r; + //room.x2 -= 1; + //room.y2 -= 1; + rooms.push(room); + for y in room.y1 .. room.y2 { + for x in room.x1 .. room.x2 { + let idx = build_data.map.xy_idx(x, y); + if idx > 0 && idx < ((build_data.map.width * build_data.map.height)-1) as usize { + build_data.map.tiles[idx] = TileType::Floor; + } + } + } + build_data.take_snapshot(); + } + + // Now we want corridors + for i in 0..rooms.len()-1 { + let room = rooms[i]; + let next_room = rooms[i+1]; + let start_x = room.x1 + (crate::rng::roll_dice(1, i32::abs(room.x1 - room.x2))-1); + let start_y = room.y1 + (crate::rng::roll_dice(1, i32::abs(room.y1 - room.y2))-1); + let end_x = next_room.x1 + (crate::rng::roll_dice(1, i32::abs(next_room.x1 - next_room.x2))-1); + let end_y = next_room.y1 + (crate::rng::roll_dice(1, i32::abs(next_room.y1 - next_room.y2))-1); + draw_corridor(&mut build_data.map, start_x, start_y, end_x, end_y); + build_data.take_snapshot(); + } + build_data.rooms = Some(rooms); + } + + fn add_subrects(&mut self, rect : Rect) { + // Remove the last rect from the list + if !self.rects.is_empty() { + self.rects.remove(self.rects.len() - 1); + } + + // Calculate boundaries + let width = rect.x2 - rect.x1; + let height = rect.y2 - rect.y1; + let half_width = width / 2; + let half_height = height / 2; + + let split = crate::rng::roll_dice(1, 4); + + if split <= 2 { + // Horizontal split + let h1 = Rect::new( rect.x1, rect.y1, half_width-1, height ); + self.rects.push( h1 ); + if half_width > MIN_ROOM_SIZE { self.add_subrects(h1); } + let h2 = Rect::new( rect.x1 + half_width, rect.y1, half_width, height ); + self.rects.push( h2 ); + if half_width > MIN_ROOM_SIZE { self.add_subrects(h2); } + } else { + // Vertical split + let v1 = Rect::new( rect.x1, rect.y1, width, half_height-1 ); + self.rects.push(v1); + if half_height > MIN_ROOM_SIZE { self.add_subrects(v1); } + let v2 = Rect::new( rect.x1, rect.y1 + half_height, width, half_height ); + self.rects.push(v2); + if half_height > MIN_ROOM_SIZE { self.add_subrects(v2); } + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/cellular_automata.rs b/chapter-75-darkplaza/src/map_builders/cellular_automata.rs new file mode 100644 index 00000000..5fbc2021 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/cellular_automata.rs @@ -0,0 +1,72 @@ +use super::{InitialMapBuilder, MetaMapBuilder, BuilderMap, TileType}; + +pub struct CellularAutomataBuilder {} + +impl InitialMapBuilder for CellularAutomataBuilder { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl MetaMapBuilder for CellularAutomataBuilder { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.apply_iteration(build_data); + } +} + +impl CellularAutomataBuilder { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(CellularAutomataBuilder{}) + } + + fn apply_iteration(&mut self, build_data : &mut BuilderMap) { + let mut newtiles = build_data.map.tiles.clone(); + + for y in 1..build_data.map.height-1 { + for x in 1..build_data.map.width-1 { + let idx = build_data.map.xy_idx(x, y); + let mut neighbors = 0; + if build_data.map.tiles[idx - 1] == TileType::Wall { neighbors += 1; } + if build_data.map.tiles[idx + 1] == TileType::Wall { neighbors += 1; } + if build_data.map.tiles[idx - build_data.map.width as usize] == TileType::Wall { neighbors += 1; } + if build_data.map.tiles[idx + build_data.map.width as usize] == TileType::Wall { neighbors += 1; } + if build_data.map.tiles[idx - (build_data.map.width as usize - 1)] == TileType::Wall { neighbors += 1; } + if build_data.map.tiles[idx - (build_data.map.width as usize + 1)] == TileType::Wall { neighbors += 1; } + if build_data.map.tiles[idx + (build_data.map.width as usize - 1)] == TileType::Wall { neighbors += 1; } + if build_data.map.tiles[idx + (build_data.map.width as usize + 1)] == TileType::Wall { neighbors += 1; } + + if neighbors > 4 || neighbors == 0 { + newtiles[idx] = TileType::Wall; + } + else { + newtiles[idx] = TileType::Floor; + } + } + } + + build_data.map.tiles = newtiles.clone(); + build_data.take_snapshot(); + } + + #[allow(clippy::map_entry)] + fn build(&mut self, build_data : &mut BuilderMap) { + // First we completely randomize the map, setting 55% of it to be floor. + for y in 1..build_data.map.height-1 { + for x in 1..build_data.map.width-1 { + let roll = crate::rng::roll_dice(1, 100); + let idx = build_data.map.xy_idx(x, y); + if roll > 55 { build_data.map.tiles[idx] = TileType::Floor } + else { build_data.map.tiles[idx] = TileType::Wall } + } + } + build_data.take_snapshot(); + + // Now we iteratively apply cellular automata rules + for _i in 0..15 { + self.apply_iteration(build_data); + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/common.rs b/chapter-75-darkplaza/src/map_builders/common.rs new file mode 100644 index 00000000..2d85d348 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/common.rs @@ -0,0 +1,117 @@ +use super::{Map, TileType}; +use std::cmp::{max, min}; + +#[derive(PartialEq, Copy, Clone)] +#[allow(dead_code)] +pub enum Symmetry { None, Horizontal, Vertical, Both } + +pub fn apply_horizontal_tunnel(map : &mut Map, x1:i32, x2:i32, y:i32) -> Vec { + let mut corridor = Vec::new(); + for x in min(x1,x2) ..= max(x1,x2) { + let idx = map.xy_idx(x, y); + if idx > 0 && idx < map.width as usize * map.height as usize && map.tiles[idx as usize] != TileType::Floor { + map.tiles[idx as usize] = TileType::Floor; + corridor.push(idx as usize); + } + } + corridor +} + +pub fn apply_vertical_tunnel(map : &mut Map, y1:i32, y2:i32, x:i32) -> Vec { + let mut corridor = Vec::new(); + for y in min(y1,y2) ..= max(y1,y2) { + let idx = map.xy_idx(x, y); + if idx > 0 && idx < map.width as usize * map.height as usize && map.tiles[idx as usize] != TileType::Floor { + corridor.push(idx); + map.tiles[idx as usize] = TileType::Floor; + } + } + corridor +} + +pub fn draw_corridor(map: &mut Map, x1:i32, y1:i32, x2:i32, y2:i32) -> Vec { + let mut corridor = Vec::new(); + let mut x = x1; + let mut y = y1; + + while x != x2 || y != y2 { + if x < x2 { + x += 1; + } else if x > x2 { + x -= 1; + } else if y < y2 { + y += 1; + } else if y > y2 { + y -= 1; + } + + let idx = map.xy_idx(x, y); + if map.tiles[idx] != TileType::Floor { + corridor.push(idx); + map.tiles[idx] = TileType::Floor; + } + } + + corridor +} + +pub fn paint(map: &mut Map, mode: Symmetry, brush_size: i32, x: i32, y:i32) { + match mode { + Symmetry::None => apply_paint(map, brush_size, x, y), + Symmetry::Horizontal => { + let center_x = map.width / 2; + if x == center_x { + apply_paint(map, brush_size, x, y); + } else { + let dist_x = i32::abs(center_x - x); + apply_paint(map, brush_size, center_x + dist_x, y); + apply_paint(map, brush_size, center_x - dist_x, y); + } + } + Symmetry::Vertical => { + let center_y = map.height / 2; + if y == center_y { + apply_paint(map, brush_size, x, y); + } else { + let dist_y = i32::abs(center_y - y); + apply_paint(map, brush_size, x, center_y + dist_y); + apply_paint(map, brush_size, x, center_y - dist_y); + } + } + Symmetry::Both => { + let center_x = map.width / 2; + let center_y = map.height / 2; + if x == center_x && y == center_y { + apply_paint(map, brush_size, x, y); + } else { + let dist_x = i32::abs(center_x - x); + apply_paint(map, brush_size, center_x + dist_x, y); + apply_paint(map, brush_size, center_x - dist_x, y); + let dist_y = i32::abs(center_y - y); + apply_paint(map, brush_size, x, center_y + dist_y); + apply_paint(map, brush_size, x, center_y - dist_y); + } + } + } +} + +fn apply_paint(map: &mut Map, brush_size: i32, x: i32, y: i32) { + match brush_size { + 1 => { + let digger_idx = map.xy_idx(x, y); + map.tiles[digger_idx] = TileType::Floor; + } + + _ => { + let half_brush_size = brush_size / 2; + for brush_y in y-half_brush_size .. y+half_brush_size { + for brush_x in x-half_brush_size .. x+half_brush_size { + if brush_x > 1 && brush_x < map.width-1 && brush_y > 1 && brush_y < map.height-1 { + let idx = map.xy_idx(brush_x, brush_y); + map.tiles[idx] = TileType::Floor; + } + } + } + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/cull_unreachable.rs b/chapter-75-darkplaza/src/map_builders/cull_unreachable.rs new file mode 100644 index 00000000..9d35d4ff --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/cull_unreachable.rs @@ -0,0 +1,36 @@ +use super::{MetaMapBuilder, BuilderMap, TileType}; + +pub struct CullUnreachable {} + +impl MetaMapBuilder for CullUnreachable { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl CullUnreachable { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(CullUnreachable{}) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + let starting_pos = build_data.starting_position.as_ref().unwrap().clone(); + let start_idx = build_data.map.xy_idx( + starting_pos.x, + starting_pos.y + ); + build_data.map.populate_blocked(); + let map_starts : Vec = vec![start_idx]; + let dijkstra_map = rltk::DijkstraMap::new(build_data.map.width as usize, build_data.map.height as usize, &map_starts , &build_data.map, 1000.0); + for (i, tile) in build_data.map.tiles.iter_mut().enumerate() { + if *tile == TileType::Floor { + let distance_to_start = dijkstra_map.map[i]; + // We can't get to this tile - so we'll make it a wall + if distance_to_start == std::f32::MAX { + *tile = TileType::Wall; + } + } + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/dark_elves.rs b/chapter-75-darkplaza/src/map_builders/dark_elves.rs new file mode 100644 index 00000000..a4d73caa --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/dark_elves.rs @@ -0,0 +1,248 @@ +use super::{BuilderChain, XStart, YStart, AreaStartingPosition, + CullUnreachable, VoronoiSpawning, + AreaEndingPosition, XEnd, YEnd, BspInteriorBuilder }; + +pub fn dark_elf_city(new_depth: i32, width: i32, height: i32) -> BuilderChain { + println!("Dark elf builder"); + let mut chain = BuilderChain::new(new_depth, width, height, "Dark Elven City"); + chain.start_with(BspInteriorBuilder::new()); + chain.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)); + chain.with(CullUnreachable::new()); + chain.with(AreaStartingPosition::new(XStart::RIGHT, YStart::CENTER)); + chain.with(AreaEndingPosition::new(XEnd::LEFT, YEnd::CENTER)); + chain.with(VoronoiSpawning::new()); + chain +} + +pub fn dark_elf_plaza(new_depth: i32, width: i32, height: i32) -> BuilderChain { + println!("Dark elf plaza builder"); + let mut chain = BuilderChain::new(new_depth, width, height, "Dark Elven Plaza"); + chain.start_with(PlazaMapBuilder::new()); + chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER)); + chain.with(CullUnreachable::new()); + chain +} + +// Plaza Builder +use super::{InitialMapBuilder, BuilderMap, TileType }; + +pub struct PlazaMapBuilder {} + +impl InitialMapBuilder for PlazaMapBuilder { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.empty_map(build_data); + self.spawn_zones(build_data); + } +} + +impl PlazaMapBuilder { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(PlazaMapBuilder{}) + } + + fn empty_map(&mut self, build_data : &mut BuilderMap) { + build_data.map.tiles.iter_mut().for_each(|t| *t = TileType::Floor); + } + + fn spawn_zones(&mut self, build_data : &mut BuilderMap) { + let mut voronoi_seeds : Vec<(usize, rltk::Point)> = Vec::new(); + + while voronoi_seeds.len() < 32 { + let vx = crate::rng::roll_dice(1, build_data.map.width-1); + let vy = crate::rng::roll_dice(1, build_data.map.height-1); + let vidx = build_data.map.xy_idx(vx, vy); + let candidate = (vidx, rltk::Point::new(vx, vy)); + if !voronoi_seeds.contains(&candidate) { + voronoi_seeds.push(candidate); + } + } + + let mut voronoi_distance = vec![(0, 0.0f32) ; 32]; + let mut voronoi_membership : Vec = vec![0 ; build_data.map.width as usize * build_data.map.height as usize]; + for (i, vid) in voronoi_membership.iter_mut().enumerate() { + let x = i as i32 % build_data.map.width; + let y = i as i32 / build_data.map.width; + + for (seed, pos) in voronoi_seeds.iter().enumerate() { + let distance = rltk::DistanceAlg::PythagorasSquared.distance2d( + rltk::Point::new(x, y), + pos.1 + ); + voronoi_distance[seed] = (seed, distance); + } + + voronoi_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap()); + + *vid = voronoi_distance[0].0 as i32; + } + + // Make a list of zone sizes and cull empty ones + let mut zone_sizes : Vec<(i32, usize)> = Vec::with_capacity(32); + for zone in 0..32 { + let num_tiles = voronoi_membership.iter().filter(|z| **z == zone).count(); + if num_tiles > 0 { + zone_sizes.push((zone, num_tiles)); + } + } + zone_sizes.sort_by(|a,b| b.1.cmp(&a.1)); + + // Start making zonal terrain + zone_sizes.iter().enumerate().for_each(|(i, (zone, _))| { + match i { + 0 => self.portal_park(build_data, &voronoi_membership, *zone, &voronoi_seeds), + 1 | 2 => self.park(build_data, &voronoi_membership, *zone, &voronoi_seeds), + i if i > 20 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::Wall), + _ => { + let roll = crate::rng::roll_dice(1, 6); + match roll { + 1 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::DeepWater), + 2 => self.fill_zone(build_data, &voronoi_membership, *zone, TileType::ShallowWater), + 3 => self.stalactite_display(build_data, &voronoi_membership, *zone), + _ => {} + } + } + } + }); + + // Clear the path + self.make_roads(build_data, &voronoi_membership); + } + + fn portal_park(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32], zone: i32, seeds: &[(usize, rltk::Point)]) { + let zone_tiles : Vec = voronoi_membership + .iter() + .enumerate() + .filter(|(_, tile_zone)| **tile_zone == zone) + .map(|(idx, _)| idx) + .collect(); + + // Start all gravel + zone_tiles.iter().for_each(|idx| build_data.map.tiles[*idx] = TileType::Gravel); + + // Add the exit + let center = seeds[zone as usize].1; + let idx = build_data.map.xy_idx(center.x, center.y); + build_data.map.tiles[idx] = TileType::DownStairs; + + // Add some altars around the exit + let altars = [ + build_data.map.xy_idx(center.x - 2, center.y), + build_data.map.xy_idx(center.x + 2, center.y), + build_data.map.xy_idx(center.x, center.y - 2), + build_data.map.xy_idx(center.x, center.y + 2), + ]; + altars.iter().for_each(|idx| build_data.spawn_list.push((*idx, "Altar".to_string()))); + + let demon_spawn = build_data.map.xy_idx(center.x+1, center.y+1); + build_data.spawn_list.push((demon_spawn, "Vokoth".to_string())); + } + + fn fill_zone(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32], zone: i32, tile_type: TileType) { + voronoi_membership + .iter() + .enumerate() + .filter(|(_, tile_zone)| **tile_zone == zone) + .for_each(|(idx, _)| build_data.map.tiles[idx] = tile_type); + } + + fn stalactite_display(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32], zone: i32) { + voronoi_membership + .iter() + .enumerate() + .filter(|(_, tile_zone)| **tile_zone == zone) + .for_each(|(idx, _)| { + build_data.map.tiles[idx] = match crate::rng::roll_dice(1,10) { + 1 => TileType::Stalactite, + 2 => TileType::Stalagmite, + _ => TileType::Grass, + }; + }); + } + + fn park(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32], zone: i32, seeds: &[(usize, rltk::Point)]) { + let zone_tiles : Vec = voronoi_membership + .iter() + .enumerate() + .filter(|(_, tile_zone)| **tile_zone == zone) + .map(|(idx, _)| idx) + .collect(); + + // Start all grass + zone_tiles.iter().for_each(|idx| build_data.map.tiles[*idx] = TileType::Grass); + + // Add a stone area in the middle + let center = seeds[zone as usize].1; + for y in center.y-2 ..= center.y+2 { + for x in center.x-2 ..= center.x+2 { + let idx = build_data.map.xy_idx(x, y); + build_data.map.tiles[idx] = TileType::Road; + if crate::rng::roll_dice(1,6) > 2 { + build_data.map.bloodstains.insert(idx); + } + } + } + + // With an altar at the center + build_data.spawn_list.push(( + build_data.map.xy_idx(center.x, center.y), + "Altar".to_string() + )); + + // And chairs for spectators, and the spectators themselves + let available_enemies = match crate::rng::roll_dice(1, 3) { + 1 => vec![ + "Arbat Dark Elf", + "Arbat Dark Elf Leader", + "Arbat Orc Slave", + ], + 2 => vec![ + "Barbo Dark Elf", + "Barbo Goblin Archer", + ], + _ => vec![ + "Cirro Dark Elf", + "Cirro Dark Priestess", + "Cirro Spider", + ] + }; + + zone_tiles.iter().for_each(|idx| { + if build_data.map.tiles[*idx] == TileType::Grass { + match crate::rng::roll_dice(1, 10) { + 1 => build_data.spawn_list.push(( + *idx, + "Chair".to_string() + )), + 2 => { + let to_spawn = crate::rng::range(0, available_enemies.len() as i32); + build_data.spawn_list.push(( + *idx, + available_enemies[to_spawn as usize].to_string() + )); + } + _ => {} + } + } + }); + } + + fn make_roads(&mut self, build_data : &mut BuilderMap, voronoi_membership: &[i32]) { + for y in 1..build_data.map.height-1 { + for x in 1..build_data.map.width-1 { + let mut neighbors = 0; + let my_idx = build_data.map.xy_idx(x, y); + let my_seed = voronoi_membership[my_idx]; + if voronoi_membership[build_data.map.xy_idx(x-1, y)] != my_seed { neighbors += 1; } + if voronoi_membership[build_data.map.xy_idx(x+1, y)] != my_seed { neighbors += 1; } + if voronoi_membership[build_data.map.xy_idx(x, y-1)] != my_seed { neighbors += 1; } + if voronoi_membership[build_data.map.xy_idx(x, y+1)] != my_seed { neighbors += 1; } + + if neighbors > 1 { + build_data.map.tiles[my_idx] = TileType::Road; + } + } + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/distant_exit.rs b/chapter-75-darkplaza/src/map_builders/distant_exit.rs new file mode 100644 index 00000000..06e17e69 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/distant_exit.rs @@ -0,0 +1,45 @@ +use super::{MetaMapBuilder, BuilderMap, TileType}; + +pub struct DistantExit {} + +impl MetaMapBuilder for DistantExit { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl DistantExit { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(DistantExit{}) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + let starting_pos = build_data.starting_position.as_ref().unwrap().clone(); + let start_idx = build_data.map.xy_idx( + starting_pos.x, + starting_pos.y + ); + build_data.map.populate_blocked(); + let map_starts : Vec = vec![start_idx]; + let dijkstra_map = rltk::DijkstraMap::new(build_data.map.width as usize, build_data.map.height as usize, &map_starts , &build_data.map, 3000.0); + let mut exit_tile = (0, 0.0f32); + for (i, tile) in build_data.map.tiles.iter_mut().enumerate() { + if *tile == TileType::Floor { + let distance_to_start = dijkstra_map.map[i]; + if distance_to_start != std::f32::MAX { + // If it is further away than our current exit candidate, move the exit + if distance_to_start > exit_tile.1 { + exit_tile.0 = i; + exit_tile.1 = distance_to_start; + } + } + } + } + + // Place a staircase + let stairs_idx = exit_tile.0; + build_data.map.tiles[stairs_idx] = TileType::DownStairs; + build_data.take_snapshot(); + } +} diff --git a/chapter-75-darkplaza/src/map_builders/dla.rs b/chapter-75-darkplaza/src/map_builders/dla.rs new file mode 100644 index 00000000..e281af8a --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/dla.rs @@ -0,0 +1,177 @@ +use super::{InitialMapBuilder, MetaMapBuilder, BuilderMap, TileType, Position, Symmetry, paint}; + +#[derive(PartialEq, Copy, Clone)] +#[allow(dead_code)] +pub enum DLAAlgorithm { WalkInwards, WalkOutwards, CentralAttractor } + +pub struct DLABuilder { + algorithm : DLAAlgorithm, + brush_size: i32, + symmetry: Symmetry, + floor_percent: f32, +} + + +impl InitialMapBuilder for DLABuilder { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl MetaMapBuilder for DLABuilder { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl DLABuilder { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(DLABuilder{ + algorithm: DLAAlgorithm::WalkInwards, + brush_size: 2, + symmetry: Symmetry::None, + floor_percent: 0.25, + }) + } + + #[allow(dead_code)] + pub fn walk_inwards() -> Box { + Box::new(DLABuilder{ + algorithm: DLAAlgorithm::WalkInwards, + brush_size: 1, + symmetry: Symmetry::None, + floor_percent: 0.25, + }) + } + + #[allow(dead_code)] + pub fn walk_outwards() -> Box { + Box::new(DLABuilder{ + algorithm: DLAAlgorithm::WalkOutwards, + brush_size: 2, + symmetry: Symmetry::None, + floor_percent: 0.25, + }) + } + + #[allow(dead_code)] + pub fn heavy_erosion() -> Box { + Box::new(DLABuilder{ + algorithm: DLAAlgorithm::WalkInwards, + brush_size: 2, + symmetry: Symmetry::None, + floor_percent: 0.35, + }) + } + + #[allow(dead_code)] + pub fn central_attractor() -> Box { + Box::new(DLABuilder{ + algorithm: DLAAlgorithm::CentralAttractor, + brush_size: 2, + symmetry: Symmetry::None, + floor_percent: 0.25, + }) + } + + #[allow(dead_code)] + pub fn insectoid() -> Box { + Box::new(DLABuilder{ + algorithm: DLAAlgorithm::CentralAttractor, + brush_size: 2, + symmetry: Symmetry::Horizontal, + floor_percent: 0.25, + }) + } + + #[allow(clippy::map_entry)] + fn build(&mut self, build_data : &mut BuilderMap) { + // Carve a starting seed + let starting_position = Position{ x: build_data.map.width/2, y : build_data.map.height/2 }; + let start_idx = build_data.map.xy_idx(starting_position.x, starting_position.y); + build_data.take_snapshot(); + build_data.map.tiles[start_idx] = TileType::Floor; + build_data.map.tiles[start_idx-1] = TileType::Floor; + build_data.map.tiles[start_idx+1] = TileType::Floor; + build_data.map.tiles[start_idx-build_data.map.width as usize] = TileType::Floor; + build_data.map.tiles[start_idx+build_data.map.width as usize] = TileType::Floor; + + // Random walker + let total_tiles = build_data.map.width * build_data.map.height; + let desired_floor_tiles = (self.floor_percent * total_tiles as f32) as usize; + let mut floor_tile_count = build_data.map.tiles.iter().filter(|a| **a == TileType::Floor).count(); + while floor_tile_count < desired_floor_tiles { + + match self.algorithm { + DLAAlgorithm::WalkInwards => { + let mut digger_x = crate::rng::roll_dice(1, build_data.map.width - 3) + 1; + let mut digger_y = crate::rng::roll_dice(1, build_data.map.height - 3) + 1; + let mut prev_x = digger_x; + let mut prev_y = digger_y; + let mut digger_idx = build_data.map.xy_idx(digger_x, digger_y); + while build_data.map.tiles[digger_idx] == TileType::Wall { + prev_x = digger_x; + prev_y = digger_y; + let stagger_direction = crate::rng::roll_dice(1, 4); + match stagger_direction { + 1 => { if digger_x > 2 { digger_x -= 1; } } + 2 => { if digger_x < build_data.map.width-2 { digger_x += 1; } } + 3 => { if digger_y > 2 { digger_y -=1; } } + _ => { if digger_y < build_data.map.height-2 { digger_y += 1; } } + } + digger_idx = build_data.map.xy_idx(digger_x, digger_y); + } + paint(&mut build_data.map, self.symmetry, self.brush_size, prev_x, prev_y); + } + + DLAAlgorithm::WalkOutwards => { + let mut digger_x = starting_position.x; + let mut digger_y = starting_position.y; + let mut digger_idx = build_data.map.xy_idx(digger_x, digger_y); + while build_data.map.tiles[digger_idx] == TileType::Floor { + let stagger_direction = crate::rng::roll_dice(1, 4); + match stagger_direction { + 1 => { if digger_x > 2 { digger_x -= 1; } } + 2 => { if digger_x < build_data.map.width-2 { digger_x += 1; } } + 3 => { if digger_y > 2 { digger_y -=1; } } + _ => { if digger_y < build_data.map.height-2 { digger_y += 1; } } + } + digger_idx = build_data.map.xy_idx(digger_x, digger_y); + } + paint(&mut build_data.map, self.symmetry, self.brush_size, digger_x, digger_y); + } + + DLAAlgorithm::CentralAttractor => { + let mut digger_x = crate::rng::roll_dice(1, build_data.map.width - 3) + 1; + let mut digger_y = crate::rng::roll_dice(1, build_data.map.height - 3) + 1; + let mut prev_x = digger_x; + let mut prev_y = digger_y; + let mut digger_idx = build_data.map.xy_idx(digger_x, digger_y); + + let mut path = rltk::line2d( + rltk::LineAlg::Bresenham, + rltk::Point::new( digger_x, digger_y ), + rltk::Point::new( starting_position.x, starting_position.y ) + ); + + while build_data.map.tiles[digger_idx] == TileType::Wall && !path.is_empty() { + prev_x = digger_x; + prev_y = digger_y; + digger_x = path[0].x; + digger_y = path[0].y; + path.remove(0); + digger_idx = build_data.map.xy_idx(digger_x, digger_y); + } + paint(&mut build_data.map, self.symmetry, self.brush_size, prev_x, prev_y); + } + } + + build_data.take_snapshot(); + + floor_tile_count = build_data.map.tiles.iter().filter(|a| **a == TileType::Floor).count(); + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/door_placement.rs b/chapter-75-darkplaza/src/map_builders/door_placement.rs new file mode 100644 index 00000000..a73e3c2a --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/door_placement.rs @@ -0,0 +1,71 @@ +use super::{MetaMapBuilder, BuilderMap, TileType }; + +pub struct DoorPlacement {} + +impl MetaMapBuilder for DoorPlacement { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.doors(build_data); + } +} + +impl DoorPlacement { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(DoorPlacement{ }) + } + + fn door_possible(&self, build_data : &mut BuilderMap, idx : usize) -> bool { + let mut blocked = false; + for spawn in build_data.spawn_list.iter() { + if spawn.0 == idx { blocked = true; } + } + if blocked { return false; } + + let x = (idx % build_data.map.width as usize) as i32; + let y = (idx / build_data.map.width as usize) as i32; + + // Check for east-west door possibility + if build_data.map.tiles[idx] == TileType::Floor && + (x > 1 && build_data.map.tiles[idx-1] == TileType::Floor) && + (x < build_data.map.width-2 && build_data.map.tiles[idx+1] == TileType::Floor) && + (y > 1 && build_data.map.tiles[idx - build_data.map.width as usize] == TileType::Wall) && + (y < build_data.map.height-2 && build_data.map.tiles[idx + build_data.map.width as usize] == TileType::Wall) + { + return true; + } + + // Check for north-south door possibility + if build_data.map.tiles[idx] == TileType::Floor && + (x > 1 && build_data.map.tiles[idx-1] == TileType::Wall) && + (x < build_data.map.width-2 && build_data.map.tiles[idx+1] == TileType::Wall) && + (y > 1 && build_data.map.tiles[idx - build_data.map.width as usize] == TileType::Floor) && + (y < build_data.map.height-2 && build_data.map.tiles[idx + build_data.map.width as usize] == TileType::Floor) + { + return true; + } + + false + } + + fn doors(&mut self, build_data : &mut BuilderMap) { + if let Some(halls_original) = &build_data.corridors { + let halls = halls_original.clone(); // To avoid nested borrowing + for hall in halls.iter() { + if hall.len() > 2 { // We aren't interested in tiny corridors + if self.door_possible(build_data, hall[0]) { + build_data.spawn_list.push((hall[0], "Door".to_string())); + } + } + } + } else { + // There are no corridors - scan for possible places + let tiles = build_data.map.tiles.clone(); + for (i, tile) in tiles.iter().enumerate() { + if *tile == TileType::Floor && self.door_possible(build_data, i) && crate::rng::roll_dice(1,3)==1 { + build_data.spawn_list.push((i, "Door".to_string())); + } + } + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/drunkard.rs b/chapter-75-darkplaza/src/map_builders/drunkard.rs new file mode 100644 index 00000000..fe615679 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/drunkard.rs @@ -0,0 +1,168 @@ +use super::{InitialMapBuilder, MetaMapBuilder, BuilderMap, TileType, Position, paint, Symmetry}; + +#[derive(PartialEq, Copy, Clone)] +#[allow(dead_code)] +pub enum DrunkSpawnMode { StartingPoint, Random } + +pub struct DrunkardSettings { + pub spawn_mode : DrunkSpawnMode, + pub drunken_lifetime : i32, + pub floor_percent: f32, + pub brush_size: i32, + pub symmetry: Symmetry +} + +pub struct DrunkardsWalkBuilder { + settings : DrunkardSettings +} + +impl InitialMapBuilder for DrunkardsWalkBuilder { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl MetaMapBuilder for DrunkardsWalkBuilder { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl DrunkardsWalkBuilder { + #[allow(dead_code)] + pub fn new(settings: DrunkardSettings) -> DrunkardsWalkBuilder { + DrunkardsWalkBuilder{ + settings + } + } + + #[allow(dead_code)] + pub fn open_area() -> Box { + Box::new(DrunkardsWalkBuilder{ + settings : DrunkardSettings{ + spawn_mode: DrunkSpawnMode::StartingPoint, + drunken_lifetime: 400, + floor_percent: 0.5, + brush_size: 1, + symmetry: Symmetry::None + } + }) + } + + #[allow(dead_code)] + pub fn open_halls() -> Box { + Box::new(DrunkardsWalkBuilder{ + settings : DrunkardSettings{ + spawn_mode: DrunkSpawnMode::Random, + drunken_lifetime: 400, + floor_percent: 0.5, + brush_size: 1, + symmetry: Symmetry::None + }, + }) + } + + #[allow(dead_code)] + pub fn winding_passages() -> Box { + Box::new(DrunkardsWalkBuilder{ + settings : DrunkardSettings{ + spawn_mode: DrunkSpawnMode::Random, + drunken_lifetime: 100, + floor_percent: 0.4, + brush_size: 1, + symmetry: Symmetry::None + }, + }) + } + + #[allow(dead_code)] + pub fn fat_passages() -> Box { + Box::new(DrunkardsWalkBuilder{ + settings : DrunkardSettings{ + spawn_mode: DrunkSpawnMode::Random, + drunken_lifetime: 100, + floor_percent: 0.4, + brush_size: 2, + symmetry: Symmetry::None + }, + }) + } + + #[allow(dead_code)] + pub fn fearful_symmetry() -> Box { + Box::new(DrunkardsWalkBuilder{ + settings : DrunkardSettings{ + spawn_mode: DrunkSpawnMode::Random, + drunken_lifetime: 100, + floor_percent: 0.4, + brush_size: 1, + symmetry: Symmetry::Both + }, + }) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + // Set a central starting point + let starting_position = Position{ x: build_data.map.width / 2, y: build_data.map.height / 2 }; + let start_idx = build_data.map.xy_idx(starting_position.x, starting_position.y); + build_data.map.tiles[start_idx] = TileType::Floor; + + let total_tiles = build_data.map.width * build_data.map.height; + let desired_floor_tiles = (self.settings.floor_percent * total_tiles as f32) as usize; + let mut floor_tile_count = build_data.map.tiles.iter().filter(|a| **a == TileType::Floor).count(); + let mut digger_count = 0; + while floor_tile_count < desired_floor_tiles { + let mut did_something = false; + let mut drunk_x; + let mut drunk_y; + match self.settings.spawn_mode { + DrunkSpawnMode::StartingPoint => { + drunk_x = starting_position.x; + drunk_y = starting_position.y; + } + DrunkSpawnMode::Random => { + if digger_count == 0 { + drunk_x = starting_position.x; + drunk_y = starting_position.y; + } else { + drunk_x = crate::rng::roll_dice(1, build_data.map.width - 3) + 1; + drunk_y = crate::rng::roll_dice(1, build_data.map.height - 3) + 1; + } + } + } + let mut drunk_life = self.settings.drunken_lifetime; + + while drunk_life > 0 { + let drunk_idx = build_data.map.xy_idx(drunk_x, drunk_y); + if build_data.map.tiles[drunk_idx] == TileType::Wall { + did_something = true; + } + paint(&mut build_data.map, self.settings.symmetry, self.settings.brush_size, drunk_x, drunk_y); + build_data.map.tiles[drunk_idx] = TileType::DownStairs; + + let stagger_direction = crate::rng::roll_dice(1, 4); + match stagger_direction { + 1 => { if drunk_x > 2 { drunk_x -= 1; } } + 2 => { if drunk_x < build_data.map.width-2 { drunk_x += 1; } } + 3 => { if drunk_y > 2 { drunk_y -=1; } } + _ => { if drunk_y < build_data.map.height-2 { drunk_y += 1; } } + } + + drunk_life -= 1; + } + if did_something { + build_data.take_snapshot(); + } + + digger_count += 1; + for t in build_data.map.tiles.iter_mut() { + if *t == TileType::DownStairs { + *t = TileType::Floor; + } + } + floor_tile_count = build_data.map.tiles.iter().filter(|a| **a == TileType::Floor).count(); + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/dwarf_fort_builder.rs b/chapter-75-darkplaza/src/map_builders/dwarf_fort_builder.rs new file mode 100644 index 00000000..dccff8eb --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/dwarf_fort_builder.rs @@ -0,0 +1,119 @@ +use super::{BuilderChain, XStart, YStart, AreaStartingPosition, RoomSorter, RoomSort, + CullUnreachable, VoronoiSpawning, BspDungeonBuilder, DistantExit, BspCorridors, + CorridorSpawner, RoomDrawer, BuilderMap, MetaMapBuilder, DLABuilder, TileType, + AreaEndingPosition, XEnd, YEnd}; + +pub fn dwarf_fort_builder(new_depth: i32, width: i32, height: i32) -> BuilderChain { + let mut chain = BuilderChain::new(new_depth, width, height, "Dwarven Fortress"); + chain.start_with(BspDungeonBuilder::new()); + chain.with(RoomSorter::new(RoomSort::CENTRAL)); + chain.with(RoomDrawer::new()); + chain.with(BspCorridors::new()); + chain.with(CorridorSpawner::new()); + chain.with(DragonsLair::new()); + + chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::TOP)); + chain.with(CullUnreachable::new()); + chain.with(AreaEndingPosition::new(XEnd::RIGHT, YEnd::BOTTOM)); + chain.with(VoronoiSpawning::new()); + chain.with(DistantExit::new()); + chain.with(DragonSpawner::new()); + chain +} + +pub struct DragonsLair {} + +impl MetaMapBuilder for DragonsLair { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl DragonsLair { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(DragonsLair{}) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + build_data.map.depth = 7; + build_data.take_snapshot(); + + let mut builder = BuilderChain::new(6, build_data.width, build_data.height, "New Map"); + builder.start_with(DLABuilder::insectoid()); + builder.build_map(); + + // Add the history to our history + for h in builder.build_data.history.iter() { + build_data.history.push(h.clone()); + } + build_data.take_snapshot(); + + // Merge the maps + for (idx, tt) in build_data.map.tiles.iter_mut().enumerate() { + if *tt == TileType::Wall && builder.build_data.map.tiles[idx] == TileType::Floor { + *tt = TileType::Floor; + } + } + build_data.take_snapshot(); + } +} + +pub struct DragonSpawner {} + +impl MetaMapBuilder for DragonSpawner { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl DragonSpawner { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(DragonSpawner{}) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + // Find a central location that isn't occupied + let seed_x = build_data.map.width / 2; + let seed_y = build_data.map.height / 2; + let mut available_floors : Vec<(usize, f32)> = Vec::new(); + for (idx, tiletype) in build_data.map.tiles.iter().enumerate() { + if crate::map::tile_walkable(*tiletype) { + available_floors.push( + ( + idx, + rltk::DistanceAlg::PythagorasSquared.distance2d( + rltk::Point::new(idx as i32 % build_data.map.width, idx as i32 / build_data.map.width), + rltk::Point::new(seed_x, seed_y) + ) + ) + ); + } + } + if available_floors.is_empty() { + panic!("No valid floors to start on"); + } + + available_floors.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap()); + + let start_x = available_floors[0].0 as i32 % build_data.map.width; + let start_y = available_floors[0].0 as i32 / build_data.map.width; + let dragon_pt = rltk::Point::new(start_x, start_y); + + // Remove all spawns within 25 tiles of the drake + let w = build_data.map.width as i32; + build_data.spawn_list.retain(|spawn| { + let spawn_pt = rltk::Point::new( + spawn.0 as i32 % w, + spawn.0 as i32 / w + ); + let distance = rltk::DistanceAlg::Pythagoras.distance2d(dragon_pt, spawn_pt); + distance > 25.0 + }); + + // Add the dragon + let dragon_idx = build_data.map.xy_idx(start_x, start_y); + build_data.spawn_list.push((dragon_idx, "Black Dragon".to_string())); + } +} diff --git a/chapter-75-darkplaza/src/map_builders/forest.rs b/chapter-75-darkplaza/src/map_builders/forest.rs new file mode 100644 index 00000000..c04fd571 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/forest.rs @@ -0,0 +1,113 @@ +use super::{BuilderChain, CellularAutomataBuilder, XStart, YStart, AreaStartingPosition, + CullUnreachable, VoronoiSpawning, MetaMapBuilder, BuilderMap, TileType}; +use crate::map; + +pub fn forest_builder(new_depth: i32, width: i32, height: i32) -> BuilderChain { + let mut chain = BuilderChain::new(new_depth, width, height, "Into the Woods"); + chain.start_with(CellularAutomataBuilder::new()); + chain.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)); + chain.with(CullUnreachable::new()); + chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER)); + chain.with(VoronoiSpawning::new()); + chain.with(YellowBrickRoad::new()); + chain +} + +pub struct YellowBrickRoad {} + +impl MetaMapBuilder for YellowBrickRoad { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl YellowBrickRoad { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(YellowBrickRoad{}) + } + + fn find_exit(&self, build_data : &mut BuilderMap, seed_x : i32, seed_y: i32) -> (i32, i32) { + let mut available_floors : Vec<(usize, f32)> = Vec::new(); + for (idx, tiletype) in build_data.map.tiles.iter().enumerate() { + if map::tile_walkable(*tiletype) { + available_floors.push( + ( + idx, + rltk::DistanceAlg::PythagorasSquared.distance2d( + rltk::Point::new(idx as i32 % build_data.map.width, idx as i32 / build_data.map.width), + rltk::Point::new(seed_x, seed_y) + ) + ) + ); + } + } + if available_floors.is_empty() { + panic!("No valid floors to start on"); + } + + available_floors.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap()); + + let end_x = available_floors[0].0 as i32 % build_data.map.width; + let end_y = available_floors[0].0 as i32 / build_data.map.width; + (end_x, end_y) + } + + fn paint_road(&self, build_data : &mut BuilderMap, x: i32, y: i32) { + if x < 1 || x > build_data.map.width-2 || y < 1 || y > build_data.map.height-2 { + return; + } + let idx = build_data.map.xy_idx(x, y); + if build_data.map.tiles[idx] != TileType::DownStairs { + build_data.map.tiles[idx] = TileType::Road; + } + } + + fn build(&mut self, build_data : &mut BuilderMap) { + let starting_pos = build_data.starting_position.as_ref().unwrap().clone(); + let start_idx = build_data.map.xy_idx(starting_pos.x, starting_pos.y); + + let (end_x, end_y) = self.find_exit(build_data, build_data.map.width - 2, build_data.map.height / 2); + let end_idx = build_data.map.xy_idx(end_x, end_y); + + build_data.map.populate_blocked(); + let path = rltk::a_star_search(start_idx, end_idx, &build_data.map); + if !path.success { + panic!("No valid path for the road"); + } + for idx in path.steps.iter() { + let x = *idx as i32 % build_data.map.width; + let y = *idx as i32 / build_data.map.width; + self.paint_road(build_data, x, y); + self.paint_road(build_data, x-1, y); + self.paint_road(build_data, x+1, y); + self.paint_road(build_data, x, y-1); + self.paint_road(build_data, x, y+1); + } + build_data.map.tiles[end_idx] = TileType::DownStairs; + build_data.take_snapshot(); + + // Place exit + let exit_dir = crate::rng::roll_dice(1, 2); + let (seed_x, seed_y, stream_startx, stream_starty) = if exit_dir == 1 { + (build_data.map.width-1, 1, 0, build_data.height-1) + } else { + (build_data.map.width-1, build_data.height-1, 1, build_data.height-1) + }; + + let (stairs_x, stairs_y) = self.find_exit(build_data, seed_x, seed_y); + let stairs_idx = build_data.map.xy_idx(stairs_x, stairs_y); + build_data.take_snapshot(); + + let (stream_x, stream_y) = self.find_exit(build_data, stream_startx, stream_starty); + let stream_idx = build_data.map.xy_idx(stream_x, stream_y) as usize; + let stream = rltk::a_star_search(stairs_idx, stream_idx, &mut build_data.map); + for tile in stream.steps.iter() { + if build_data.map.tiles[*tile as usize] == TileType::Floor { + build_data.map.tiles[*tile as usize] = TileType::ShallowWater; + } + } + build_data.map.tiles[stairs_idx] = TileType::DownStairs; + build_data.take_snapshot(); + } +} diff --git a/chapter-75-darkplaza/src/map_builders/limestone_cavern.rs b/chapter-75-darkplaza/src/map_builders/limestone_cavern.rs new file mode 100644 index 00000000..c7ae7502 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/limestone_cavern.rs @@ -0,0 +1,152 @@ +use super::{BuilderChain, DrunkardsWalkBuilder, XStart, YStart, AreaStartingPosition, + CullUnreachable, VoronoiSpawning, MetaMapBuilder, BuilderMap, TileType, DistantExit, + DLABuilder, PrefabBuilder, CellularAutomataBuilder, AreaEndingPosition, + BspDungeonBuilder, RoomSorter, RoomSort, NearestCorridors, RoomExploder, RoomDrawer, + RoomBasedSpawner, XEnd, YEnd}; + +pub fn limestone_cavern_builder(new_depth: i32, width: i32, height: i32) -> BuilderChain { + let mut chain = BuilderChain::new(new_depth, width, height, "Limestone Caverns"); + chain.start_with(DrunkardsWalkBuilder::winding_passages()); + chain.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)); + chain.with(CullUnreachable::new()); + chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER)); + chain.with(VoronoiSpawning::new()); + chain.with(DistantExit::new()); + chain.with(CaveDecorator::new()); + chain +} + +pub fn limestone_deep_cavern_builder(new_depth: i32, width: i32, height: i32) -> BuilderChain { + let mut chain = BuilderChain::new(new_depth, width, height, "Deep Limestone Caverns"); + chain.start_with(DLABuilder::central_attractor()); + chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::TOP)); + chain.with(VoronoiSpawning::new()); + chain.with(DistantExit::new()); + chain.with(CaveDecorator::new()); + chain.with(PrefabBuilder::sectional(super::prefab_builder::prefab_sections::ORC_CAMP)); + chain +} + +pub fn limestone_transition_builder(new_depth: i32, width: i32, height: i32) -> BuilderChain { + let mut chain = BuilderChain::new(new_depth, width, height, "Dwarf Fort - Upper Reaches"); + chain.start_with(CellularAutomataBuilder::new()); + chain.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)); + chain.with(CullUnreachable::new()); + chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER)); + chain.with(VoronoiSpawning::new()); + chain.with(CaveDecorator::new()); + chain.with(CaveTransition::new()); + chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTER)); + chain.with(CullUnreachable::new()); + chain.with(AreaEndingPosition::new(XEnd::RIGHT, YEnd::CENTER)); + chain +} + +pub struct CaveDecorator {} + +impl MetaMapBuilder for CaveDecorator { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl CaveDecorator { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(CaveDecorator{}) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + let old_map = build_data.map.clone(); + for (idx,tt) in build_data.map.tiles.iter_mut().enumerate() { + // Gravel Spawning + if *tt == TileType::Floor && crate::rng::roll_dice(1, 6)==1 { + *tt = TileType::Gravel; + } else if *tt == TileType::Floor && crate::rng::roll_dice(1, 10)==1 { + // Spawn passable pools + *tt = TileType::ShallowWater; + } else if *tt == TileType::Wall { + // Spawn deep pools and stalactites + let mut neighbors = 0; + let x = idx as i32 % old_map.width; + let y = idx as i32 / old_map.width; + if x > 0 && old_map.tiles[idx-1] == TileType::Wall { neighbors += 1; } + if x < old_map.width - 2 && old_map.tiles[idx+1] == TileType::Wall { neighbors += 1; } + if y > 0 && old_map.tiles[idx-old_map.width as usize] == TileType::Wall { neighbors += 1; } + if y < old_map.height - 2 && old_map.tiles[idx+old_map.width as usize] == TileType::Wall { neighbors += 1; } + if neighbors == 2 { + *tt = TileType::DeepWater; + } else if neighbors == 1 { + let roll = crate::rng::roll_dice(1, 4); + match roll { + 1 => *tt = TileType::Stalactite, + 2 => *tt = TileType::Stalagmite, + _ => {} + } + } + } + } + build_data.take_snapshot(); + build_data.map.outdoors = false; + } +} + +pub struct CaveTransition {} + +impl MetaMapBuilder for CaveTransition { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl CaveTransition { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(CaveTransition{}) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + build_data.map.depth = 5; + build_data.take_snapshot(); + + // Build a BSP-based dungeon + let mut builder = BuilderChain::new(5, build_data.width, build_data.height, "New Map"); + builder.start_with(BspDungeonBuilder::new()); + builder.with(RoomDrawer::new()); + builder.with(RoomSorter::new(RoomSort::RIGHTMOST)); + builder.with(NearestCorridors::new()); + builder.with(RoomExploder::new()); + builder.with(RoomBasedSpawner::new()); + builder.build_map(); + + // Add the history to our history + for h in builder.build_data.history.iter() { + build_data.history.push(h.clone()); + } + build_data.take_snapshot(); + + // Copy the right half of the BSP map into our map + for x in build_data.map.width / 2 .. build_data.map.width { + for y in 0 .. build_data.map.height { + let idx = build_data.map.xy_idx(x, y); + build_data.map.tiles[idx] = builder.build_data.map.tiles[idx]; + } + } + build_data.take_snapshot(); + + // Keep Voronoi spawn data from the left half of the map + let w = build_data.map.width; + build_data.spawn_list.retain(|s| { + let x = s.0 as i32 / w; + x < w / 2 + }); + + // Keep room spawn data from the right half of the map + for s in builder.build_data.spawn_list.iter() { + let x = s.0 as i32 / w; + if x > w / 2 { + build_data.spawn_list.push(s.clone()); + } + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/maze.rs b/chapter-75-darkplaza/src/map_builders/maze.rs new file mode 100644 index 00000000..08cb1765 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/maze.rs @@ -0,0 +1,197 @@ +use super::{Map, InitialMapBuilder, BuilderMap, TileType}; + +pub struct MazeBuilder {} + +impl InitialMapBuilder for MazeBuilder { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl MazeBuilder { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(MazeBuilder{}) + } + + #[allow(clippy::map_entry)] + fn build(&mut self, build_data : &mut BuilderMap) { + // Maze gen + let mut maze = Grid::new((build_data.map.width / 2)-2, (build_data.map.height / 2)-2); + maze.generate_maze(build_data); + } +} + +/* Maze code taken under MIT from https://github.com/cyucelen/mazeGenerator/ */ + +const TOP : usize = 0; +const RIGHT : usize = 1; +const BOTTOM : usize = 2; +const LEFT : usize = 3; + +#[derive(Copy, Clone)] +struct Cell { + row: i32, + column: i32, + walls: [bool; 4], + visited: bool, +} + +impl Cell { + fn new(row: i32, column: i32) -> Cell { + Cell{ + row, + column, + walls: [true, true, true, true], + visited: false + } + } + + fn remove_walls(&mut self, next : &mut Cell) { + let x = self.column - next.column; + let y = self.row - next.row; + + if x == 1 { + self.walls[LEFT] = false; + next.walls[RIGHT] = false; + } + else if x == -1 { + self.walls[RIGHT] = false; + next.walls[LEFT] = false; + } + else if y == 1 { + self.walls[TOP] = false; + next.walls[BOTTOM] = false; + } + else if y == -1 { + self.walls[BOTTOM] = false; + next.walls[TOP] = false; + } + } +} + +struct Grid { + width: i32, + height: i32, + cells: Vec, + backtrace: Vec, + current: usize +} + +impl Grid { + fn new(width: i32, height:i32) -> Grid { + let mut grid = Grid{ + width, + height, + cells: Vec::new(), + backtrace: Vec::new(), + current: 0 + }; + + for row in 0..height { + for column in 0..width { + grid.cells.push(Cell::new(row, column)); + } + } + + grid + } + + fn calculate_index(&self, row: i32, column: i32) -> i32 { + if row < 0 || column < 0 || column > self.width-1 || row > self.height-1 { + -1 + } else { + column + (row * self.width) + } + } + + fn get_available_neighbors(&self) -> Vec { + let mut neighbors : Vec = Vec::new(); + + let current_row = self.cells[self.current].row; + let current_column = self.cells[self.current].column; + + let neighbor_indices : [i32; 4] = [ + self.calculate_index(current_row -1, current_column), + self.calculate_index(current_row, current_column + 1), + self.calculate_index(current_row + 1, current_column), + self.calculate_index(current_row, current_column - 1) + ]; + + for i in neighbor_indices.iter() { + if *i != -1 && !self.cells[*i as usize].visited { + neighbors.push(*i as usize); + } + } + + neighbors + } + + fn find_next_cell(&mut self) -> Option { + let neighbors = self.get_available_neighbors(); + if !neighbors.is_empty() { + if neighbors.len() == 1 { + return Some(neighbors[0]); + } else { + return Some(neighbors[(crate::rng::roll_dice(1, neighbors.len() as i32)-1) as usize]); + } + } + None + } + + fn generate_maze(&mut self, build_data : &mut BuilderMap) { + let mut i = 0; + loop { + self.cells[self.current].visited = true; + let next = self.find_next_cell(); + + match next { + Some(next) => { + self.cells[next].visited = true; + self.backtrace.push(self.current); + // __lower_part__ __higher_part_ + // / \ / \ + // --------cell1------ | cell2----------- + let (lower_part, higher_part) = + self.cells.split_at_mut(std::cmp::max(self.current, next)); + let cell1 = &mut lower_part[std::cmp::min(self.current, next)]; + let cell2 = &mut higher_part[0]; + cell1.remove_walls(cell2); + self.current = next; + } + None => { + if !self.backtrace.is_empty() { + self.current = self.backtrace[0]; + self.backtrace.remove(0); + } else { + break; + } + } + } + + if i % 50 == 0 { + self.copy_to_map(&mut build_data.map); + build_data.take_snapshot(); + } + i += 1; + } + } + + fn copy_to_map(&self, map : &mut Map) { + // Clear the map + for i in map.tiles.iter_mut() { *i = TileType::Wall; } + + for cell in self.cells.iter() { + let x = cell.column + 1; + let y = cell.row + 1; + let idx = map.xy_idx(x * 2, y * 2); + + map.tiles[idx] = TileType::Floor; + if !cell.walls[TOP] { map.tiles[idx - map.width as usize] = TileType::Floor } + if !cell.walls[RIGHT] { map.tiles[idx + 1] = TileType::Floor } + if !cell.walls[BOTTOM] { map.tiles[idx + map.width as usize] = TileType::Floor } + if !cell.walls[LEFT] { map.tiles[idx - 1] = TileType::Floor } + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/mod.rs b/chapter-75-darkplaza/src/map_builders/mod.rs new file mode 100644 index 00000000..96e1b59e --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/mod.rs @@ -0,0 +1,325 @@ +use super::{Map, Rect, TileType, Position, spawner, SHOW_MAPGEN_VISUALIZER}; +use specs::prelude::*; +mod simple_map; +mod bsp_dungeon; +mod bsp_interior; +mod cellular_automata; +mod drunkard; +mod maze; +mod dla; +mod common; +mod voronoi; +mod waveform_collapse; +mod prefab_builder; +mod room_based_spawner; +mod room_based_starting_position; +mod room_based_stairs; +mod area_starting_points; +mod cull_unreachable; +mod voronoi_spawning; +mod distant_exit; +mod room_exploder; +mod room_corner_rounding; +mod rooms_corridors_dogleg; +mod rooms_corridors_bsp; +mod room_sorter; +mod room_draw; +mod rooms_corridors_nearest; +mod rooms_corridors_lines; +mod room_corridor_spawner; +mod door_placement; +mod town; +mod forest; +mod limestone_cavern; +mod dwarf_fort_builder; +mod area_ending_point; +use forest::forest_builder; +use limestone_cavern::*; +use dwarf_fort_builder::*; +use distant_exit::DistantExit; +use simple_map::SimpleMapBuilder; +use bsp_dungeon::BspDungeonBuilder; +use bsp_interior::BspInteriorBuilder; +use cellular_automata::CellularAutomataBuilder; +use drunkard::DrunkardsWalkBuilder; +use voronoi::VoronoiCellBuilder; +use waveform_collapse::WaveformCollapseBuilder; +use prefab_builder::PrefabBuilder; +use room_based_spawner::RoomBasedSpawner; +use room_based_starting_position::RoomBasedStartingPosition; +use room_based_stairs::RoomBasedStairs; +use area_starting_points::{AreaStartingPosition, XStart, YStart}; +use cull_unreachable::CullUnreachable; +use voronoi_spawning::VoronoiSpawning; +use maze::MazeBuilder; +use dla::DLABuilder; +use common::*; +use room_exploder::RoomExploder; +use room_corner_rounding::RoomCornerRounder; +use rooms_corridors_dogleg::DoglegCorridors; +use rooms_corridors_bsp::BspCorridors; +use room_sorter::{RoomSorter, RoomSort}; +use room_draw::RoomDrawer; +use rooms_corridors_nearest::NearestCorridors; +use rooms_corridors_lines::StraightLineCorridors; +use room_corridor_spawner::CorridorSpawner; +use door_placement::DoorPlacement; +use town::town_builder; +use area_ending_point::*; +mod mushroom_forest; +use mushroom_forest::*; +mod dark_elves; +use dark_elves::*; + +pub struct BuilderMap { + pub spawn_list : Vec<(usize, String)>, + pub map : Map, + pub starting_position : Option, + pub rooms: Option>, + pub corridors: Option>>, + pub history : Vec, + pub width: i32, + pub height: i32 +} + +impl BuilderMap { + fn take_snapshot(&mut self) { + if SHOW_MAPGEN_VISUALIZER { + let mut snapshot = self.map.clone(); + for v in snapshot.revealed_tiles.iter_mut() { + *v = true; + } + self.history.push(snapshot); + } + } +} + +pub struct BuilderChain { + starter: Option>, + builders: Vec>, + pub build_data : BuilderMap +} + +impl BuilderChain { + pub fn new(new_depth : i32, width: i32, height: i32, name : S) -> BuilderChain { + BuilderChain{ + starter: None, + builders: Vec::new(), + build_data : BuilderMap { + spawn_list: Vec::new(), + map: Map::new(new_depth, width, height, name), + starting_position: None, + rooms: None, + corridors: None, + history : Vec::new(), + width, + height + } + } + } + + pub fn start_with(&mut self, starter : Box) { + match self.starter { + None => self.starter = Some(starter), + Some(_) => panic!("You can only have one starting builder.") + }; + } + + pub fn with(&mut self, metabuilder : Box) { + self.builders.push(metabuilder); + } + + pub fn build_map(&mut self) { + match &mut self.starter { + None => panic!("Cannot run a map builder chain without a starting build system"), + Some(starter) => { + // Build the starting map + starter.build_map(&mut self.build_data); + } + } + + // Build additional layers in turn + for metabuilder in self.builders.iter_mut() { + metabuilder.build_map(&mut self.build_data); + } + } + + pub fn spawn_entities(&mut self, ecs : &mut World) { + for entity in self.build_data.spawn_list.iter() { + spawner::spawn_entity(ecs, &(&entity.0, &entity.1)); + } + } +} + +pub trait InitialMapBuilder { + fn build_map(&mut self, build_data : &mut BuilderMap); +} + +pub trait MetaMapBuilder { + fn build_map(&mut self, build_data : &mut BuilderMap); +} + +fn random_start_position() -> (XStart, YStart) { + let x; + let xroll = crate::rng::roll_dice(1, 3); + match xroll { + 1 => x = XStart::LEFT, + 2 => x = XStart::CENTER, + _ => x = XStart::RIGHT + } + + let y; + let yroll = crate::rng::roll_dice(1, 3); + match yroll { + 1 => y = YStart::BOTTOM, + 2 => y = YStart::CENTER, + _ => y = YStart::TOP + } + + (x, y) +} + +fn random_room_builder(builder : &mut BuilderChain) { + let build_roll = crate::rng::roll_dice(1, 3); + match build_roll { + 1 => builder.start_with(SimpleMapBuilder::new()), + 2 => builder.start_with(BspDungeonBuilder::new()), + _ => builder.start_with(BspInteriorBuilder::new()) + } + + // BSP Interior still makes holes in the walls + if build_roll != 3 { + // Sort by one of the 5 available algorithms + let sort_roll = crate::rng::roll_dice(1, 5); + match sort_roll { + 1 => builder.with(RoomSorter::new(RoomSort::LEFTMOST)), + 2 => builder.with(RoomSorter::new(RoomSort::RIGHTMOST)), + 3 => builder.with(RoomSorter::new(RoomSort::TOPMOST)), + 4 => builder.with(RoomSorter::new(RoomSort::BOTTOMMOST)), + _ => builder.with(RoomSorter::new(RoomSort::CENTRAL)), + } + + builder.with(RoomDrawer::new()); + + let corridor_roll = crate::rng::roll_dice(1, 4); + match corridor_roll { + 1 => builder.with(DoglegCorridors::new()), + 2 => builder.with(NearestCorridors::new()), + 3 => builder.with(StraightLineCorridors::new()), + _ => builder.with(BspCorridors::new()) + } + + let cspawn_roll = crate::rng::roll_dice(1, 2); + if cspawn_roll == 1 { + builder.with(CorridorSpawner::new()); + } + + let modifier_roll = crate::rng::roll_dice(1, 6); + match modifier_roll { + 1 => builder.with(RoomExploder::new()), + 2 => builder.with(RoomCornerRounder::new()), + _ => {} + } + } + + let start_roll = crate::rng::roll_dice(1, 2); + match start_roll { + 1 => builder.with(RoomBasedStartingPosition::new()), + _ => { + let (start_x, start_y) = random_start_position(); + builder.with(AreaStartingPosition::new(start_x, start_y)); + } + } + + let exit_roll = crate::rng::roll_dice(1, 2); + match exit_roll { + 1 => builder.with(RoomBasedStairs::new()), + _ => builder.with(DistantExit::new()) + } + + let spawn_roll = crate::rng::roll_dice(1, 2); + match spawn_roll { + 1 => builder.with(RoomBasedSpawner::new()), + _ => builder.with(VoronoiSpawning::new()) + } +} + +fn random_shape_builder(builder : &mut BuilderChain) { + let builder_roll = crate::rng::roll_dice(1, 16); + match builder_roll { + 1 => builder.start_with(CellularAutomataBuilder::new()), + 2 => builder.start_with(DrunkardsWalkBuilder::open_area()), + 3 => builder.start_with(DrunkardsWalkBuilder::open_halls()), + 4 => builder.start_with(DrunkardsWalkBuilder::winding_passages()), + 5 => builder.start_with(DrunkardsWalkBuilder::fat_passages()), + 6 => builder.start_with(DrunkardsWalkBuilder::fearful_symmetry()), + 7 => builder.start_with(MazeBuilder::new()), + 8 => builder.start_with(DLABuilder::walk_inwards()), + 9 => builder.start_with(DLABuilder::walk_outwards()), + 10 => builder.start_with(DLABuilder::central_attractor()), + 11 => builder.start_with(DLABuilder::insectoid()), + 12 => builder.start_with(VoronoiCellBuilder::pythagoras()), + 13 => builder.start_with(VoronoiCellBuilder::manhattan()), + _ => builder.start_with(PrefabBuilder::constant(prefab_builder::prefab_levels::WFC_POPULATED)), + } + + // Set the start to the center and cull + builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)); + builder.with(CullUnreachable::new()); + + // Now set the start to a random starting area + let (start_x, start_y) = random_start_position(); + builder.with(AreaStartingPosition::new(start_x, start_y)); + + // Setup an exit and spawn mobs + builder.with(VoronoiSpawning::new()); + builder.with(DistantExit::new()); +} + +pub fn random_builder(new_depth: i32, width: i32, height: i32) -> BuilderChain { + let mut builder = BuilderChain::new(new_depth, width, height, "New Map"); + let type_roll = crate::rng::roll_dice(1, 2); + match type_roll { + 1 => random_room_builder(&mut builder), + _ => random_shape_builder(&mut builder) + } + + if crate::rng::roll_dice(1, 3)==1 { + builder.with(WaveformCollapseBuilder::new()); + + // Now set the start to a random starting area + let (start_x, start_y) = random_start_position(); + builder.with(AreaStartingPosition::new(start_x, start_y)); + + // Setup an exit and spawn mobs + builder.with(VoronoiSpawning::new()); + builder.with(DistantExit::new()); + } + + if crate::rng::roll_dice(1, 20)==1 { + builder.with(PrefabBuilder::sectional(prefab_builder::prefab_sections::UNDERGROUND_FORT)); + } + + builder.with(DoorPlacement::new()); + builder.with(PrefabBuilder::vaults()); + + builder +} + +pub fn level_builder(new_depth: i32, width: i32, height: i32) -> BuilderChain { + rltk::console::log(format!("Depth: {}", new_depth)); + match new_depth { + 1 => town_builder(new_depth, width, height), + 2 => forest_builder(new_depth, width, height), + 3 => limestone_cavern_builder(new_depth, width, height), + 4 => limestone_deep_cavern_builder(new_depth, width, height), + 5 => limestone_transition_builder(new_depth, width, height), + 6 => dwarf_fort_builder(new_depth, width, height), + 7 => mushroom_entrance(new_depth, width, height), + 8 => mushroom_builder(new_depth, width, height), + 9 => mushroom_exit(new_depth, width, height), + 10 => dark_elf_city(new_depth, width, height), + 11 => dark_elf_plaza(new_depth, width, height), + _ => random_builder(new_depth, width, height) + } +} diff --git a/chapter-75-darkplaza/src/map_builders/mushroom_forest.rs b/chapter-75-darkplaza/src/map_builders/mushroom_forest.rs new file mode 100644 index 00000000..669a2425 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/mushroom_forest.rs @@ -0,0 +1,42 @@ +use super::{BuilderChain, XStart, YStart, AreaStartingPosition, + CullUnreachable, VoronoiSpawning, + AreaEndingPosition, XEnd, YEnd, CellularAutomataBuilder, PrefabBuilder, WaveformCollapseBuilder}; +use crate::map_builders::prefab_builder::prefab_sections::*; + +pub fn mushroom_entrance(new_depth: i32, width: i32, height: i32) -> BuilderChain { + let mut chain = BuilderChain::new(new_depth, width, height, "Into The Mushroom Grove"); + chain.start_with(CellularAutomataBuilder::new()); + chain.with(WaveformCollapseBuilder::new()); + chain.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)); + chain.with(CullUnreachable::new()); + chain.with(AreaStartingPosition::new(XStart::RIGHT, YStart::CENTER)); + chain.with(AreaEndingPosition::new(XEnd::LEFT, YEnd::CENTER)); + chain.with(VoronoiSpawning::new()); + chain.with(PrefabBuilder::sectional(UNDERGROUND_FORT)); + chain +} + +pub fn mushroom_builder(new_depth: i32, width: i32, height: i32) -> BuilderChain { + let mut chain = BuilderChain::new(new_depth, width, height, "Into The Mushroom Grove"); + chain.start_with(CellularAutomataBuilder::new()); + chain.with(WaveformCollapseBuilder::new()); + chain.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)); + chain.with(CullUnreachable::new()); + chain.with(AreaStartingPosition::new(XStart::RIGHT, YStart::CENTER)); + chain.with(AreaEndingPosition::new(XEnd::LEFT, YEnd::CENTER)); + chain.with(VoronoiSpawning::new()); + chain +} + +pub fn mushroom_exit(new_depth: i32, width: i32, height: i32) -> BuilderChain { + let mut chain = BuilderChain::new(new_depth, width, height, "Into The Mushroom Grove"); + chain.start_with(CellularAutomataBuilder::new()); + chain.with(WaveformCollapseBuilder::new()); + chain.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)); + chain.with(CullUnreachable::new()); + chain.with(AreaStartingPosition::new(XStart::RIGHT, YStart::CENTER)); + chain.with(AreaEndingPosition::new(XEnd::LEFT, YEnd::CENTER)); + chain.with(VoronoiSpawning::new()); + chain.with(PrefabBuilder::sectional(DROW_ENTRY)); + chain +} diff --git a/chapter-75-darkplaza/src/map_builders/prefab_builder/mod.rs b/chapter-75-darkplaza/src/map_builders/prefab_builder/mod.rs new file mode 100644 index 00000000..307aceb2 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/prefab_builder/mod.rs @@ -0,0 +1,326 @@ +use super::{InitialMapBuilder, MetaMapBuilder, BuilderMap, TileType, Position}; +pub mod prefab_levels; +pub mod prefab_sections; +pub mod prefab_rooms; +use std::collections::HashSet; + +#[derive(PartialEq, Copy, Clone)] +#[allow(dead_code)] +pub enum PrefabMode { + RexLevel{ template : &'static str }, + Constant{ level : prefab_levels::PrefabLevel }, + Sectional{ section : prefab_sections::PrefabSection }, + RoomVaults +} + +#[allow(dead_code)] +pub struct PrefabBuilder { + mode: PrefabMode +} + +impl MetaMapBuilder for PrefabBuilder { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl InitialMapBuilder for PrefabBuilder { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl PrefabBuilder { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(PrefabBuilder{ + mode : PrefabMode::RoomVaults, + }) + } + + #[allow(dead_code)] + pub fn rex_level(template : &'static str) -> Box { + Box::new(PrefabBuilder{ + mode : PrefabMode::RexLevel{ template }, + }) + } + + #[allow(dead_code)] + pub fn constant(level : prefab_levels::PrefabLevel) -> Box { + Box::new(PrefabBuilder{ + mode : PrefabMode::Constant{ level }, + }) + } + + #[allow(dead_code)] + pub fn sectional(section : prefab_sections::PrefabSection) -> Box { + Box::new(PrefabBuilder{ + mode : PrefabMode::Sectional{ section }, + }) + } + + #[allow(dead_code)] + pub fn vaults() -> Box { + Box::new(PrefabBuilder{ + mode : PrefabMode::RoomVaults, + }) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + match self.mode { + PrefabMode::RexLevel{template} => self.load_rex_map(&template, build_data), + PrefabMode::Constant{level} => self.load_ascii_map(&level, build_data), + PrefabMode::Sectional{section} => self.apply_sectional(§ion, build_data), + PrefabMode::RoomVaults => self.apply_room_vaults(build_data) + } + build_data.take_snapshot(); + } + + fn char_to_map(&mut self, ch : char, idx: usize, build_data : &mut BuilderMap) { + // Bounds check + if idx >= build_data.map.tiles.len()-1 { + return; + } + match ch { + ' ' => build_data.map.tiles[idx] = TileType::Floor, + '#' => build_data.map.tiles[idx] = TileType::Wall, + '≈' => build_data.map.tiles[idx] = TileType::DeepWater, + '@' => { + let x = idx as i32 % build_data.map.width; + let y = idx as i32 / build_data.map.width; + build_data.map.tiles[idx] = TileType::Floor; + build_data.starting_position = Some(Position{ x:x as i32, y:y as i32 }); + } + '>' => build_data.map.tiles[idx] = TileType::DownStairs, + 'e' => { + build_data.map.tiles[idx] = TileType::Floor; + build_data.spawn_list.push((idx, "Dark Elf".to_string())); + } + 'g' => { + build_data.map.tiles[idx] = TileType::Floor; + build_data.spawn_list.push((idx, "Goblin".to_string())); + } + 'o' => { + build_data.map.tiles[idx] = TileType::Floor; + build_data.spawn_list.push((idx, "Orc".to_string())); + } + 'O' => { + build_data.map.tiles[idx] = TileType::Floor; + build_data.spawn_list.push((idx, "Orc Leader".to_string())); + } + '^' => { + build_data.map.tiles[idx] = TileType::Floor; + build_data.spawn_list.push((idx, "Bear Trap".to_string())); + } + '%' => { + build_data.map.tiles[idx] = TileType::Floor; + build_data.spawn_list.push((idx, "Rations".to_string())); + } + '!' => { + build_data.map.tiles[idx] = TileType::Floor; + build_data.spawn_list.push((idx, "Health Potion".to_string())); + } + '☼' => { + build_data.map.tiles[idx] = TileType::Floor; + build_data.spawn_list.push((idx, "Watch Fire".to_string())); + } + _ => { + rltk::console::log(format!("Unknown glyph loading map: {}", (ch as u8) as char)); + } + } + } + + #[allow(dead_code)] + fn load_rex_map(&mut self, path: &str, build_data : &mut BuilderMap) { + let xp_file = rltk::rex::XpFile::from_resource(path).unwrap(); + + for layer in &xp_file.layers { + for y in 0..layer.height { + for x in 0..layer.width { + let cell = layer.get(x, y).unwrap(); + if x < build_data.map.width as usize && y < build_data.map.height as usize { + let idx = build_data.map.xy_idx(x as i32, y as i32); + // We're doing some nasty casting to make it easier to type things like '#' in the match + self.char_to_map(cell.ch as u8 as char, idx, build_data); + } + } + } + } + } + + fn read_ascii_to_vec(template : &str) -> Vec { + let mut string_vec : Vec = template.chars().filter(|a| *a != '\r' && *a !='\n').collect(); + for c in string_vec.iter_mut() { if *c as u8 == 160u8 { *c = ' '; } } + string_vec + } + + #[allow(dead_code)] + fn load_ascii_map(&mut self, level: &prefab_levels::PrefabLevel, build_data : &mut BuilderMap) { + let string_vec = PrefabBuilder::read_ascii_to_vec(level.template); + + let mut i = 0; + for ty in 0..level.height { + for tx in 0..level.width { + if tx < build_data.map.width as usize && ty < build_data.map.height as usize { + let idx = build_data.map.xy_idx(tx as i32, ty as i32); + self.char_to_map(string_vec[i], idx, build_data); + } + i += 1; + } + } + } + + fn apply_previous_iteration(&mut self, mut filter: F, build_data : &mut BuilderMap) + where F : FnMut(i32, i32) -> bool + { + let width = build_data.map.width; + build_data.spawn_list.retain(|(idx, _name)| { + let x = *idx as i32 % width; + let y = *idx as i32 / width; + filter(x, y) + }); + build_data.take_snapshot(); + } + + #[allow(dead_code)] + fn apply_sectional(&mut self, section : &prefab_sections::PrefabSection, build_data : &mut BuilderMap) { + use prefab_sections::*; + + let string_vec = PrefabBuilder::read_ascii_to_vec(section.template); + + // Place the new section + let chunk_x; + match section.placement.0 { + HorizontalPlacement::Left => chunk_x = 0, + HorizontalPlacement::Center => chunk_x = (build_data.map.width / 2) - (section.width as i32 / 2), + HorizontalPlacement::Right => chunk_x = (build_data.map.width-1) - section.width as i32 + } + + let chunk_y; + match section.placement.1 { + VerticalPlacement::Top => chunk_y = 0, + VerticalPlacement::Center => chunk_y = (build_data.map.height / 2) - (section.height as i32 / 2), + VerticalPlacement::Bottom => chunk_y = (build_data.map.height-1) - section.height as i32 + } + + // Build the map + self.apply_previous_iteration(|x,y| { + x < chunk_x || x > (chunk_x + section.width as i32) || y < chunk_y || y > (chunk_y + section.height as i32) + }, build_data); + + let mut i = 0; + for ty in 0..section.height { + for tx in 0..section.width { + if tx > 0 && tx < build_data.map.width as usize -1 && ty < build_data.map.height as usize -1 && ty > 0 { + let idx = build_data.map.xy_idx(tx as i32 + chunk_x, ty as i32 + chunk_y); + if i < string_vec.len() { self.char_to_map(string_vec[i], idx, build_data); } + } + i += 1; + } + } + build_data.take_snapshot(); + } + + fn apply_room_vaults(&mut self, build_data : &mut BuilderMap) { + use prefab_rooms::*; + + // Apply the previous builder, and keep all entities it spawns (for now) + self.apply_previous_iteration(|_x,_y| true, build_data); + + // Do we want a vault at all? + let vault_roll = crate::rng::roll_dice(1, 6) + build_data.map.depth; + if vault_roll < 4 { return; } + + // Note that this is a place-holder and will be moved out of this function + let master_vault_list = vec![TOTALLY_NOT_A_TRAP, CHECKERBOARD, SILLY_SMILE]; + + // Filter the vault list down to ones that are applicable to the current depth + let mut possible_vaults : Vec<&PrefabRoom> = master_vault_list + .iter() + .filter(|v| { build_data.map.depth >= v.first_depth && build_data.map.depth <= v.last_depth }) + .collect(); + + if possible_vaults.is_empty() { return; } // Bail out if there's nothing to build + + let n_vaults = i32::min(crate::rng::roll_dice(1, 3), possible_vaults.len() as i32); + let mut used_tiles : HashSet = HashSet::new(); + + for _i in 0..n_vaults { + + let vault_index = if possible_vaults.len() == 1 { 0 } else { (crate::rng::roll_dice(1, possible_vaults.len() as i32)-1) as usize }; + let vault = possible_vaults[vault_index]; + + // We'll make a list of places in which the vault could fit + let mut vault_positions : Vec = Vec::new(); + + let mut idx = 0usize; + loop { + let x = (idx % build_data.map.width as usize) as i32; + let y = (idx / build_data.map.width as usize) as i32; + + // Check that we won't overflow the map + if x > 1 + && (x+vault.width as i32) < build_data.map.width-2 + && y > 1 + && (y+vault.height as i32) < build_data.map.height-2 + { + + let mut possible = true; + for ty in 0..vault.height as i32 { + for tx in 0..vault.width as i32 { + + let idx = build_data.map.xy_idx(tx + x, ty + y); + if build_data.map.tiles[idx] != TileType::Floor { + possible = false; + } + if used_tiles.contains(&idx) { + possible = false; + } + } + } + + if possible { + vault_positions.push(Position{ x,y }); + break; + } + + } + + idx += 1; + if idx >= build_data.map.tiles.len()-1 { break; } + } + + if !vault_positions.is_empty() { + let pos_idx = if vault_positions.len()==1 { 0 } else { (crate::rng::roll_dice(1, vault_positions.len() as i32)-1) as usize }; + let pos = &vault_positions[pos_idx]; + + let chunk_x = pos.x; + let chunk_y = pos.y; + + let width = build_data.map.width; // The borrow checker really doesn't like it + let height = build_data.map.height; // when we access `self` inside the `retain` + build_data.spawn_list.retain(|e| { + let idx = e.0 as i32; + let x = idx % width; + let y = idx / height; + x < chunk_x || x > chunk_x + vault.width as i32 || y < chunk_y || y > chunk_y + vault.height as i32 + }); + + let string_vec = PrefabBuilder::read_ascii_to_vec(vault.template); + let mut i = 0; + for ty in 0..vault.height { + for tx in 0..vault.width { + let idx = build_data.map.xy_idx(tx as i32 + chunk_x, ty as i32 + chunk_y); + self.char_to_map(string_vec[i], idx, build_data); + used_tiles.insert(idx); + i += 1; + } + } + build_data.take_snapshot(); + + possible_vaults.remove(vault_index); + } + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/prefab_builder/prefab_levels.rs b/chapter-75-darkplaza/src/map_builders/prefab_builder/prefab_levels.rs new file mode 100644 index 00000000..e1226d45 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/prefab_builder/prefab_levels.rs @@ -0,0 +1,61 @@ +#[derive(PartialEq, Copy, Clone)] +pub struct PrefabLevel { + pub template : &'static str, + pub width : usize, + pub height: usize +} + +#[allow(dead_code)] +pub const WFC_POPULATED : PrefabLevel = PrefabLevel{ + template : LEVEL_MAP, + width: 80, + height: 43 +}; + +#[allow(dead_code)] +const LEVEL_MAP : &str = +" +################################################################################ +#          ########################################################    ######### +#    @     ######    #########       ####     ###################        ####### +#          ####   g  #                          ###############            ##### +#          #### #    # #######       ####       #############                ### +##### ######### #    # #######       #########  ####    #####                ### +##### ######### ###### #######   o   #########  #### ## #####                ### +##                        ####       #########   ### ##         o            ### +##### ######### ###       ####       #######         ## #####                ### +##### ######### ###       ####       ####### #   ### ## #####                ### +##### ######### ###       ####       ####### #######    #####     o          ### +###          ## ###       ####       ####### ################                ### +###          ## ###   o   ###### ########### #   ############                ### +###          ## ###       ###### ###########     ###                         ### +###    %                  ###### ########### #   ###   !   ##                ### +###          ## ###              ######   ## #######       ##                ### +###          ## ###       ## ### #####     # ########################      ##### +###          ## ###       ## ### #####     # #   ######################    ##### +#### ## ####### ###### ##### ### ####          o ###########     ######    ##### +#### ## ####### ###### ####   ## ####        #   #########         ###### ###### +#    ## ####### ###### ####   ## ####        ############           ##### ###### +# g  ## ####### ###### ####   ##        %    ###########   o      o  #### #    # +#    ## ###            ####   ## ####        #   #######   ##    ##  ####   g  # +#######                  ####### ####            ######     !    !    ### #    # +######                     ##### ####        #   ######               ### ###### +#####                            #####     # ##########               ### ###### +#####           !           ### ######     # ##########      o##o     ### #   ## +#####                       ### #######   ## #   ######               ###   g ## +#   ##                     #### ######## ###   o #######  ^########^ #### #   ## +# g    #                 ###### ######## #####   #######  ^        ^ #### ###### +#   ##g####           ######    ######## ################           ##### ###### +#   ## ########## ##########    ######## #################         ######      # +#####   ######### ########## %  ######## ###################     ######## ##   # +#### ### ######## ##########    ######## #################### ##########   #   # +### ##### ######   #########    ########          ########### #######   # g#   # +### #####           ###############      ###      ########### #######   ####   # +### ##### ####       ############## ######## g  g ########### ####         # ^ # +#### ###^####         ############# ########      #####       ####      # g#   # +#####   ######       ###            ########      ##### g     ####   !  ####^^ # +#!%^## ###  ##           ########## ########  gg                 g         # > # +#!%^   ###  ###     ############### ########      ##### g     ####      # g#   # +# %^##  ^   ###     ############### ########      #####       ################## +################################################################################ +"; diff --git a/chapter-75-darkplaza/src/map_builders/prefab_builder/prefab_rooms.rs b/chapter-75-darkplaza/src/map_builders/prefab_builder/prefab_rooms.rs new file mode 100644 index 00000000..c980bc43 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/prefab_builder/prefab_rooms.rs @@ -0,0 +1,65 @@ +#[allow(dead_code)] +#[derive(PartialEq, Copy, Clone)] +pub struct PrefabRoom { + pub template : &'static str, + pub width : usize, + pub height: usize, + pub first_depth: i32, + pub last_depth: i32 +} + +#[allow(dead_code)] +pub const TOTALLY_NOT_A_TRAP : PrefabRoom = PrefabRoom{ + template : TOTALLY_NOT_A_TRAP_MAP, + width: 5, + height: 5, + first_depth: 0, + last_depth: 100 +}; + +#[allow(dead_code)] +const TOTALLY_NOT_A_TRAP_MAP : &str = " + + ^^^ + ^!^ + ^^^ + +"; + +#[allow(dead_code)] +pub const SILLY_SMILE : PrefabRoom = PrefabRoom{ + template : SILLY_SMILE_MAP, + width: 6, + height: 6, + first_depth: 0, + last_depth: 100 +}; + +#[allow(dead_code)] +const SILLY_SMILE_MAP : &str = " + + ^ ^ + ## + + #### + +"; + +#[allow(dead_code)] +pub const CHECKERBOARD : PrefabRoom = PrefabRoom{ + template : CHECKERBOARD_MAP, + width: 6, + height: 6, + first_depth: 0, + last_depth: 100 +}; + +#[allow(dead_code)] +const CHECKERBOARD_MAP : &str = " + + #^# + g#%# + #!# + ^# # + +"; diff --git a/chapter-75-darkplaza/src/map_builders/prefab_builder/prefab_sections.rs b/chapter-75-darkplaza/src/map_builders/prefab_builder/prefab_sections.rs new file mode 100644 index 00000000..17da663c --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/prefab_builder/prefab_sections.rs @@ -0,0 +1,119 @@ +#[allow(dead_code)] +#[derive(PartialEq, Copy, Clone)] +pub enum HorizontalPlacement { Left, Center, Right } + +#[allow(dead_code)] +#[derive(PartialEq, Copy, Clone)] +pub enum VerticalPlacement { Top, Center, Bottom } + +#[allow(dead_code)] +#[derive(PartialEq, Copy, Clone)] +pub struct PrefabSection { + pub template : &'static str, + pub width : usize, + pub height: usize, + pub placement : (HorizontalPlacement, VerticalPlacement) +} + +#[allow(dead_code)] +pub const UNDERGROUND_FORT : PrefabSection = PrefabSection{ + template : RIGHT_FORT, + width: 15, + height: 43, + placement: ( HorizontalPlacement::Right, VerticalPlacement::Top ) +}; + +#[allow(dead_code)] +// The padding needs to be here! +const RIGHT_FORT : &str = " +     # +  ####### +  #     # +  #     ####### +  #  g        # +  #     ####### +  #     # +  ### ### +    # # +    # # +    # ## +    ^ +    ^ +    # ## +    # # +    # # +    # # +    # # +  ### ### +  #     # +  #     # +  #  g  # +  #     # +  #     # +  ### ### +    # # +    # # +    # # +    # ## +    ^ +    ^ +    # ## +    # # +    # # +    # # +  ### ### +  #     # +  #     ####### +  #  g        # +  #     ####### +  #     # +  ####### +     # +"; + +#[allow(dead_code)] +pub const ORC_CAMP : PrefabSection = PrefabSection{ + template : ORC_CAMP_TXT, + width: 12, + height: 12, + placement: ( HorizontalPlacement::Center, VerticalPlacement::Center ) +}; + +#[allow(dead_code)] +const ORC_CAMP_TXT : &str = " + + ########## + ≈☼ ☼≈ + ≈ g ≈ + ≈ ≈ + ≈ g ≈ + o O o + ≈ ≈ + ≈ g ≈ + ≈ g ≈ + ≈☼ ☼≈ + ≈≈≈≈o≈≈≈≈≈ + +"; + +#[allow(dead_code)] +pub const DROW_ENTRY : PrefabSection = PrefabSection{ + template : DROW_ENTRY_TXT, + width: 12, + height: 10, + placement: ( HorizontalPlacement::Center, VerticalPlacement::Center ) +}; + +#[allow(dead_code)] +const DROW_ENTRY_TXT : &str = " + + ########## + # # + # > # + # # + #e # + e # + #e # + ########## + +"; diff --git a/chapter-75-darkplaza/src/map_builders/room_based_spawner.rs b/chapter-75-darkplaza/src/map_builders/room_based_spawner.rs new file mode 100644 index 00000000..93e67724 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/room_based_spawner.rs @@ -0,0 +1,26 @@ +use super::{MetaMapBuilder, BuilderMap, spawner}; + +pub struct RoomBasedSpawner {} + +impl MetaMapBuilder for RoomBasedSpawner { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl RoomBasedSpawner { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(RoomBasedSpawner{}) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + if let Some(rooms) = &build_data.rooms { + for room in rooms.iter().skip(1) { + spawner::spawn_room(&build_data.map, room, build_data.map.depth, &mut build_data.spawn_list); + } + } else { + panic!("Room Based Spawning only works after rooms have been created"); + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/room_based_stairs.rs b/chapter-75-darkplaza/src/map_builders/room_based_stairs.rs new file mode 100644 index 00000000..6529221d --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/room_based_stairs.rs @@ -0,0 +1,26 @@ +use super::{MetaMapBuilder, BuilderMap, TileType}; +pub struct RoomBasedStairs {} + +impl MetaMapBuilder for RoomBasedStairs { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl RoomBasedStairs { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(RoomBasedStairs{}) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + if let Some(rooms) = &build_data.rooms { + let stairs_position = rooms[rooms.len()-1].center(); + let stairs_idx = build_data.map.xy_idx(stairs_position.0, stairs_position.1); + build_data.map.tiles[stairs_idx] = TileType::DownStairs; + build_data.take_snapshot(); + } else { + panic!("Room Based Stairs only works after rooms have been created"); + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/room_based_starting_position.rs b/chapter-75-darkplaza/src/map_builders/room_based_starting_position.rs new file mode 100644 index 00000000..d571f040 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/room_based_starting_position.rs @@ -0,0 +1,25 @@ +use super::{MetaMapBuilder, BuilderMap, Position}; + +pub struct RoomBasedStartingPosition {} + +impl MetaMapBuilder for RoomBasedStartingPosition { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl RoomBasedStartingPosition { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(RoomBasedStartingPosition{}) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + if let Some(rooms) = &build_data.rooms { + let start_pos = rooms[0].center(); + build_data.starting_position = Some(Position{ x: start_pos.0, y: start_pos.1 }); + } else { + panic!("Room Based Staring Position only works after rooms have been created"); + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/room_corner_rounding.rs b/chapter-75-darkplaza/src/map_builders/room_corner_rounding.rs new file mode 100644 index 00000000..ebb8eaa9 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/room_corner_rounding.rs @@ -0,0 +1,49 @@ +use super::{MetaMapBuilder, BuilderMap, TileType, Rect}; + +pub struct RoomCornerRounder {} + +impl MetaMapBuilder for RoomCornerRounder { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl RoomCornerRounder { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(RoomCornerRounder{}) + } + + fn fill_if_corner(&mut self, x: i32, y: i32, build_data : &mut BuilderMap) { + let w = build_data.map.width; + let h = build_data.map.height; + let idx = build_data.map.xy_idx(x, y); + let mut neighbor_walls = 0; + if x > 0 && build_data.map.tiles[idx-1] == TileType::Wall { neighbor_walls += 1; } + if y > 0 && build_data.map.tiles[idx-w as usize] == TileType::Wall { neighbor_walls += 1; } + if x < w-2 && build_data.map.tiles[idx+1] == TileType::Wall { neighbor_walls += 1; } + if y < h-2 && build_data.map.tiles[idx+w as usize] == TileType::Wall { neighbor_walls += 1; } + + if neighbor_walls == 2 { + build_data.map.tiles[idx] = TileType::Wall; + } + } + + fn build(&mut self, build_data : &mut BuilderMap) { + let rooms : Vec; + if let Some(rooms_builder) = &build_data.rooms { + rooms = rooms_builder.clone(); + } else { + panic!("Room Rounding require a builder with room structures"); + } + + for room in rooms.iter() { + self.fill_if_corner(room.x1+1, room.y1+1, build_data); + self.fill_if_corner(room.x2, room.y1+1, build_data); + self.fill_if_corner(room.x1+1, room.y2, build_data); + self.fill_if_corner(room.x2, room.y2, build_data); + + build_data.take_snapshot(); + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/room_corridor_spawner.rs b/chapter-75-darkplaza/src/map_builders/room_corridor_spawner.rs new file mode 100644 index 00000000..53608263 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/room_corridor_spawner.rs @@ -0,0 +1,30 @@ +use super::{MetaMapBuilder, BuilderMap, spawner}; + +pub struct CorridorSpawner {} + +impl MetaMapBuilder for CorridorSpawner { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl CorridorSpawner { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(CorridorSpawner{}) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + if let Some(corridors) = &build_data.corridors { + for c in corridors.iter() { + let depth = build_data.map.depth; + spawner::spawn_region(&build_data.map, + &c, + depth, + &mut build_data.spawn_list); + } + } else { + panic!("Corridor Based Spawning only works after corridors have been created"); + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/room_draw.rs b/chapter-75-darkplaza/src/map_builders/room_draw.rs new file mode 100644 index 00000000..d7c4d152 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/room_draw.rs @@ -0,0 +1,65 @@ +use super::{MetaMapBuilder, BuilderMap, TileType, Rect}; + +pub struct RoomDrawer {} + +impl MetaMapBuilder for RoomDrawer { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl RoomDrawer { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(RoomDrawer{}) + } + + #[allow(dead_code)] + fn rectangle(&mut self, build_data : &mut BuilderMap, room : &Rect) { + for y in room.y1 +1 ..= room.y2 { + for x in room.x1 + 1 ..= room.x2 { + let idx = build_data.map.xy_idx(x, y); + if idx > 0 && idx < ((build_data.map.width * build_data.map.height)-1) as usize { + build_data.map.tiles[idx] = TileType::Floor; + } + } + } + } + + #[allow(dead_code)] + fn circle(&mut self, build_data : &mut BuilderMap, room : &Rect) { + let radius = i32::min(room.x2 - room.x1, room.y2 - room.y1) as f32 / 2.0; + let center = room.center(); + let center_pt = rltk::Point::new(center.0, center.1); + for y in room.y1 ..= room.y2 { + for x in room.x1 ..= room.x2 { + let idx = build_data.map.xy_idx(x, y); + let distance = rltk::DistanceAlg::Pythagoras.distance2d(center_pt, rltk::Point::new(x, y)); + if idx > 0 + && idx < ((build_data.map.width * build_data.map.height)-1) as usize + && distance <= radius + { + build_data.map.tiles[idx] = TileType::Floor; + } + } + } + } + + fn build(&mut self, build_data : &mut BuilderMap) { + let rooms : Vec; + if let Some(rooms_builder) = &build_data.rooms { + rooms = rooms_builder.clone(); + } else { + panic!("Room Drawing require a builder with room structures"); + } + + for room in rooms.iter() { + let room_type = crate::rng::roll_dice(1,4); + match room_type { + 1 => self.circle(build_data, room), + _ => self.rectangle(build_data, room) + } + build_data.take_snapshot(); + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/room_exploder.rs b/chapter-75-darkplaza/src/map_builders/room_exploder.rs new file mode 100644 index 00000000..0037834f --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/room_exploder.rs @@ -0,0 +1,67 @@ +use super::{MetaMapBuilder, BuilderMap, TileType, paint, Symmetry, Rect}; + +pub struct RoomExploder {} + +impl MetaMapBuilder for RoomExploder { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl RoomExploder { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(RoomExploder{}) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + let rooms : Vec; + if let Some(rooms_builder) = &build_data.rooms { + rooms = rooms_builder.clone(); + } else { + panic!("Room Explosions require a builder with room structures"); + } + + for room in rooms.iter() { + let start = room.center(); + let n_diggers = crate::rng::roll_dice(1, 20)-5; + if n_diggers > 0 { + for _i in 0..n_diggers { + let mut drunk_x = start.0; + let mut drunk_y = start.1; + + let mut drunk_life = 20; + let mut did_something = false; + + while drunk_life > 0 { + let drunk_idx = build_data.map.xy_idx(drunk_x, drunk_y); + if build_data.map.tiles[drunk_idx] == TileType::Wall { + did_something = true; + } + paint(&mut build_data.map, Symmetry::None, 1, drunk_x, drunk_y); + build_data.map.tiles[drunk_idx] = TileType::DownStairs; + + let stagger_direction = crate::rng::roll_dice(1, 4); + match stagger_direction { + 1 => { if drunk_x > 2 { drunk_x -= 1; } } + 2 => { if drunk_x < build_data.map.width-2 { drunk_x += 1; } } + 3 => { if drunk_y > 2 { drunk_y -=1; } } + _ => { if drunk_y < build_data.map.height-2 { drunk_y += 1; } } + } + + drunk_life -= 1; + } + if did_something { + build_data.take_snapshot(); + } + + for t in build_data.map.tiles.iter_mut() { + if *t == TileType::DownStairs { + *t = TileType::Floor; + } + } + } + } + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/room_sorter.rs b/chapter-75-darkplaza/src/map_builders/room_sorter.rs new file mode 100644 index 00000000..07c9abb7 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/room_sorter.rs @@ -0,0 +1,45 @@ +use super::{MetaMapBuilder, BuilderMap, Rect }; + +#[allow(dead_code)] +pub enum RoomSort { LEFTMOST, RIGHTMOST, TOPMOST, BOTTOMMOST, CENTRAL } + +pub struct RoomSorter { + sort_by : RoomSort +} + +impl MetaMapBuilder for RoomSorter { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.sorter(build_data); + } +} + +impl RoomSorter { + #[allow(dead_code)] + pub fn new(sort_by : RoomSort) -> Box { + Box::new(RoomSorter{ sort_by }) + } + + fn sorter(&mut self, build_data : &mut BuilderMap) { + match self.sort_by { + RoomSort::LEFTMOST => build_data.rooms.as_mut().unwrap().sort_by(|a,b| a.x1.cmp(&b.x1) ), + RoomSort::RIGHTMOST => build_data.rooms.as_mut().unwrap().sort_by(|a,b| b.x2.cmp(&a.x2) ), + RoomSort::TOPMOST => build_data.rooms.as_mut().unwrap().sort_by(|a,b| a.y1.cmp(&b.y1) ), + RoomSort::BOTTOMMOST => build_data.rooms.as_mut().unwrap().sort_by(|a,b| b.y2.cmp(&a.y2) ), + RoomSort::CENTRAL => { + let map_center = rltk::Point::new( build_data.map.width / 2, build_data.map.height / 2 ); + let center_sort = |a : &Rect, b : &Rect| { + let a_center = a.center(); + let a_center_pt = rltk::Point::new(a_center.0, a_center.1); + let b_center = b.center(); + let b_center_pt = rltk::Point::new(b_center.0, b_center.1); + let distance_a = rltk::DistanceAlg::Pythagoras.distance2d(a_center_pt, map_center); + let distance_b = rltk::DistanceAlg::Pythagoras.distance2d(b_center_pt, map_center); + distance_a.partial_cmp(&distance_b).unwrap() + }; + + build_data.rooms.as_mut().unwrap().sort_by(center_sort); + } + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/rooms_corridors_bsp.rs b/chapter-75-darkplaza/src/map_builders/rooms_corridors_bsp.rs new file mode 100644 index 00000000..4ade3091 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/rooms_corridors_bsp.rs @@ -0,0 +1,41 @@ +use super::{MetaMapBuilder, BuilderMap, Rect, draw_corridor }; + +pub struct BspCorridors {} + +impl MetaMapBuilder for BspCorridors { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.corridors(build_data); + } +} + +impl BspCorridors { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(BspCorridors{}) + } + + fn corridors(&mut self, build_data : &mut BuilderMap) { + let rooms : Vec; + if let Some(rooms_builder) = &build_data.rooms { + rooms = rooms_builder.clone(); + } else { + panic!("BSP Corridors require a builder with room structures"); + } + let mut corridors : Vec> = Vec::new(); + + for i in 0..rooms.len()-1 { + let room = rooms[i]; + let next_room = rooms[i+1]; + let start_x = room.x1 + (crate::rng::roll_dice(1, i32::abs(room.x1 - room.x2))-1); + let start_y = room.y1 + (crate::rng::roll_dice(1, i32::abs(room.y1 - room.y2))-1); + let end_x = next_room.x1 + (crate::rng::roll_dice(1, i32::abs(next_room.x1 - next_room.x2))-1); + let end_y = next_room.y1 + (crate::rng::roll_dice(1, i32::abs(next_room.y1 - next_room.y2))-1); + let corridor = draw_corridor(&mut build_data.map, start_x, start_y, end_x, end_y); + corridors.push(corridor); + build_data.take_snapshot(); + } + + build_data.corridors = Some(corridors); + } +} diff --git a/chapter-75-darkplaza/src/map_builders/rooms_corridors_dogleg.rs b/chapter-75-darkplaza/src/map_builders/rooms_corridors_dogleg.rs new file mode 100644 index 00000000..758890fd --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/rooms_corridors_dogleg.rs @@ -0,0 +1,47 @@ +use super::{MetaMapBuilder, BuilderMap, Rect, apply_horizontal_tunnel, apply_vertical_tunnel }; + +pub struct DoglegCorridors {} + +impl MetaMapBuilder for DoglegCorridors { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.corridors(build_data); + } +} + +impl DoglegCorridors { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(DoglegCorridors{}) + } + + fn corridors(&mut self, build_data : &mut BuilderMap) { + let rooms : Vec; + if let Some(rooms_builder) = &build_data.rooms { + rooms = rooms_builder.clone(); + } else { + panic!("Dogleg Corridors require a builder with room structures"); + } + + let mut corridors : Vec> = Vec::new(); + for (i,room) in rooms.iter().enumerate() { + if i > 0 { + let (new_x, new_y) = room.center(); + let (prev_x, prev_y) = rooms[i as usize -1].center(); + if crate::rng::range(0,2) == 1 { + let mut c1 = apply_horizontal_tunnel(&mut build_data.map, prev_x, new_x, prev_y); + let mut c2 = apply_vertical_tunnel(&mut build_data.map, prev_y, new_y, new_x); + c1.append(&mut c2); + corridors.push(c1); + } else { + let mut c1 = apply_vertical_tunnel(&mut build_data.map, prev_y, new_y, prev_x); + let mut c2 = apply_horizontal_tunnel(&mut build_data.map, prev_x, new_x, new_y); + c1.append(&mut c2); + corridors.push(c1); + } + build_data.take_snapshot(); + } + } + build_data.corridors = Some(corridors); + } +} diff --git a/chapter-75-darkplaza/src/map_builders/rooms_corridors_lines.rs b/chapter-75-darkplaza/src/map_builders/rooms_corridors_lines.rs new file mode 100644 index 00000000..a9ae12fa --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/rooms_corridors_lines.rs @@ -0,0 +1,68 @@ +use super::{MetaMapBuilder, BuilderMap, Rect, TileType }; +use std::collections::HashSet; + +pub struct StraightLineCorridors {} + +impl MetaMapBuilder for StraightLineCorridors { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.corridors(build_data); + } +} + +impl StraightLineCorridors { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(StraightLineCorridors{}) + } + + fn corridors(&mut self, build_data : &mut BuilderMap) { + let rooms : Vec; + if let Some(rooms_builder) = &build_data.rooms { + rooms = rooms_builder.clone(); + } else { + panic!("Straight Line Corridors require a builder with room structures"); + } + + let mut connected : HashSet = HashSet::new(); + let mut corridors : Vec> = Vec::new(); + for (i,room) in rooms.iter().enumerate() { + let mut room_distance : Vec<(usize, f32)> = Vec::new(); + let room_center = room.center(); + let room_center_pt = rltk::Point::new(room_center.0, room_center.1); + for (j,other_room) in rooms.iter().enumerate() { + if i != j && !connected.contains(&j) { + let other_center = other_room.center(); + let other_center_pt = rltk::Point::new(other_center.0, other_center.1); + let distance = rltk::DistanceAlg::Pythagoras.distance2d( + room_center_pt, + other_center_pt + ); + room_distance.push((j, distance)); + } + } + + if !room_distance.is_empty() { + room_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap() ); + let dest_center = rooms[room_distance[0].0].center(); + let line = rltk::line2d( + rltk::LineAlg::Bresenham, + room_center_pt, + rltk::Point::new(dest_center.0, dest_center.1) + ); + let mut corridor = Vec::new(); + for cell in line.iter() { + let idx = build_data.map.xy_idx(cell.x, cell.y); + if build_data.map.tiles[idx] != TileType::Floor { + build_data.map.tiles[idx] = TileType::Floor; + corridor.push(idx); + } + } + corridors.push(corridor); + connected.insert(i); + build_data.take_snapshot(); + } + } + build_data.corridors = Some(corridors); + } +} diff --git a/chapter-75-darkplaza/src/map_builders/rooms_corridors_nearest.rs b/chapter-75-darkplaza/src/map_builders/rooms_corridors_nearest.rs new file mode 100644 index 00000000..64ebcae0 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/rooms_corridors_nearest.rs @@ -0,0 +1,60 @@ +use super::{MetaMapBuilder, BuilderMap, Rect, draw_corridor }; +use std::collections::HashSet; + +pub struct NearestCorridors {} + +impl MetaMapBuilder for NearestCorridors { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.corridors(build_data); + } +} + +impl NearestCorridors { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(NearestCorridors{}) + } + + fn corridors(&mut self, build_data : &mut BuilderMap) { + let rooms : Vec; + if let Some(rooms_builder) = &build_data.rooms { + rooms = rooms_builder.clone(); + } else { + panic!("Nearest Corridors require a builder with room structures"); + } + + let mut connected : HashSet = HashSet::new(); + let mut corridors : Vec> = Vec::new(); + for (i,room) in rooms.iter().enumerate() { + let mut room_distance : Vec<(usize, f32)> = Vec::new(); + let room_center = room.center(); + let room_center_pt = rltk::Point::new(room_center.0, room_center.1); + for (j,other_room) in rooms.iter().enumerate() { + if i != j && !connected.contains(&j) { + let other_center = other_room.center(); + let other_center_pt = rltk::Point::new(other_center.0, other_center.1); + let distance = rltk::DistanceAlg::Pythagoras.distance2d( + room_center_pt, + other_center_pt + ); + room_distance.push((j, distance)); + } + } + + if !room_distance.is_empty() { + room_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap() ); + let dest_center = rooms[room_distance[0].0].center(); + let corridor = draw_corridor( + &mut build_data.map, + room_center.0, room_center.1, + dest_center.0, dest_center.1 + ); + connected.insert(i); + build_data.take_snapshot(); + corridors.push(corridor); + } + } + build_data.corridors = Some(corridors); + } +} diff --git a/chapter-75-darkplaza/src/map_builders/simple_map.rs b/chapter-75-darkplaza/src/map_builders/simple_map.rs new file mode 100644 index 00000000..b98bdba6 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/simple_map.rs @@ -0,0 +1,40 @@ +use super::{InitialMapBuilder, BuilderMap, Rect }; + +pub struct SimpleMapBuilder {} + +impl InitialMapBuilder for SimpleMapBuilder { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build_rooms(build_data); + } +} + +impl SimpleMapBuilder { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(SimpleMapBuilder{}) + } + + fn build_rooms(&mut self, build_data : &mut BuilderMap) { + const MAX_ROOMS : i32 = 30; + const MIN_SIZE : i32 = 6; + const MAX_SIZE : i32 = 10; + let mut rooms : Vec = Vec::new(); + + for _i in 0..MAX_ROOMS { + let w = crate::rng::range(MIN_SIZE, MAX_SIZE); + let h = crate::rng::range(MIN_SIZE, MAX_SIZE); + let x = crate::rng::roll_dice(1, build_data.map.width - w - 1) - 1; + let y = crate::rng::roll_dice(1, build_data.map.height - h - 1) - 1; + let new_room = Rect::new(x, y, w, h); + let mut ok = true; + for other_room in rooms.iter() { + if new_room.intersect(other_room) { ok = false } + } + if ok { + rooms.push(new_room); + } + } + build_data.rooms = Some(rooms); + } +} diff --git a/chapter-75-darkplaza/src/map_builders/town.rs b/chapter-75-darkplaza/src/map_builders/town.rs new file mode 100644 index 00000000..b5791987 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/town.rs @@ -0,0 +1,439 @@ +use super::{BuilderChain, BuilderMap, InitialMapBuilder, TileType, Position}; +use std::collections::HashSet; + +pub fn town_builder(new_depth: i32, width: i32, height: i32) -> BuilderChain { + let mut chain = BuilderChain::new(new_depth, width, height, "The Town of Bracketon"); + chain.start_with(TownBuilder::new()); + chain +} + +pub struct TownBuilder {} + +impl InitialMapBuilder for TownBuilder { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build_rooms(build_data); + } +} + +enum BuildingTag { + Pub, Temple, Blacksmith, Clothier, Alchemist, PlayerHouse, Hovel, Abandoned, Unassigned +} + +impl TownBuilder { + pub fn new() -> Box { + Box::new(TownBuilder{}) + } + + pub fn build_rooms(&mut self, build_data : &mut BuilderMap) { + self.grass_layer(build_data); + self.water_and_piers(build_data); + let (mut available_building_tiles, wall_gap_y) = self.town_walls(build_data); + let mut buildings = self.buildings(build_data, &mut available_building_tiles); + let doors = self.add_doors(build_data, &mut buildings, wall_gap_y); + self.add_paths(build_data, &doors); + + for y in wall_gap_y-3 .. wall_gap_y + 4 { + let exit_idx = build_data.map.xy_idx(build_data.width-2, y); + build_data.map.tiles[exit_idx] = TileType::DownStairs; + } + + let building_size = self.sort_buildings(&buildings); + self.building_factory(build_data, &buildings, &building_size); + + self.spawn_dockers(build_data); + self.spawn_townsfolk(build_data, &mut available_building_tiles); + + // Make visible for screenshot + for t in build_data.map.visible_tiles.iter_mut() { + *t = true; + } + build_data.take_snapshot(); + } + + fn grass_layer(&mut self, build_data : &mut BuilderMap) { + // We'll start with a nice layer of grass + for t in build_data.map.tiles.iter_mut() { + *t = TileType::Grass; + } + build_data.take_snapshot(); + } + + fn water_and_piers(&mut self, build_data : &mut BuilderMap) { + let mut n = (crate::rng::roll_dice(1, 65535) as f32) / 65535f32; + let mut water_width : Vec = Vec::new(); + for y in 0..build_data.height { + let n_water = (f32::sin(n) * 10.0) as i32 + 14 + crate::rng::roll_dice(1, 6); + water_width.push(n_water); + n += 0.1; + for x in 0..n_water { + let idx = build_data.map.xy_idx(x, y); + build_data.map.tiles[idx] = TileType::DeepWater; + } + for x in n_water .. n_water+3 { + let idx = build_data.map.xy_idx(x, y); + build_data.map.tiles[idx] = TileType::ShallowWater; + } + } + build_data.take_snapshot(); + + // Add piers + for _i in 0..crate::rng::roll_dice(1, 4)+6 { + let y = crate::rng::roll_dice(1, build_data.height)-1; + for x in 2 + crate::rng::roll_dice(1, 6) .. water_width[y as usize] + 4 { + let idx = build_data.map.xy_idx(x, y); + build_data.map.tiles[idx] = TileType::Bridge; + } + } + build_data.take_snapshot(); + } + + fn town_walls(&mut self, build_data : &mut BuilderMap) + -> (HashSet, i32) + { + let mut available_building_tiles : HashSet = HashSet::new(); + let wall_gap_y = crate::rng::roll_dice(1, build_data.height - 9) + 5; + for y in 1 .. build_data.height-2 { + if !(y > wall_gap_y-4 && y < wall_gap_y+4) { + let idx = build_data.map.xy_idx(30, y); + build_data.map.tiles[idx] = TileType::Wall; + build_data.map.tiles[idx-1] = TileType::Floor; + let idx_right = build_data.map.xy_idx(build_data.width - 2, y); + build_data.map.tiles[idx_right] = TileType::Wall; + for x in 31 .. build_data.width-2 { + let gravel_idx = build_data.map.xy_idx(x, y); + build_data.map.tiles[gravel_idx] = TileType::Gravel; + if y > 2 && y < build_data.height-1 { + available_building_tiles.insert(gravel_idx); + } + } + } else { + for x in 30 .. build_data.width { + let road_idx = build_data.map.xy_idx(x, y); + build_data.map.tiles[road_idx] = TileType::Road; + } + } + } + build_data.take_snapshot(); + + for x in 30 .. build_data.width-1 { + let idx_top = build_data.map.xy_idx(x, 1); + build_data.map.tiles[idx_top] = TileType::Wall; + let idx_bot = build_data.map.xy_idx(x, build_data.height-2); + build_data.map.tiles[idx_bot] = TileType::Wall; + } + build_data.take_snapshot(); + + (available_building_tiles, wall_gap_y) + } + + fn buildings(&mut self, + build_data : &mut BuilderMap, + available_building_tiles : &mut HashSet) + -> Vec<(i32, i32, i32, i32)> + { + let mut buildings : Vec<(i32, i32, i32, i32)> = Vec::new(); + let mut n_buildings = 0; + while n_buildings < 12 { + let bx = crate::rng::roll_dice(1, build_data.map.width - 32) + 30; + let by = crate::rng::roll_dice(1, build_data.map.height)-2; + let bw = crate::rng::roll_dice(1, 8)+4; + let bh = crate::rng::roll_dice(1, 8)+4; + let mut possible = true; + for y in by .. by+bh { + for x in bx .. bx+bw { + if x < 0 || x > build_data.width-1 || y < 0 || y > build_data.height-1 { + possible = false; + } else { + let idx = build_data.map.xy_idx(x, y); + if !available_building_tiles.contains(&idx) { possible = false; } + } + } + } + if possible { + n_buildings += 1; + buildings.push((bx, by, bw, bh)); + for y in by .. by+bh { + for x in bx .. bx+bw { + let idx = build_data.map.xy_idx(x, y); + build_data.map.tiles[idx] = TileType::WoodFloor; + available_building_tiles.remove(&idx); + available_building_tiles.remove(&(idx+1)); + available_building_tiles.remove(&(idx+build_data.width as usize)); + available_building_tiles.remove(&(idx-1)); + available_building_tiles.remove(&(idx-build_data.width as usize)); + } + } + build_data.take_snapshot(); + } + } + + // Outline buildings + let mut mapclone = build_data.map.clone(); + for y in 2..build_data.height-2 { + for x in 32..build_data.width-2 { + let idx = build_data.map.xy_idx(x, y); + if build_data.map.tiles[idx] == TileType::WoodFloor { + let mut neighbors = 0; + if build_data.map.tiles[idx - 1] != TileType::WoodFloor { neighbors +=1; } + if build_data.map.tiles[idx + 1] != TileType::WoodFloor { neighbors +=1; } + if build_data.map.tiles[idx-build_data.width as usize] != TileType::WoodFloor { neighbors +=1; } + if build_data.map.tiles[idx+build_data.width as usize] != TileType::WoodFloor { neighbors +=1; } + if neighbors > 0 { + mapclone.tiles[idx] = TileType::Wall; + } + } + } + } + build_data.map = mapclone; + build_data.take_snapshot(); + buildings + } + + fn add_doors(&mut self, + build_data : &mut BuilderMap, + buildings: &mut Vec<(i32, i32, i32, i32)>, + wall_gap_y : i32) + -> Vec + { + let mut doors = Vec::new(); + for building in buildings.iter() { + let door_x = building.0 + 1 + crate::rng::roll_dice(1, building.2 - 3); + let cy = building.1 + (building.3 / 2); + let idx = if cy > wall_gap_y { + // Door on the north wall + build_data.map.xy_idx(door_x, building.1) + } else { + build_data.map.xy_idx(door_x, building.1 + building.3 - 1) + }; + build_data.map.tiles[idx] = TileType::Floor; + build_data.spawn_list.push((idx, "Door".to_string())); + doors.push(idx); + } + build_data.take_snapshot(); + doors + } + + fn add_paths(&mut self, + build_data : &mut BuilderMap, + doors : &[usize]) + { + let mut roads = Vec::new(); + for y in 0..build_data.height { + for x in 0..build_data.width { + let idx = build_data.map.xy_idx(x, y); + if build_data.map.tiles[idx] == TileType::Road { + roads.push(idx); + } + } + } + + build_data.map.populate_blocked(); + for door_idx in doors.iter() { + let mut nearest_roads : Vec<(usize, f32)> = Vec::new(); + let door_pt = rltk::Point::new( *door_idx as i32 % build_data.map.width as i32, *door_idx as i32 / build_data.map.width as i32 ); + for r in roads.iter() { + nearest_roads.push(( + *r, + rltk::DistanceAlg::PythagorasSquared.distance2d( + door_pt, + rltk::Point::new( *r as i32 % build_data.map.width, *r as i32 / build_data.map.width ) + ) + )); + } + nearest_roads.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap()); + + let destination = nearest_roads[0].0; + let path = rltk::a_star_search(*door_idx, destination, &build_data.map); + if path.success { + for step in path.steps.iter() { + let idx = *step as usize; + build_data.map.tiles[idx] = TileType::Road; + roads.push(idx); + } + } + build_data.take_snapshot(); + } + } + + fn sort_buildings(&mut self, buildings: &[(i32, i32, i32, i32)]) -> Vec<(usize, i32, BuildingTag)> + { + let mut building_size : Vec<(usize, i32, BuildingTag)> = Vec::new(); + for (i,building) in buildings.iter().enumerate() { + building_size.push(( + i, + building.2 * building.3, + BuildingTag::Unassigned + )); + } + building_size.sort_by(|a,b| b.1.cmp(&a.1)); + building_size[0].2 = BuildingTag::Pub; + building_size[1].2 = BuildingTag::Temple; + building_size[2].2 = BuildingTag::Blacksmith; + building_size[3].2 = BuildingTag::Clothier; + building_size[4].2 = BuildingTag::Alchemist; + building_size[5].2 = BuildingTag::PlayerHouse; + for b in building_size.iter_mut().skip(6) { + b.2 = BuildingTag::Hovel; + } + let last_index = building_size.len()-1; + building_size[last_index].2 = BuildingTag::Abandoned; + building_size + } + + fn building_factory(&mut self, + build_data : &mut BuilderMap, + buildings: &[(i32, i32, i32, i32)], + building_index : &[(usize, i32, BuildingTag)]) + { + for (i,building) in buildings.iter().enumerate() { + let build_type = &building_index[i].2; + match build_type { + BuildingTag::Pub => self.build_pub(&building, build_data), + BuildingTag::Temple => self.build_temple(&building, build_data), + BuildingTag::Blacksmith => self.build_smith(&building, build_data), + BuildingTag::Clothier => self.build_clothier(&building, build_data), + BuildingTag::Alchemist => self.build_alchemist(&building, build_data), + BuildingTag::PlayerHouse => self.build_my_house(&building, build_data), + BuildingTag::Hovel => self.build_hovel(&building, build_data), + BuildingTag::Abandoned => self.build_abandoned_house(&building, build_data), + _ => {} + } + } + } + + fn random_building_spawn( + &mut self, + building: &(i32, i32, i32, i32), + build_data : &mut BuilderMap, + to_place : &mut Vec<&str>, + player_idx : usize) + { + for y in building.1 .. building.1 + building.3 { + for x in building.0 .. building.0 + building.2 { + let idx = build_data.map.xy_idx(x, y); + if build_data.map.tiles[idx] == TileType::WoodFloor && idx != player_idx && crate::rng::roll_dice(1, 3)==1 && !to_place.is_empty() { + let entity_tag = to_place[0]; + to_place.remove(0); + build_data.spawn_list.push((idx, entity_tag.to_string())); + } + } + } + } + + fn build_pub(&mut self, + building: &(i32, i32, i32, i32), + build_data : &mut BuilderMap) + { + // Place the player + build_data.starting_position = Some(Position{ + x : building.0 + (building.2 / 2), + y : building.1 + (building.3 / 2) + }); + let player_idx = build_data.map.xy_idx(building.0 + (building.2 / 2), + building.1 + (building.3 / 2)); + + // Place other items + let mut to_place : Vec<&str> = vec!["Barkeep", "Shady Salesman", "Patron", "Patron", "Keg", + "Table", "Chair", "Table", "Chair"]; + self.random_building_spawn(building, build_data, &mut to_place, player_idx); + } + + fn build_temple(&mut self, + building: &(i32, i32, i32, i32), + build_data : &mut BuilderMap) + { + // Place items + let mut to_place : Vec<&str> = vec!["Priest", "Altar", "Parishioner", "Parishioner", "Chair", "Chair", "Candle", "Candle"]; + self.random_building_spawn(building, build_data, &mut to_place, 0); + } + + fn build_smith(&mut self, + building: &(i32, i32, i32, i32), + build_data : &mut BuilderMap) + { + // Place items + let mut to_place : Vec<&str> = vec!["Blacksmith", "Anvil", "Water Trough", "Weapon Rack", "Armor Stand"]; + self.random_building_spawn(building, build_data, &mut to_place, 0); + } + + fn build_clothier(&mut self, + building: &(i32, i32, i32, i32), + build_data : &mut BuilderMap) + { + // Place items + let mut to_place : Vec<&str> = vec!["Clothier", "Cabinet", "Table", "Loom", "Hide Rack"]; + self.random_building_spawn(building, build_data, &mut to_place, 0); + } + + fn build_alchemist(&mut self, + building: &(i32, i32, i32, i32), + build_data : &mut BuilderMap) + { + // Place items + let mut to_place : Vec<&str> = vec!["Alchemist", "Chemistry Set", "Dead Thing", "Chair", "Table"]; + self.random_building_spawn(building, build_data, &mut to_place, 0); + } + + fn build_my_house(&mut self, + building: &(i32, i32, i32, i32), + build_data : &mut BuilderMap) + { + // Place items + let mut to_place : Vec<&str> = vec!["Mom", "Bed", "Cabinet", "Chair", "Table"]; + self.random_building_spawn(building, build_data, &mut to_place, 0); + } + + fn build_hovel(&mut self, + building: &(i32, i32, i32, i32), + build_data : &mut BuilderMap) + { + // Place items + let mut to_place : Vec<&str> = vec!["Peasant", "Bed", "Chair", "Table"]; + self.random_building_spawn(building, build_data, &mut to_place, 0); + } + + fn build_abandoned_house(&mut self, + building: &(i32, i32, i32, i32), + build_data : &mut BuilderMap) + { + for y in building.1 .. building.1 + building.3 { + for x in building.0 .. building.0 + building.2 { + let idx = build_data.map.xy_idx(x, y); + if build_data.map.tiles[idx] == TileType::WoodFloor && idx != 0 && crate::rng::roll_dice(1, 2)==1 { + build_data.spawn_list.push((idx, "Rat".to_string())); + } + } + } + } + + fn spawn_dockers(&mut self, build_data : &mut BuilderMap) { + for (idx, tt) in build_data.map.tiles.iter().enumerate() { + if *tt == TileType::Bridge && crate::rng::roll_dice(1, 6)==1 { + let roll = crate::rng::roll_dice(1, 3); + match roll { + 1 => build_data.spawn_list.push((idx, "Dock Worker".to_string())), + 2 => build_data.spawn_list.push((idx, "Wannabe Pirate".to_string())), + _ => build_data.spawn_list.push((idx, "Fisher".to_string())), + } + } + } + } + + fn spawn_townsfolk(&mut self, + build_data : &mut BuilderMap, + available_building_tiles : &mut HashSet) + { + for idx in available_building_tiles.iter() { + if crate::rng::roll_dice(1, 10)==1 { + let roll = crate::rng::roll_dice(1, 4); + match roll { + 1 => build_data.spawn_list.push((*idx, "Peasant".to_string())), + 2 => build_data.spawn_list.push((*idx, "Drunk".to_string())), + 3 => build_data.spawn_list.push((*idx, "Dock Worker".to_string())), + _ => build_data.spawn_list.push((*idx, "Fisher".to_string())), + } + } + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/voronoi.rs b/chapter-75-darkplaza/src/map_builders/voronoi.rs new file mode 100644 index 00000000..34569f74 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/voronoi.rs @@ -0,0 +1,113 @@ +use super::{InitialMapBuilder, BuilderMap, TileType}; + +#[derive(PartialEq, Copy, Clone)] +#[allow(dead_code)] +pub enum DistanceAlgorithm { Pythagoras, Manhattan, Chebyshev } + +pub struct VoronoiCellBuilder { + n_seeds: usize, + distance_algorithm: DistanceAlgorithm +} + + +impl InitialMapBuilder for VoronoiCellBuilder { + #[allow(dead_code)] + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl VoronoiCellBuilder { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(VoronoiCellBuilder{ + n_seeds: 64, + distance_algorithm: DistanceAlgorithm::Pythagoras, + }) + } + + #[allow(dead_code)] + pub fn pythagoras() -> Box { + Box::new(VoronoiCellBuilder{ + n_seeds: 64, + distance_algorithm: DistanceAlgorithm::Pythagoras, + }) + } + + #[allow(dead_code)] + pub fn manhattan() -> Box { + Box::new(VoronoiCellBuilder{ + n_seeds: 64, + distance_algorithm: DistanceAlgorithm::Manhattan, + }) + } + + #[allow(clippy::map_entry)] + fn build(&mut self, build_data : &mut BuilderMap) { + // Make a Voronoi diagram. We'll do this the hard way to learn about the technique! + let mut voronoi_seeds : Vec<(usize, rltk::Point)> = Vec::new(); + + while voronoi_seeds.len() < self.n_seeds { + let vx = crate::rng::roll_dice(1, build_data.map.width-1); + let vy = crate::rng::roll_dice(1, build_data.map.height-1); + let vidx = build_data.map.xy_idx(vx, vy); + let candidate = (vidx, rltk::Point::new(vx, vy)); + if !voronoi_seeds.contains(&candidate) { + voronoi_seeds.push(candidate); + } + } + + let mut voronoi_distance = vec![(0, 0.0f32) ; self.n_seeds]; + let mut voronoi_membership : Vec = vec![0 ; build_data.map.width as usize * build_data.map.height as usize]; + for (i, vid) in voronoi_membership.iter_mut().enumerate() { + let x = i as i32 % build_data.map.width; + let y = i as i32 / build_data.map.width; + + for (seed, pos) in voronoi_seeds.iter().enumerate() { + let distance; + match self.distance_algorithm { + DistanceAlgorithm::Pythagoras => { + distance = rltk::DistanceAlg::PythagorasSquared.distance2d( + rltk::Point::new(x, y), + pos.1 + ); + } + DistanceAlgorithm::Manhattan => { + distance = rltk::DistanceAlg::Manhattan.distance2d( + rltk::Point::new(x, y), + pos.1 + ); + } + DistanceAlgorithm::Chebyshev => { + distance = rltk::DistanceAlg::Chebyshev.distance2d( + rltk::Point::new(x, y), + pos.1 + ); + } + } + voronoi_distance[seed] = (seed, distance); + } + + voronoi_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap()); + + *vid = voronoi_distance[0].0 as i32; + } + + for y in 1..build_data.map.height-1 { + for x in 1..build_data.map.width-1 { + let mut neighbors = 0; + let my_idx = build_data.map.xy_idx(x, y); + let my_seed = voronoi_membership[my_idx]; + if voronoi_membership[build_data.map.xy_idx(x-1, y)] != my_seed { neighbors += 1; } + if voronoi_membership[build_data.map.xy_idx(x+1, y)] != my_seed { neighbors += 1; } + if voronoi_membership[build_data.map.xy_idx(x, y-1)] != my_seed { neighbors += 1; } + if voronoi_membership[build_data.map.xy_idx(x, y+1)] != my_seed { neighbors += 1; } + + if neighbors < 2 { + build_data.map.tiles[my_idx] = TileType::Floor; + } + } + build_data.take_snapshot(); + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/voronoi_spawning.rs b/chapter-75-darkplaza/src/map_builders/voronoi_spawning.rs new file mode 100644 index 00000000..2bfae7b7 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/voronoi_spawning.rs @@ -0,0 +1,47 @@ +use super::{MetaMapBuilder, BuilderMap, TileType, spawner}; +use std::collections::HashMap; + +pub struct VoronoiSpawning {} + +impl MetaMapBuilder for VoronoiSpawning { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl VoronoiSpawning { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(VoronoiSpawning{}) + } + + #[allow(clippy::map_entry)] + fn build(&mut self, build_data : &mut BuilderMap) { + let mut noise_areas : HashMap> = HashMap::new(); + let mut noise = rltk::FastNoise::seeded(crate::rng::roll_dice(1, 65536) as u64); + noise.set_noise_type(rltk::NoiseType::Cellular); + noise.set_frequency(0.08); + noise.set_cellular_distance_function(rltk::CellularDistanceFunction::Manhattan); + + for y in 1 .. build_data.map.height-1 { + for x in 1 .. build_data.map.width-1 { + let idx = build_data.map.xy_idx(x, y); + if build_data.map.tiles[idx] == TileType::Floor { + let cell_value_f = noise.get_noise(x as f32, y as f32) * 10240.0; + let cell_value = cell_value_f as i32; + + if noise_areas.contains_key(&cell_value) { + noise_areas.get_mut(&cell_value).unwrap().push(idx); + } else { + noise_areas.insert(cell_value, vec![idx]); + } + } + } + } + + // Spawn the entities + for area in noise_areas.iter() { + spawner::spawn_region(&build_data.map, area.1, build_data.map.depth, &mut build_data.spawn_list); + } + } +} diff --git a/chapter-75-darkplaza/src/map_builders/waveform_collapse/common.rs b/chapter-75-darkplaza/src/map_builders/waveform_collapse/common.rs new file mode 100644 index 00000000..3c038527 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/waveform_collapse/common.rs @@ -0,0 +1,13 @@ +use super::TileType; + +#[derive(PartialEq, Eq, Hash, Clone)] +pub struct MapChunk { + pub pattern : Vec, + pub exits: [Vec; 4], + pub has_exits: bool, + pub compatible_with: [Vec; 4] +} + +pub fn tile_idx_in_chunk(chunk_size: i32, x:i32, y:i32) -> usize { + ((y * chunk_size) + x) as usize +} diff --git a/chapter-75-darkplaza/src/map_builders/waveform_collapse/constraints.rs b/chapter-75-darkplaza/src/map_builders/waveform_collapse/constraints.rs new file mode 100644 index 00000000..44e7c52e --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/waveform_collapse/constraints.rs @@ -0,0 +1,208 @@ +use super::{TileType, Map, MapChunk, tile_idx_in_chunk}; +use std::collections::HashSet; + +pub fn build_patterns(map : &Map, chunk_size: i32, include_flipping: bool, dedupe: bool) -> Vec> { + let chunks_x = map.width / chunk_size; + let chunks_y = map.height / chunk_size; + let mut patterns = Vec::new(); + + for cy in 0..chunks_y { + for cx in 0..chunks_x { + // Normal orientation + let mut pattern : Vec = Vec::new(); + let start_x = cx * chunk_size; + let end_x = (cx+1) * chunk_size; + let start_y = cy * chunk_size; + let end_y = (cy+1) * chunk_size; + + for y in start_y .. end_y { + for x in start_x .. end_x { + let idx = map.xy_idx(x, y); + pattern.push(map.tiles[idx]); + } + } + patterns.push(pattern); + + if include_flipping { + // Flip horizontal + pattern = Vec::new(); + for y in start_y .. end_y { + for x in start_x .. end_x { + let idx = map.xy_idx(end_x - (x+1), y); + pattern.push(map.tiles[idx]); + } + } + patterns.push(pattern); + + // Flip vertical + pattern = Vec::new(); + for y in start_y .. end_y { + for x in start_x .. end_x { + let idx = map.xy_idx(x, end_y - (y+1)); + pattern.push(map.tiles[idx]); + } + } + patterns.push(pattern); + + // Flip both + pattern = Vec::new(); + for y in start_y .. end_y { + for x in start_x .. end_x { + let idx = map.xy_idx(end_x - (x+1), end_y - (y+1)); + pattern.push(map.tiles[idx]); + } + } + patterns.push(pattern); + } + } + } + + // Dedupe + if dedupe { + rltk::console::log(format!("Pre de-duplication, there are {} patterns", patterns.len())); + let set: HashSet> = patterns.drain(..).collect(); // dedup + patterns.extend(set.into_iter()); + rltk::console::log(format!("There are {} patterns", patterns.len())); + } + + patterns +} + +pub fn render_pattern_to_map(map : &mut Map, chunk: &MapChunk, chunk_size: i32, start_x : i32, start_y: i32) { + let mut i = 0usize; + for tile_y in 0..chunk_size { + for tile_x in 0..chunk_size { + let map_idx = map.xy_idx(start_x + tile_x, start_y + tile_y); + map.tiles[map_idx] = chunk.pattern[i]; + map.visible_tiles[map_idx] = true; + i += 1; + } + } + + for (x,northbound) in chunk.exits[0].iter().enumerate() { + if *northbound { + let map_idx = map.xy_idx(start_x + x as i32, start_y); + map.tiles[map_idx] = TileType::DownStairs; + } + } + for (x,southbound) in chunk.exits[1].iter().enumerate() { + if *southbound { + let map_idx = map.xy_idx(start_x + x as i32, start_y + chunk_size -1); + map.tiles[map_idx] = TileType::DownStairs; + } + } + for (x,westbound) in chunk.exits[2].iter().enumerate() { + if *westbound { + let map_idx = map.xy_idx(start_x, start_y + x as i32); + map.tiles[map_idx] = TileType::DownStairs; + } + } + for (x,eastbound) in chunk.exits[3].iter().enumerate() { + if *eastbound { + let map_idx = map.xy_idx(start_x + chunk_size - 1, start_y + x as i32); + map.tiles[map_idx] = TileType::DownStairs; + } + } +} + +pub fn patterns_to_constraints(patterns: Vec>, chunk_size : i32) -> Vec { + // Move into the new constraints object + let mut constraints : Vec = Vec::new(); + for p in patterns { + let mut new_chunk = MapChunk{ + pattern: p, + exits: [ Vec::new(), Vec::new(), Vec::new(), Vec::new() ], + has_exits : true, + compatible_with: [ Vec::new(), Vec::new(), Vec::new(), Vec::new() ] + }; + for exit in new_chunk.exits.iter_mut() { + for _i in 0..chunk_size { + exit.push(false); + } + } + + let mut n_exits = 0; + for x in 0..chunk_size { + // Check for north-bound exits + let north_idx = tile_idx_in_chunk(chunk_size, x, 0); + if new_chunk.pattern[north_idx] == TileType::Floor { + new_chunk.exits[0][x as usize] = true; + n_exits += 1; + } + + // Check for south-bound exits + let south_idx = tile_idx_in_chunk(chunk_size, x, chunk_size-1); + if new_chunk.pattern[south_idx] == TileType::Floor { + new_chunk.exits[1][x as usize] = true; + n_exits += 1; + } + + // Check for west-bound exits + let west_idx = tile_idx_in_chunk(chunk_size, 0, x); + if new_chunk.pattern[west_idx] == TileType::Floor { + new_chunk.exits[2][x as usize] = true; + n_exits += 1; + } + + // Check for east-bound exits + let east_idx = tile_idx_in_chunk(chunk_size, chunk_size-1, x); + if new_chunk.pattern[east_idx] == TileType::Floor { + new_chunk.exits[3][x as usize] = true; + n_exits += 1; + } + } + + if n_exits == 0 { + new_chunk.has_exits = false; + } + + constraints.push(new_chunk); + } + + // Build compatibility matrix + let ch = constraints.clone(); + for c in constraints.iter_mut() { + for (j,potential) in ch.iter().enumerate() { + // If there are no exits at all, it's compatible + if !c.has_exits || !potential.has_exits { + for compat in c.compatible_with.iter_mut() { + compat.push(j); + } + } else { + // Evaluate compatibilty by direction + for (direction, exit_list) in c.exits.iter_mut().enumerate() { + let opposite = match direction { + 0 => 1, // Our North, Their South + 1 => 0, // Our South, Their North + 2 => 3, // Our West, Their East + _ => 2 // Our East, Their West + }; + + let mut it_fits = false; + let mut has_any = false; + for (slot, can_enter) in exit_list.iter().enumerate() { + if *can_enter { + has_any = true; + if potential.exits[opposite][slot] { + it_fits = true; + } + } + } + if it_fits { + c.compatible_with[direction].push(j); + } + if !has_any { + // There's no exits on this side, let's match only if + // the other edge also has no exits + let matching_exit_count = potential.exits[opposite].iter().filter(|a| !**a).count(); + if matching_exit_count == 0 { + c.compatible_with[direction].push(j); + } + } + } + } + } + } + + constraints +} diff --git a/chapter-75-darkplaza/src/map_builders/waveform_collapse/mod.rs b/chapter-75-darkplaza/src/map_builders/waveform_collapse/mod.rs new file mode 100644 index 00000000..44096272 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/waveform_collapse/mod.rs @@ -0,0 +1,85 @@ +use super::{MetaMapBuilder, BuilderMap, Map, TileType}; +mod common; +use common::*; +mod constraints; +use constraints::*; +mod solver; +use solver::*; + +/// Provides a map builder using the Wave Function Collapse algorithm. +pub struct WaveformCollapseBuilder {} + +impl MetaMapBuilder for WaveformCollapseBuilder { + fn build_map(&mut self, build_data : &mut BuilderMap) { + self.build(build_data); + } +} + +impl WaveformCollapseBuilder { + /// Constructor for waveform collapse. + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(WaveformCollapseBuilder{}) + } + + fn build(&mut self, build_data : &mut BuilderMap) { + const CHUNK_SIZE :i32 = 8; + build_data.take_snapshot(); + + let patterns = build_patterns(&build_data.map, CHUNK_SIZE, true, true); + let constraints = patterns_to_constraints(patterns, CHUNK_SIZE); + self.render_tile_gallery(&constraints, CHUNK_SIZE, build_data); + + let old_map = build_data.map.clone(); + + build_data.map = Map::new(build_data.map.depth, build_data.width, build_data.height, &build_data.map.name); + build_data.spawn_list.clear(); + build_data.rooms = None; + build_data.corridors = None; + let mut tries = 0; + loop { + let mut solver = Solver::new(constraints.clone(), CHUNK_SIZE, &build_data.map); + while !solver.iteration(&mut build_data.map) { + build_data.take_snapshot(); + } + build_data.take_snapshot(); + if solver.possible { break; } // If it has hit an impossible condition, try again + tries += 1; + if tries > 10 { break; } + } + + if tries > 10 { + // Restore the old one + build_data.map = old_map; + } + } + + fn render_tile_gallery(&mut self, constraints: &[MapChunk], chunk_size: i32, build_data : &mut BuilderMap) { + build_data.map = Map::new(build_data.map.depth, build_data.width, build_data.height, &build_data.map.name); + let mut counter = 0; + let mut x = 1; + let mut y = 1; + while counter < constraints.len() { + render_pattern_to_map(&mut build_data.map, &constraints[counter], chunk_size, x, y); + + x += chunk_size + 1; + if x + chunk_size > build_data.map.width { + // Move to the next row + x = 1; + y += chunk_size + 1; + + if y + chunk_size > build_data.map.height { + // Move to the next page + build_data.take_snapshot(); + build_data.map = Map::new(build_data.map.depth, build_data.width, build_data.height, &build_data.map.name); + + x = 1; + y = 1; + } + } + + counter += 1; + } + build_data.take_snapshot(); + } +} diff --git a/chapter-75-darkplaza/src/map_builders/waveform_collapse/solver.rs b/chapter-75-darkplaza/src/map_builders/waveform_collapse/solver.rs new file mode 100644 index 00000000..9599d3f9 --- /dev/null +++ b/chapter-75-darkplaza/src/map_builders/waveform_collapse/solver.rs @@ -0,0 +1,228 @@ +use super::{MapChunk, Map}; +use std::collections::HashSet; + +pub struct Solver { + constraints: Vec, + chunk_size : i32, + chunks : Vec>, + chunks_x : usize, + chunks_y : usize, + remaining : Vec<(usize, i32)>, // (index, # neighbors) + pub possible: bool +} + +impl Solver { + pub fn new(constraints: Vec, chunk_size: i32, map : &Map) -> Solver { + let chunks_x = (map.width / chunk_size) as usize; + let chunks_y = (map.height / chunk_size) as usize; + let mut remaining : Vec<(usize, i32)> = Vec::new(); + for i in 0..(chunks_x*chunks_y) { + remaining.push((i, 0)); + } + + Solver { + constraints, + chunk_size, + chunks: vec![None; chunks_x * chunks_y], + chunks_x, + chunks_y, + remaining, + possible: true + } + } + + fn chunk_idx(&self, x:usize, y:usize) -> usize { + ((y * self.chunks_x) + x) as usize + } + + fn count_neighbors(&self, chunk_x:usize, chunk_y:usize) -> i32 { + let mut neighbors = 0; + + if chunk_x > 0 { + let left_idx = self.chunk_idx(chunk_x-1, chunk_y); + match self.chunks[left_idx] { + None => {} + Some(_) => { + neighbors += 1; + } + } + } + + if chunk_x < self.chunks_x-1 { + let right_idx = self.chunk_idx(chunk_x+1, chunk_y); + match self.chunks[right_idx] { + None => {} + Some(_) => { + neighbors += 1; + } + } + } + + if chunk_y > 0 { + let up_idx = self.chunk_idx(chunk_x, chunk_y-1); + match self.chunks[up_idx] { + None => {} + Some(_) => { + neighbors += 1; + } + } + } + + if chunk_y < self.chunks_y-1 { + let down_idx = self.chunk_idx(chunk_x, chunk_y+1); + match self.chunks[down_idx] { + None => {} + Some(_) => { + neighbors += 1; + } + } + } + neighbors + } + + pub fn iteration(&mut self, map: &mut Map) -> bool { + if self.remaining.is_empty() { return true; } + + // Populate the neighbor count of the remaining list + let mut remain_copy = self.remaining.clone(); + let mut neighbors_exist = false; + for r in remain_copy.iter_mut() { + let idx = r.0; + let chunk_x = idx % self.chunks_x; + let chunk_y = idx / self.chunks_x; + let neighbor_count = self.count_neighbors(chunk_x, chunk_y); + if neighbor_count > 0 { neighbors_exist = true; } + *r = (r.0, neighbor_count); + } + remain_copy.sort_by(|a,b| b.1.cmp(&a.1)); + self.remaining = remain_copy; + + // Pick a random chunk we haven't dealt with yet and get its index, remove from remaining list + let remaining_index = if !neighbors_exist { + (crate::rng::roll_dice(1, self.remaining.len() as i32)-1) as usize + } else { + 0usize + }; + let chunk_index = self.remaining[remaining_index].0; + self.remaining.remove(remaining_index); + + let chunk_x = chunk_index % self.chunks_x; + let chunk_y = chunk_index / self.chunks_x; + + let mut neighbors = 0; + let mut options : Vec> = Vec::new(); + + if chunk_x > 0 { + let left_idx = self.chunk_idx(chunk_x-1, chunk_y); + match self.chunks[left_idx] { + None => {} + Some(nt) => { + neighbors += 1; + options.push(self.constraints[nt].compatible_with[3].clone()); + } + } + } + + if chunk_x < self.chunks_x-1 { + let right_idx = self.chunk_idx(chunk_x+1, chunk_y); + match self.chunks[right_idx] { + None => {} + Some(nt) => { + neighbors += 1; + options.push(self.constraints[nt].compatible_with[2].clone()); + } + } + } + + if chunk_y > 0 { + let up_idx = self.chunk_idx(chunk_x, chunk_y-1); + match self.chunks[up_idx] { + None => {} + Some(nt) => { + neighbors += 1; + options.push(self.constraints[nt].compatible_with[1].clone()); + } + } + } + + if chunk_y < self.chunks_y-1 { + let down_idx = self.chunk_idx(chunk_x, chunk_y+1); + match self.chunks[down_idx] { + None => {} + Some(nt) => { + neighbors += 1; + options.push(self.constraints[nt].compatible_with[0].clone()); + } + } + } + + if neighbors == 0 { + // There is nothing nearby, so we can have anything! + let new_chunk_idx = (crate::rng::roll_dice(1, self.constraints.len() as i32)-1) as usize; + self.chunks[chunk_index] = Some(new_chunk_idx); + let left_x = chunk_x as i32 * self.chunk_size as i32; + let right_x = (chunk_x as i32+1) * self.chunk_size as i32; + let top_y = chunk_y as i32 * self.chunk_size as i32; + let bottom_y = (chunk_y as i32+1) * self.chunk_size as i32; + + + let mut i : usize = 0; + for y in top_y .. bottom_y { + for x in left_x .. right_x { + let mapidx = map.xy_idx(x, y); + let tile = self.constraints[new_chunk_idx].pattern[i]; + map.tiles[mapidx] = tile; + i += 1; + } + } + } + else { + // There are neighbors, so we try to be compatible with them + let mut options_to_check : HashSet = HashSet::new(); + for o in options.iter() { + for i in o.iter() { + options_to_check.insert(*i); + } + } + + let mut possible_options : Vec = Vec::new(); + for new_chunk_idx in options_to_check.iter() { + let mut possible = true; + for o in options.iter() { + if !o.contains(new_chunk_idx) { possible = false; } + } + if possible { + possible_options.push(*new_chunk_idx); + } + } + + if possible_options.is_empty() { + rltk::console::log("Oh no! It's not possible!"); + self.possible = false; + return true; + } else { + let new_chunk_idx = if possible_options.len() == 1 { 0 } + else { crate::rng::roll_dice(1, possible_options.len() as i32)-1 }; + + self.chunks[chunk_index] = Some(possible_options[new_chunk_idx as usize]); + let left_x = chunk_x as i32 * self.chunk_size as i32; + let right_x = (chunk_x as i32+1) * self.chunk_size as i32; + let top_y = chunk_y as i32 * self.chunk_size as i32; + let bottom_y = (chunk_y as i32+1) * self.chunk_size as i32; + + + let mut i : usize = 0; + for y in top_y .. bottom_y { + for x in left_x .. right_x { + let mapidx = map.xy_idx(x, y); + let tile = self.constraints[possible_options[new_chunk_idx as usize]].pattern[i]; + map.tiles[mapidx] = tile; + i += 1; + } + } + } + } + + false + } +} diff --git a/chapter-75-darkplaza/src/player.rs b/chapter-75-darkplaza/src/player.rs new file mode 100644 index 00000000..0dbda903 --- /dev/null +++ b/chapter-75-darkplaza/src/player.rs @@ -0,0 +1,485 @@ +use rltk::{VirtualKeyCode, Rltk, Point}; +use specs::prelude::*; +use std::cmp::{max, min}; +use super::{Position, Player, Viewshed, State, Map, RunState, Attributes, WantsToMelee, Item, + WantsToPickupItem, TileType, HungerClock, HungerState, + EntityMoved, Door, BlocksTile, BlocksVisibility, Renderable, Pools, Faction, + raws::Reaction, Vendor, VendorMode, WantsToCastSpell, Target, Equipped, Weapon, + WantsToShoot, Name}; + +fn get_player_target_list(ecs : &mut World) -> Vec<(f32,Entity)> { + let mut possible_targets : Vec<(f32,Entity)> = Vec::new(); + let viewsheds = ecs.read_storage::(); + let player_entity = ecs.fetch::(); + let equipped = ecs.read_storage::(); + let weapon = ecs.read_storage::(); + let map = ecs.fetch::(); + let positions = ecs.read_storage::(); + let factions = ecs.read_storage::(); + for (equipped, weapon) in (&equipped, &weapon).join() { + if equipped.owner == *player_entity && weapon.range.is_some() { + let range = weapon.range.unwrap(); + + if let Some(vs) = viewsheds.get(*player_entity) { + let player_pos = positions.get(*player_entity).unwrap(); + for tile_point in vs.visible_tiles.iter() { + let tile_idx = map.xy_idx(tile_point.x, tile_point.y); + let distance_to_target = rltk::DistanceAlg::Pythagoras.distance2d(*tile_point, rltk::Point::new(player_pos.x, player_pos.y)); + if distance_to_target < range as f32 { + crate::spatial::for_each_tile_content(tile_idx, |possible_target| { + if possible_target != *player_entity && factions.get(possible_target).is_some() { + possible_targets.push((distance_to_target, possible_target)); + } + }); + } + } + } + } + } + + possible_targets.sort_by(|a,b| a.0.partial_cmp(&b.0).unwrap()); + possible_targets +} + +pub fn end_turn_targeting(ecs: &mut World) { + let possible_targets = get_player_target_list(ecs); + let mut targets = ecs.write_storage::(); + targets.clear(); + + if !possible_targets.is_empty() { + targets.insert(possible_targets[0].1, Target{}).expect("Insert fail"); + } +} + +fn fire_on_target(ecs: &mut World) -> RunState { + let targets = ecs.write_storage::(); + let entities = ecs.entities(); + let mut current_target : Option = None; + + for (e,_t) in (&entities, &targets).join() { + current_target = Some(e); + } + + if let Some(target) = current_target { + let player_entity = ecs.fetch::(); + let mut shoot_store = ecs.write_storage::(); + let names = ecs.read_storage::(); + if let Some(name) = names.get(target) { + crate::gamelog::Logger::new() + .append("You fire at") + .color(rltk::CYAN) + .append(&name.name) + .log(); + } + shoot_store.insert(*player_entity, WantsToShoot{ target }).expect("Insert Fail"); + + RunState::Ticking + } else { + crate::gamelog::Logger::new().append("You don't have a target selected!").log(); + RunState::AwaitingInput + } + +} + +fn cycle_target(ecs: &mut World) { + let possible_targets = get_player_target_list(ecs); + let mut targets = ecs.write_storage::(); + let entities = ecs.entities(); + let mut current_target : Option = None; + + for (e,_t) in (&entities, &targets).join() { + current_target = Some(e); + } + + targets.clear(); + if let Some(current_target) = current_target { + if !possible_targets.len() > 1 { + let mut index = 0; + for (i, target) in possible_targets.iter().enumerate() { + if target.1 == current_target { + index = i; + } + } + + if index > possible_targets.len()-2 { + targets.insert(possible_targets[0].1, Target{}).expect("Insert fail"); + } else { + targets.insert(possible_targets[index+1].1, Target{}).expect("Insert fail"); + } + } + } +} + +pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> RunState { + let mut positions = ecs.write_storage::(); + let players = ecs.read_storage::(); + let mut viewsheds = ecs.write_storage::(); + let entities = ecs.entities(); + let combat_stats = ecs.read_storage::(); + let map = ecs.fetch::(); + let mut wants_to_melee = ecs.write_storage::(); + let mut entity_moved = ecs.write_storage::(); + let mut doors = ecs.write_storage::(); + let mut blocks_visibility = ecs.write_storage::(); + let mut blocks_movement = ecs.write_storage::(); + let mut renderables = ecs.write_storage::(); + let factions = ecs.read_storage::(); + let vendors = ecs.read_storage::(); + let mut result = RunState::AwaitingInput; + + let mut swap_entities : Vec<(Entity, i32, i32)> = Vec::new(); + + for (entity, _player, pos, viewshed) in (&entities, &players, &mut positions, &mut viewsheds).join() { + if pos.x + delta_x < 1 || pos.x + delta_x > map.width-1 || pos.y + delta_y < 1 || pos.y + delta_y > map.height-1 { return RunState::AwaitingInput; } + let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y); + + result = crate::spatial::for_each_tile_content_with_gamemode(destination_idx, |potential_target| { + if let Some(_vendor) = vendors.get(potential_target) { + return Some(RunState::ShowVendor{ vendor: potential_target, mode : VendorMode::Sell }); + } + + let mut hostile = true; + if combat_stats.get(potential_target).is_some() { + if let Some(faction) = factions.get(potential_target) { + let reaction = crate::raws::faction_reaction( + &faction.name, + "Player", + &crate::raws::RAWS.lock().unwrap() + ); + if reaction != Reaction::Attack { hostile = false; } + } + } + if !hostile { + // Note that we want to move the bystander + swap_entities.push((potential_target, pos.x, pos.y)); + + // Move the player + pos.x = min(map.width-1 , max(0, pos.x + delta_x)); + pos.y = min(map.height-1, max(0, pos.y + delta_y)); + entity_moved.insert(entity, EntityMoved{}).expect("Unable to insert marker"); + + viewshed.dirty = true; + let mut ppos = ecs.write_resource::(); + ppos.x = pos.x; + ppos.y = pos.y; + return Some(RunState::Ticking); + } else { + let target = combat_stats.get(potential_target); + if let Some(_target) = target { + wants_to_melee.insert(entity, WantsToMelee{ target: potential_target }).expect("Add target failed"); + return Some(RunState::Ticking); + } + } + let door = doors.get_mut(potential_target); + if let Some(door) = door { + door.open = true; + blocks_visibility.remove(potential_target); + blocks_movement.remove(potential_target); + let glyph = renderables.get_mut(potential_target).unwrap(); + glyph.glyph = rltk::to_cp437('/'); + viewshed.dirty = true; + return Some(RunState::Ticking); + } + None + }); + + if !crate::spatial::is_blocked(destination_idx) { + pos.x = min(map.width-1 , max(0, pos.x + delta_x)); + pos.y = min(map.height-1, max(0, pos.y + delta_y)); + entity_moved.insert(entity, EntityMoved{}).expect("Unable to insert marker"); + + viewshed.dirty = true; + let mut ppos = ecs.write_resource::(); + ppos.x = pos.x; + ppos.y = pos.y; + result = RunState::Ticking; + match map.tiles[destination_idx] { + TileType::DownStairs => result = RunState::NextLevel, + TileType::UpStairs => result = RunState::PreviousLevel, + _ => {} + } + } + } + + for m in swap_entities.iter() { + let their_pos = positions.get_mut(m.0); + if let Some(their_pos) = their_pos { + their_pos.x = m.1; + their_pos.y = m.2; + } + } + + result +} + +pub fn try_next_level(ecs: &mut World) -> bool { + let player_pos = ecs.fetch::(); + let map = ecs.fetch::(); + let player_idx = map.xy_idx(player_pos.x, player_pos.y); + if map.tiles[player_idx] == TileType::DownStairs { + true + } else { + crate::gamelog::Logger::new().append("There is no way down from here.").log(); + false + } +} + +pub fn try_previous_level(ecs: &mut World) -> bool { + let player_pos = ecs.fetch::(); + let map = ecs.fetch::(); + let player_idx = map.xy_idx(player_pos.x, player_pos.y); + if map.tiles[player_idx] == TileType::UpStairs { + true + } else { + crate::gamelog::Logger::new().append("There is no way up from here.").log(); + false + } +} + +fn get_item(ecs: &mut World) { + let player_pos = ecs.fetch::(); + let player_entity = ecs.fetch::(); + let entities = ecs.entities(); + let items = ecs.read_storage::(); + let positions = ecs.read_storage::(); + + let mut target_item : Option = None; + for (item_entity, _item, position) in (&entities, &items, &positions).join() { + if position.x == player_pos.x && position.y == player_pos.y { + target_item = Some(item_entity); + } + } + + match target_item { + None => crate::gamelog::Logger::new().append("There is nothing here to pick up.").log(), + Some(item) => { + let mut pickup = ecs.write_storage::(); + pickup.insert(*player_entity, WantsToPickupItem{ collected_by: *player_entity, item }).expect("Unable to insert want to pickup"); + } + } +} + +fn skip_turn(ecs: &mut World) -> RunState { + let player_entity = ecs.fetch::(); + let viewshed_components = ecs.read_storage::(); + let factions = ecs.read_storage::(); + + let worldmap_resource = ecs.fetch::(); + + let mut can_heal = true; + let viewshed = viewshed_components.get(*player_entity).unwrap(); + for tile in viewshed.visible_tiles.iter() { + let idx = worldmap_resource.xy_idx(tile.x, tile.y); + crate::spatial::for_each_tile_content(idx, |entity_id| { + let faction = factions.get(entity_id); + match faction { + None => {} + Some(faction) => { + let reaction = crate::raws::faction_reaction( + &faction.name, + "Player", + &crate::raws::RAWS.lock().unwrap() + ); + if reaction == Reaction::Attack { + can_heal = false; + } + } + } + }); + } + + let hunger_clocks = ecs.read_storage::(); + let hc = hunger_clocks.get(*player_entity); + if let Some(hc) = hc { + match hc.state { + HungerState::Hungry => can_heal = false, + HungerState::Starving => can_heal = false, + _ => {} + } + } + + if can_heal { + let mut health_components = ecs.write_storage::(); + let pools = health_components.get_mut(*player_entity).unwrap(); + pools.hit_points.current = i32::min(pools.hit_points.current + 1, pools.hit_points.max); + if crate::rng::roll_dice(1,6)==1 { + pools.mana.current = i32::min(pools.mana.current + 1, pools.mana.max); + } + } + + RunState::Ticking +} + +fn use_consumable_hotkey(gs: &mut State, key: i32) -> RunState { + use super::{Consumable, InBackpack, WantsToUseItem}; + + let consumables = gs.ecs.read_storage::(); + let backpack = gs.ecs.read_storage::(); + let player_entity = gs.ecs.fetch::(); + let entities = gs.ecs.entities(); + let mut carried_consumables = Vec::new(); + for (entity, carried_by, _consumable) in (&entities, &backpack, &consumables).join() { + if carried_by.owner == *player_entity { + carried_consumables.push(entity); + } + } + + if (key as usize) < carried_consumables.len() { + use crate::components::Ranged; + if let Some(ranged) = gs.ecs.read_storage::().get(carried_consumables[key as usize]) { + return RunState::ShowTargeting{ range: ranged.range, item: carried_consumables[key as usize] }; + } + let mut intent = gs.ecs.write_storage::(); + intent.insert( + *player_entity, + WantsToUseItem{ item: carried_consumables[key as usize], target: None } + ).expect("Unable to insert intent"); + return RunState::Ticking; + } + RunState::Ticking +} + +fn use_spell_hotkey(gs: &mut State, key: i32) -> RunState { + use super::KnownSpells; + use super::raws::find_spell_entity; + + let player_entity = gs.ecs.fetch::(); + let known_spells_storage = gs.ecs.read_storage::(); + let known_spells = &known_spells_storage.get(*player_entity).unwrap().spells; + + if (key as usize) < known_spells.len() { + let pools = gs.ecs.read_storage::(); + let player_pools = pools.get(*player_entity).unwrap(); + if player_pools.mana.current >= known_spells[key as usize].mana_cost { + if let Some(spell_entity) = find_spell_entity(&gs.ecs, &known_spells[key as usize].display_name) { + use crate::components::Ranged; + if let Some(ranged) = gs.ecs.read_storage::().get(spell_entity) { + return RunState::ShowTargeting{ range: ranged.range, item: spell_entity }; + }; + let mut intent = gs.ecs.write_storage::(); + intent.insert( + *player_entity, + WantsToCastSpell{ spell: spell_entity, target: None } + ).expect("Unable to insert intent"); + return RunState::Ticking; + } + } else { + crate::gamelog::Logger::new().append("You don't have enough mana to cast that!").log(); + } + } + + RunState::Ticking +} + +pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { + // Hotkeys + if ctx.shift && ctx.key.is_some() { + let key : Option = + match ctx.key.unwrap() { + VirtualKeyCode::Key1 => Some(1), + VirtualKeyCode::Key2 => Some(2), + VirtualKeyCode::Key3 => Some(3), + VirtualKeyCode::Key4 => Some(4), + VirtualKeyCode::Key5 => Some(5), + VirtualKeyCode::Key6 => Some(6), + VirtualKeyCode::Key7 => Some(7), + VirtualKeyCode::Key8 => Some(8), + VirtualKeyCode::Key9 => Some(9), + _ => None + }; + if let Some(key) = key { + return use_consumable_hotkey(gs, key-1); + } + } + if ctx.control && ctx.key.is_some() { + let key : Option = + match ctx.key.unwrap() { + VirtualKeyCode::Key1 => Some(1), + VirtualKeyCode::Key2 => Some(2), + VirtualKeyCode::Key3 => Some(3), + VirtualKeyCode::Key4 => Some(4), + VirtualKeyCode::Key5 => Some(5), + VirtualKeyCode::Key6 => Some(6), + VirtualKeyCode::Key7 => Some(7), + VirtualKeyCode::Key8 => Some(8), + VirtualKeyCode::Key9 => Some(9), + _ => None + }; + if let Some(key) = key { + return use_spell_hotkey(gs, key-1); + } + } + + // Player movement + match ctx.key { + None => { return RunState::AwaitingInput } // Nothing happened + Some(key) => match key { + VirtualKeyCode::Left | + VirtualKeyCode::Numpad4 | + VirtualKeyCode::H => return try_move_player(-1, 0, &mut gs.ecs), + + VirtualKeyCode::Right | + VirtualKeyCode::Numpad6 | + VirtualKeyCode::L => return try_move_player(1, 0, &mut gs.ecs), + + VirtualKeyCode::Up | + VirtualKeyCode::Numpad8 | + VirtualKeyCode::K => return try_move_player(0, -1, &mut gs.ecs), + + VirtualKeyCode::Down | + VirtualKeyCode::Numpad2 | + VirtualKeyCode::J => return try_move_player(0, 1, &mut gs.ecs), + + // Diagonals + VirtualKeyCode::Numpad9 | + VirtualKeyCode::U => return try_move_player(1, -1, &mut gs.ecs), + + VirtualKeyCode::Numpad7 | + VirtualKeyCode::Y => return try_move_player(-1, -1, &mut gs.ecs), + + VirtualKeyCode::Numpad3 | + VirtualKeyCode::N => return try_move_player(1, 1, &mut gs.ecs), + + VirtualKeyCode::Numpad1 | + VirtualKeyCode::B => return try_move_player(-1, 1, &mut gs.ecs), + + // Skip Turn + VirtualKeyCode::Numpad5 | + VirtualKeyCode::Space => return skip_turn(&mut gs.ecs), + + // Level changes + VirtualKeyCode::Period => { + if try_next_level(&mut gs.ecs) { + return RunState::NextLevel; + } + } + VirtualKeyCode::Comma => { + if try_previous_level(&mut gs.ecs) { + return RunState::PreviousLevel; + } + } + + // Picking up items + VirtualKeyCode::G => get_item(&mut gs.ecs), + VirtualKeyCode::I => return RunState::ShowInventory, + VirtualKeyCode::D => return RunState::ShowDropItem, + VirtualKeyCode::R => return RunState::ShowRemoveItem, + + // Ranged + VirtualKeyCode::V => { + cycle_target(&mut gs.ecs); + return RunState::AwaitingInput; + } + VirtualKeyCode::F => return fire_on_target(&mut gs.ecs), + + // Save and Quit + VirtualKeyCode::Escape => return RunState::SaveGame, + + // Cheating! + VirtualKeyCode::Backslash => return RunState::ShowCheatMenu, + + _ => { return RunState::AwaitingInput } + }, + } + RunState::Ticking +} diff --git a/chapter-75-darkplaza/src/random_table.rs b/chapter-75-darkplaza/src/random_table.rs new file mode 100644 index 00000000..e70e88fe --- /dev/null +++ b/chapter-75-darkplaza/src/random_table.rs @@ -0,0 +1,83 @@ +use crate::raws::{SpawnTableType, spawn_type_by_name, RawMaster}; + +pub struct RandomEntry { + name : String, + weight : i32 +} + +impl RandomEntry { + pub fn new(name: S, weight: i32) -> RandomEntry { + RandomEntry{ name: name.to_string(), weight } + } +} + +#[derive(Default)] +pub struct MasterTable { + items : RandomTable, + mobs : RandomTable, + props : RandomTable +} + +impl MasterTable { + pub fn new() -> MasterTable { + MasterTable{ + items : RandomTable::new(), + mobs : RandomTable::new(), + props : RandomTable::new() + } + } + + pub fn add(&mut self, name : S, weight: i32, raws: &RawMaster) { + match spawn_type_by_name(raws, &name.to_string()) { + SpawnTableType::Item => self.items.add(name, weight), + SpawnTableType::Mob => self.mobs.add(name, weight), + SpawnTableType::Prop => self.props.add(name, weight), + } + } + + pub fn roll(&self) -> String { + let roll = crate::rng::roll_dice(1, 4); + match roll { + 1 => self.items.roll(), + 2 => self.props.roll(), + 3 => self.mobs.roll(), + _ => "None".to_string() + } + } +} + +#[derive(Default)] +pub struct RandomTable { + entries : Vec, + total_weight : i32 +} + +impl RandomTable { + pub fn new() -> RandomTable { + RandomTable{ entries: Vec::new(), total_weight: 0 } + } + + pub fn add(&mut self, name : S, weight: i32) { + if weight > 0 { + self.total_weight += weight; + self.entries.push(RandomEntry::new(name.to_string(), weight)); + } + } + + pub fn roll(&self) -> String { + if self.total_weight == 0 { return "None".to_string(); } + let mut roll = crate::rng::roll_dice(1, self.total_weight)-1; + let mut index : usize = 0; + + while roll > 0 { + if roll < self.entries[index].weight { + return self.entries[index].name.clone(); + } + + roll -= self.entries[index].weight; + index += 1; + } + + "None".to_string() + } +} diff --git a/chapter-75-darkplaza/src/raws/faction_structs.rs b/chapter-75-darkplaza/src/raws/faction_structs.rs new file mode 100644 index 00000000..6d0a51f4 --- /dev/null +++ b/chapter-75-darkplaza/src/raws/faction_structs.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize}; +use std::collections::HashMap; + +#[derive(Deserialize, Debug)] +pub struct FactionInfo { + pub name : String, + pub responses : HashMap +} + +#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)] +pub enum Reaction { + Ignore, Attack, Flee +} diff --git a/chapter-75-darkplaza/src/raws/item_structs.rs b/chapter-75-darkplaza/src/raws/item_structs.rs new file mode 100644 index 00000000..0b496791 --- /dev/null +++ b/chapter-75-darkplaza/src/raws/item_structs.rs @@ -0,0 +1,74 @@ +use serde::{Deserialize}; +use std::collections::HashMap; + +#[derive(Deserialize, Debug, Clone)] +pub struct Item { + pub name : String, + pub renderable : Option, + pub consumable : Option, + pub weapon : Option, + pub wearable : Option, + pub initiative_penalty : Option, + pub weight_lbs : Option, + pub base_value : Option, + pub vendor_category : Option, + pub magic : Option, + pub attributes : Option, + pub template_magic : Option +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Renderable { + pub glyph: String, + pub fg : String, + pub bg : String, + pub order: i32, + pub x_size : Option, + pub y_size : Option +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Consumable { + pub effects : HashMap, + pub charges : Option +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Weapon { + pub range: String, + pub attribute: String, + pub base_damage: String, + pub hit_bonus: i32, + pub proc_chance : Option, + pub proc_target : Option, + pub proc_effects : Option> +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Wearable { + pub armor_class: f32, + pub slot : String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct MagicItem { + pub class: String, + pub naming: String, + pub cursed: Option +} + +#[derive(Deserialize, Debug, Clone)] +pub struct ItemAttributeBonus { + pub might : Option, + pub fitness : Option, + pub quickness : Option, + pub intelligence : Option +} + +#[derive(Deserialize, Debug, Clone)] +pub struct ItemMagicTemplate { + pub unidentified_name: String, + pub bonus_min: i32, + pub bonus_max: i32, + pub include_cursed: bool +} diff --git a/chapter-75-darkplaza/src/raws/loot_structs.rs b/chapter-75-darkplaza/src/raws/loot_structs.rs new file mode 100644 index 00000000..3e1de3d4 --- /dev/null +++ b/chapter-75-darkplaza/src/raws/loot_structs.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize}; + +#[derive(Deserialize, Debug)] +pub struct LootTable { + pub name : String, + pub drops : Vec +} + +#[derive(Deserialize, Debug)] +pub struct LootDrop { + pub name : String, + pub weight : i32 +} diff --git a/chapter-75-darkplaza/src/raws/mob_structs.rs b/chapter-75-darkplaza/src/raws/mob_structs.rs new file mode 100644 index 00000000..138c89e5 --- /dev/null +++ b/chapter-75-darkplaza/src/raws/mob_structs.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize}; +use super::{Renderable}; +use std::collections::HashMap; + +#[derive(Deserialize, Debug)] +pub struct Mob { + pub name : String, + pub renderable : Option, + pub blocks_tile : bool, + pub vision_range : i32, + pub movement : String, + pub quips : Option>, + pub attributes : MobAttributes, + pub skills : Option>, + pub level : Option, + pub hp : Option, + pub mana : Option, + pub equipped : Option>, + pub natural : Option, + pub loot_table : Option, + pub light : Option, + pub faction : Option, + pub gold : Option, + pub vendor : Option>, + pub abilities : Option>, + pub on_death : Option> +} + +#[derive(Deserialize, Debug)] +pub struct MobAttributes { + pub might : Option, + pub fitness : Option, + pub quickness : Option, + pub intelligence : Option +} + +#[derive(Deserialize, Debug)] +pub struct MobNatural { + pub armor_class : Option, + pub attacks: Option> +} + +#[derive(Deserialize, Debug)] +pub struct NaturalAttack { + pub name : String, + pub hit_bonus : i32, + pub damage : String +} + + +#[derive(Deserialize, Debug)] +pub struct MobLight { + pub range : i32, + pub color : String +} + +#[derive(Deserialize, Debug)] +pub struct MobAbility { + pub spell : String, + pub chance : f32, + pub range : f32, + pub min_range : f32 +} diff --git a/chapter-75-darkplaza/src/raws/mod.rs b/chapter-75-darkplaza/src/raws/mod.rs new file mode 100644 index 00000000..6e3f5546 --- /dev/null +++ b/chapter-75-darkplaza/src/raws/mod.rs @@ -0,0 +1,53 @@ +mod item_structs; +use item_structs::*; +mod mob_structs; +use mob_structs::*; +mod prop_structs; +use prop_structs::*; +mod spawn_table_structs; +use spawn_table_structs::*; +mod loot_structs; +use loot_structs::*; +mod faction_structs; +pub use faction_structs::*; +mod spell_structs; +pub use spell_structs::Spell; +mod weapon_traits; +pub use weapon_traits::*; + +mod rawmaster; +pub use rawmaster::*; +use serde::{Deserialize}; +use std::sync::Mutex; + +rltk::embedded_resource!(RAW_FILE, "../../raws/spawns.json"); + +lazy_static! { + pub static ref RAWS : Mutex = Mutex::new(RawMaster::empty()); +} + +#[derive(Deserialize, Debug)] +pub struct Raws { + pub items : Vec, + pub mobs : Vec, + pub props : Vec, + pub spawn_table : Vec, + pub loot_tables : Vec, + pub faction_table : Vec, + pub spells : Vec, + pub weapon_traits : Vec +} + +pub fn load_raws() { + rltk::link_resource!(RAW_FILE, "../../raws/spawns.json"); + + // Retrieve the raw data as an array of u8 (8-bit unsigned chars) + let raw_data = rltk::embedding::EMBED + .lock() + .get_resource("../../raws/spawns.json".to_string()) + .unwrap(); + let raw_string = std::str::from_utf8(&raw_data).expect("Unable to convert to a valid UTF-8 string."); + let decoder : Raws = serde_json::from_str(&raw_string).expect("Unable to parse JSON"); + + RAWS.lock().unwrap().load(decoder); +} diff --git a/chapter-75-darkplaza/src/raws/prop_structs.rs b/chapter-75-darkplaza/src/raws/prop_structs.rs new file mode 100644 index 00000000..a62ce51f --- /dev/null +++ b/chapter-75-darkplaza/src/raws/prop_structs.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize}; +use super::{Renderable}; +use std::collections::HashMap; + +#[derive(Deserialize, Debug)] +pub struct Prop { + pub name : String, + pub renderable : Option, + pub hidden : Option, + pub blocks_tile : Option, + pub blocks_visibility : Option, + pub door_open : Option, + pub entry_trigger : Option, + pub light : Option, +} + +#[derive(Deserialize, Debug)] +pub struct EntryTrigger { + pub effects : HashMap +} diff --git a/chapter-75-darkplaza/src/raws/rawmaster.rs b/chapter-75-darkplaza/src/raws/rawmaster.rs new file mode 100644 index 00000000..a409cef9 --- /dev/null +++ b/chapter-75-darkplaza/src/raws/rawmaster.rs @@ -0,0 +1,870 @@ +use std::collections::{HashMap, HashSet}; +use specs::prelude::*; +use crate::components::*; +use super::{Raws, faction_structs::Reaction}; +use crate::random_table::{MasterTable, RandomTable}; +use crate::{attr_bonus, npc_hp, mana_at_level}; +use regex::Regex; +use specs::saveload::{MarkedBuilder, SimpleMarker}; + +pub fn parse_dice_string(dice : &str) -> (i32, i32, i32) { + lazy_static! { + static ref DICE_RE : Regex = Regex::new(r"(\d+)d(\d+)([\+\-]\d+)?").unwrap(); + } + let mut n_dice = 1; + let mut die_type = 4; + let mut die_bonus = 0; + for cap in DICE_RE.captures_iter(dice) { + if let Some(group) = cap.get(1) { + n_dice = group.as_str().parse::().expect("Not a digit"); + } + if let Some(group) = cap.get(2) { + die_type = group.as_str().parse::().expect("Not a digit"); + } + if let Some(group) = cap.get(3) { + die_bonus = group.as_str().parse::().expect("Not a digit"); + } + + } + (n_dice, die_type, die_bonus) +} + +#[derive(PartialEq, Eq, Hash, Copy, Clone)] +pub enum SpawnType { + AtPosition { x: i32, y: i32 }, + Equipped { by: Entity }, + Carried { by: Entity } +} + +pub struct RawMaster { + raws : Raws, + item_index : HashMap, + mob_index : HashMap, + prop_index : HashMap, + loot_index : HashMap, + faction_index : HashMap>, + spell_index : HashMap +} + +struct NewMagicItem { + name : String, + bonus : i32 +} + +impl RawMaster { + pub fn empty() -> RawMaster { + RawMaster { + raws : Raws{ + items: Vec::new(), + mobs: Vec::new(), + props: Vec::new(), + spawn_table: Vec::new(), + loot_tables: Vec::new(), + faction_table : Vec::new(), + spells : Vec::new(), + weapon_traits : Vec::new() + }, + item_index : HashMap::new(), + mob_index : HashMap::new(), + prop_index : HashMap::new(), + loot_index : HashMap::new(), + faction_index : HashMap::new(), + spell_index : HashMap::new() + } + } + + fn append_magic_template(items_to_build : &mut Vec, item : &super::Item) { + if let Some(template) = &item.template_magic { + if item.weapon.is_some() || item.wearable.is_some() { + if template.include_cursed { + items_to_build.push(NewMagicItem{ + name : item.name.clone(), + bonus : -1 + }); + } + for bonus in template.bonus_min ..= template.bonus_max { + items_to_build.push(NewMagicItem{ + name : item.name.clone(), + bonus + }); + } + } else { + rltk::console::log(format!("{} is marked as templated, but isn't a weapon or armor.", item.name)); + } + } + } + + fn build_base_magic_item(&self, nmw : &NewMagicItem) -> super::Item { + let base_item_index = self.item_index[&nmw.name]; + let mut base_item_copy = self.raws.items[base_item_index].clone(); + + if nmw.bonus == -1 { + base_item_copy.name = format!("{} -1", nmw.name); + } else { + base_item_copy.name = format!("{} +{}", nmw.name, nmw.bonus); + } + + base_item_copy.magic = Some(super::MagicItem{ + class : match nmw.bonus { + 2 => "rare".to_string(), + 3 => "rare".to_string(), + 4 => "rare".to_string(), + 5 => "legendary".to_string(), + _ => "common".to_string() + }, + naming : base_item_copy.template_magic.as_ref().unwrap().unidentified_name.clone(), + cursed: if nmw.bonus == -1 { Some(true) } else { None } + }); + + if let Some(initiative_penalty) = base_item_copy.initiative_penalty.as_mut() { + *initiative_penalty -= nmw.bonus as f32; + } + if let Some(base_value) = base_item_copy.base_value.as_mut() { + *base_value += (nmw.bonus as f32 + 1.0) * 50.0; + } + if let Some(mut weapon) = base_item_copy.weapon.as_mut() { + weapon.hit_bonus += nmw.bonus; + let (n,die,plus) = parse_dice_string(&weapon.base_damage); + let final_bonus = plus+nmw.bonus; + if final_bonus > 0 { + weapon.base_damage = format!("{}d{}+{}", n, die, final_bonus); + } else if final_bonus < 0 { + weapon.base_damage = format!("{}d{}-{}", n, die, i32::abs(final_bonus)); + } + } + if let Some(mut armor) = base_item_copy.wearable.as_mut() { + armor.armor_class += nmw.bonus as f32; + } + base_item_copy + } + + fn build_magic_weapon_or_armor(&mut self, items_to_build : &[NewMagicItem]) { + for nmw in items_to_build.iter() { + let base_item_copy = self.build_base_magic_item(&nmw); + + let real_name = base_item_copy.name.clone(); + self.raws.items.push(base_item_copy); + self.item_index.insert(real_name.clone(), self.raws.items.len()-1); + + self.raws.spawn_table.push(super::SpawnTableEntry{ + name : real_name.clone(), + weight : 10 - i32::abs(nmw.bonus), + min_depth : 1 + i32::abs((nmw.bonus-1)*3), + max_depth : 100, + add_map_depth_to_weight : None + }); + } + } + + fn build_traited_weapons(&mut self, items_to_build : &[NewMagicItem]) { + items_to_build.iter().filter(|i| i.bonus > 0).for_each(|nmw| { + for wt in self.raws.weapon_traits.iter() { + let mut base_item_copy = self.build_base_magic_item(&nmw); + if let Some(mut weapon) = base_item_copy.weapon.as_mut() { + base_item_copy.name = format!("{} {}", wt.name, base_item_copy.name); + if let Some(base_value) = base_item_copy.base_value.as_mut() { + *base_value *= 2.0; + } + weapon.proc_chance = Some(0.25); + weapon.proc_effects = Some(wt.effects.clone()); + + let real_name = base_item_copy.name.clone(); + self.raws.items.push(base_item_copy); + self.item_index.insert(real_name.clone(), self.raws.items.len()-1); + + self.raws.spawn_table.push(super::SpawnTableEntry{ + name : real_name.clone(), + weight : 9 - i32::abs(nmw.bonus), + min_depth : 2 + i32::abs((nmw.bonus-1)*3), + max_depth : 100, + add_map_depth_to_weight : None + }); + } + } + }); + } + + pub fn load(&mut self, raws : Raws) { + self.raws = raws; + self.item_index = HashMap::new(); + let mut used_names : HashSet = HashSet::new(); + let mut items_to_build = Vec::new(); + + for (i,item) in self.raws.items.iter().enumerate() { + if used_names.contains(&item.name) { + rltk::console::log(format!("WARNING - duplicate item name in raws [{}]", item.name)); + } + self.item_index.insert(item.name.clone(), i); + used_names.insert(item.name.clone()); + + RawMaster::append_magic_template(&mut items_to_build, item); + } + for (i,mob) in self.raws.mobs.iter().enumerate() { + if used_names.contains(&mob.name) { + rltk::console::log(format!("WARNING - duplicate mob name in raws [{}]", mob.name)); + } + self.mob_index.insert(mob.name.clone(), i); + used_names.insert(mob.name.clone()); + } + for (i,prop) in self.raws.props.iter().enumerate() { + if used_names.contains(&prop.name) { + rltk::console::log(format!("WARNING - duplicate prop name in raws [{}]", prop.name)); + } + self.prop_index.insert(prop.name.clone(), i); + used_names.insert(prop.name.clone()); + } + + for spawn in self.raws.spawn_table.iter() { + if !used_names.contains(&spawn.name) { + rltk::console::log(format!("WARNING - Spawn tables references unspecified entity {}", spawn.name)); + } + } + + for (i,loot) in self.raws.loot_tables.iter().enumerate() { + self.loot_index.insert(loot.name.clone(), i); + } + + for faction in self.raws.faction_table.iter() { + let mut reactions : HashMap = HashMap::new(); + for other in faction.responses.iter() { + reactions.insert( + other.0.clone(), + match other.1.as_str() { + "ignore" => Reaction::Ignore, + "flee" => Reaction::Flee, + _ => Reaction::Attack + } + ); + } + self.faction_index.insert(faction.name.clone(), reactions); + } + + for (i,spell) in self.raws.spells.iter().enumerate() { + self.spell_index.insert(spell.name.clone(), i); + } + + self.build_magic_weapon_or_armor(&items_to_build); + self.build_traited_weapons(&items_to_build); + } +} + +#[inline(always)] +pub fn faction_reaction(my_faction : &str, their_faction : &str, raws : &RawMaster) -> Reaction { + //println!("Looking for reaction to [{}] by [{}]", my_faction, their_faction); + if raws.faction_index.contains_key(my_faction) { + let mf = &raws.faction_index[my_faction]; + if mf.contains_key(their_faction) { + //println!(" : {:?}", mf[their_faction]); + return mf[their_faction]; + } else if mf.contains_key("Default") { + //println!(" : {:?}", mf["Default"]); + return mf["Default"]; + } else { + //println!(" : IGNORE"); + return Reaction::Ignore; + } + } + //println!(" : IGNORE"); + Reaction::Ignore +} + +fn find_slot_for_equippable_item(tag : &str, raws: &RawMaster) -> EquipmentSlot { + if !raws.item_index.contains_key(tag) { + panic!("Trying to equip an unknown item: {}", tag); + } + let item_index = raws.item_index[tag]; + let item = &raws.raws.items[item_index]; + if let Some(_wpn) = &item.weapon { + return EquipmentSlot::Melee; + } else if let Some(wearable) = &item.wearable { + return string_to_slot(&wearable.slot); + } + panic!("Trying to equip {}, but it has no slot tag.", tag); +} + +pub fn get_vendor_items(categories: &[String], raws : &RawMaster) -> Vec<(String, f32)> { + let mut result : Vec<(String, f32)> = Vec::new(); + + for item in raws.raws.items.iter() { + if let Some(cat) = &item.vendor_category { + if categories.contains(cat) && item.base_value.is_some() { + result.push(( + item.name.clone(), + item.base_value.unwrap() + )); + } + } + } + + result +} + +pub fn get_scroll_tags() -> Vec { + let raws = &super::RAWS.lock().unwrap(); + let mut result = Vec::new(); + + for item in raws.raws.items.iter() { + if let Some(magic) = &item.magic { + if &magic.naming == "scroll" { + result.push(item.name.clone()); + } + } + } + + result +} + +pub fn get_potion_tags() -> Vec { + let raws = &super::RAWS.lock().unwrap(); + let mut result = Vec::new(); + + for item in raws.raws.items.iter() { + if let Some(magic) = &item.magic { + if &magic.naming == "potion" { + result.push(item.name.clone()); + } + } + } + + result +} + +pub fn is_tag_magic(tag : &str) -> bool { + let raws = &super::RAWS.lock().unwrap(); + if raws.item_index.contains_key(tag) { + let item_template = &raws.raws.items[raws.item_index[tag]]; + item_template.magic.is_some() + } else { + false + } +} + +fn spawn_position<'a>(pos : SpawnType, new_entity : EntityBuilder<'a>, tag : &str, raws: &RawMaster) -> EntityBuilder<'a> { + let eb = new_entity; + + // Spawn in the specified location + match pos { + SpawnType::AtPosition{x,y} => eb.with(Position{ x, y }), + SpawnType::Carried{by} => eb.with(InBackpack{ owner: by }), + SpawnType::Equipped{by} => { + let slot = find_slot_for_equippable_item(tag, raws); + eb.with(Equipped{ owner: by, slot }) + } + } +} + +fn get_renderable_component(renderable : &super::item_structs::Renderable) -> crate::components::Renderable { + crate::components::Renderable{ + glyph: rltk::to_cp437(renderable.glyph.chars().next().unwrap()), + fg : rltk::RGB::from_hex(&renderable.fg).expect("Invalid RGB"), + bg : rltk::RGB::from_hex(&renderable.bg).expect("Invalid RGB"), + render_order : renderable.order + } +} + +pub fn string_to_slot(slot : &str) -> EquipmentSlot { + match slot { + "Shield" => EquipmentSlot::Shield, + "Head" => EquipmentSlot::Head, + "Torso" => EquipmentSlot::Torso, + "Legs" => EquipmentSlot::Legs, + "Feet" => EquipmentSlot::Feet, + "Hands" => EquipmentSlot::Hands, + "Melee" => EquipmentSlot::Melee, + _ => { rltk::console::log(format!("Warning: unknown equipment slot type [{}])", slot)); EquipmentSlot::Melee } + } +} + +fn parse_particle_line(n : &str) -> SpawnParticleLine { + let tokens : Vec<_> = n.split(';').collect(); + SpawnParticleLine{ + glyph : rltk::to_cp437(tokens[0].chars().next().unwrap()), + color : rltk::RGB::from_hex(tokens[1]).expect("Bad RGB"), + lifetime_ms : tokens[2].parse::().unwrap() + } +} + +fn parse_particle(n : &str) -> SpawnParticleBurst { + let tokens : Vec<_> = n.split(';').collect(); + SpawnParticleBurst{ + glyph : rltk::to_cp437(tokens[0].chars().next().unwrap()), + color : rltk::RGB::from_hex(tokens[1]).expect("Bad RGB"), + lifetime_ms : tokens[2].parse::().unwrap() + } +} + +macro_rules! apply_effects { + ( $effects:expr, $eb:expr ) => { + for effect in $effects.iter() { + let effect_name = effect.0.as_str(); + match effect_name { + "provides_healing" => $eb = $eb.with(ProvidesHealing{ heal_amount: effect.1.parse::().unwrap() }), + "provides_mana" => $eb = $eb.with(ProvidesMana{ mana_amount: effect.1.parse::().unwrap() }), + "teach_spell" => $eb = $eb.with(TeachesSpell{ spell: effect.1.to_string() }), + "ranged" => $eb = $eb.with(Ranged{ range: effect.1.parse::().unwrap() }), + "damage" => $eb = $eb.with(InflictsDamage{ damage : effect.1.parse::().unwrap() }), + "area_of_effect" => $eb = $eb.with(AreaOfEffect{ radius: effect.1.parse::().unwrap() }), + "confusion" => { + $eb = $eb.with(Confusion{}); + $eb = $eb.with(Duration{ turns: effect.1.parse::().unwrap() }); + } + "magic_mapping" => $eb = $eb.with(MagicMapper{}), + "town_portal" => $eb = $eb.with(TownPortal{}), + "food" => $eb = $eb.with(ProvidesFood{}), + "single_activation" => $eb = $eb.with(SingleActivation{}), + "particle_line" => $eb = $eb.with(parse_particle_line(&effect.1)), + "particle" => $eb = $eb.with(parse_particle(&effect.1)), + "remove_curse" => $eb = $eb.with(ProvidesRemoveCurse{}), + "identify" => $eb = $eb.with(ProvidesIdentification{}), + "slow" => $eb = $eb.with(Slow{ initiative_penalty : effect.1.parse::().unwrap() }), + "damage_over_time" => $eb = $eb.with( DamageOverTime { damage : effect.1.parse::().unwrap() } ), + "target_self" => $eb = $eb.with( AlwaysTargetsSelf{} ), + _ => rltk::console::log(format!("Warning: consumable effect {} not implemented.", effect_name)) + } + } + }; +} + +pub fn spawn_named_item(raws: &RawMaster, ecs : &mut World, key : &str, pos : SpawnType) -> Option { + if raws.item_index.contains_key(key) { + let item_template = &raws.raws.items[raws.item_index[key]]; + + let dm = ecs.fetch::(); + let scroll_names = dm.scroll_mappings.clone(); + let potion_names = dm.potion_mappings.clone(); + let identified = dm.identified_items.clone(); + std::mem::drop(dm); + let mut eb = ecs.create_entity().marked::>(); + + // Spawn in the specified location + eb = spawn_position(pos, eb, key, raws); + + // Renderable + if let Some(renderable) = &item_template.renderable { + eb = eb.with(get_renderable_component(renderable)); + if renderable.x_size.is_some() || renderable.y_size.is_some() { + eb = eb.with(TileSize{ x : renderable.x_size.unwrap_or(1), y : renderable.y_size.unwrap_or(1) }); + } + } + + eb = eb.with(Name{ name : item_template.name.clone() }); + + eb = eb.with(crate::components::Item{ + initiative_penalty : item_template.initiative_penalty.unwrap_or(0.0), + weight_lbs : item_template.weight_lbs.unwrap_or(0.0), + base_value : item_template.base_value.unwrap_or(0.0) + }); + + if let Some(consumable) = &item_template.consumable { + let max_charges = consumable.charges.unwrap_or(1); + eb = eb.with(crate::components::Consumable{ max_charges, charges : max_charges }); + apply_effects!(consumable.effects, eb); + } + + if let Some(weapon) = &item_template.weapon { + eb = eb.with(Equippable{ slot: EquipmentSlot::Melee }); + let (n_dice, die_type, bonus) = parse_dice_string(&weapon.base_damage); + let mut wpn = Weapon{ + range : if weapon.range == "melee" { None } else { Some(weapon.range.parse::().expect("Not a number")) }, + attribute : WeaponAttribute::Might, + damage_n_dice : n_dice, + damage_die_type : die_type, + damage_bonus : bonus, + hit_bonus : weapon.hit_bonus, + proc_chance : weapon.proc_chance, + proc_target : weapon.proc_target.clone() + }; + match weapon.attribute.as_str() { + "Quickness" => wpn.attribute = WeaponAttribute::Quickness, + _ => wpn.attribute = WeaponAttribute::Might + } + eb = eb.with(wpn); + if let Some(proc_effects) =& weapon.proc_effects { + apply_effects!(proc_effects, eb); + } + } + + if let Some(wearable) = &item_template.wearable { + let slot = string_to_slot(&wearable.slot); + eb = eb.with(Equippable{ slot }); + eb = eb.with(Wearable{ slot, armor_class: wearable.armor_class }); + } + + if let Some(magic) = &item_template.magic { + let class = match magic.class.as_str() { + "rare" => MagicItemClass::Rare, + "legendary" => MagicItemClass::Legendary, + _ => MagicItemClass::Common + }; + eb = eb.with(MagicItem{ class }); + + if !identified.contains(&item_template.name) { + match magic.naming.as_str() { + "scroll" => { + eb = eb.with(ObfuscatedName{ name : scroll_names[&item_template.name].clone() }); + } + "potion" => { + eb = eb.with(ObfuscatedName{ name: potion_names[&item_template.name].clone() }); + } + _ => { + eb = eb.with(ObfuscatedName{ name : magic.naming.clone() }); + } + } + } + + if let Some(cursed) = magic.cursed { + if cursed { eb = eb.with(CursedItem{}); } + } + } + + if let Some(ab) = &item_template.attributes { + eb = eb.with(AttributeBonus{ + might : ab.might, + fitness : ab.fitness, + quickness : ab.quickness, + intelligence : ab.intelligence, + }); + } + + return Some(eb.build()); + } + None +} + +#[allow(clippy::cognitive_complexity)] +pub fn spawn_named_mob(raws: &RawMaster, ecs : &mut World, key : &str, pos : SpawnType) -> Option { + if raws.mob_index.contains_key(key) { + let mob_template = &raws.raws.mobs[raws.mob_index[key]]; + + let mut eb = ecs.create_entity().marked::>(); + + // Spawn in the specified location + eb = spawn_position(pos, eb, key, raws); + + // Initiative of 2 + eb = eb.with(Initiative{current: 2}); + + // Renderable + if let Some(renderable) = &mob_template.renderable { + eb = eb.with(get_renderable_component(renderable)); + if renderable.x_size.is_some() || renderable.y_size.is_some() { + eb = eb.with(TileSize{ x : renderable.x_size.unwrap_or(1), y : renderable.y_size.unwrap_or(1) }); + } + } + + eb = eb.with(Name{ name : mob_template.name.clone() }); + + match mob_template.movement.as_ref() { + "random" => eb = eb.with(MoveMode{ mode: Movement::Random }), + "random_waypoint" => eb = eb.with(MoveMode{ mode: Movement::RandomWaypoint{ path: None } }), + _ => eb = eb.with(MoveMode{ mode: Movement::Static }) + } + + if let Some(quips) = &mob_template.quips { + eb = eb.with(Quips{ + available: quips.clone() + }); + } + + if mob_template.blocks_tile { + eb = eb.with(BlocksTile{}); + } + + let mut mob_fitness = 11; + let mut mob_int = 11; + let mut attr = Attributes{ + might: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, + fitness: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, + quickness: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, + intelligence: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, + }; + if let Some(might) = mob_template.attributes.might { + attr.might = Attribute{ base: might, modifiers: 0, bonus: attr_bonus(might) }; + } + if let Some(fitness) = mob_template.attributes.fitness { + attr.fitness = Attribute{ base: fitness, modifiers: 0, bonus: attr_bonus(fitness) }; + mob_fitness = fitness; + } + if let Some(quickness) = mob_template.attributes.quickness { + attr.quickness = Attribute{ base: quickness, modifiers: 0, bonus: attr_bonus(quickness) }; + } + if let Some(intelligence) = mob_template.attributes.intelligence { + attr.intelligence = Attribute{ base: intelligence, modifiers: 0, bonus: attr_bonus(intelligence) }; + mob_int = intelligence; + } + eb = eb.with(attr); + + let mob_level = if mob_template.level.is_some() { mob_template.level.unwrap() } else { 1 }; + let mob_hp = npc_hp(mob_fitness, mob_level); + let mob_mana = mana_at_level(mob_int, mob_level); + + let pools = Pools{ + level: mob_level, + xp: 0, + hit_points : Pool{ current: mob_hp, max: mob_hp }, + mana: Pool{current: mob_mana, max: mob_mana}, + total_weight : 0.0, + total_initiative_penalty : 0.0, + gold : if let Some(gold) = &mob_template.gold { + let (n, d, b) = parse_dice_string(&gold); + (crate::rng::roll_dice(n, d) + b) as f32 + } else { + 0.0 + }, + god_mode : false + }; + eb = eb.with(pools); + eb = eb.with(EquipmentChanged{}); + + let mut skills = Skills{ skills: HashMap::new() }; + skills.skills.insert(Skill::Melee, 1); + skills.skills.insert(Skill::Defense, 1); + skills.skills.insert(Skill::Magic, 1); + if let Some(mobskills) = &mob_template.skills { + for sk in mobskills.iter() { + match sk.0.as_str() { + "Melee" => { skills.skills.insert(Skill::Melee, *sk.1); } + "Defense" => { skills.skills.insert(Skill::Defense, *sk.1); } + "Magic" => { skills.skills.insert(Skill::Magic, *sk.1); } + _ => { rltk::console::log(format!("Unknown skill referenced: [{}]", sk.0)); } + } + } + } + eb = eb.with(skills); + + eb = eb.with(Viewshed{ visible_tiles : Vec::new(), range: mob_template.vision_range, dirty: true }); + + if let Some(na) = &mob_template.natural { + let mut nature = NaturalAttackDefense{ + armor_class : na.armor_class, + attacks: Vec::new() + }; + if let Some(attacks) = &na.attacks { + for nattack in attacks.iter() { + let (n, d, b) = parse_dice_string(&nattack.damage); + let attack = NaturalAttack{ + name : nattack.name.clone(), + hit_bonus : nattack.hit_bonus, + damage_n_dice : n, + damage_die_type : d, + damage_bonus: b + }; + nature.attacks.push(attack); + } + } + eb = eb.with(nature); + } + + if let Some(loot) = &mob_template.loot_table { + eb = eb.with(LootTable{table: loot.clone()}); + } + + if let Some(light) = &mob_template.light { + eb = eb.with(LightSource{ range: light.range, color : rltk::RGB::from_hex(&light.color).expect("Bad color") }); + } + + if let Some(faction) = &mob_template.faction { + eb = eb.with(Faction{ name: faction.clone() }); + } else { + eb = eb.with(Faction{ name : "Mindless".to_string() }) + } + + if let Some(vendor) = &mob_template.vendor { + eb = eb.with(Vendor{ categories : vendor.clone() }); + } + + if let Some(ability_list) = &mob_template.abilities { + let mut a = SpecialAbilities { abilities : Vec::new() }; + for ability in ability_list.iter() { + a.abilities.push( + SpecialAbility{ + chance : ability.chance, + spell : ability.spell.clone(), + range : ability.range, + min_range : ability.min_range + } + ); + } + eb = eb.with(a); + } + + if let Some(ability_list) = &mob_template.on_death { + let mut a = OnDeath{ abilities : Vec::new() }; + for ability in ability_list.iter() { + a.abilities.push( + SpecialAbility{ + chance : ability.chance, + spell : ability.spell.clone(), + range : ability.range, + min_range : ability.min_range + } + ); + } + eb = eb.with(a); + } + + let new_mob = eb.build(); + + // Are they wielding anyting? + if let Some(wielding) = &mob_template.equipped { + for tag in wielding.iter() { + spawn_named_entity(raws, ecs, tag, SpawnType::Equipped{ by: new_mob }); + } + } + + return Some(new_mob); + } + None +} + +pub fn spawn_named_prop(raws: &RawMaster, ecs : &mut World, key : &str, pos : SpawnType) -> Option { + if raws.prop_index.contains_key(key) { + let prop_template = &raws.raws.props[raws.prop_index[key]]; + + let mut eb = ecs.create_entity().marked::>(); + + // Spawn in the specified location + eb = spawn_position(pos, eb, key, raws); + + // Renderable + if let Some(renderable) = &prop_template.renderable { + eb = eb.with(get_renderable_component(renderable)); + if renderable.x_size.is_some() || renderable.y_size.is_some() { + eb = eb.with(TileSize{ x : renderable.x_size.unwrap_or(1), y : renderable.y_size.unwrap_or(1) }); + } + } + + eb = eb.with(Name{ name : prop_template.name.clone() }); + + if let Some(hidden) = prop_template.hidden { + if hidden { eb = eb.with(Hidden{}) }; + } + if let Some(blocks_tile) = prop_template.blocks_tile { + if blocks_tile { eb = eb.with(BlocksTile{}) }; + } + if let Some(blocks_visibility) = prop_template.blocks_visibility { + if blocks_visibility { eb = eb.with(BlocksVisibility{}) }; + } + if let Some(door_open) = prop_template.door_open { + eb = eb.with(Door{ open: door_open }); + } + if let Some(entry_trigger) = &prop_template.entry_trigger { + eb = eb.with(EntryTrigger{}); + apply_effects!(entry_trigger.effects, eb); + } + if let Some(light) = &prop_template.light { + eb = eb.with(LightSource{ range: light.range, color : rltk::RGB::from_hex(&light.color).expect("Bad color") }); + eb = eb.with(Viewshed{ range: light.range, dirty: true, visible_tiles: Vec::new() }); + } + + + return Some(eb.build()); + } + None +} + +pub fn spawn_named_spell(raws: &RawMaster, ecs : &mut World, key : &str) -> Option { + if raws.spell_index.contains_key(key) { + let spell_template = &raws.raws.spells[raws.spell_index[key]]; + + let mut eb = ecs.create_entity().marked::>(); + eb = eb.with(SpellTemplate{ mana_cost : spell_template.mana_cost }); + eb = eb.with(Name{ name : spell_template.name.clone() }); + apply_effects!(spell_template.effects, eb); + + return Some(eb.build()); + } + None +} + +pub fn spawn_all_spells(ecs : &mut World) { + let raws = &super::RAWS.lock().unwrap(); + for spell in raws.raws.spells.iter() { + spawn_named_spell(raws, ecs, &spell.name); + } +} + +pub fn find_spell_entity(ecs : &World, name : &str) -> Option { + let names = ecs.read_storage::(); + let spell_templates = ecs.read_storage::(); + let entities = ecs.entities(); + + for (entity, sname, _template) in (&entities, &names, &spell_templates).join() { + if name == sname.name { + return Some(entity); + } + } + None +} + +pub fn find_spell_entity_by_name( + name : &str, + names : &ReadStorage::, + spell_templates : &ReadStorage::, + entities : &Entities) -> Option +{ + for (entity, sname, _template) in (entities, names, spell_templates).join() { + if name == sname.name { + return Some(entity); + } + } + None +} + +pub fn spawn_named_entity(raws: &RawMaster, ecs : &mut World, key : &str, pos : SpawnType) -> Option { + if raws.item_index.contains_key(key) { + return spawn_named_item(raws, ecs, key, pos); + } else if raws.mob_index.contains_key(key) { + return spawn_named_mob(raws, ecs, key, pos); + } else if raws.prop_index.contains_key(key) { + return spawn_named_prop(raws, ecs, key, pos); + } + + None +} + +pub enum SpawnTableType { Item, Mob, Prop } + +pub fn spawn_type_by_name(raws: &RawMaster, key : &str) -> SpawnTableType { + if raws.item_index.contains_key(key) { + SpawnTableType::Item + } else if raws.mob_index.contains_key(key) { + SpawnTableType::Mob + } else { + SpawnTableType::Prop + } +} + +pub fn get_spawn_table_for_depth(raws: &RawMaster, depth: i32) -> MasterTable { + use super::SpawnTableEntry; + + let available_options : Vec<&SpawnTableEntry> = raws.raws.spawn_table + .iter() + .filter(|a| depth >= a.min_depth && depth <= a.max_depth) + .collect(); + + let mut rt = MasterTable::new(); + for e in available_options.iter() { + let mut weight = e.weight; + if e.add_map_depth_to_weight.is_some() { + weight += depth; + } + rt.add(e.name.clone(), weight, raws); + } + + rt +} + +pub fn get_item_drop(raws: &RawMaster, table: &str) -> Option { + if raws.loot_index.contains_key(table) { + let mut rt = RandomTable::new(); + let available_options = &raws.raws.loot_tables[raws.loot_index[table]]; + for item in available_options.drops.iter() { + rt.add(item.name.clone(), item.weight); + } + let result =rt.roll(); + return Some(result); + } + + None +} diff --git a/chapter-75-darkplaza/src/raws/spawn_table_structs.rs b/chapter-75-darkplaza/src/raws/spawn_table_structs.rs new file mode 100644 index 00000000..fae83096 --- /dev/null +++ b/chapter-75-darkplaza/src/raws/spawn_table_structs.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize}; + +#[derive(Deserialize, Debug)] +pub struct SpawnTableEntry { + pub name : String, + pub weight : i32, + pub min_depth: i32, + pub max_depth: i32, + pub add_map_depth_to_weight : Option +} diff --git a/chapter-75-darkplaza/src/raws/spell_structs.rs b/chapter-75-darkplaza/src/raws/spell_structs.rs new file mode 100644 index 00000000..f57e2221 --- /dev/null +++ b/chapter-75-darkplaza/src/raws/spell_structs.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize}; +use std::collections::HashMap; + +#[derive(Deserialize, Debug)] +pub struct Spell { + pub name : String, + pub mana_cost : i32, + pub effects : HashMap +} diff --git a/chapter-75-darkplaza/src/raws/weapon_traits.rs b/chapter-75-darkplaza/src/raws/weapon_traits.rs new file mode 100644 index 00000000..33dd9123 --- /dev/null +++ b/chapter-75-darkplaza/src/raws/weapon_traits.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize}; +use std::collections::HashMap; + +#[derive(Deserialize, Debug)] +pub struct WeaponTrait { + pub name : String, + pub effects : HashMap +} diff --git a/chapter-75-darkplaza/src/rect.rs b/chapter-75-darkplaza/src/rect.rs new file mode 100644 index 00000000..f415726c --- /dev/null +++ b/chapter-75-darkplaza/src/rect.rs @@ -0,0 +1,35 @@ +use serde::{Serialize, Deserialize}; +use std::collections::HashSet; + +#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] +pub struct Rect { + pub x1 : i32, + pub x2 : i32, + pub y1 : i32, + pub y2 : i32 +} + +impl Rect { + pub fn new(x:i32, y: i32, w:i32, h:i32) -> Rect { + Rect{x1:x, y1:y, x2:x+w, y2:y+h} + } + + // Returns true if this overlaps with other + pub fn intersect(&self, other:&Rect) -> bool { + self.x1 <= other.x2 && self.x2 >= other.x1 && self.y1 <= other.y2 && self.y2 >= other.y1 + } + + pub fn center(&self) -> (i32, i32) { + ((self.x1 + self.x2)/2, (self.y1 + self.y2)/2) + } + + pub fn get_all_tiles(&self) -> HashSet<(i32,i32)> { + let mut result = HashSet::new(); + for y in self.y1 .. self.y2 { + for x in self.x1 .. self.x2 { + result.insert((x,y)); + } + } + result + } +} diff --git a/chapter-75-darkplaza/src/rex_assets.rs b/chapter-75-darkplaza/src/rex_assets.rs new file mode 100644 index 00000000..70c6ae5f --- /dev/null +++ b/chapter-75-darkplaza/src/rex_assets.rs @@ -0,0 +1,22 @@ +use rltk::{rex::XpFile}; + +rltk::embedded_resource!(SMALL_DUNGEON, "../../resources/SmallDungeon_80x50.xp"); +rltk::embedded_resource!(WFC_DEMO_IMAGE1, "../../resources/wfc-demo1.xp"); +rltk::embedded_resource!(WFC_POPULATED, "../../resources/wfc-populated.xp"); + +pub struct RexAssets { + pub menu : XpFile +} + +impl RexAssets { + #[allow(clippy::new_without_default)] + pub fn new() -> RexAssets { + rltk::link_resource!(SMALL_DUNGEON, "../../resources/SmallDungeon_80x50.xp"); + rltk::link_resource!(WFC_DEMO_IMAGE1, "../../resources/wfc-demo1.xp"); + rltk::link_resource!(WFC_POPULATED, "../../resources/wfc-populated.xp"); + + RexAssets{ + menu : XpFile::from_resource("../../resources/SmallDungeon_80x50.xp").unwrap() + } + } +} diff --git a/chapter-75-darkplaza/src/rng.rs b/chapter-75-darkplaza/src/rng.rs new file mode 100644 index 00000000..4280d9d6 --- /dev/null +++ b/chapter-75-darkplaza/src/rng.rs @@ -0,0 +1,20 @@ +use std::sync::Mutex; +use rltk::prelude::*; + +lazy_static! { + static ref RNG: Mutex = + Mutex::new(RandomNumberGenerator::new()); +} + +pub fn reseed(seed: u64) { + *RNG.lock().unwrap() = RandomNumberGenerator::seeded(seed); +} + +pub fn roll_dice(n:i32, die_type: i32) -> i32 { + RNG.lock().unwrap().roll_dice(n, die_type) +} + +pub fn range(min: i32, max: i32) -> i32 +{ + RNG.lock().unwrap().range(min, max) +} diff --git a/chapter-75-darkplaza/src/saveload_system.rs b/chapter-75-darkplaza/src/saveload_system.rs new file mode 100644 index 00000000..846aa367 --- /dev/null +++ b/chapter-75-darkplaza/src/saveload_system.rs @@ -0,0 +1,163 @@ +use specs::prelude::*; +use specs::saveload::{SimpleMarker, SimpleMarkerAllocator, SerializeComponents, DeserializeComponents, MarkedBuilder}; +use specs::error::NoError; +use super::components::*; +use std::fs::File; +use std::path::Path; +use std::fs; + +macro_rules! serialize_individually { + ($ecs:expr, $ser:expr, $data:expr, $( $type:ty),*) => { + $( + SerializeComponents::>::serialize( + &( $ecs.read_storage::<$type>(), ), + &$data.0, + &$data.1, + &mut $ser, + ) + .unwrap(); + )* + }; +} + +#[cfg(target_arch = "wasm32")] +pub fn save_game(_ecs : &mut World) { +} + +#[cfg(not(target_arch = "wasm32"))] +pub fn save_game(ecs : &mut World) { + // Create helper + let mapcopy = ecs.get_mut::().unwrap().clone(); + let dungeon_master = ecs.get_mut::().unwrap().clone(); + let savehelper = ecs + .create_entity() + .with(SerializationHelper{ map : mapcopy }) + .marked::>() + .build(); + let savehelper2 = ecs + .create_entity() + .with(DMSerializationHelper{ + map : dungeon_master, + log: crate::gamelog::clone_log(), + events : crate::gamelog::clone_events() + }) + .marked::>() + .build(); + + // Actually serialize + { + let data = ( ecs.entities(), ecs.read_storage::>() ); + + let writer = File::create("./savegame.json").unwrap(); + let mut serializer = serde_json::Serializer::new(writer); + serialize_individually!(ecs, serializer, data, Position, Renderable, Player, Viewshed, + Name, BlocksTile, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, + AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem, + WantsToDropItem, SerializationHelper, Equippable, Equipped, Weapon, Wearable, + WantsToRemoveItem, ParticleLifetime, HungerClock, ProvidesFood, MagicMapper, Hidden, + EntryTrigger, EntityMoved, SingleActivation, BlocksVisibility, Door, + Quips, Attributes, Skills, Pools, NaturalAttackDefense, LootTable, + OtherLevelPosition, DMSerializationHelper, LightSource, Initiative, MyTurn, Faction, + WantsToApproach, WantsToFlee, MoveMode, Chasing, EquipmentChanged, Vendor, TownPortal, + TeleportTo, ApplyMove, ApplyTeleport, MagicItem, ObfuscatedName, IdentifiedItem, + SpawnParticleBurst, SpawnParticleLine, CursedItem, ProvidesRemoveCurse, ProvidesIdentification, + AttributeBonus, StatusEffect, Duration, KnownSpells, SpellTemplate, WantsToCastSpell, TeachesSpell, + ProvidesMana, Slow, DamageOverTime, SpecialAbilities, TileSize, OnDeath, AlwaysTargetsSelf, + Target, WantsToShoot + ); + } + + // Clean up + ecs.delete_entity(savehelper).expect("Crash on cleanup"); + ecs.delete_entity(savehelper2).expect("Crash on cleanup"); +} + +pub fn does_save_exist() -> bool { + Path::new("./savegame.json").exists() +} + +macro_rules! deserialize_individually { + ($ecs:expr, $de:expr, $data:expr, $( $type:ty),*) => { + $( + DeserializeComponents::::deserialize( + &mut ( &mut $ecs.write_storage::<$type>(), ), + &$data.0, // entities + &mut $data.1, // marker + &mut $data.2, // allocater + &mut $de, + ) + .unwrap(); + )* + }; +} + +pub fn load_game(ecs: &mut World) { + { + // Delete everything + let mut to_delete = Vec::new(); + for e in ecs.entities().join() { + to_delete.push(e); + } + for del in to_delete.iter() { + ecs.delete_entity(*del).expect("Deletion failed"); + } + } + + let data = fs::read_to_string("./savegame.json").unwrap(); + let mut de = serde_json::Deserializer::from_str(&data); + + { + let mut d = (&mut ecs.entities(), &mut ecs.write_storage::>(), &mut ecs.write_resource::>()); + + deserialize_individually!(ecs, de, d, Position, Renderable, Player, Viewshed, + Name, BlocksTile, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, + AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem, + WantsToDropItem, SerializationHelper, Equippable, Equipped, Weapon, Wearable, + WantsToRemoveItem, ParticleLifetime, HungerClock, ProvidesFood, MagicMapper, Hidden, + EntryTrigger, EntityMoved, SingleActivation, BlocksVisibility, Door, + Quips, Attributes, Skills, Pools, NaturalAttackDefense, LootTable, + OtherLevelPosition, DMSerializationHelper, LightSource, Initiative, MyTurn, Faction, + WantsToApproach, WantsToFlee, MoveMode, Chasing, EquipmentChanged, Vendor, TownPortal, + TeleportTo, ApplyMove, ApplyTeleport, MagicItem, ObfuscatedName, IdentifiedItem, + SpawnParticleBurst, SpawnParticleLine, CursedItem, ProvidesRemoveCurse, ProvidesIdentification, + AttributeBonus, StatusEffect, Duration, KnownSpells, SpellTemplate, WantsToCastSpell, TeachesSpell, + ProvidesMana, Slow, DamageOverTime, SpecialAbilities, TileSize, OnDeath, AlwaysTargetsSelf, + Target, WantsToShoot + ); + } + + let mut deleteme : Option = None; + let mut deleteme2 : Option = None; + { + let entities = ecs.entities(); + let helper = ecs.read_storage::(); + let helper2 = ecs.read_storage::(); + let player = ecs.read_storage::(); + let position = ecs.read_storage::(); + for (e,h) in (&entities, &helper).join() { + let mut worldmap = ecs.write_resource::(); + *worldmap = h.map.clone(); + crate::spatial::set_size((worldmap.height * worldmap.width) as usize); + deleteme = Some(e); + } + for (e,h) in (&entities, &helper2).join() { + let mut dungeonmaster = ecs.write_resource::(); + *dungeonmaster = h.map.clone(); + deleteme2 = Some(e); + crate::gamelog::restore_log(&mut h.log.clone()); + crate::gamelog::load_events(h.events.clone()); + } + for (e,_p,pos) in (&entities, &player, &position).join() { + let mut ppos = ecs.write_resource::(); + *ppos = rltk::Point::new(pos.x, pos.y); + let mut player_resource = ecs.write_resource::(); + *player_resource = e; + } + } + ecs.delete_entity(deleteme.unwrap()).expect("Unable to delete helper"); + ecs.delete_entity(deleteme2.unwrap()).expect("Unable to delete helper"); +} + +pub fn delete_save() { + if Path::new("./savegame.json").exists() { std::fs::remove_file("./savegame.json").expect("Unable to delete file"); } +} diff --git a/chapter-75-darkplaza/src/spatial/mod.rs b/chapter-75-darkplaza/src/spatial/mod.rs new file mode 100644 index 00000000..10ff88bf --- /dev/null +++ b/chapter-75-darkplaza/src/spatial/mod.rs @@ -0,0 +1,117 @@ +use std::sync::Mutex; +use specs::prelude::*; +use crate::{ Map, tile_walkable, RunState }; + +struct SpatialMap { + blocked : Vec<(bool, bool)>, + tile_content : Vec> +} + +impl SpatialMap { + fn new() -> Self { + Self { + blocked: Vec::new(), + tile_content: Vec::new() + } + } +} + +lazy_static! { + static ref SPATIAL_MAP : Mutex = Mutex::new(SpatialMap::new()); +} + +pub fn set_size(map_tile_count: usize) { + let mut lock = SPATIAL_MAP.lock().unwrap(); + lock.blocked = vec![(false, false); map_tile_count]; + lock.tile_content = vec![Vec::new(); map_tile_count]; +} + +pub fn clear() { + let mut lock = SPATIAL_MAP.lock().unwrap(); + lock.blocked.iter_mut().for_each(|b| { b.0 = false; b.1 = false; }); + for content in lock.tile_content.iter_mut() { + content.clear(); + } +} + +pub fn populate_blocked_from_map(map: &Map) { + let mut lock = SPATIAL_MAP.lock().unwrap(); + for (i,tile) in map.tiles.iter().enumerate() { + lock.blocked[i].0 = !tile_walkable(*tile); + } +} + +pub fn index_entity(entity: Entity, idx: usize, blocks_tile: bool) { + let mut lock = SPATIAL_MAP.lock().unwrap(); + lock.tile_content[idx].push((entity, blocks_tile)); + if blocks_tile { + lock.blocked[idx].1 = true; + } +} + +pub fn is_blocked(idx: usize) -> bool { + let lock = SPATIAL_MAP.lock().unwrap(); + lock.blocked[idx].0 || lock.blocked[idx].1 +} + +pub fn set_blocked(idx: usize, blocked: bool) { + let mut lock = SPATIAL_MAP.lock().unwrap(); + lock.blocked[idx] = (lock.blocked[idx].0, blocked); +} + +pub fn for_each_tile_content(idx: usize, mut f: F) +where F : FnMut(Entity) +{ + let lock = SPATIAL_MAP.lock().unwrap(); + for entity in lock.tile_content[idx].iter() { + f(entity.0); + } +} + +pub fn for_each_tile_content_with_gamemode(idx: usize, mut f: F) -> RunState +where F : FnMut(Entity)->Option +{ + let lock = SPATIAL_MAP.lock().unwrap(); + for entity in lock.tile_content[idx].iter() { + if let Some(rs) = f(entity.0) { + return rs; + } + } + + RunState::AwaitingInput +} + +pub fn get_tile_content_clone(idx:usize) -> Vec { + let lock = SPATIAL_MAP.lock().unwrap(); + lock.tile_content[idx].iter().map(|(e,_)| *e).collect() +} + +pub fn move_entity(entity: Entity, moving_from: usize, moving_to: usize) { + let mut lock = SPATIAL_MAP.lock().unwrap(); + let mut entity_blocks = false; + lock.tile_content[moving_from].retain(|(e, blocks) | { + if *e == entity { + entity_blocks = *blocks; + false + } else { + true + } + }); + lock.tile_content[moving_to].push((entity, entity_blocks)); + + // Recalculate blocks for both tiles + let mut from_blocked = false; + let mut to_blocked = false; + lock.tile_content[moving_from].iter().for_each(|(_,blocks)| if *blocks { from_blocked = true; } ); + lock.tile_content[moving_to].iter().for_each(|(_,blocks)| if *blocks { to_blocked = true; } ); + lock.blocked[moving_from].1 = from_blocked; + lock.blocked[moving_to].1 = to_blocked; +} + +pub fn remove_entity(entity: Entity, idx: usize) { + let mut lock = SPATIAL_MAP.lock().unwrap(); + lock.tile_content[idx].retain(|(e, _)| *e != entity ); + let mut from_blocked = false; + lock.tile_content[idx].iter().for_each(|(_,blocks)| if *blocks { from_blocked = true; } ); + lock.blocked[idx].1 = from_blocked; +} \ No newline at end of file diff --git a/chapter-75-darkplaza/src/spawner.rs b/chapter-75-darkplaza/src/spawner.rs new file mode 100644 index 00000000..2a594fef --- /dev/null +++ b/chapter-75-darkplaza/src/spawner.rs @@ -0,0 +1,196 @@ +use rltk::{ RGB }; +use specs::prelude::*; +use super::{Pools, Pool, Player, Renderable, Name, Position, Viewshed, Rect, + SerializeMe, random_table::MasterTable, HungerClock, HungerState, Map, TileType, raws::*, + Attribute, Attributes, Skills, Skill, LightSource, Initiative, Faction, EquipmentChanged, + OtherLevelPosition, MasterDungeonMap, EntryTrigger, TeleportTo, SingleActivation, + StatusEffect, Duration, AttributeBonus, KnownSpells }; +use specs::saveload::{MarkedBuilder, SimpleMarker}; +use std::collections::HashMap; +use crate::{attr_bonus, player_hp_at_level, mana_at_level}; + +/// Spawns the player and returns his/her entity object. +pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity { + spawn_all_spells(ecs); + + let mut skills = Skills{ skills: HashMap::new() }; + skills.skills.insert(Skill::Melee, 1); + skills.skills.insert(Skill::Defense, 1); + skills.skills.insert(Skill::Magic, 1); + + let player = ecs + .create_entity() + .with(Position { x: player_x, y: player_y }) + .with(Renderable { + glyph: rltk::to_cp437('@'), + fg: RGB::named(rltk::YELLOW), + bg: RGB::named(rltk::BLACK), + render_order: 0 + }) + .with(Player{}) + .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true }) + .with(Name{name: "Player".to_string() }) + .with(HungerClock{ state: HungerState::WellFed, duration: 20 }) + .with(Attributes{ + might: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, + fitness: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, + quickness: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, + intelligence: Attribute{ base: 11, modifiers: 0, bonus: attr_bonus(11) }, + }) + .with(skills) + .with(Pools{ + hit_points : Pool{ + current: player_hp_at_level(11, 1), + max: player_hp_at_level(11, 1) + }, + mana: Pool{ + current: mana_at_level(11, 1), + max: mana_at_level(11, 1) + }, + xp: 0, + level: 1, + total_weight : 0.0, + total_initiative_penalty : 0.0, + gold : 0.0, + god_mode : false + }) + .with(EquipmentChanged{}) + .with(LightSource{ color: rltk::RGB::from_f32(1.0, 1.0, 0.5), range: 8 }) + .with(Initiative{current: 0}) + .with(Faction{name : "Player".to_string() }) + .with(KnownSpells{ spells : Vec::new() }) + .marked::>() + .build(); + + // Starting equipment + spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Rusty Longsword", SpawnType::Equipped{by : player}); + spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Dried Sausage", SpawnType::Carried{by : player} ); + spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Beer", SpawnType::Carried{by : player}); + spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Stained Tunic", SpawnType::Equipped{by : player}); + spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Torn Trousers", SpawnType::Equipped{by : player}); + spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Old Boots", SpawnType::Equipped{by : player}); + spawn_named_entity(&RAWS.lock().unwrap(), ecs, "Shortbow", SpawnType::Carried{by : player}); + + // Starting hangover + ecs.create_entity() + .with(StatusEffect{ target : player }) + .with(Duration{ turns:10 }) + .with(Name{ name: "Hangover".to_string() }) + .with(AttributeBonus{ + might : Some(-1), + fitness : None, + quickness : Some(-1), + intelligence : Some(-1) + }) + .marked::>() + .build(); + + player +} + +const MAX_MONSTERS : i32 = 4; + +fn room_table(map_depth: i32) -> MasterTable { + get_spawn_table_for_depth(&RAWS.lock().unwrap(), map_depth) +} + +/// Fills a room with stuff! +pub fn spawn_room(map: &Map, room : &Rect, map_depth: i32, spawn_list : &mut Vec<(usize, String)>) { + let mut possible_targets : Vec = Vec::new(); + { // Borrow scope - to keep access to the map separated + for y in room.y1 + 1 .. room.y2 { + for x in room.x1 + 1 .. room.x2 { + let idx = map.xy_idx(x, y); + if map.tiles[idx] == TileType::Floor { + possible_targets.push(idx); + } + } + } + } + + spawn_region(map, &possible_targets, map_depth, spawn_list); +} + +/// Fills a region with stuff! +pub fn spawn_region(_map: &Map, area : &[usize], map_depth: i32, spawn_list : &mut Vec<(usize, String)>) { + let spawn_table = room_table(map_depth); + let mut spawn_points : HashMap = HashMap::new(); + let mut areas : Vec = Vec::from(area); + + // Scope to keep the borrow checker happy + { + let num_spawns = i32::min(areas.len() as i32, crate::rng::roll_dice(1, MAX_MONSTERS + 3) + (map_depth - 1) - 3); + if num_spawns == 0 { return; } + + for _i in 0 .. num_spawns { + let array_index = if areas.len() == 1 { 0usize } else { (crate::rng::roll_dice(1, areas.len() as i32)-1) as usize }; + + let map_idx = areas[array_index]; + spawn_points.insert(map_idx, spawn_table.roll()); + areas.remove(array_index); + } + } + + // Actually spawn the monsters + for spawn in spawn_points.iter() { + spawn_list.push((*spawn.0, spawn.1.to_string())); + } +} + +/// Spawns a named entity (name in tuple.1) at the location in (tuple.0) +pub fn spawn_entity(ecs: &mut World, spawn : &(&usize, &String)) { + let map = ecs.fetch::(); + let width = map.width as usize; + let x = (*spawn.0 % width) as i32; + let y = (*spawn.0 / width) as i32; + std::mem::drop(map); + + let spawn_result = spawn_named_entity(&RAWS.lock().unwrap(), ecs, &spawn.1, SpawnType::AtPosition{ x, y}); + if spawn_result.is_some() { + return; + } + + if spawn.1 != "None" { + rltk::console::log(format!("WARNING: We don't know how to spawn [{}]!", spawn.1)); + } +} + +pub fn spawn_town_portal(ecs: &mut World) { + // Get current position & depth + let map = ecs.fetch::(); + let player_depth = map.depth; + let player_pos = ecs.fetch::(); + let player_x = player_pos.x; + let player_y = player_pos.y; + std::mem::drop(player_pos); + std::mem::drop(map); + + // Find part of the town for the portal + let dm = ecs.fetch::(); + let town_map = dm.get_map(1).unwrap(); + let mut stairs_idx = 0; + for (idx, tt) in town_map.tiles.iter().enumerate() { + if *tt == TileType::DownStairs { + stairs_idx = idx; + } + } + let portal_x = (stairs_idx as i32 % town_map.width)-2; + let portal_y = stairs_idx as i32 / town_map.width; + + std::mem::drop(dm); + + // Spawn the portal itself + ecs.create_entity() + .with(OtherLevelPosition { x: portal_x, y: portal_y, depth: 1 }) + .with(Renderable { + glyph: rltk::to_cp437('♥'), + fg: RGB::named(rltk::CYAN), + bg: RGB::named(rltk::BLACK), + render_order: 0 + }) + .with(EntryTrigger{}) + .with(TeleportTo{ x: player_x, y: player_y, depth: player_depth, player_only: true }) + .with(SingleActivation{}) + .with(Name{ name : "Town Portal".to_string() }) + .build(); +} diff --git a/chapter-75-darkplaza/src/systems/ai/adjacent_ai_system.rs b/chapter-75-darkplaza/src/systems/ai/adjacent_ai_system.rs new file mode 100644 index 00000000..72cf15c5 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/ai/adjacent_ai_system.rs @@ -0,0 +1,82 @@ +use specs::prelude::*; +use crate::{MyTurn, Faction, Position, Map, raws::Reaction, WantsToMelee, TileSize}; + +pub struct AdjacentAI {} + +impl<'a> System<'a> for AdjacentAI { + #[allow(clippy::type_complexity)] + type SystemData = ( + WriteStorage<'a, MyTurn>, + ReadStorage<'a, Faction>, + ReadStorage<'a, Position>, + ReadExpect<'a, Map>, + WriteStorage<'a, WantsToMelee>, + Entities<'a>, + ReadExpect<'a, Entity>, + ReadStorage<'a, TileSize> + ); + + fn run(&mut self, data : Self::SystemData) { + let (mut turns, factions, positions, map, mut want_melee, entities, player, sizes) = data; + + let mut turn_done : Vec = Vec::new(); + for (entity, _turn, my_faction, pos) in (&entities, &turns, &factions, &positions).join() { + if entity != *player { + let mut reactions : Vec<(Entity, Reaction)> = Vec::new(); + let idx = map.xy_idx(pos.x, pos.y); + let w = map.width; + let h = map.height; + + if let Some(size) = sizes.get(entity) { + use crate::rect::Rect; + let mob_rect = Rect::new(pos.x, pos.y, size.x, size.y).get_all_tiles(); + let parent_rect = Rect::new(pos.x -1, pos.y -1, size.x+2, size.y + 2); + parent_rect.get_all_tiles().iter().filter(|t| !mob_rect.contains(t)).for_each(|t| { + if t.0 > 0 && t.0 < w-1 && t.1 > 0 && t.1 < h-1 { + let target_idx = map.xy_idx(t.0, t.1); + evaluate(target_idx, &map, &factions, &my_faction.name, &mut reactions); + } + }); + } else { + + // Add possible reactions to adjacents for each direction + if pos.x > 0 { evaluate(idx-1, &map, &factions, &my_faction.name, &mut reactions); } + if pos.x < w-1 { evaluate(idx+1, &map, &factions, &my_faction.name, &mut reactions); } + if pos.y > 0 { evaluate(idx-w as usize, &map, &factions, &my_faction.name, &mut reactions); } + if pos.y < h-1 { evaluate(idx+w as usize, &map, &factions, &my_faction.name, &mut reactions); } + if pos.y > 0 && pos.x > 0 { evaluate((idx-w as usize)-1, &map, &factions, &my_faction.name, &mut reactions); } + if pos.y > 0 && pos.x < w-1 { evaluate((idx-w as usize)+1, &map, &factions, &my_faction.name, &mut reactions); } + if pos.y < h-1 && pos.x > 0 { evaluate((idx+w as usize)-1, &map, &factions, &my_faction.name, &mut reactions); } + if pos.y < h-1 && pos.x < w-1 { evaluate((idx+w as usize)+1, &map, &factions, &my_faction.name, &mut reactions); } + + } + + let mut done = false; + for reaction in reactions.iter() { + if let Reaction::Attack = reaction.1 { + want_melee.insert(entity, WantsToMelee{ target: reaction.0 }).expect("Error inserting melee"); + done = true; + } + } + + if done { turn_done.push(entity); } + } + } + + // Remove turn marker for those that are done + for done in turn_done.iter() { + turns.remove(*done); + } + } +} + +fn evaluate(idx : usize, map : &Map, factions : &ReadStorage, my_faction : &str, reactions : &mut Vec<(Entity, Reaction)>) { + crate::spatial::for_each_tile_content(idx, |other_entity| { + if let Some(faction) = factions.get(other_entity) { + reactions.push(( + other_entity, + crate::raws::faction_reaction(my_faction, &faction.name, &crate::raws::RAWS.lock().unwrap()) + )); + } + }); +} diff --git a/chapter-75-darkplaza/src/systems/ai/approach_ai_system.rs b/chapter-75-darkplaza/src/systems/ai/approach_ai_system.rs new file mode 100644 index 00000000..fd88fbc0 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/ai/approach_ai_system.rs @@ -0,0 +1,43 @@ +use specs::prelude::*; +use crate::{MyTurn, WantsToApproach, Position, Map, ApplyMove}; + +pub struct ApproachAI {} + +impl<'a> System<'a> for ApproachAI { + #[allow(clippy::type_complexity)] + type SystemData = ( + WriteStorage<'a, MyTurn>, + WriteStorage<'a, WantsToApproach>, + ReadStorage<'a, Position>, + WriteExpect<'a, Map>, + Entities<'a>, + WriteStorage<'a, ApplyMove> + ); + + fn run(&mut self, data : Self::SystemData) { + let (mut turns, mut want_approach, positions, mut map, + entities, mut apply_move) = data; + + let mut turn_done : Vec = Vec::new(); + for (entity, pos, approach, _myturn) in + (&entities, &positions, &want_approach, &turns).join() + { + turn_done.push(entity); + let path = rltk::a_star_search( + map.xy_idx(pos.x, pos.y), + map.xy_idx(approach.idx % map.width, approach.idx / map.width), + &mut *map + ); + if path.success && path.steps.len()>1 { + apply_move.insert(entity, ApplyMove{ dest_idx: path.steps[1] }).expect("Unable to insert"); + } + } + + want_approach.clear(); + + // Remove turn marker for those that are done + for done in turn_done.iter() { + turns.remove(*done); + } + } +} diff --git a/chapter-75-darkplaza/src/systems/ai/chase_ai_system.rs b/chapter-75-darkplaza/src/systems/ai/chase_ai_system.rs new file mode 100644 index 00000000..26e47bf8 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/ai/chase_ai_system.rs @@ -0,0 +1,77 @@ +use specs::prelude::*; +use crate::{MyTurn, Chasing, Position, Map, ApplyMove, TileSize}; +use std::collections::HashMap; + +pub struct ChaseAI {} + +impl<'a> System<'a> for ChaseAI { + #[allow(clippy::type_complexity)] + type SystemData = ( + WriteStorage<'a, MyTurn>, + WriteStorage<'a, Chasing>, + ReadStorage<'a, Position>, + WriteExpect<'a, Map>, + Entities<'a>, + WriteStorage<'a, ApplyMove>, + ReadStorage<'a, TileSize> + ); + + fn run(&mut self, data : Self::SystemData) { + let (mut turns, mut chasing, positions, mut map, + entities, mut apply_move, sizes) = data; + + let mut targets : HashMap = HashMap::new(); + let mut end_chase : Vec = Vec::new(); + for (entity, _turn, chasing) in (&entities, &turns, &chasing).join() { + let target_pos = positions.get(chasing.target); + if let Some(target_pos) = target_pos { + targets.insert(entity, (target_pos.x, target_pos.y)); + } else { + end_chase.push(entity); + } + } + + for done in end_chase.iter() { + chasing.remove(*done); + } + end_chase.clear(); + + let mut turn_done : Vec = Vec::new(); + for (entity, pos, _chase, _myturn) in + (&entities, &positions, &chasing, &turns).join() + { + turn_done.push(entity); + let target_pos = targets[&entity]; + let path; + + if let Some(size) = sizes.get(entity) { + let mut map_copy = map.clone(); + map_copy.populate_blocked_multi(size.x, size.y); + path = rltk::a_star_search( + map_copy.xy_idx(pos.x, pos.y), + map_copy.xy_idx(target_pos.0, target_pos.1), + &mut map_copy + ); + } else { + path = rltk::a_star_search( + map.xy_idx(pos.x, pos.y), + map.xy_idx(target_pos.0, target_pos.1), + &mut *map + ); + } + if path.success && path.steps.len()>1 && path.steps.len()<15 { + apply_move.insert(entity, ApplyMove{ dest_idx: path.steps[1] }).expect("Unable to insert"); + turn_done.push(entity); + } else { + end_chase.push(entity); + } + } + + for done in end_chase.iter() { + chasing.remove(*done); + } + for done in turn_done.iter() { + turns.remove(*done); + } + } +} diff --git a/chapter-75-darkplaza/src/systems/ai/default_move_system.rs b/chapter-75-darkplaza/src/systems/ai/default_move_system.rs new file mode 100644 index 00000000..6bd00dd8 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/ai/default_move_system.rs @@ -0,0 +1,92 @@ +use specs::prelude::*; +use crate::{MyTurn, MoveMode, Movement, Position, Map, map::tile_walkable, ApplyMove}; + +pub struct DefaultMoveAI {} + +impl<'a> System<'a> for DefaultMoveAI { + #[allow(clippy::type_complexity)] + type SystemData = ( + WriteStorage<'a, MyTurn>, + WriteStorage<'a, MoveMode>, + ReadStorage<'a, Position>, + WriteExpect<'a, Map>, + WriteStorage<'a, ApplyMove>, + Entities<'a> + ); + + fn run(&mut self, data : Self::SystemData) { + let (mut turns, mut move_mode, positions, mut map, + mut apply_move, entities) = data; + + let mut turn_done : Vec = Vec::new(); + for (entity, pos, mut mode, _myturn) in + (&entities, &positions, &mut move_mode, &turns).join() + { + turn_done.push(entity); + + match &mut mode.mode { + Movement::Static => {}, + + Movement::Random => { + let mut x = pos.x; + let mut y = pos.y; + let move_roll = crate::rng::roll_dice(1, 5); + match move_roll { + 1 => x -= 1, + 2 => x += 1, + 3 => y -= 1, + 4 => y += 1, + _ => {} + } + + if x > 0 && x < map.width-1 && y > 0 && y < map.height-1 { + let dest_idx = map.xy_idx(x, y); + if !crate::spatial::is_blocked(dest_idx) { + apply_move.insert(entity, ApplyMove{ dest_idx }) + .expect("Unable to insert"); + turn_done.push(entity); + } + } + }, + + Movement::RandomWaypoint{path} => { + if let Some(path) = path { + // We have a target - go there + if path.len()>1 { + if !crate::spatial::is_blocked(path[1] as usize) { + apply_move.insert(entity, ApplyMove{ dest_idx : path[1] }) + .expect("Unable to insert"); + path.remove(0); // Remove the first step in the path + turn_done.push(entity); + } + // Otherwise we wait a turn to see if the path clears up + } else { + mode.mode = Movement::RandomWaypoint{ path : None }; + } + } else { + let target_x = crate::rng::roll_dice(1, map.width-2); + let target_y = crate::rng::roll_dice(1, map.height-2); + let idx = map.xy_idx(target_x, target_y); + if tile_walkable(map.tiles[idx]) { + let path = rltk::a_star_search( + map.xy_idx(pos.x, pos.y), + map.xy_idx(target_x, target_y), + &mut *map + ); + if path.success && path.steps.len()>1 { + mode.mode = Movement::RandomWaypoint{ + path: Some(path.steps) + }; + } + } + } + } + } + } + + // Remove turn marker for those that are done + for done in turn_done.iter() { + turns.remove(*done); + } + } +} diff --git a/chapter-75-darkplaza/src/systems/ai/encumbrance_system.rs b/chapter-75-darkplaza/src/systems/ai/encumbrance_system.rs new file mode 100644 index 00000000..1e816bb9 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/ai/encumbrance_system.rs @@ -0,0 +1,122 @@ +use specs::prelude::*; +use crate::{EquipmentChanged, Item, InBackpack, Equipped, Pools, Attributes, AttributeBonus, + gamesystem::attr_bonus, StatusEffect, Slow}; +use std::collections::HashMap; + +pub struct EncumbranceSystem {} + +impl<'a> System<'a> for EncumbranceSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( + WriteStorage<'a, EquipmentChanged>, + Entities<'a>, + ReadStorage<'a, Item>, + ReadStorage<'a, InBackpack>, + ReadStorage<'a, Equipped>, + WriteStorage<'a, Pools>, + WriteStorage<'a, Attributes>, + ReadExpect<'a, Entity>, + ReadStorage<'a, AttributeBonus>, + ReadStorage<'a, StatusEffect>, + ReadStorage<'a, Slow> + ); + + fn run(&mut self, data : Self::SystemData) { + let (mut equip_dirty, entities, items, backpacks, wielded, + mut pools, mut attributes, player, attrbonus, statuses, slowed) = data; + + if equip_dirty.is_empty() { return; } + + struct ItemUpdate { + weight : f32, + initiative : f32, + might : i32, + fitness : i32, + quickness : i32, + intelligence : i32 + } + + // Build the map of who needs updating + let mut to_update : HashMap = HashMap::new(); // (weight, intiative) + for (entity, _dirty) in (&entities, &equip_dirty).join() { + to_update.insert(entity, ItemUpdate{ weight: 0.0, initiative: 0.0, might: 0, fitness: 0, quickness: 0, intelligence: 0 }); + } + + // Remove all dirty statements + equip_dirty.clear(); + + // Total up equipped items + for (item, equipped, entity) in (&items, &wielded, &entities).join() { + if to_update.contains_key(&equipped.owner) { + let totals = to_update.get_mut(&equipped.owner).unwrap(); + totals.weight += item.weight_lbs; + totals.initiative += item.initiative_penalty; + if let Some(attr) = attrbonus.get(entity) { + totals.might += attr.might.unwrap_or(0); + totals.fitness += attr.fitness.unwrap_or(0); + totals.quickness += attr.quickness.unwrap_or(0); + totals.intelligence += attr.intelligence.unwrap_or(0); + } + } + } + + // Total up carried items + for (item, carried) in (&items, &backpacks).join() { + if to_update.contains_key(&carried.owner) { + let totals = to_update.get_mut(&carried.owner).unwrap(); + totals.weight += item.weight_lbs; + totals.initiative += item.initiative_penalty; + } + } + + // Total up status effect modifiers + for (status, attr) in (&statuses, &attrbonus).join() { + if to_update.contains_key(&status.target) { + let totals = to_update.get_mut(&status.target).unwrap(); + totals.might += attr.might.unwrap_or(0); + totals.fitness += attr.fitness.unwrap_or(0); + totals.quickness += attr.quickness.unwrap_or(0); + totals.intelligence += attr.intelligence.unwrap_or(0); + } + } + + // Total up haste/slow + for (status, slow) in (&statuses, &slowed).join() { + if to_update.contains_key(&status.target) { + let totals = to_update.get_mut(&status.target).unwrap(); + totals.initiative += slow.initiative_penalty; + } + } + + // Apply the data to Pools + for (entity, item) in to_update.iter() { + if let Some(pool) = pools.get_mut(*entity) { + pool.total_weight = item.weight; + pool.total_initiative_penalty = item.initiative; + + if let Some(attr) = attributes.get_mut(*entity) { + attr.might.modifiers = item.might; + attr.fitness.modifiers = item.fitness; + attr.quickness.modifiers = item.quickness; + attr.intelligence.modifiers = item.intelligence; + attr.might.bonus = attr_bonus(attr.might.base + attr.might.modifiers); + attr.fitness.bonus = attr_bonus(attr.fitness.base + attr.fitness.modifiers); + attr.quickness.bonus = attr_bonus(attr.quickness.base + attr.quickness.modifiers); + attr.intelligence.bonus = attr_bonus(attr.intelligence.base + attr.intelligence.modifiers); + + let carry_capacity_lbs = (attr.might.base + attr.might.modifiers) * 15; + if pool.total_weight as i32 > carry_capacity_lbs { + // Overburdened + pool.total_initiative_penalty += 4.0; + if *entity == *player { + crate::gamelog::Logger::new() + .color(rltk::ORANGE) + .append("You are overburdened, and suffering an initiative penalty.") + .log(); + } + } + } + } + } + } +} diff --git a/chapter-75-darkplaza/src/systems/ai/flee_ai_system.rs b/chapter-75-darkplaza/src/systems/ai/flee_ai_system.rs new file mode 100644 index 00000000..b62b4fc8 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/ai/flee_ai_system.rs @@ -0,0 +1,45 @@ +use specs::prelude::*; +use crate::{MyTurn, WantsToFlee, Position, Map, ApplyMove}; + +pub struct FleeAI {} + +impl<'a> System<'a> for FleeAI { + #[allow(clippy::type_complexity)] + type SystemData = ( + WriteStorage<'a, MyTurn>, + WriteStorage<'a, WantsToFlee>, + WriteStorage<'a, Position>, + WriteExpect<'a, Map>, + Entities<'a>, + WriteStorage<'a, ApplyMove> + ); + + fn run(&mut self, data : Self::SystemData) { + let (mut turns, mut want_flee, positions, mut map, + entities, mut apply_move) = data; + + let mut turn_done : Vec = Vec::new(); + for (entity, pos, flee, _myturn) in + (&entities, &positions, &want_flee, &turns).join() + { + turn_done.push(entity); + let my_idx = map.xy_idx(pos.x, pos.y); + map.populate_blocked(); + let flee_map = rltk::DijkstraMap::new(map.width as usize, map.height as usize, &flee.indices, &*map, 100.0); + let flee_target = rltk::DijkstraMap::find_highest_exit(&flee_map, my_idx, &*map); + if let Some(flee_target) = flee_target { + if !crate::spatial::is_blocked(flee_target as usize) { + apply_move.insert(entity, ApplyMove{ dest_idx : flee_target }).expect("Unable to insert"); + turn_done.push(entity); + } + } + } + + want_flee.clear(); + + // Remove turn marker for those that are done + for done in turn_done.iter() { + turns.remove(*done); + } + } +} diff --git a/chapter-75-darkplaza/src/systems/ai/initiative_system.rs b/chapter-75-darkplaza/src/systems/ai/initiative_system.rs new file mode 100644 index 00000000..e4687a0d --- /dev/null +++ b/chapter-75-darkplaza/src/systems/ai/initiative_system.rs @@ -0,0 +1,96 @@ +use specs::prelude::*; +use crate::{Initiative, Position, MyTurn, Attributes, RunState, Pools, Duration, + EquipmentChanged, StatusEffect, DamageOverTime}; + +pub struct InitiativeSystem {} + +impl<'a> System<'a> for InitiativeSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( WriteStorage<'a, Initiative>, + ReadStorage<'a, Position>, + WriteStorage<'a, MyTurn>, + Entities<'a>, + ReadStorage<'a, Attributes>, + WriteExpect<'a, RunState>, + ReadExpect<'a, Entity>, + ReadExpect<'a, rltk::Point>, + ReadStorage<'a, Pools>, + WriteStorage<'a, Duration>, + WriteStorage<'a, EquipmentChanged>, + ReadStorage<'a, StatusEffect>, + ReadStorage<'a, DamageOverTime> + ); + + fn run(&mut self, data : Self::SystemData) { + let (mut initiatives, positions, mut turns, entities, attributes, + mut runstate, player, player_pos, pools, mut durations, mut dirty, + statuses, dots) = data; + + if *runstate != RunState::Ticking { return; } + + // Clear any remaining MyTurn we left by mistkae + turns.clear(); + + // Roll initiative + for (entity, initiative, pos) in (&entities, &mut initiatives, &positions).join() { + initiative.current -= 1; + if initiative.current < 1 { + let mut myturn = true; + + // Re-roll + initiative.current = 6 + crate::rng::roll_dice(1, 6); + + // Give a bonus for quickness + if let Some(attr) = attributes.get(entity) { + initiative.current -= attr.quickness.bonus; + } + + // Apply pool penalty + if let Some(pools) = pools.get(entity) { + initiative.current += f32::floor(pools.total_initiative_penalty) as i32; + } + + // TODO: More initiative granting boosts/penalties will go here later + + // If its the player, we want to go to an AwaitingInput state + if entity == *player { + // Give control to the player + *runstate = RunState::AwaitingInput; + } else { + let distance = rltk::DistanceAlg::Pythagoras.distance2d(*player_pos, rltk::Point::new(pos.x, pos.y)); + if distance > 20.0 { + myturn = false; + } + } + + // It's my turn! + if myturn { + turns.insert(entity, MyTurn{}).expect("Unable to insert turn"); + } + + } + } + + // Handle durations + if *runstate == RunState::AwaitingInput { + use crate::effects::*; + for (effect_entity, duration, status) in (&entities, &mut durations, &statuses).join() { + if entities.is_alive(status.target) { + duration.turns -= 1; + if let Some(dot) = dots.get(effect_entity) { + add_effect( + None, + EffectType::Damage{ amount : dot.damage }, + Targets::Single{ target : status.target + } + ); + } + if duration.turns < 1 { + dirty.insert(status.target, EquipmentChanged{}).expect("Unable to insert"); + entities.delete(effect_entity).expect("Unable to delete"); + } + } + } + } + } +} diff --git a/chapter-75-darkplaza/src/systems/ai/mod.rs b/chapter-75-darkplaza/src/systems/ai/mod.rs new file mode 100644 index 00000000..314b8d36 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/ai/mod.rs @@ -0,0 +1,20 @@ +mod initiative_system; +mod turn_status; +mod quipping; +mod adjacent_ai_system; +mod visible_ai_system; +mod approach_ai_system; +mod flee_ai_system; +mod default_move_system; +mod chase_ai_system; +mod encumbrance_system; +pub use initiative_system::InitiativeSystem; +pub use turn_status::TurnStatusSystem; +pub use quipping::QuipSystem; +pub use adjacent_ai_system::AdjacentAI; +pub use visible_ai_system::VisibleAI; +pub use approach_ai_system::ApproachAI; +pub use flee_ai_system::FleeAI; +pub use default_move_system::DefaultMoveAI; +pub use chase_ai_system::ChaseAI; +pub use encumbrance_system::EncumbranceSystem; diff --git a/chapter-75-darkplaza/src/systems/ai/quipping.rs b/chapter-75-darkplaza/src/systems/ai/quipping.rs new file mode 100644 index 00000000..88e5c05d --- /dev/null +++ b/chapter-75-darkplaza/src/systems/ai/quipping.rs @@ -0,0 +1,33 @@ +use specs::prelude::*; +use crate::{Quips, Name, MyTurn, Viewshed}; + +pub struct QuipSystem {} + +impl<'a> System<'a> for QuipSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( + WriteStorage<'a, Quips>, + ReadStorage<'a, Name>, + ReadStorage<'a, MyTurn>, + ReadExpect<'a, rltk::Point>, + ReadStorage<'a, Viewshed>); + + fn run(&mut self, data : Self::SystemData) { + let (mut quips, names, turns, player_pos, viewsheds) = data; + + for (quip, name, viewshed, _turn) in (&mut quips, &names, &viewsheds, &turns).join() { + if !quip.available.is_empty() && viewshed.visible_tiles.contains(&player_pos) && crate::rng::roll_dice(1,6)==1 { + let quip_index = + if quip.available.len() == 1 { 0 } + else { (crate::rng::roll_dice(1, quip.available.len() as i32)-1) as usize }; + + crate::gamelog::Logger::new() + .npc_name(&name.name) + .append("says") + .item_name(&quip.available[quip_index]) + .log(); + quip.available.remove(quip_index); + } + } + } +} diff --git a/chapter-75-darkplaza/src/systems/ai/turn_status.rs b/chapter-75-darkplaza/src/systems/ai/turn_status.rs new file mode 100644 index 00000000..e866c071 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/ai/turn_status.rs @@ -0,0 +1,52 @@ +use specs::prelude::*; +use crate::{MyTurn, Confusion, RunState, StatusEffect, effects::add_effect, effects::EffectType, effects::Targets}; +use std::collections::HashSet; + +pub struct TurnStatusSystem {} + +impl<'a> System<'a> for TurnStatusSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( WriteStorage<'a, MyTurn>, + ReadStorage<'a, Confusion>, + Entities<'a>, + ReadExpect<'a, RunState>, + ReadStorage<'a, StatusEffect> + ); + + fn run(&mut self, data : Self::SystemData) { + let (mut turns, confusion, entities, runstate, statuses) = data; + + if *runstate != RunState::Ticking { return; } + + // Collect a set of all entities whose turn it is + let mut entity_turns = HashSet::new(); + for (entity, _turn) in (&entities, &turns).join() { + entity_turns.insert(entity); + } + + // Find status effects affecting entities whose turn it is + let mut not_my_turn : Vec = Vec::new(); + for (effect_entity, status_effect) in (&entities, &statuses).join() { + if entity_turns.contains(&status_effect.target) { + // Skip turn for confusion + if confusion.get(effect_entity).is_some() { + add_effect( + None, + EffectType::Particle{ + glyph : rltk::to_cp437('?'), + fg : rltk::RGB::named(rltk::CYAN), + bg : rltk::RGB::named(rltk::BLACK), + lifespan: 200.0 + }, + Targets::Single{ target:status_effect.target } + ); + not_my_turn.push(status_effect.target); + } + } + } + + for e in not_my_turn { + turns.remove(e); + } + } +} diff --git a/chapter-75-darkplaza/src/systems/ai/visible_ai_system.rs b/chapter-75-darkplaza/src/systems/ai/visible_ai_system.rs new file mode 100644 index 00000000..d2f9817e --- /dev/null +++ b/chapter-75-darkplaza/src/systems/ai/visible_ai_system.rs @@ -0,0 +1,118 @@ +use specs::prelude::*; +use crate::{MyTurn, Faction, Position, Map, raws::Reaction, Viewshed, WantsToFlee, + WantsToApproach, Chasing, SpecialAbilities, WantsToCastSpell, Name, SpellTemplate, + Equipped, Weapon, WantsToShoot}; + +pub struct VisibleAI {} + +impl<'a> System<'a> for VisibleAI { + #[allow(clippy::type_complexity)] + type SystemData = ( + ReadStorage<'a, MyTurn>, + ReadStorage<'a, Faction>, + ReadStorage<'a, Position>, + ReadExpect<'a, Map>, + WriteStorage<'a, WantsToApproach>, + WriteStorage<'a, WantsToFlee>, + Entities<'a>, + ReadExpect<'a, Entity>, + ReadStorage<'a, Viewshed>, + WriteStorage<'a, Chasing>, + ReadStorage<'a, SpecialAbilities>, + WriteStorage<'a, WantsToCastSpell>, + ReadStorage<'a, Name>, + ReadStorage<'a, SpellTemplate>, + ReadStorage<'a, Equipped>, + ReadStorage<'a, Weapon>, + WriteStorage<'a, WantsToShoot> + ); + + fn run(&mut self, data : Self::SystemData) { + let (turns, factions, positions, map, mut want_approach, mut want_flee, entities, player, + viewsheds, mut chasing, abilities, mut casting, names, spells, + equipped, weapons, mut wants_shoot) = data; + + for (entity, _turn, my_faction, pos, viewshed) in (&entities, &turns, &factions, &positions, &viewsheds).join() { + if entity != *player { + let my_idx = map.xy_idx(pos.x, pos.y); + let mut reactions : Vec<(usize, Reaction, Entity)> = Vec::new(); + let mut flee : Vec = Vec::new(); + for visible_tile in viewshed.visible_tiles.iter() { + let idx = map.xy_idx(visible_tile.x, visible_tile.y); + if my_idx != idx { + evaluate(idx, &map, &factions, &my_faction.name, &mut reactions); + } + } + + let mut done = false; + for reaction in reactions.iter() { + match reaction.1 { + Reaction::Attack => { + let range = rltk::DistanceAlg::Pythagoras.distance2d( + rltk::Point::new(pos.x, pos.y), + rltk::Point::new(reaction.0 as i32 % map.width, reaction.0 as i32 / map.width) + ); + if let Some(abilities) = abilities.get(entity) { + for ability in abilities.abilities.iter() { + if range >= ability.min_range && range <= ability.range && + crate::rng::roll_dice(1,100) <= (ability.chance * 100.0) as i32 + { + use crate::raws::find_spell_entity_by_name; + casting.insert( + entity, + WantsToCastSpell{ + spell : find_spell_entity_by_name(&ability.spell, &names, &spells, &entities).unwrap(), + target : Some(rltk::Point::new(reaction.0 as i32 % map.width, reaction.0 as i32 / map.width))} + ).expect("Unable to insert"); + done = true; + } + } + } + + if !done { + for (weapon, equip) in (&weapons, &equipped).join() { + if let Some(wrange) = weapon.range { + if equip.owner == entity { + //rltk::console::log(format!("Owner found. Ranges: {}/{}", wrange, range)); + if wrange >= range as i32 { + //rltk::console::log("Inserting shoot"); + wants_shoot.insert(entity, WantsToShoot{ target: reaction.2 }).expect("Insert fail"); + done = true; + } + } + } + } + } + + if !done { + want_approach.insert(entity, WantsToApproach{ idx: reaction.0 as i32 }).expect("Unable to insert"); + chasing.insert(entity, Chasing{ target: reaction.2}).expect("Unable to insert"); + done = true; + } + } + Reaction::Flee => { + flee.push(reaction.0); + } + _ => {} + } + } + + if !done && !flee.is_empty() { + want_flee.insert(entity, WantsToFlee{ indices : flee }).expect("Unable to insert"); + } + } + } + } +} + +fn evaluate(idx : usize, map : &Map, factions : &ReadStorage, my_faction : &str, reactions : &mut Vec<(usize, Reaction, Entity)>) { + crate::spatial::for_each_tile_content(idx, |other_entity| { + if let Some(faction) = factions.get(other_entity) { + reactions.push(( + idx, + crate::raws::faction_reaction(my_faction, &faction.name, &crate::raws::RAWS.lock().unwrap()), + other_entity + )); + } + }); +} diff --git a/chapter-75-darkplaza/src/systems/dispatcher/mod.rs b/chapter-75-darkplaza/src/systems/dispatcher/mod.rs new file mode 100644 index 00000000..3e273b5e --- /dev/null +++ b/chapter-75-darkplaza/src/systems/dispatcher/mod.rs @@ -0,0 +1,53 @@ +#[cfg(target_arch = "wasm32")] +#[macro_use] +mod single_thread; + +#[cfg(not(target_arch = "wasm32"))] +#[macro_use] +mod multi_thread; + +#[cfg(target_arch = "wasm32")] +pub use single_thread::*; + +#[cfg(not(target_arch = "wasm32"))] +pub use multi_thread::*; + +use specs::prelude::World; +use super::*; + +pub trait UnifiedDispatcher { + fn run_now(&mut self, ecs : *mut World); +} + +construct_dispatcher!( + (MapIndexingSystem, "map_index", &[]), + (VisibilitySystem, "visibility", &[]), + (EncumbranceSystem, "encumbrance", &[]), + (InitiativeSystem, "initiative", &[]), + (TurnStatusSystem, "turnstatus", &[]), + (QuipSystem, "quips", &[]), + (AdjacentAI, "adjacent", &[]), + (VisibleAI, "visible", &[]), + (ApproachAI, "approach", &[]), + (FleeAI, "flee", &[]), + (ChaseAI, "chase", &[]), + (DefaultMoveAI, "default_move", &[]), + (MovementSystem, "movement", &[]), + (TriggerSystem, "triggers", &[]), + (MeleeCombatSystem, "melee", &[]), + (RangedCombatSystem, "ranged", &[]), + (ItemCollectionSystem, "pickup", &[]), + (ItemEquipOnUse, "equip", &[]), + (ItemUseSystem, "use", &[]), + (SpellUseSystem, "spells", &[]), + (ItemIdentificationSystem, "itemid", &[]), + (ItemDropSystem, "drop", &[]), + (ItemRemoveSystem, "remove", &[]), + (HungerSystem, "hunger", &[]), + (ParticleSpawnSystem, "particle_spawn", &[]), + (LightingSystem, "lighting", &[]) +); + +pub fn new() -> Box { + new_dispatch() +} \ No newline at end of file diff --git a/chapter-75-darkplaza/src/systems/dispatcher/multi_thread.rs b/chapter-75-darkplaza/src/systems/dispatcher/multi_thread.rs new file mode 100644 index 00000000..bf73dc64 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/dispatcher/multi_thread.rs @@ -0,0 +1,43 @@ +use super::UnifiedDispatcher; +use specs::prelude::*; + +macro_rules! construct_dispatcher { + ( + $( + ( + $type:ident, + $name:expr, + $deps:expr + ) + ),* + ) => { + fn new_dispatch() -> Box { + use specs::DispatcherBuilder; + + let dispatcher = DispatcherBuilder::new() + $( + .with($type{}, $name, $deps) + )* + .build(); + + let dispatch = MultiThreadedDispatcher{ + dispatcher : dispatcher + }; + + return Box::new(dispatch); + } + }; +} + +pub struct MultiThreadedDispatcher { + pub dispatcher: specs::Dispatcher<'static, 'static> +} + +impl<'a> UnifiedDispatcher for MultiThreadedDispatcher { + fn run_now(&mut self, ecs : *mut World) { + unsafe { + self.dispatcher.dispatch(&mut *ecs); + crate::effects::run_effects_queue(&mut *ecs); + } + } +} \ No newline at end of file diff --git a/chapter-75-darkplaza/src/systems/dispatcher/single_thread.rs b/chapter-75-darkplaza/src/systems/dispatcher/single_thread.rs new file mode 100644 index 00000000..529684f4 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/dispatcher/single_thread.rs @@ -0,0 +1,42 @@ +use super::super::*; +use super::UnifiedDispatcher; +use specs::prelude::*; + +macro_rules! construct_dispatcher { + ( + $( + ( + $type:ident, + $name:expr, + $deps:expr + ) + ),* + ) => { + fn new_dispatch() -> Box { + let mut dispatch = SingleThreadedDispatcher{ + systems : Vec::new() + }; + + $( + dispatch.systems.push( Box::new( $type {} )); + )* + + return Box::new(dispatch); + } + }; +} + +pub struct SingleThreadedDispatcher<'a> { + pub systems : Vec>> +} + +impl<'a> UnifiedDispatcher for SingleThreadedDispatcher<'a> { + fn run_now(&mut self, ecs : *mut World) { + unsafe { + for sys in self.systems.iter_mut() { + sys.run_now(&*ecs); + } + crate::effects::run_effects_queue(&mut *ecs); + } + } +} diff --git a/chapter-75-darkplaza/src/systems/hunger_system.rs b/chapter-75-darkplaza/src/systems/hunger_system.rs new file mode 100644 index 00000000..d7def9b5 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/hunger_system.rs @@ -0,0 +1,70 @@ +use specs::prelude::*; +use crate::{HungerClock, HungerState, MyTurn, effects::*}; + +pub struct HungerSystem {} + +impl<'a> System<'a> for HungerSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( + Entities<'a>, + WriteStorage<'a, HungerClock>, + ReadExpect<'a, Entity>, // The player + ReadStorage<'a, MyTurn> + ); + + fn run(&mut self, data : Self::SystemData) { + let (entities, mut hunger_clock, player_entity, turns) = data; + + for (entity, mut clock, _myturn) in (&entities, &mut hunger_clock, &turns).join() { + clock.duration -= 1; + if clock.duration < 1 { + match clock.state { + HungerState::WellFed => { + clock.state = HungerState::Normal; + clock.duration = 200; + if entity == *player_entity { + crate::gamelog::Logger::new() + .color(rltk::ORANGE) + .append("You are no longer well fed") + .log(); + } + } + HungerState::Normal => { + clock.state = HungerState::Hungry; + clock.duration = 200; + if entity == *player_entity { + crate::gamelog::Logger::new() + .color(rltk::ORANGE) + .append("You are hungry") + .log(); + } + } + HungerState::Hungry => { + clock.state = HungerState::Starving; + clock.duration = 200; + if entity == *player_entity { + crate::gamelog::Logger::new() + .color(rltk::RED) + .append("You are starving!") + .log(); + } + } + HungerState::Starving => { + // Inflict damage from hunger + if entity == *player_entity { + crate::gamelog::Logger::new() + .color(rltk::RED) + .append("Your hunger pangs are getting painful! You suffer 1 hp damage.") + .log(); + } + add_effect( + None, + EffectType::Damage{ amount: 1}, + Targets::Single{ target: entity } + ); + } + } + } + } + } +} diff --git a/chapter-75-darkplaza/src/systems/inventory_system/collection_system.rs b/chapter-75-darkplaza/src/systems/inventory_system/collection_system.rs new file mode 100644 index 00000000..9d319171 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/inventory_system/collection_system.rs @@ -0,0 +1,41 @@ +use specs::prelude::*; +use super::{WantsToPickupItem, Name, InBackpack, Position, EquipmentChanged, + MagicItem, ObfuscatedName, MasterDungeonMap }; + +pub struct ItemCollectionSystem {} + +impl<'a> System<'a> for ItemCollectionSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( ReadExpect<'a, Entity>, + WriteStorage<'a, WantsToPickupItem>, + WriteStorage<'a, Position>, + ReadStorage<'a, Name>, + WriteStorage<'a, InBackpack>, + WriteStorage<'a, EquipmentChanged>, + ReadStorage<'a, MagicItem>, + ReadStorage<'a, ObfuscatedName>, + ReadExpect<'a, MasterDungeonMap> + ); + + fn run(&mut self, data : Self::SystemData) { + let (player_entity, mut wants_pickup, mut positions, names, + mut backpack, mut dirty, magic_items, obfuscated_names, dm) = data; + + for pickup in wants_pickup.join() { + positions.remove(pickup.item); + backpack.insert(pickup.item, InBackpack{ owner: pickup.collected_by }).expect("Unable to insert backpack entry"); + dirty.insert(pickup.collected_by, EquipmentChanged{}).expect("Unable to insert"); + + if pickup.collected_by == *player_entity { + crate::gamelog::Logger::new() + .append("You pick up the") + .item_name( + super::obfuscate_name(pickup.item, &names, &magic_items, &obfuscated_names, &dm) + ) + .log(); + } + } + + wants_pickup.clear(); + } +} diff --git a/chapter-75-darkplaza/src/systems/inventory_system/drop_system.rs b/chapter-75-darkplaza/src/systems/inventory_system/drop_system.rs new file mode 100644 index 00000000..56a2c807 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/inventory_system/drop_system.rs @@ -0,0 +1,48 @@ +use specs::prelude::*; +use super::{Name, InBackpack, Position, WantsToDropItem, EquipmentChanged, + MagicItem, ObfuscatedName, MasterDungeonMap}; + +pub struct ItemDropSystem {} + +impl<'a> System<'a> for ItemDropSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( ReadExpect<'a, Entity>, + Entities<'a>, + WriteStorage<'a, WantsToDropItem>, + ReadStorage<'a, Name>, + WriteStorage<'a, Position>, + WriteStorage<'a, InBackpack>, + WriteStorage<'a, EquipmentChanged>, + ReadStorage<'a, MagicItem>, + ReadStorage<'a, ObfuscatedName>, + ReadExpect<'a, MasterDungeonMap> + ); + + fn run(&mut self, data : Self::SystemData) { + let (player_entity, entities, mut wants_drop, names, mut positions, + mut backpack, mut dirty, magic_items, obfuscated_names, dm) = data; + + for (entity, to_drop) in (&entities, &wants_drop).join() { + let mut dropper_pos : Position = Position{x:0, y:0}; + { + let dropped_pos = positions.get(entity).unwrap(); + dropper_pos.x = dropped_pos.x; + dropper_pos.y = dropped_pos.y; + } + positions.insert(to_drop.item, Position{ x : dropper_pos.x, y : dropper_pos.y }).expect("Unable to insert position"); + backpack.remove(to_drop.item); + dirty.insert(entity, EquipmentChanged{}).expect("Unable to insert"); + + if entity == *player_entity { + crate::gamelog::Logger::new() + .append("You drop the") + .item_name( + super::obfuscate_name(to_drop.item, &names, &magic_items, &obfuscated_names, &dm) + ) + .log(); + } + } + + wants_drop.clear(); + } +} diff --git a/chapter-75-darkplaza/src/systems/inventory_system/equip_use.rs b/chapter-75-darkplaza/src/systems/inventory_system/equip_use.rs new file mode 100644 index 00000000..ee68c765 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/inventory_system/equip_use.rs @@ -0,0 +1,91 @@ +use specs::prelude::*; +use super::{Name, InBackpack, WantsToUseItem, Equippable, Equipped, EquipmentChanged, + IdentifiedItem, CursedItem}; + +pub struct ItemEquipOnUse {} + +impl<'a> System<'a> for ItemEquipOnUse { + #[allow(clippy::type_complexity)] + type SystemData = ( ReadExpect<'a, Entity>, + Entities<'a>, + WriteStorage<'a, WantsToUseItem>, + ReadStorage<'a, Name>, + ReadStorage<'a, Equippable>, + WriteStorage<'a, Equipped>, + WriteStorage<'a, InBackpack>, + WriteStorage<'a, EquipmentChanged>, + WriteStorage<'a, IdentifiedItem>, + ReadStorage<'a, CursedItem> + ); + + #[allow(clippy::cognitive_complexity)] + fn run(&mut self, data : Self::SystemData) { + let (player_entity, entities, mut wants_use, names, equippable, + mut equipped, mut backpack, mut dirty, mut identified_item, cursed) = data; + + let mut remove_use : Vec = Vec::new(); + for (target, useitem) in (&entities, &wants_use).join() { + // If it is equippable, then we want to equip it - and unequip whatever else was in that slot + if let Some(can_equip) = equippable.get(useitem.item) { + let target_slot = can_equip.slot; + + // Remove any items the target has in the item's slot + let mut can_equip = true; + let mut to_unequip : Vec = Vec::new(); + for (item_entity, already_equipped, name) in (&entities, &equipped, &names).join() { + if already_equipped.owner == target && already_equipped.slot == target_slot { + if cursed.get(item_entity).is_some() { + crate::gamelog::Logger::new() + .append("You cannot unequip") + .item_name(&name.name) + .append("- it is cursed!") + .log(); + can_equip = false; + } else { + to_unequip.push(item_entity); + if target == *player_entity { + crate::gamelog::Logger::new() + .append("You unequip") + .item_name(&name.name) + .log(); + } + } + } + } + + if can_equip { + // Identify the item + if target == *player_entity { + identified_item.insert(target, IdentifiedItem{ name: names.get(useitem.item).unwrap().name.clone() }) + .expect("Unable to insert"); + } + + + for item in to_unequip.iter() { + equipped.remove(*item); + backpack.insert(*item, InBackpack{ owner: target }).expect("Unable to insert backpack entry"); + } + + // Wield the item + equipped.insert(useitem.item, Equipped{ owner: target, slot: target_slot }).expect("Unable to insert equipped component"); + backpack.remove(useitem.item); + if target == *player_entity { + crate::gamelog::Logger::new() + .append("You equip") + .item_name(&names.get(useitem.item).unwrap().name) + .log(); + } + + dirty.insert(target, EquipmentChanged{}).expect("Unable to insert"); + } + + // Done with item + remove_use.push(target); + } + } + + remove_use.iter().for_each(|e| { + wants_use.remove(*e).expect("Unable to remove"); + }); + } +} diff --git a/chapter-75-darkplaza/src/systems/inventory_system/identification_system.rs b/chapter-75-darkplaza/src/systems/inventory_system/identification_system.rs new file mode 100644 index 00000000..daaaf003 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/inventory_system/identification_system.rs @@ -0,0 +1,36 @@ +use specs::prelude::*; +use super::{Name, IdentifiedItem, Item, ObfuscatedName}; + +pub struct ItemIdentificationSystem {} + +impl<'a> System<'a> for ItemIdentificationSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( + ReadStorage<'a, crate::components::Player>, + WriteStorage<'a, IdentifiedItem>, + WriteExpect<'a, crate::map::MasterDungeonMap>, + ReadStorage<'a, Item>, + ReadStorage<'a, Name>, + WriteStorage<'a, ObfuscatedName>, + Entities<'a> + ); + + fn run(&mut self, data : Self::SystemData) { + let (player, mut identified, mut dm, items, names, mut obfuscated_names, entities) = data; + + for (_p, id) in (&player, &identified).join() { + if !dm.identified_items.contains(&id.name) && crate::raws::is_tag_magic(&id.name) { + dm.identified_items.insert(id.name.clone()); + + for (entity, _item, name) in (&entities, &items, &names).join() { + if name.name == id.name { + obfuscated_names.remove(entity); + } + } + } + } + + // Clean up + identified.clear(); + } +} diff --git a/chapter-75-darkplaza/src/systems/inventory_system/mod.rs b/chapter-75-darkplaza/src/systems/inventory_system/mod.rs new file mode 100644 index 00000000..4de2a746 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/inventory_system/mod.rs @@ -0,0 +1,43 @@ +use crate::{WantsToPickupItem, Name, InBackpack, Position, WantsToUseItem, + WantsToDropItem, Map, AreaOfEffect, Equippable, Equipped, WantsToRemoveItem, EquipmentChanged, + IdentifiedItem, Item, ObfuscatedName, MagicItem, MasterDungeonMap, CursedItem, WantsToCastSpell }; + +mod collection_system; +pub use collection_system::ItemCollectionSystem; +mod use_system; +pub use use_system::{ItemUseSystem, SpellUseSystem}; +mod drop_system; +pub use drop_system::ItemDropSystem; +mod remove_system; +pub use remove_system::ItemRemoveSystem; +mod identification_system; +pub use identification_system::ItemIdentificationSystem; +mod equip_use; +pub use equip_use::ItemEquipOnUse; +use specs::prelude::*; + +pub fn obfuscate_name( + item: Entity, + names: &ReadStorage::, + magic_items : &ReadStorage::, + obfuscated_names : &ReadStorage::, + dm : &MasterDungeonMap, +) -> String +{ + if let Some(name) = names.get(item) { + if magic_items.get(item).is_some() { + if dm.identified_items.contains(&name.name) { + name.name.clone() + } else if let Some(obfuscated) = obfuscated_names.get(item) { + obfuscated.name.clone() + } else { + "Unidentified magic item".to_string() + } + } else { + name.name.clone() + } + + } else { + "Nameless item (bug)".to_string() + } +} diff --git a/chapter-75-darkplaza/src/systems/inventory_system/remove_system.rs b/chapter-75-darkplaza/src/systems/inventory_system/remove_system.rs new file mode 100644 index 00000000..aa4ddfac --- /dev/null +++ b/chapter-75-darkplaza/src/systems/inventory_system/remove_system.rs @@ -0,0 +1,37 @@ +use specs::prelude::*; +use super::{InBackpack, Equipped, WantsToRemoveItem, CursedItem, Name, EquipmentChanged}; + +pub struct ItemRemoveSystem {} + +impl<'a> System<'a> for ItemRemoveSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( + Entities<'a>, + WriteStorage<'a, WantsToRemoveItem>, + WriteStorage<'a, Equipped>, + WriteStorage<'a, InBackpack>, + ReadStorage<'a, CursedItem>, + ReadStorage<'a, Name>, + WriteStorage<'a, EquipmentChanged> + ); + + fn run(&mut self, data : Self::SystemData) { + let (entities, mut wants_remove, mut equipped, mut backpack, cursed, names, mut dirty) = data; + + for (entity, to_remove) in (&entities, &wants_remove).join() { + if cursed.get(to_remove.item).is_some() { + crate::gamelog::Logger::new() + .append("You cannot remove") + .item_name(&names.get(to_remove.item).unwrap().name) + .append(" - it is cursed.") + .log(); + } else { + equipped.remove(to_remove.item); + backpack.insert(to_remove.item, InBackpack{ owner: entity }).expect("Unable to insert backpack"); + } + dirty.insert(entity, EquipmentChanged{}).expect("Unable to insert"); + } + + wants_remove.clear(); + } +} diff --git a/chapter-75-darkplaza/src/systems/inventory_system/use_system.rs b/chapter-75-darkplaza/src/systems/inventory_system/use_system.rs new file mode 100644 index 00000000..c5e2beb0 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/inventory_system/use_system.rs @@ -0,0 +1,103 @@ +use specs::prelude::*; +use super::{Name, WantsToUseItem,Map, AreaOfEffect, EquipmentChanged, IdentifiedItem, WantsToCastSpell}; +use crate::effects::*; + +pub struct ItemUseSystem {} + +impl<'a> System<'a> for ItemUseSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( ReadExpect<'a, Entity>, + WriteExpect<'a, Map>, + Entities<'a>, + WriteStorage<'a, WantsToUseItem>, + ReadStorage<'a, Name>, + ReadStorage<'a, AreaOfEffect>, + WriteStorage<'a, EquipmentChanged>, + WriteStorage<'a, IdentifiedItem> + ); + + #[allow(clippy::cognitive_complexity)] + fn run(&mut self, data : Self::SystemData) { + let (player_entity, map, entities, mut wants_use, names, + aoe, mut dirty, mut identified_item) = data; + + for (entity, useitem) in (&entities, &wants_use).join() { + dirty.insert(entity, EquipmentChanged{}).expect("Unable to insert"); + + // Identify + if entity == *player_entity { + identified_item.insert(entity, IdentifiedItem{ name: names.get(useitem.item).unwrap().name.clone() }) + .expect("Unable to insert"); + } + + // Call the effects system + add_effect( + Some(entity), + EffectType::ItemUse{ item : useitem.item }, + match useitem.target { + None => Targets::Single{ target: *player_entity }, + Some(target) => { + if let Some(aoe) = aoe.get(useitem.item) { + Targets::Tiles{ tiles: aoe_tiles(&*map, target, aoe.radius) } + } else { + Targets::Tile{ tile_idx : map.xy_idx(target.x, target.y) as i32 } + } + } + } + ); + + } + + wants_use.clear(); + } +} + +pub struct SpellUseSystem {} + +impl<'a> System<'a> for SpellUseSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( ReadExpect<'a, Entity>, + WriteExpect<'a, Map>, + Entities<'a>, + WriteStorage<'a, WantsToCastSpell>, + ReadStorage<'a, Name>, + ReadStorage<'a, AreaOfEffect>, + WriteStorage<'a, EquipmentChanged>, + WriteStorage<'a, IdentifiedItem> + ); + + #[allow(clippy::cognitive_complexity)] + fn run(&mut self, data : Self::SystemData) { + let (player_entity, map, entities, mut wants_use, names, + aoe, mut dirty, mut identified_item) = data; + + for (entity, useitem) in (&entities, &wants_use).join() { + dirty.insert(entity, EquipmentChanged{}).expect("Unable to insert"); + + // Identify + if entity == *player_entity { + identified_item.insert(entity, IdentifiedItem{ name: names.get(useitem.spell).unwrap().name.clone() }) + .expect("Unable to insert"); + } + + // Call the effects system + add_effect( + Some(entity), + EffectType::SpellUse{ spell : useitem.spell }, + match useitem.target { + None => Targets::Single{ target: *player_entity }, + Some(target) => { + if let Some(aoe) = aoe.get(useitem.spell) { + Targets::Tiles{ tiles: aoe_tiles(&*map, target, aoe.radius) } + } else { + Targets::Tile{ tile_idx : map.xy_idx(target.x, target.y) as i32 } + } + } + } + ); + + } + + wants_use.clear(); + } +} diff --git a/chapter-75-darkplaza/src/systems/lighting_system.rs b/chapter-75-darkplaza/src/systems/lighting_system.rs new file mode 100644 index 00000000..cb01684d --- /dev/null +++ b/chapter-75-darkplaza/src/systems/lighting_system.rs @@ -0,0 +1,40 @@ +use specs::prelude::*; +use crate::{Viewshed, Position, Map, LightSource}; +use rltk::RGB; + +pub struct LightingSystem {} + +impl<'a> System<'a> for LightingSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( WriteExpect<'a, Map>, + ReadStorage<'a, Viewshed>, + ReadStorage<'a, Position>, + ReadStorage<'a, LightSource>); + + fn run(&mut self, data : Self::SystemData) { + let (mut map, viewshed, positions, lighting) = data; + + if map.outdoors { + return; + } + + let black = RGB::from_f32(0.0, 0.0, 0.0); + for l in map.light.iter_mut() { + *l = black; + } + + for (viewshed, pos, light) in (&viewshed, &positions, &lighting).join() { + let light_point = rltk::Point::new(pos.x, pos.y); + let range_f = light.range as f32; + for t in viewshed.visible_tiles.iter() { + if t.x > 0 && t.x < map.width && t.y > 0 && t.y < map.height { + let idx = map.xy_idx(t.x, t.y); + let distance = rltk::DistanceAlg::Pythagoras.distance2d(light_point, *t); + let intensity = (range_f - distance) / range_f; + + map.light[idx] = map.light[idx] + (light.color * intensity); + } + } + } + } +} diff --git a/chapter-75-darkplaza/src/systems/map_indexing_system.rs b/chapter-75-darkplaza/src/systems/map_indexing_system.rs new file mode 100644 index 00000000..839e5783 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/map_indexing_system.rs @@ -0,0 +1,46 @@ +use specs::prelude::*; +use crate::{Map, Position, BlocksTile, Pools, spatial, TileSize}; + +pub struct MapIndexingSystem {} + +impl<'a> System<'a> for MapIndexingSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( ReadExpect<'a, Map>, + ReadStorage<'a, Position>, + ReadStorage<'a, BlocksTile>, + ReadStorage<'a, Pools>, + ReadStorage<'a, TileSize>, + Entities<'a>,); + + fn run(&mut self, data : Self::SystemData) { + let (map, position, blockers, pools, sizes, entities) = data; + + spatial::clear(); + spatial::populate_blocked_from_map(&*map); + for (entity, position) in (&entities, &position).join() { + let mut alive = true; + if let Some(pools) = pools.get(entity) { + if pools.hit_points.current < 1 { + alive = false; + } + } + if alive { + if let Some(size) = sizes.get(entity) { + // Multi-tile + for y in position.y .. position.y + size.y { + for x in position.x .. position.x + size.x { + if x > 0 && x < map.width-1 && y > 0 && y < map.height-1 { + let idx = map.xy_idx(x, y); + spatial::index_entity(entity, idx, blockers.get(entity).is_some()); + } + } + } + } else { + // Single tile + let idx = map.xy_idx(position.x, position.y); + spatial::index_entity(entity, idx, blockers.get(entity).is_some()); + } + } + } + } +} diff --git a/chapter-75-darkplaza/src/systems/melee_combat_system.rs b/chapter-75-darkplaza/src/systems/melee_combat_system.rs new file mode 100644 index 00000000..f48ff4bf --- /dev/null +++ b/chapter-75-darkplaza/src/systems/melee_combat_system.rs @@ -0,0 +1,187 @@ +use specs::prelude::*; +use crate::{Attributes, Skills, WantsToMelee, Name, + HungerClock, HungerState, Pools, skill_bonus, + Skill, Equipped, Weapon, EquipmentSlot, WeaponAttribute, Wearable, NaturalAttackDefense, + effects::*}; + +pub struct MeleeCombatSystem {} + +impl<'a> System<'a> for MeleeCombatSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( Entities<'a>, + WriteStorage<'a, WantsToMelee>, + ReadStorage<'a, Name>, + ReadStorage<'a, Attributes>, + ReadStorage<'a, Skills>, + ReadStorage<'a, HungerClock>, + ReadStorage<'a, Pools>, + ReadStorage<'a, Equipped>, + ReadStorage<'a, Weapon>, + ReadStorage<'a, Wearable>, + ReadStorage<'a, NaturalAttackDefense> + ); + + fn run(&mut self, data : Self::SystemData) { + let (entities, mut wants_melee, names, attributes, skills, + hunger_clock, pools, equipped_items, weapon, wearables, natural) = data; + + for (entity, wants_melee, name, attacker_attributes, attacker_skills, attacker_pools) in (&entities, &wants_melee, &names, &attributes, &skills, &pools).join() { + // Are the attacker and defender alive? Only attack if they are + let target_pools = pools.get(wants_melee.target).unwrap(); + let target_attributes = attributes.get(wants_melee.target).unwrap(); + let target_skills = skills.get(wants_melee.target).unwrap(); + if attacker_pools.hit_points.current > 0 && target_pools.hit_points.current > 0 { + let target_name = names.get(wants_melee.target).unwrap(); + + // Define the basic unarmed attack - overridden by wielding check below if a weapon is equipped + let mut weapon_info = Weapon{ + range: None, + attribute : WeaponAttribute::Might, + hit_bonus : 0, + damage_n_dice : 1, + damage_die_type : 4, + damage_bonus : 0, + proc_chance : None, + proc_target : None + }; + + if let Some(nat) = natural.get(entity) { + if !nat.attacks.is_empty() { + let attack_index = if nat.attacks.len()==1 { 0 } else { crate::rng::roll_dice(1, nat.attacks.len() as i32) as usize -1 }; + weapon_info.hit_bonus = nat.attacks[attack_index].hit_bonus; + weapon_info.damage_n_dice = nat.attacks[attack_index].damage_n_dice; + weapon_info.damage_die_type = nat.attacks[attack_index].damage_die_type; + weapon_info.damage_bonus = nat.attacks[attack_index].damage_bonus; + } + } + + let mut weapon_entity : Option = None; + for (weaponentity,wielded,melee) in (&entities, &equipped_items, &weapon).join() { + if wielded.owner == entity && wielded.slot == EquipmentSlot::Melee { + weapon_info = melee.clone(); + weapon_entity = Some(weaponentity); + } + } + + let natural_roll = crate::rng::roll_dice(1, 20); + let attribute_hit_bonus = if weapon_info.attribute == WeaponAttribute::Might + { attacker_attributes.might.bonus } + else { attacker_attributes.quickness.bonus}; + let skill_hit_bonus = skill_bonus(Skill::Melee, &*attacker_skills); + let weapon_hit_bonus = weapon_info.hit_bonus; + let mut status_hit_bonus = 0; + if let Some(hc) = hunger_clock.get(entity) { // Well-Fed grants +1 + if hc.state == HungerState::WellFed { + status_hit_bonus += 1; + } + } + let modified_hit_roll = natural_roll + attribute_hit_bonus + skill_hit_bonus + + weapon_hit_bonus + status_hit_bonus; + //println!("Natural roll: {}", natural_roll); + //println!("Modified hit roll: {}", modified_hit_roll); + + let mut armor_item_bonus_f = 0.0; + for (wielded,armor) in (&equipped_items, &wearables).join() { + if wielded.owner == wants_melee.target { + armor_item_bonus_f += armor.armor_class; + } + } + let base_armor_class = match natural.get(wants_melee.target) { + None => 10, + Some(nat) => nat.armor_class.unwrap_or(10) + }; + let armor_quickness_bonus = target_attributes.quickness.bonus; + let armor_skill_bonus = skill_bonus(Skill::Defense, &*target_skills); + let armor_item_bonus = armor_item_bonus_f as i32; + let armor_class = base_armor_class + armor_quickness_bonus + armor_skill_bonus + + armor_item_bonus; + + //println!("Armor class: {}", armor_class); + if natural_roll != 1 && (natural_roll == 20 || modified_hit_roll > armor_class) { + // Target hit! Until we support weapons, we're going with 1d4 + let base_damage = crate::rng::roll_dice(weapon_info.damage_n_dice, weapon_info.damage_die_type); + let attr_damage_bonus = attacker_attributes.might.bonus; + let skill_damage_bonus = skill_bonus(Skill::Melee, &*attacker_skills); + let weapon_damage_bonus = weapon_info.damage_bonus; + + let damage = i32::max(0, base_damage + attr_damage_bonus + + skill_damage_bonus + weapon_damage_bonus); + + /*println!("Damage: {} + {}attr + {}skill + {}weapon = {}", + base_damage, attr_damage_bonus, skill_damage_bonus, + weapon_damage_bonus, damage + );*/ + add_effect( + Some(entity), + EffectType::Damage{ amount: damage }, + Targets::Single{ target: wants_melee.target } + ); + crate::gamelog::Logger::new() + .npc_name(&name.name) + .append("hits") + .npc_name(&target_name.name) + .append("for") + .damage(damage) + .append("hp.") + .log(); + + // Proc effects + if let Some(chance) = &weapon_info.proc_chance { + let roll = crate::rng::roll_dice(1, 100); + //println!("Roll {}, Chance {}", roll, chance); + if roll <= (chance * 100.0) as i32 { + //println!("Proc!"); + let effect_target = if weapon_info.proc_target.unwrap() == "Self" { + Targets::Single{ target: entity } + } else { + Targets::Single { target : wants_melee.target } + }; + add_effect( + Some(entity), + EffectType::ItemUse{ item: weapon_entity.unwrap() }, + effect_target + ) + } + } + + } else if natural_roll == 1 { + // Natural 1 miss + crate::gamelog::Logger::new() + .color(rltk::CYAN) + .append(&name.name) + .color(rltk::WHITE) + .append("considers attacking") + .color(rltk::CYAN) + .append(&target_name.name) + .color(rltk::WHITE) + .append("but misjudges the timing!") + .log(); + add_effect( + None, + EffectType::Particle{ glyph: rltk::to_cp437('‼'), fg: rltk::RGB::named(rltk::BLUE), bg : rltk::RGB::named(rltk::BLACK), lifespan: 200.0 }, + Targets::Single{ target: wants_melee.target } + ); + } else { + // Miss + crate::gamelog::Logger::new() + .color(rltk::CYAN) + .append(&name.name) + .color(rltk::WHITE) + .append("attacks") + .color(rltk::CYAN) + .append(&target_name.name) + .color(rltk::WHITE) + .append("but can't connect.") + .log(); + add_effect( + None, + EffectType::Particle{ glyph: rltk::to_cp437('‼'), fg: rltk::RGB::named(rltk::CYAN), bg : rltk::RGB::named(rltk::BLACK), lifespan: 200.0 }, + Targets::Single{ target: wants_melee.target } + ); + } + } + } + + wants_melee.clear(); + } +} diff --git a/chapter-75-darkplaza/src/systems/mod.rs b/chapter-75-darkplaza/src/systems/mod.rs new file mode 100644 index 00000000..43a5ec43 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/mod.rs @@ -0,0 +1,30 @@ +mod dispatcher; +pub use dispatcher::UnifiedDispatcher; + +// System imports +mod map_indexing_system; +use map_indexing_system::MapIndexingSystem; +mod visibility_system; +use visibility_system::VisibilitySystem; +mod ai; +use ai::*; +mod movement_system; +use movement_system::MovementSystem; +mod trigger_system; +use trigger_system::TriggerSystem; +mod melee_combat_system; +use melee_combat_system::MeleeCombatSystem; +mod ranged_combat_system; +use ranged_combat_system::RangedCombatSystem; +mod inventory_system; +use inventory_system::*; +mod hunger_system; +use hunger_system::HungerSystem; +pub mod particle_system; +use particle_system::ParticleSpawnSystem; +mod lighting_system; +use lighting_system::LightingSystem; + +pub fn build() -> Box { + dispatcher::new() +} \ No newline at end of file diff --git a/chapter-75-darkplaza/src/systems/movement_system.rs b/chapter-75-darkplaza/src/systems/movement_system.rs new file mode 100644 index 00000000..dcc5d56d --- /dev/null +++ b/chapter-75-darkplaza/src/systems/movement_system.rs @@ -0,0 +1,61 @@ +use specs::prelude::*; +use crate::{Map, Position, BlocksTile, ApplyMove, ApplyTeleport, OtherLevelPosition, EntityMoved, + Viewshed, RunState}; + +pub struct MovementSystem {} + +impl<'a> System<'a> for MovementSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( WriteExpect<'a, Map>, + WriteStorage<'a, Position>, + ReadStorage<'a, BlocksTile>, + Entities<'a>, + WriteStorage<'a, ApplyMove>, + WriteStorage<'a, ApplyTeleport>, + WriteStorage<'a, OtherLevelPosition>, + WriteStorage<'a, EntityMoved>, + WriteStorage<'a, Viewshed>, + ReadExpect<'a, Entity>, + WriteExpect<'a, RunState>); + + fn run(&mut self, data : Self::SystemData) { + let (mut map, mut position, blockers, entities, mut apply_move, + mut apply_teleport, mut other_level, mut moved, + mut viewsheds, player_entity, mut runstate) = data; + + // Apply teleports + for (entity, teleport) in (&entities, &apply_teleport).join() { + if teleport.dest_depth == map.depth { + apply_move.insert(entity, ApplyMove{ dest_idx: map.xy_idx(teleport.dest_x, teleport.dest_y) }) + .expect("Unable to insert"); + } else if entity == *player_entity { + *runstate = RunState::TeleportingToOtherLevel{ x: teleport.dest_x, y: teleport.dest_y, depth: teleport.dest_depth }; + } else if let Some(pos) = position.get(entity) { + let idx = map.xy_idx(pos.x, pos.y); + let dest_idx = map.xy_idx(teleport.dest_x, teleport.dest_y); + crate::spatial::move_entity(entity, idx, dest_idx); + other_level.insert(entity, OtherLevelPosition{ + x: teleport.dest_x, + y: teleport.dest_y, + depth: teleport.dest_depth }) + .expect("Unable to insert"); + position.remove(entity); + } + } + apply_teleport.clear(); + + // Apply broad movement + for (entity, movement, mut pos) in (&entities, &apply_move, &mut position).join() { + let start_idx = map.xy_idx(pos.x, pos.y); + let dest_idx = movement.dest_idx as usize; + crate::spatial::move_entity(entity, start_idx, dest_idx); + pos.x = movement.dest_idx as i32 % map.width; + pos.y = movement.dest_idx as i32 / map.width; + if let Some(vs) = viewsheds.get_mut(entity) { + vs.dirty = true; + } + moved.insert(entity, EntityMoved{}).expect("Unable to insert"); + } + apply_move.clear(); + } +} diff --git a/chapter-75-darkplaza/src/systems/particle_system.rs b/chapter-75-darkplaza/src/systems/particle_system.rs new file mode 100644 index 00000000..0e4faa9d --- /dev/null +++ b/chapter-75-darkplaza/src/systems/particle_system.rs @@ -0,0 +1,86 @@ +use specs::prelude::*; +use crate::{ Rltk, ParticleLifetime, Position, Renderable }; +use rltk::RGB; + +pub fn update_particles(ecs : &mut World, ctx : &Rltk) { + let mut dead_particles : Vec = Vec::new(); + { + // Age out particles + let mut particles = ecs.write_storage::(); + let entities = ecs.entities(); + for (entity, mut particle) in (&entities, &mut particles).join() { + if let Some(animation) = &mut particle.animation { + animation.timer += ctx.frame_time_ms; + if animation.timer > animation.step_time && animation.current_step < animation.path.len()-2 { + animation.current_step += 1; + + if let Some(pos) = ecs.write_storage::().get_mut(entity) { + pos.x = animation.path[animation.current_step].x; + pos.y = animation.path[animation.current_step].y; + } + } + } + + particle.lifetime_ms -= ctx.frame_time_ms; + if particle.lifetime_ms < 0.0 { + dead_particles.push(entity); + } + } + } + for dead in dead_particles.iter() { + ecs.delete_entity(*dead).expect("Particle will not die"); + } +} + +struct ParticleRequest { + x: i32, + y: i32, + fg: RGB, + bg: RGB, + glyph: rltk::FontCharType, + lifetime: f32 +} + +pub struct ParticleBuilder { + requests : Vec +} + +impl ParticleBuilder { + #[allow(clippy::new_without_default)] + pub fn new() -> ParticleBuilder { + ParticleBuilder{ requests : Vec::new() } + } + + pub fn request(&mut self, x:i32, y:i32, fg: RGB, bg:RGB, glyph: rltk::FontCharType, lifetime: f32) { + self.requests.push( + ParticleRequest{ + x, y, fg, bg, glyph, lifetime + } + ); + } +} + +pub struct ParticleSpawnSystem {} + +impl<'a> System<'a> for ParticleSpawnSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( + Entities<'a>, + WriteStorage<'a, Position>, + WriteStorage<'a, Renderable>, + WriteStorage<'a, ParticleLifetime>, + WriteExpect<'a, ParticleBuilder> + ); + + fn run(&mut self, data : Self::SystemData) { + let (entities, mut positions, mut renderables, mut particles, mut particle_builder) = data; + for new_particle in particle_builder.requests.iter() { + let p = entities.create(); + positions.insert(p, Position{ x: new_particle.x, y: new_particle.y }).expect("Unable to inser position"); + renderables.insert(p, Renderable{ fg: new_particle.fg, bg: new_particle.bg, glyph: new_particle.glyph, render_order: 0 }).expect("Unable to insert renderable"); + particles.insert(p, ParticleLifetime{ lifetime_ms: new_particle.lifetime, animation: None }).expect("Unable to insert lifetime"); + } + + particle_builder.requests.clear(); + } +} diff --git a/chapter-75-darkplaza/src/systems/ranged_combat_system.rs b/chapter-75-darkplaza/src/systems/ranged_combat_system.rs new file mode 100644 index 00000000..7cbc904e --- /dev/null +++ b/chapter-75-darkplaza/src/systems/ranged_combat_system.rs @@ -0,0 +1,205 @@ +use specs::prelude::*; +use crate::{Attributes, Skills, WantsToShoot, Name, + HungerClock, HungerState, Pools, skill_bonus, + Skill, Equipped, Weapon, EquipmentSlot, WeaponAttribute, + Wearable, NaturalAttackDefense, + effects::*, Map, Position}; +use rltk::{to_cp437, RGB, Point}; + +pub struct RangedCombatSystem {} + +impl<'a> System<'a> for RangedCombatSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( Entities<'a>, + WriteStorage<'a, WantsToShoot>, + ReadStorage<'a, Name>, + ReadStorage<'a, Attributes>, + ReadStorage<'a, Skills>, + ReadStorage<'a, HungerClock>, + ReadStorage<'a, Pools>, + ReadStorage<'a, Equipped>, + ReadStorage<'a, Weapon>, + ReadStorage<'a, Wearable>, + ReadStorage<'a, NaturalAttackDefense>, + ReadStorage<'a, Position>, + ReadExpect<'a, Map> + ); + + fn run(&mut self, data : Self::SystemData) { + let (entities, mut wants_shoot, names, attributes, skills, + hunger_clock, pools, equipped_items, weapon, wearables, natural, + positions, map) = data; + + for (entity, wants_shoot, name, attacker_attributes, attacker_skills, attacker_pools) in (&entities, &wants_shoot, &names, &attributes, &skills, &pools).join() { + // Are the attacker and defender alive? Only attack if they are + let target_pools = pools.get(wants_shoot.target).unwrap(); + let target_attributes = attributes.get(wants_shoot.target).unwrap(); + let target_skills = skills.get(wants_shoot.target).unwrap(); + if attacker_pools.hit_points.current > 0 && target_pools.hit_points.current > 0 { + let target_name = names.get(wants_shoot.target).unwrap(); + + // Fire projectile effect + let apos = positions.get(entity).unwrap(); + let dpos = positions.get(wants_shoot.target).unwrap(); + add_effect( + None, + EffectType::ParticleProjectile{ + glyph: to_cp437('*'), + fg : RGB::named(rltk::CYAN), + bg : RGB::named(rltk::BLACK), + lifespan : 300.0, + speed: 50.0, + path: rltk::line2d( + rltk::LineAlg::Bresenham, + Point::new(apos.x, apos.y), + Point::new(dpos.x, dpos.y) + ) + }, + Targets::Tile{tile_idx : map.xy_idx(apos.x, apos.y) as i32} + ); + + // Define the basic unarmed attack - overridden by wielding check below if a weapon is equipped + let mut weapon_info = Weapon{ + range: None, + attribute : WeaponAttribute::Might, + hit_bonus : 0, + damage_n_dice : 1, + damage_die_type : 4, + damage_bonus : 0, + proc_chance : None, + proc_target : None + }; + + if let Some(nat) = natural.get(entity) { + if !nat.attacks.is_empty() { + let attack_index = if nat.attacks.len()==1 { 0 } else { crate::rng::roll_dice(1, nat.attacks.len() as i32) as usize -1 }; + weapon_info.hit_bonus = nat.attacks[attack_index].hit_bonus; + weapon_info.damage_n_dice = nat.attacks[attack_index].damage_n_dice; + weapon_info.damage_die_type = nat.attacks[attack_index].damage_die_type; + weapon_info.damage_bonus = nat.attacks[attack_index].damage_bonus; + } + } + + let mut weapon_entity : Option = None; + for (weaponentity,wielded,melee) in (&entities, &equipped_items, &weapon).join() { + if wielded.owner == entity && wielded.slot == EquipmentSlot::Melee { + weapon_info = melee.clone(); + weapon_entity = Some(weaponentity); + } + } + + let natural_roll = crate::rng::roll_dice(1, 20); + let attribute_hit_bonus = if weapon_info.attribute == WeaponAttribute::Might + { attacker_attributes.might.bonus } + else { attacker_attributes.quickness.bonus}; + let skill_hit_bonus = skill_bonus(Skill::Melee, &*attacker_skills); + let weapon_hit_bonus = weapon_info.hit_bonus; + let mut status_hit_bonus = 0; + if let Some(hc) = hunger_clock.get(entity) { // Well-Fed grants +1 + if hc.state == HungerState::WellFed { + status_hit_bonus += 1; + } + } + let modified_hit_roll = natural_roll + attribute_hit_bonus + skill_hit_bonus + + weapon_hit_bonus + status_hit_bonus; + //println!("Natural roll: {}", natural_roll); + //println!("Modified hit roll: {}", modified_hit_roll); + + let mut armor_item_bonus_f = 0.0; + for (wielded,armor) in (&equipped_items, &wearables).join() { + if wielded.owner == wants_shoot.target { + armor_item_bonus_f += armor.armor_class; + } + } + let base_armor_class = match natural.get(wants_shoot.target) { + None => 10, + Some(nat) => nat.armor_class.unwrap_or(10) + }; + let armor_quickness_bonus = target_attributes.quickness.bonus; + let armor_skill_bonus = skill_bonus(Skill::Defense, &*target_skills); + let armor_item_bonus = armor_item_bonus_f as i32; + let armor_class = base_armor_class + armor_quickness_bonus + armor_skill_bonus + + armor_item_bonus; + + //println!("Armor class: {}", armor_class); + if natural_roll != 1 && (natural_roll == 20 || modified_hit_roll > armor_class) { + // Target hit! Until we support weapons, we're going with 1d4 + let base_damage = crate::rng::roll_dice(weapon_info.damage_n_dice, weapon_info.damage_die_type); + let attr_damage_bonus = attacker_attributes.might.bonus; + let skill_damage_bonus = skill_bonus(Skill::Melee, &*attacker_skills); + let weapon_damage_bonus = weapon_info.damage_bonus; + + let damage = i32::max(0, base_damage + attr_damage_bonus + + skill_damage_bonus + weapon_damage_bonus); + + /*println!("Damage: {} + {}attr + {}skill + {}weapon = {}", + base_damage, attr_damage_bonus, skill_damage_bonus, + weapon_damage_bonus, damage + );*/ + add_effect( + Some(entity), + EffectType::Damage{ amount: damage }, + Targets::Single{ target: wants_shoot.target } + ); + crate::gamelog::Logger::new() + .npc_name(&name.name) + .append("hits") + .npc_name(&target_name.name) + .append("for") + .damage(damage) + .append("hp.") + .log(); + + // Proc effects + if let Some(chance) = &weapon_info.proc_chance { + let roll = crate::rng::roll_dice(1, 100); + //println!("Roll {}, Chance {}", roll, chance); + if roll <= (chance * 100.0) as i32 { + //println!("Proc!"); + let effect_target = if weapon_info.proc_target.unwrap() == "Self" { + Targets::Single{ target: entity } + } else { + Targets::Single { target : wants_shoot.target } + }; + add_effect( + Some(entity), + EffectType::ItemUse{ item: weapon_entity.unwrap() }, + effect_target + ) + } + } + + } else if natural_roll == 1 { + // Natural 1 miss + crate::gamelog::Logger::new() + .npc_name(&name.name) + .append("considers attacking") + .npc_name(&target_name.name) + .append("but misjudges the timing!") + .log(); + add_effect( + None, + EffectType::Particle{ glyph: rltk::to_cp437('‼'), fg: rltk::RGB::named(rltk::BLUE), bg : rltk::RGB::named(rltk::BLACK), lifespan: 200.0 }, + Targets::Single{ target: wants_shoot.target } + ); + } else { + // Miss + crate::gamelog::Logger::new() + .npc_name(&name.name) + .append("attacks") + .npc_name(&target_name.name) + .color(rltk::WHITE) + .append("but can't connect.") + .log(); + add_effect( + None, + EffectType::Particle{ glyph: rltk::to_cp437('‼'), fg: rltk::RGB::named(rltk::CYAN), bg : rltk::RGB::named(rltk::BLACK), lifespan: 200.0 }, + Targets::Single{ target: wants_shoot.target } + ); + } + } + } + + wants_shoot.clear(); + } +} diff --git a/chapter-75-darkplaza/src/systems/trigger_system.rs b/chapter-75-darkplaza/src/systems/trigger_system.rs new file mode 100644 index 00000000..9131758a --- /dev/null +++ b/chapter-75-darkplaza/src/systems/trigger_system.rs @@ -0,0 +1,60 @@ +use specs::prelude::*; +use crate::{EntityMoved, Position, EntryTrigger, Map, Name, + effects::*, AreaOfEffect}; + +pub struct TriggerSystem {} + +impl<'a> System<'a> for TriggerSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( ReadExpect<'a, Map>, + WriteStorage<'a, EntityMoved>, + ReadStorage<'a, Position>, + ReadStorage<'a, EntryTrigger>, + ReadStorage<'a, Name>, + Entities<'a>, + ReadStorage<'a, AreaOfEffect>); + + fn run(&mut self, data : Self::SystemData) { + let (map, mut entity_moved, position, entry_trigger, + names, entities, area_of_effect) = data; + + // Iterate the entities that moved and their final position + for (entity, mut _entity_moved, pos) in (&entities, &mut entity_moved, &position).join() { + let idx = map.xy_idx(pos.x, pos.y); + crate::spatial::for_each_tile_content(idx, |entity_id| { + if entity != entity_id { // Do not bother to check yourself for being a trap! + let maybe_trigger = entry_trigger.get(entity_id); + match maybe_trigger { + None => {}, + Some(_trigger) => { + // We triggered it + let name = names.get(entity_id); + if let Some(name) = name { + crate::gamelog::Logger::new() + .item_name(&name.name) + .append("triggers!") + .log(); + } + + // Call the effects system + add_effect( + Some(entity), + EffectType::TriggerFire{ trigger : entity_id }, + if let Some(aoe) = area_of_effect.get(entity_id) { + Targets::Tiles{ + tiles : aoe_tiles(&*map, rltk::Point::new(pos.x, pos.y), aoe.radius) + } + } else { + Targets::Tile{ tile_idx: idx as i32 } + } + ); + } + } + } + }); + } + + // Remove all entity movement markers + entity_moved.clear(); + } +} diff --git a/chapter-75-darkplaza/src/systems/visibility_system.rs b/chapter-75-darkplaza/src/systems/visibility_system.rs new file mode 100644 index 00000000..ee691f34 --- /dev/null +++ b/chapter-75-darkplaza/src/systems/visibility_system.rs @@ -0,0 +1,66 @@ +use specs::prelude::*; +use crate::{Viewshed, Position, Map, Player, Hidden, BlocksVisibility, Name}; +use rltk::{field_of_view, Point}; + +pub struct VisibilitySystem {} + +impl<'a> System<'a> for VisibilitySystem { + #[allow(clippy::type_complexity)] + type SystemData = ( WriteExpect<'a, Map>, + Entities<'a>, + WriteStorage<'a, Viewshed>, + ReadStorage<'a, Position>, + ReadStorage<'a, Player>, + WriteStorage<'a, Hidden>, + ReadStorage<'a, Name>, + ReadStorage<'a, BlocksVisibility>); + + fn run(&mut self, data : Self::SystemData) { + let (mut map, entities, mut viewshed, pos, player, + mut hidden, names, blocks_visibility) = data; + + map.view_blocked.clear(); + for (block_pos, _block) in (&pos, &blocks_visibility).join() { + let idx = map.xy_idx(block_pos.x, block_pos.y); + map.view_blocked.insert(idx); + } + + for (ent,viewshed,pos) in (&entities, &mut viewshed, &pos).join() { + if viewshed.dirty { + viewshed.dirty = false; + viewshed.visible_tiles = field_of_view(Point::new(pos.x, pos.y), viewshed.range, &*map); + viewshed.visible_tiles.retain(|p| p.x >= 0 && p.x < map.width && p.y >= 0 && p.y < map.height ); + + // If this is the player, reveal what they can see + let _p : Option<&Player> = player.get(ent); + if let Some(_p) = _p { + for t in map.visible_tiles.iter_mut() { *t = false }; + for vis in viewshed.visible_tiles.iter() { + if vis.x > 0 && vis.x < map.width-1 && vis.y > 0 && vis.y < map.height-1 { + let idx = map.xy_idx(vis.x, vis.y); + map.revealed_tiles[idx] = true; + map.visible_tiles[idx] = true; + + // Chance to reveal hidden things + crate::spatial::for_each_tile_content(idx, |e| { + let maybe_hidden = hidden.get(e); + if let Some(_maybe_hidden) = maybe_hidden { + if crate::rng::roll_dice(1,24)==1 { + let name = names.get(e); + if let Some(name) = name { + crate::gamelog::Logger::new() + .append("You spotted:") + .npc_name(&name.name) + .log(); + } + hidden.remove(e); + } + } + }); + } + } + } + } + } + } +}