Compare commits

...

126 commits

Author SHA1 Message Date
Llywelwyn
c29b93337c
Update README.md 2025-03-09 11:12:11 +00:00
Llywelwyn
0584d07a1f
Update cargo-build-test.yml to use ubuntu-22.04 2025-03-09 11:04:32 +00:00
Llywelwyn
45b9b33039
Update cargo-build-test.yml 2025-03-09 11:02:16 +00:00
Lewis Wynne
bdcd55c8a5 Fixes miscoloured logs (fixes #26) 2025-03-09 10:50:35 +00:00
Llywelwyn
99c17f8521 draw hunger uses Point 2024-06-17 23:22:30 +01:00
Llywelwyn
6324449c16 draw_hunger() 2024-06-17 23:19:20 +01:00
Llywelwyn
d465592c0f abstracts ui drawing 2024-06-16 10:31:06 +01:00
Llywelwyn
f494efbf3f bump ver 2024-06-15 20:41:21 +01:00
Llywelwyn
ba5d120fef fix warns, bump ver 2024-06-15 20:40:51 +01:00
Llywelwyn
c5106a63b5 static inventory keys - items remember their slots
this is the biggest refactor of my entire life
2024-06-15 20:14:38 +01:00
Llywelwyn
9719ebbe88 docs tldr 2024-06-15 17:35:40 +01:00
Llywelwyn
2eaf431942 disable show_mapgen for default config 2024-06-15 17:24:37 +01:00
Llywelwyn
a7c5d2167c back to serde_json 2024-06-15 16:46:15 +01:00
Llywelwyn
678636c57d Revert "Infallible -> NoError"
This reverts commit 9c8f301491.
2024-06-15 16:44:13 +01:00
Llywelwyn
30697a98bb rm room_accretion for now 2024-06-15 16:43:17 +01:00
Llywelwyn
c73f9a5458 Revert "cherry pick -> serde_json saves to bincode"
This reverts commit 180532ee3e.
2024-06-15 16:42:59 +01:00
Llywelwyn
9c8f301491 Infallible -> NoError 2023-10-24 23:32:42 +01:00
Llywelwyn
180532ee3e cherry pick -> serde_json saves to bincode 2023-10-24 11:13:43 +01:00
Llywelwyn
46bbe14bea added effects for adding intrinsics 2023-10-02 23:02:46 +01:00
Llywelwyn
fa4612cf1f changed get_noncursed() to helper on BUC struct 2023-10-02 23:02:34 +01:00
Llywelwyn
4d21bd46d4 add_intr!() macro for adding intrinsics to the Player
If needed, player can just be replaced by another arg to the macro so this works on every other entity - but right now the player is the only thing to gain/lose intrinsics.
2023-10-02 22:14:00 +01:00
Llywelwyn
b5743819ec .describe() for Intrinsics, for use in tooltips later 2023-10-02 21:11:12 +01:00
Llywelwyn
190543a361 move all doors to the ends of corridors 2023-10-02 07:39:45 +01:00
Llywelwyn
7a27321bec initial tweaks - starting room w/ corridors + doors 2023-10-02 07:00:28 +01:00
Llywelwyn
97ca3a25e3 doors and door directions - RA 2023-10-02 04:43:01 +01:00
Llywelwyn
1fa7432dfe room accretion - initial 2023-10-01 20:56:46 +01:00
Llywelwyn
fa3b906dce adds article to ID morgue msg 2023-09-23 11:21:34 +01:00
Llywelwyn
2d33c90af8 fixes infini-dungeon difficulty 2023-09-23 11:12:28 +01:00
Llywelwyn
23a42bab80 Some panics to unreachable!, better error msging 2023-09-23 00:12:05 +01:00
Llywelwyn
ae8f0d15a0 unreachable! for debug entries 2023-09-22 23:59:15 +01:00
Llywelwyn
b6abfbce4a damage types: phys, magic, fire, cold, poison 2023-09-21 22:46:14 +01:00
Llywelwyn
de0aa33107 swaps over to bracket-lib's parse_dice_string(), and cl 2023-09-21 05:26:25 +01:00
Llywelwyn
921fee2ecc intrinsic speed + regeneration 2023-09-21 05:06:52 +01:00
Llywelwyn
e8b5f6d997 adds component tests to tests/mod.rs 2023-09-21 01:10:02 +01:00
Llywelwyn
654aea9a32 damage mod multiplier unit tests 2023-09-21 01:08:01 +01:00
Llywelwyn
dc4bcbe618 adds damage types to items 2023-09-21 00:53:04 +01:00
Llywelwyn
8a44c94272 adds damage types and mods (weak/resist/immune), for all damage events 2023-09-21 00:52:54 +01:00
Llywelwyn
66013667d8 gamelog events unit tests 2023-09-20 23:21:38 +01:00
Llywelwyn
58e4742f12 more map tests 2023-09-20 21:56:57 +01:00
Llywelwyn
727ca09e74 defaults placeholder player to human ancestry 2023-09-20 21:56:50 +01:00
Llywelwyn
222c7cc914 dels redunant resources - most of these are built-in with bracket-lib 2023-09-20 21:37:05 +01:00
Llywelwyn
335af8ee60 cl to md + versioned, instead of dated 2023-09-20 21:31:59 +01:00
Llywelwyn
954991fd9c defaults vision range in raws, only needs specifying if abnormal 2023-09-20 20:33:05 +01:00
Llywelwyn
c4a87d9813 removes trailing commas 2023-09-20 20:25:29 +01:00
Llywelwyn
421c87c972 mobs: wargs, felines 2023-09-20 14:51:16 +01:00
Llywelwyn
583afa7078 mobs: insects 2023-09-20 13:14:56 +01:00
Llywelwyn
c4aa3de640 more linter clean-up 2023-09-18 21:54:29 +01:00
Llywelwyn
27c1fe9a48 cleans up linter warns 2023-09-18 21:54:18 +01:00
Llywelwyn
6d80b80a82 adds some abstractions for readability 2023-09-11 00:01:34 +01:00
Llywelwyn
65564e6f4c readme update w/ link to blog 2023-09-10 23:59:09 +01:00
Llywelwyn
85efe13dc5 made the switch to using bracket-lib directly, instead of rltk wrapper
this should solve the build issues; it makes using the non-crashing github build a lot easier, because it lets the explicit rltk dependency be removed.
2023-09-05 02:23:31 +01:00
Llywelwyn
455b8f2d80 updated wasm url 2023-09-04 10:13:30 +01:00
Llywelwyn
6c3af2878d manual wasm build
v0.8.7 bracket-lib has an error with building for wasm, so building locally for now
2023-09-04 05:53:46 +01:00
Llywelwyn
54b9b27aea Merge remote-tracking branch 'origin/master' 2023-09-04 05:42:09 +01:00
Llywelwyn
5c61d5bcc0
Delete Cargo.lock 2023-09-04 05:41:55 +01:00
Llywelwyn
e03c40776a Merge remote-tracking branch 'origin/master' 2023-09-04 05:41:52 +01:00
Llywelwyn
3962bd5e6f ignore cargo.lock 2023-09-04 05:41:30 +01:00
Llywelwyn
a7eb3da89d
Delete wasm/index.html 2023-09-04 05:40:52 +01:00
Llywelwyn
bece876c53
Delete wasm/index.css 2023-09-04 05:40:45 +01:00
Llywelwyn
4f1027df77 ignore index/css
these are just for local testing - we don't want them pushed through to gh-pages
2023-09-04 05:40:26 +01:00
Llywelwyn
6d0ae8edf8 spellcheck 2023-09-03 23:54:04 +01:00
Llywelwyn
fefc4349d5 Merge remote-tracking branch 'origin/master' 2023-09-03 22:46:11 +01:00
Llywelwyn
599729bd4c
only deploy when using a version tag 2023-09-03 22:44:14 +01:00
Llywelwyn
94ad42e0b0
inverts / 2023-09-03 22:38:46 +01:00
Llywelwyn
195d206dd7
Update deploy wasm to gh-pages.yml 2023-09-03 22:34:27 +01:00
Llywelwyn
9afb57badd
testing workflows for ci 2023-09-03 22:27:55 +01:00
Llywelwyn
d018d9077d fixes negative starting mana 2023-09-02 22:37:21 +01:00
Llywelwyn
fcb2bd2815 swapped over to bracket-lib geometry's Rects, improved feature spawning 2023-08-31 20:07:15 +01:00
Llywelwyn
5a8114ec7e removes debugging msg 2023-08-31 03:49:37 +01:00
Llywelwyn
56071a1537 w 2023-08-31 03:47:09 +01:00
Llywelwyn
1b12d70b23 various fixes: moved turnloss handling into energy system, anims 2023-08-31 03:44:04 +01:00
Llywelwyn
7b5cd0ec70 write config file if not present 2023-08-31 02:18:07 +01:00
Llywelwyn
cb7d9082d1 bugfix 2023-08-31 02:00:47 +01:00
Llywelwyn
45312108af bleeding 2 save 2023-08-31 01:28:44 +01:00
Llywelwyn
a48d85e056 map tests 2023-08-31 01:27:58 +01:00
Llywelwyn
a5f13d72c6 Merge remote-tracking branch 'origin/master' 2023-08-31 01:20:33 +01:00
Llywelwyn
3464e9447c imports to lib.rs, creating unit tests 2023-08-31 01:20:21 +01:00
Llywelwyn
568df55795 restricts overmap actions (item use/drop/kick/open/close) 2023-08-30 23:54:28 +01:00
Llywelwyn
2c936efb0e
Delete .prettierrc.json 2023-08-30 23:40:54 +01:00
Llywelwyn
45461495fd prettier config 2023-08-30 23:39:53 +01:00
Llywelwyn
a038a3f586 chance to log dungeon features per clock turn 2023-08-30 12:08:03 +01:00
Llywelwyn
050973eae4 barracks dungeon feature 2023-08-30 11:41:39 +01:00
Llywelwyn
6f99b879ff
Update README.md 2023-08-30 09:50:31 +01:00
Llywelwyn
9150ed39e4 cl
compounded everything into today's date. will try to update this as i go in the future
2023-08-30 09:45:43 +01:00
Llywelwyn
1f95bf14ee slows treants back down, reduces spawnrate in grassy rooms 2023-08-30 09:19:48 +01:00
Llywelwyn
64caf0dc1a less blocking - targets will try to path to any space around their tar 2023-08-30 09:15:45 +01:00
Llywelwyn
340aefa9e1 darkens sapling renderable so it blends in more 2023-08-30 07:16:10 +01:00
Llywelwyn
5d14bb8354 treant sapling 2023-08-30 07:14:28 +01:00
Llywelwyn
207f3b7671 fixes traffic jams - if path is blocked, npcs will recalc path to dest 2023-08-30 06:15:29 +01:00
Llywelwyn
454c2aab63 player now blockstile 2023-08-30 06:07:40 +01:00
Llywelwyn
8eb98b5baa player turn awaits empty particle queue 2023-08-30 05:58:08 +01:00
Llywelwyn
b5c4cb1fb5 ticker awaits an empty particle queue 2023-08-30 05:57:03 +01:00
Llywelwyn
9427ebd7c1 better ranged targeting visuals 2023-08-30 05:40:45 +01:00
Llywelwyn
e22261d012 see tolocal() tile descs in chat 2023-08-30 05:02:51 +01:00
Llywelwyn
9fb791039f map shortnames/identifiers 2023-08-30 04:51:32 +01:00
Llywelwyn
849a554055 foliage on world map 2023-08-30 03:55:12 +01:00
Llywelwyn
d6b9be628a targeting -> kb control 2023-08-30 03:53:14 +01:00
Llywelwyn
7da00ed2b1 x to farlook, now playable with 0 mouse input 2023-08-30 03:13:46 +01:00
Llywelwyn
81a48d5a6e reduces carry capacity per pt of strength 2023-08-30 02:16:49 +01:00
Llywelwyn
7d201b5736 removes wait on comma 2023-08-30 02:16:41 +01:00
Llywelwyn
fdb5156a48 reduces item spawns 2023-08-30 02:14:16 +01:00
Llywelwyn
3301feaae0 forest tweaks - foliage and proper grass 2023-08-30 02:01:29 +01:00
Llywelwyn
d032c847a0 some more options to config - colour offsets, and all-black tile bgs 2023-08-30 01:35:53 +01:00
Llywelwyn
72ec24c6b6 combat log to config 2023-08-27 23:53:54 +01:00
Llywelwyn
038e616500 cleans up chatlog colours - renderables for non-item, beatitude for item 2023-08-27 22:26:58 +01:00
Llywelwyn
96e69d5c5e unused vars cleanup 2023-08-27 18:08:29 +01:00
Llywelwyn
537e19c4e7 tooltips for overmap tiles, and distinct fg-bg offsets 2023-08-27 18:04:53 +01:00
Llywelwyn
2890c16a3c FillEdges map builder, to fill walkable edges with overmap transition 2023-08-27 16:36:33 +01:00
Llywelwyn
8e3ed5cead huge refactor of overmap-local map travel 2023-08-27 16:21:15 +01:00
Llywelwyn
02be487334 town -> woods -> one floor of dungeon; infinite dungeon elsewhere
just trying things out.
2023-08-27 03:43:25 +01:00
Llywelwyn
486807fc84 names to file 2023-08-27 03:13:58 +01:00
Llywelwyn
e1eae7efaf overmap travel - needs refactoring urgently, but it works for now 2023-08-27 03:00:48 +01:00
Llywelwyn
00dea1a55e fixes master dungeon map bug - map params weren't being saved
it was saving the version of the map at creation (e.g. no viewsheds, no bloodstains, no tile changes), and never updating it on transition
2023-08-27 00:21:01 +01:00
Llywelwyn
38eed3e483 refactor, and overmap (heavy wip) 2023-08-27 00:06:29 +01:00
Llywelwyn
746de971f0 overmap, refactor offsets 2023-08-26 22:46:04 +01:00
Llywelwyn
9e294a1680 better death messages, and morgue file map fix 2023-08-26 17:48:04 +01:00
Llywelwyn
9ac2adc5d6 improves morgue file significant event logging, bumps wasm build 2023-08-26 12:45:24 +01:00
Llywelwyn
738484436b significant events in morgue file, better event logging 2023-08-25 22:43:50 +01:00
Llywelwyn
de5dacb2ba improves morgue files - writes to console on wasm 2023-08-25 06:58:54 +01:00
Llywelwyn
14a6e46669 morgue files 2023-08-25 03:24:20 +01:00
Llywelwyn
ad9d40d8ed removes redundant error type 2023-08-24 23:01:33 +01:00
Llywelwyn
73f511775e slightly more graceful config reading: reads entry by entry
reads from default only if an entry is not present, and then writes those defaults to the file if anything was changed
2023-08-24 22:59:25 +01:00
Llywelwyn
6c727b056e Merge remote-tracking branch 'origin/master' 2023-08-24 22:34:19 +01:00
Llywelwyn
3023a33cc5 i/o stuff: skeleton framework for morgue files, and a config.toml file 2023-08-24 22:34:05 +01:00
Llywelwyn
2976c4ea06
cargo build/test wf 2023-08-24 00:30:00 +01:00
Llywelwyn
da77a01067
build instructions 2023-08-23 23:14:41 +01:00
142 changed files with 6725 additions and 6043 deletions

22
.github/workflows/cargo-build-test.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: Rust
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

View file

@ -0,0 +1,34 @@
on: push
name: Build and deploy web page with WASM version
jobs:
build:
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout project
uses: actions/checkout@v3
- name: Setup - Rust
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
target: wasm32-unknown-unknown
override: true
- name: Setup - wasm-bindgen
uses: jetli/wasm-bindgen-action@v0.2.0
- name: Build, bind WASM
run: |
cargo build --release --target wasm32-unknown-unknown
wasm-bindgen target/wasm32-unknown-unknown/release/rust-rl.wasm --out-dir wasm --no-modules --no-typescript
- name: Publish web\ to gh-pages
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
cd wasm/
git init --initial-branch=master
git config user.name "GitHub Actions"
git config user.email "github-actions-bot@users.noreply.github.com"
git add .
git commit -m "Deploy ${GITHUB_REPOSITORY} to ${GITHUB_REPOSITORY}:gh-pages"
git push --force "https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" master:gh-pages
echo "Deploy complete"

12
.gitignore vendored
View file

@ -1,11 +1,19 @@
# Build files, documentation, benchmarks # Build files, documentation, benchmarks
target target
wasm/index.css
wasm/index.html
docs/gifs/* docs/gifs/*
# VSCode/IDE config files # VSCode/IDE config files
Cargo.lock
.vscode/* .vscode/*
.rustfmt.toml .rustfmt.toml
.prettierignore .prettierignore
.prettierrc.json
# Savegame # Save files, morgue files
savegame.json savegame.json
morgue
# A default user config will be written on first run, if not present
config.toml

View file

@ -1,3 +0,0 @@
{
"printWidth": 120
}

3358
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,18 @@
[package] [package]
name = "rust-rl" name = "rust-rl"
version = "0.1.1" version = "0.1.4"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
rltk = { version = "^0.8.7", features = ["serde"] }
bracket-lib = { git = "https://github.com/amethyst/bracket-lib.git", rev = "851f6f08675444fb6fa088b9e67bee9fd75554c6", features = ["serde"] } bracket-lib = { git = "https://github.com/amethyst/bracket-lib.git", rev = "851f6f08675444fb6fa088b9e67bee9fd75554c6", features = ["serde"] }
regex = "1.3.6" regex = "1.3.6"
specs = { version = "0.16.1", features = ["serde"] } specs = { version = "0.16.1", features = ["serde"] }
specs-derive = "0.4.1" specs-derive = "0.4.1"
serde = { version = "1.0.93", features = ["derive"]} serde = { version = "1.0.93", features = ["derive"]}
serde_json = "1.0.39" serde_json = "1.0.39"
toml = "0.5"
lazy_static = "1.4.0" lazy_static = "1.4.0"
[dev-dependencies] [dev-dependencies]

View file

@ -1,15 +1,19 @@
## a roguelike in rust, playable @ [llywelwyn.github.io](https://llywelwyn.github.io/) ## a roguelike in rust, playable @ [llyw.co.uk/rl/](https://llyw.co.uk/rl/)
#### using _rltk/bracket-lib_, and _specs_ #### using _rltk/bracket-lib_, and _specs_
[![Rust](https://github.com/Llywelwyn/rust-rl/actions/workflows/cargo-build-test.yml/badge.svg)](https://github.com/Llywelwyn/rust-rl/actions/workflows/cargo-build-test.yml)
check out the page in the header for the wasm version, pick [a release](https://github.com/Llywelwyn/rust-rl/releases), or build manually with:
`git clone https://github.com/Llywelwyn/rust-rl/ && cd rust-rl && cargo build --release`,
![image](https://github.com/Llywelwyn/rust-rl/assets/82828093/b05e4f0b-2062-4abe-9fee-c679f9ef420d) ![image](https://github.com/Llywelwyn/rust-rl/assets/82828093/b05e4f0b-2062-4abe-9fee-c679f9ef420d)
this year for roguelikedev does the complete tutorial, i'm following along with thebracket's [_roguelike tutorial - in rust_](https://bfnightly.bracketproductions.com). for most of the 8 weeks, i'll probably just be working through the content rather than diverging too much into doing my own thing, since it's lengthy and i'd rather finish in time. that said, the ultimate aim here is to strip out the vast majority of the existing entities and replace them with my own, using the systems and components from the tutorial as a jumping-off point for something of my own making.
i'll try to remember to update the web version on my page at the end of every week
--- ---
<details>
<summary>boring details about the sprint where this project started</summary>
<details> <details>
<summary>week 1</summary> <summary>week 1</summary>
@ -153,3 +157,4 @@ i'll try to remember to update the web version on my page at the end of every we
![squares](https://github.com/Llywelwyn/rust-rl/assets/82828093/b752e1cb-340d-475d-84ae-68fdb4977a80) ![squares](https://github.com/Llywelwyn/rust-rl/assets/82828093/b752e1cb-340d-475d-84ae-68fdb4977a80)
</details> </details>
</details>

View file

@ -1,5 +1,5 @@
use criterion::{ black_box, criterion_group, criterion_main, Criterion }; use criterion::{ black_box, criterion_group, criterion_main, Criterion };
use rltk::RGB; use bracket_lib::prelude::*;
/// Benchmarks methods from rltk used to desaturate non-visible tiles. /// Benchmarks methods from rltk used to desaturate non-visible tiles.
// Greyscale is significantly faster, but generally looks worse - the // Greyscale is significantly faster, but generally looks worse - the

20
changelog.md Normal file
View file

@ -0,0 +1,20 @@
## v0.1.4
### added
- **overmap**: bare, but exists. player now starts on the overworld, and can move to local maps (like the old starting town) via >. can leave local maps back to the overmap by walking out of the map boundaries.
- **intrinsics**: speed, regeneration
- **damage types**: immunities, weaknesses, and resistances
- **full keyboard support**: examining and targeting can now be done via keyboard only
- **a config file** read at runtime, unfortunately not compatible with WASM builds yet
- **morgue files**: y/n prompt to write a morgue file on death to /morgue/foo.txt, or to the console for WASM builds
- **dungeon features**: just the basics so far. a grassy, forested treant room, some barracks, etc.
- **named maps**: "Town", "Dungeon"
- **map messages/hints**: "You hear <...>."
### changed
- **colour offsets** are now per-tile (and per-theme) instead of +-% globally. i.e. varying fg/bg offset on a per-tiletype basis
- **chatlog colours** are now consistent
### fixed
- negative starting mana
- status effects only ticking if mob turn aligned with turnclock turn
- map params not being saved on map transition
- mob turns not awaiting the particle queue (mobs moving around mid-animation)
- mobs not re-pathing if their path was blocked, causing traffic jams

View file

@ -1,4 +1,4 @@
a - A - a - insects A -
b - B - b - B -
c - chickens C - c - chickens C -
d - canines D - d - canines D -

View file

@ -46,5 +46,10 @@ Complex example, with negative AC:
- At worst (AC rolls a 14), the monster must roll less than -1 to hit you. Impossible. - At worst (AC rolls a 14), the monster must roll less than -1 to hit you. Impossible.
- It rolls a 9, and your AC rolls a 2. 9 is less than 11 (10 + 3 - 2), so it hits. - It rolls a 9, and your AC rolls a 2. 9 is less than 11 (10 + 3 - 2), so it hits.
- It rolls 1d8 for damage, and gets a 6. - It rolls 1d8 for damage, and gets a 6.
bloodstains: if starts on bloodied tile, remove blood + heal, gain xp, grow (little dog -> dog), etc.
- You have negative AC, so you roll 1d14 for damage reduction, and get an 8. - You have negative AC, so you roll 1d14 for damage reduction, and get an 8.
- The total damage is 6 - 8 = -2, but damage can't be negative, so you take 1 point of damage. - The total damage is 6 - 8 = -2, but damage can't be negative, so you take 1 point of damage.
tl;dr
1. Lower AC is better
2. Aim for 0 AC - it's an important breakpoint. Every point of AC before 0 counts for a lot.

View file

@ -3,91 +3,100 @@
"id": "potion_health", "id": "potion_health",
"name": { "name": "potion of health", "plural": "potions of health" }, "name": { "name": "potion of health", "plural": "potions of health" },
"renderable": { "glyph": "!", "fg": "#FF00FF", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "!", "fg": "#FF00FF", "bg": "#000000", "order": 2 },
"class": "potion",
"weight": 1, "weight": 1,
"value": 50, "value": 50,
"flags": ["CONSUMABLE", "DESTRUCTIBLE"], "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"],
"effects": { "healing": "4d4+2" }, "effects": { "heal": "4d4+2" },
"magic": { "class": "uncommon", "naming": "potion" } "magic": { "class": "uncommon", "naming": "potion" }
}, },
{ {
"id": "potion_health_weak", "id": "potion_health_weak",
"name": { "name": "potion of lesser health", "plural": "potions of lesser health" }, "name": { "name": "potion of lesser health", "plural": "potions of lesser health" },
"renderable": { "glyph": "!", "fg": "#FF00FF", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "!", "fg": "#FF00FF", "bg": "#000000", "order": 2 },
"class": "potion",
"weight": 1, "weight": 1,
"value": 25, "value": 25,
"flags": ["CONSUMABLE", "DESTRUCTIBLE"], "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"],
"effects": { "healing": "2d4+2" }, "effects": { "heal": "2d4+2" },
"magic": { "class": "uncommon", "naming": "potion" } "magic": { "class": "uncommon", "naming": "potion" }
}, },
{ {
"id": "scroll_identify", "id": "scroll_identify",
"name": { "name": "scroll of identify", "plural": "scrolls of identify" }, "name": { "name": "scroll of identify", "plural": "scrolls of identify" },
"renderable": { "glyph": "?", "fg": "#0FFFFF", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "?", "fg": "#0FFFFF", "bg": "#000000", "order": 2 },
"class": "scroll",
"weight": 0.5, "weight": 0.5,
"value": 100, "value": 100,
"flags": ["CONSUMABLE", "DESTRUCTIBLE", "IDENTIFY"], "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE", "IDENTIFY"],
"magic": { "class": "uncommon", "naming": "scroll" } "magic": { "class": "uncommon", "naming": "scroll" }
}, },
{ {
"id": "scroll_removecurse", "id": "scroll_removecurse",
"name": { "name": "scroll of remove curse", "plural": "scrolls of remove curse" }, "name": { "name": "scroll of remove curse", "plural": "scrolls of remove curse" },
"renderable": { "glyph": "?", "fg": "#0FFFFF", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "?", "fg": "#0FFFFF", "bg": "#000000", "order": 2 },
"class": "scroll",
"weight": 0.5, "weight": 0.5,
"value": 200, "value": 200,
"flags": ["CONSUMABLE", "DESTRUCTIBLE", "REMOVE_CURSE"], "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE", "REMOVE_CURSE"],
"magic": { "class": "rare", "naming": "scroll" } "magic": { "class": "rare", "naming": "scroll" }
}, },
{ {
"id": "scroll_health", "id": "scroll_health",
"name": { "name": "scroll of healing word", "plural": "scrolls of healing word" }, "name": { "name": "scroll of healing word", "plural": "scrolls of healing word" },
"renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"class": "scroll",
"weight": 0.5, "weight": 0.5,
"value": 50, "value": 50,
"flags": ["CONSUMABLE", "DESTRUCTIBLE"], "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"],
"effects": { "particle_line": "*;-;#53f06d;75.0;#f9ff9f;100.0", "ranged": "12", "healing": "1d4+2" }, "effects": { "particle_line": "*;-;#53f06d;75.0;#f9ff9f;100.0", "ranged": "12", "heal": "1d4+2" },
"magic": { "class": "uncommon", "naming": "scroll" } "magic": { "class": "uncommon", "naming": "scroll" }
}, },
{ {
"id": "scroll_mass_health", "id": "scroll_mass_health",
"name": { "name": "scroll of mass healing word", "plural": "scrolls of mass healing word" }, "name": { "name": "scroll of mass healing word", "plural": "scrolls of mass healing word" },
"renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"class": "scroll",
"weight": 0.5, "weight": 0.5,
"value": 200, "value": 200,
"flags": ["CONSUMABLE", "DESTRUCTIBLE"], "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"],
"effects": { "particle": "*;#53f06d;200.0", "ranged": "12", "aoe": "3", "healing": "1d4+2" }, "effects": { "particle": "*;#53f06d;200.0", "ranged": "12", "aoe": "3", "heal": "1d4+2" },
"magic": { "class": "rare", "naming": "scroll" } "magic": { "class": "rare", "naming": "scroll" }
}, },
{ {
"id": "scroll_magicmissile", "id": "scroll_magicmissile",
"name": { "name": "scroll of magic missile", "plural": "scrolls of magic missile" }, "name": { "name": "scroll of magic missile", "plural": "scrolls of magic missile" },
"renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"class": "scroll",
"weight": 0.5, "weight": 0.5,
"value": 50, "value": 50,
"flags": ["CONSUMABLE", "DESTRUCTIBLE"], "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"],
"effects": { "particle_line": "*;-;#00b7ff;75.0;#f4fc83;100.0", "ranged": "12", "damage": "3d4+3" }, "effects": { "particle_line": "*;-;#00b7ff;75.0;#f4fc83;100.0", "ranged": "12", "damage": "3d4+3;magic" },
"magic": { "class": "uncommon", "naming": "scroll" } "magic": { "class": "uncommon", "naming": "scroll" }
}, },
{ {
"id": "scroll_embers", "id": "scroll_embers",
"name": { "name": "scroll of embers", "plural": "scrolls of embers" }, "name": { "name": "scroll of embers", "plural": "scrolls of embers" },
"renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"class": "scroll",
"weight": 0.5, "weight": 0.5,
"value": 100, "value": 100,
"flags": ["CONSUMABLE", "DESTRUCTIBLE"], "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"],
"effects": { "particle": "*;#FFA500;200.0", "ranged": "10", "damage": "4d6", "aoe": "2" }, "effects": { "particle": "*;#FFA500;200.0", "ranged": "10", "damage": "4d6;fire", "aoe": "2" },
"magic": { "class": "uncommon", "naming": "scroll" } "magic": { "class": "uncommon", "naming": "scroll" }
}, },
{ {
"id": "scroll_fireball", "id": "scroll_fireball",
"name": { "name": "scroll of fireball", "plural": "scrolls of fireball" }, "name": { "name": "scroll of fireball", "plural": "scrolls of fireball" },
"renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"class": "scroll",
"weight": 0.5, "weight": 0.5,
"value": 200, "value": 200,
"flags": ["CONSUMABLE", "DESTRUCTIBLE"], "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"],
"effects": { "effects": {
"particle_burst": "▓;*;~;#FFA500;#000000;500.0;#ffd381;60.0", "particle_burst": "▓;*;~;#FFA500;#000000;500.0;#ffd381;60.0",
"ranged": "10", "ranged": "10",
"damage": "8d6", "damage": "8d6;fire",
"aoe": "3" "aoe": "3"
}, },
"magic": { "class": "rare", "naming": "scroll" } "magic": { "class": "rare", "naming": "scroll" }
@ -96,9 +105,10 @@
"id": "scroll_confusion", "id": "scroll_confusion",
"name": { "name": "scroll of confusion", "plural": "scrolls of confusion" }, "name": { "name": "scroll of confusion", "plural": "scrolls of confusion" },
"renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"class": "scroll",
"weight": 0.5, "weight": 0.5,
"value": 100, "value": 100,
"flags": ["CONSUMABLE", "DESTRUCTIBLE"], "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"],
"effects": { "particle_line": "*;-;#ad56a6;75.0;#cacaca;100.0", "ranged": "10", "confusion": "4" }, "effects": { "particle_line": "*;-;#ad56a6;75.0;#cacaca;100.0", "ranged": "10", "confusion": "4" },
"magic": { "class": "uncommon", "naming": "scroll" } "magic": { "class": "uncommon", "naming": "scroll" }
}, },
@ -106,9 +116,10 @@
"id": "scroll_mass_confusion", "id": "scroll_mass_confusion",
"name": { "name": "scroll of mass confusion", "plural": "scrolls of mass confusion" }, "name": { "name": "scroll of mass confusion", "plural": "scrolls of mass confusion" },
"renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"class": "scroll",
"weight": 0.5, "weight": 0.5,
"value": 200, "value": 200,
"flags": ["CONSUMABLE", "DESTRUCTIBLE"], "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"],
"effects": { "particle": "*;#ad56a6;200.0", "ranged": "10", "aoe": "3", "confusion": "3" }, "effects": { "particle": "*;#ad56a6;200.0", "ranged": "10", "aoe": "3", "confusion": "3" },
"magic": { "class": "veryrare", "naming": "scroll" } "magic": { "class": "veryrare", "naming": "scroll" }
}, },
@ -116,9 +127,10 @@
"id": "scroll_magicmap", "id": "scroll_magicmap",
"name": { "name": "scroll of magic mapping", "plural": "scrolls of magic mapping" }, "name": { "name": "scroll of magic mapping", "plural": "scrolls of magic mapping" },
"renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"class": "scroll",
"weight": 0.5, "weight": 0.5,
"value": 50, "value": 50,
"flags": ["CONSUMABLE", "DESTRUCTIBLE", "MAGICMAP"], "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE", "MAGICMAP"],
"effects": {}, "effects": {},
"magic": { "class": "common", "naming": "scroll" } "magic": { "class": "common", "naming": "scroll" }
}, },
@ -126,6 +138,7 @@
"id": "equip_dagger", "id": "equip_dagger",
"name": { "name": "dagger", "plural": "daggers" }, "name": { "name": "dagger", "plural": "daggers" },
"renderable": { "glyph": ")", "fg": "#808080", "bg": "#000000", "order": 2 }, "renderable": { "glyph": ")", "fg": "#808080", "bg": "#000000", "order": 2 },
"class": "weapon",
"weight": 1, "weight": 1,
"value": 2, "value": 2,
"flags": ["EQUIP_MELEE"], "flags": ["EQUIP_MELEE"],
@ -135,6 +148,7 @@
"id": "equip_shortsword", "id": "equip_shortsword",
"name": { "name": "shortsword", "plural": "shortswords" }, "name": { "name": "shortsword", "plural": "shortswords" },
"renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 },
"class": "weapon",
"weight": 2, "weight": 2,
"value": 10, "value": 10,
"flags": ["EQUIP_MELEE"], "flags": ["EQUIP_MELEE"],
@ -144,6 +158,7 @@
"id": "equip_rapier", "id": "equip_rapier",
"name": { "name": "rapier", "plural": "rapiers" }, "name": { "name": "rapier", "plural": "rapiers" },
"renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 },
"class": "weapon",
"weight": 2, "weight": 2,
"value": 10, "value": 10,
"flags": ["EQUIP_MELEE"], "flags": ["EQUIP_MELEE"],
@ -153,6 +168,7 @@
"id": "equip_pitchfork", "id": "equip_pitchfork",
"name": { "name": "pitchfork", "plural": "pitchforks" }, "name": { "name": "pitchfork", "plural": "pitchforks" },
"renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 },
"class": "weapon",
"weight": 2, "weight": 2,
"value": 5, "value": 5,
"flags": ["EQUIP_MELEE"], "flags": ["EQUIP_MELEE"],
@ -162,6 +178,7 @@
"id": "equip_sickle", "id": "equip_sickle",
"name": { "name": "sickle", "plural": "sickles" }, "name": { "name": "sickle", "plural": "sickles" },
"renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 },
"class": "weapon",
"weight": 2, "weight": 2,
"value": 5, "value": 5,
"flags": ["EQUIP_MELEE"], "flags": ["EQUIP_MELEE"],
@ -171,6 +188,7 @@
"id": "equip_handaxe", "id": "equip_handaxe",
"name": { "name": "handaxe", "plural": "handaxes" }, "name": { "name": "handaxe", "plural": "handaxes" },
"renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 },
"class": "weapon",
"weight": 2, "weight": 2,
"value": 5, "value": 5,
"flags": ["EQUIP_MELEE"], "flags": ["EQUIP_MELEE"],
@ -180,6 +198,7 @@
"id": "equip_longsword", "id": "equip_longsword",
"name": { "name": "longsword", "plural": "longswords" }, "name": { "name": "longsword", "plural": "longswords" },
"renderable": { "glyph": ")", "fg": "#FFF8DC", "bg": "#000000", "order": 2 }, "renderable": { "glyph": ")", "fg": "#FFF8DC", "bg": "#000000", "order": 2 },
"class": "weapon",
"weight": 3, "weight": 3,
"value": 15, "value": 15,
"flags": ["EQUIP_MELEE"], "flags": ["EQUIP_MELEE"],
@ -189,6 +208,7 @@
"id": "equip_smallshield", "id": "equip_smallshield",
"name": { "name": "buckler", "plural": "bucklers" }, "name": { "name": "buckler", "plural": "bucklers" },
"renderable": { "glyph": "[", "fg": "#808080", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#808080", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 2, "weight": 2,
"value": 5, "value": 5,
"flags": ["EQUIP_SHIELD"], "flags": ["EQUIP_SHIELD"],
@ -198,6 +218,7 @@
"id": "equip_mediumshield", "id": "equip_mediumshield",
"name": { "name": "medium shield", "plural": "medium shields" }, "name": { "name": "medium shield", "plural": "medium shields" },
"renderable": { "glyph": "[", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#C0C0C0", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 6, "weight": 6,
"value": 10, "value": 10,
"flags": ["EQUIP_SHIELD"], "flags": ["EQUIP_SHIELD"],
@ -207,6 +228,7 @@
"id": "equip_largeshield", "id": "equip_largeshield",
"name": { "name": "large shield", "plural": "large shields" }, "name": { "name": "large shield", "plural": "large shields" },
"renderable": { "glyph": "[", "fg": "#FFF8DC", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#FFF8DC", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 12, "weight": 12,
"value": 35, "value": 35,
"flags": ["EQUIP_SHIELD"], "flags": ["EQUIP_SHIELD"],
@ -216,6 +238,7 @@
"id": "equip_body_weakleather", "id": "equip_body_weakleather",
"name": { "name": "leather jacket", "plural": "leather jackets" }, "name": { "name": "leather jacket", "plural": "leather jackets" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 8, "weight": 8,
"value": 5, "value": 5,
"flags": ["EQUIP_BODY"], "flags": ["EQUIP_BODY"],
@ -225,6 +248,7 @@
"id": "equip_body_leather", "id": "equip_body_leather",
"name": { "name": "leather chestpiece", "plural": "leather chestpiece" }, "name": { "name": "leather chestpiece", "plural": "leather chestpiece" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 10, "weight": 10,
"value": 10, "value": 10,
"flags": ["EQUIP_BODY"], "flags": ["EQUIP_BODY"],
@ -234,6 +258,7 @@
"id": "equip_body_studdedleather", "id": "equip_body_studdedleather",
"name": { "name": "studded leather chestpiece", "plural": "studded leather chestpieces" }, "name": { "name": "studded leather chestpiece", "plural": "studded leather chestpieces" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 13, "weight": 13,
"value": 45, "value": 45,
"flags": ["EQUIP_BODY"], "flags": ["EQUIP_BODY"],
@ -243,6 +268,7 @@
"id": "equip_body_ringmail_o", "id": "equip_body_ringmail_o",
"name": { "name": "orcish ring mail", "plural": "orcish ring mail" }, "name": { "name": "orcish ring mail", "plural": "orcish ring mail" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 45, "weight": 45,
"value": 50, "value": 50,
"flags": ["EQUIP_BODY"], "flags": ["EQUIP_BODY"],
@ -252,6 +278,7 @@
"id": "equip_body_ringmail", "id": "equip_body_ringmail",
"name": { "name": "ring mail", "plural": "ring mail" }, "name": { "name": "ring mail", "plural": "ring mail" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 45, "weight": 45,
"value": 70, "value": 70,
"flags": ["EQUIP_BODY"], "flags": ["EQUIP_BODY"],
@ -261,6 +288,7 @@
"id": "equip_head_leather", "id": "equip_head_leather",
"name": { "name": "leather cap", "plural": "leather caps" }, "name": { "name": "leather cap", "plural": "leather caps" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 2, "weight": 2,
"value": 10, "value": 10,
"flags": ["EQUIP_HEAD"], "flags": ["EQUIP_HEAD"],
@ -270,6 +298,7 @@
"id": "equip_head_elvish", "id": "equip_head_elvish",
"name": { "name": "elvish leather helm", "plural": "elvish leather helms" }, "name": { "name": "elvish leather helm", "plural": "elvish leather helms" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 2, "weight": 2,
"value": 25, "value": 25,
"flags": ["EQUIP_HEAD"], "flags": ["EQUIP_HEAD"],
@ -279,6 +308,7 @@
"id": "equip_head_o", "id": "equip_head_o",
"name": { "name": "orcish helm", "plural": "orcish helm" }, "name": { "name": "orcish helm", "plural": "orcish helm" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 6, "weight": 6,
"value": 25, "value": 25,
"flags": ["EQUIP_HEAD"], "flags": ["EQUIP_HEAD"],
@ -288,6 +318,7 @@
"id": "equip_head_iron", "id": "equip_head_iron",
"name": { "name": "iron helm", "plural": "iron helm" }, "name": { "name": "iron helm", "plural": "iron helm" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 10, "weight": 10,
"value": 45, "value": 45,
"flags": ["EQUIP_HEAD"], "flags": ["EQUIP_HEAD"],
@ -297,6 +328,7 @@
"id": "equip_feet_leather", "id": "equip_feet_leather",
"name": { "name": "leather shoes", "plural": "leather shoes" }, "name": { "name": "leather shoes", "plural": "leather shoes" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 2, "weight": 2,
"value": 10, "value": 10,
"flags": ["EQUIP_FEET"] "flags": ["EQUIP_FEET"]
@ -305,6 +337,7 @@
"id": "equip_feet_elvish", "id": "equip_feet_elvish",
"name": { "name": "elvish leather shoes", "plural": "elvish leather shoes" }, "name": { "name": "elvish leather shoes", "plural": "elvish leather shoes" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 2, "weight": 2,
"value": 25, "value": 25,
"flags": ["EQUIP_FEET"], "flags": ["EQUIP_FEET"],
@ -314,6 +347,7 @@
"id": "equip_feet_o", "id": "equip_feet_o",
"name": { "name": "orcish boots", "plural": "orcish boots" }, "name": { "name": "orcish boots", "plural": "orcish boots" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 6, "weight": 6,
"value": 25, "value": 25,
"flags": ["EQUIP_FEET"], "flags": ["EQUIP_FEET"],
@ -323,6 +357,7 @@
"id": "equip_feet_iron", "id": "equip_feet_iron",
"name": { "name": "iron boots", "plural": "iron boots" }, "name": { "name": "iron boots", "plural": "iron boots" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 10, "weight": 10,
"value": 45, "value": 45,
"flags": ["EQUIP_FEET"], "flags": ["EQUIP_FEET"],
@ -332,6 +367,7 @@
"id": "equip_neck_protection", "id": "equip_neck_protection",
"name": { "name": "amulet of protection", "plural": "amulets of protection" }, "name": { "name": "amulet of protection", "plural": "amulets of protection" },
"renderable": { "glyph": "\"", "fg": "#aa6000", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "\"", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"class": "amulet",
"weight": 1, "weight": 1,
"value": 200, "value": 200,
"flags": ["EQUIP_NECK"], "flags": ["EQUIP_NECK"],
@ -341,6 +377,7 @@
"id": "equip_back_protection", "id": "equip_back_protection",
"name": { "name": "cloak of protection", "plural": "cloaks of protection" }, "name": { "name": "cloak of protection", "plural": "cloaks of protection" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"class": "armour",
"weight": 1, "weight": 1,
"value": 200, "value": 200,
"flags": ["EQUIP_BACK"], "flags": ["EQUIP_BACK"],
@ -350,26 +387,29 @@
"id": "wand_magicmissile", "id": "wand_magicmissile",
"name": { "name": "wand of magic missile", "plural": "wands of magic missile" }, "name": { "name": "wand of magic missile", "plural": "wands of magic missile" },
"renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"class": "wand",
"weight": 2, "weight": 2,
"value": 100, "value": 100,
"flags": ["CHARGES"], "flags": ["CHARGES"],
"effects": { "ranged": "12", "damage": "3d4+3" }, "effects": { "ranged": "12", "damage": "3d4+3;magic" },
"magic": { "class": "uncommon", "naming": "wand" } "magic": { "class": "uncommon", "naming": "wand" }
}, },
{ {
"id": "wand_fireball", "id": "wand_fireball",
"name": { "name": "wand of fireball", "plural": "wands of fireball" }, "name": { "name": "wand of fireball", "plural": "wands of fireball" },
"renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"class": "wand",
"weight": 2, "weight": 2,
"value": 300, "value": 300,
"flags": ["CHARGES"], "flags": ["CHARGES"],
"effects": { "ranged": "10", "damage": "8d6", "aoe": "3" }, "effects": { "ranged": "10", "damage": "8d6;fire", "aoe": "3" },
"magic": { "class": "rare", "naming": "wand" } "magic": { "class": "rare", "naming": "wand" }
}, },
{ {
"id": "wand_confusion", "id": "wand_confusion",
"name": { "name": "wand of confusion", "plural": "wands of confusion" }, "name": { "name": "wand of confusion", "plural": "wands of confusion" },
"renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"class": "wand",
"weight": 2, "weight": 2,
"value": 200, "value": 200,
"flags": ["CHARGES"], "flags": ["CHARGES"],
@ -380,6 +420,7 @@
"id": "wand_digging", "id": "wand_digging",
"name": { "name": "wand of digging", "plural": "wands of digging" }, "name": { "name": "wand of digging", "plural": "wands of digging" },
"renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"class": "wand",
"weight": 2, "weight": 2,
"value": 300, "value": 300,
"flags": ["CHARGES", "DIGGER"], "flags": ["CHARGES", "DIGGER"],
@ -390,16 +431,18 @@
"id": "food_rations", "id": "food_rations",
"name": { "name": "rations", "plural": "rations" }, "name": { "name": "rations", "plural": "rations" },
"renderable": { "glyph": "%", "fg": "#FFA07A", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "%", "fg": "#FFA07A", "bg": "#000000", "order": 2 },
"class": "comestible",
"weight": 1, "weight": 1,
"value": 1, "value": 1,
"flags": ["FOOD", "CONSUMABLE"] "flags": ["FOOD", "CONSUMABLE", "STACKABLE"]
}, },
{ {
"id": "food_apple", "id": "food_apple",
"name": { "name": "apple", "plural": "apples" }, "name": { "name": "apple", "plural": "apples" },
"renderable": { "glyph": "%", "fg": "#00FF00", "bg": "#000000", "order": 2 }, "renderable": { "glyph": "%", "fg": "#00FF00", "bg": "#000000", "order": 2 },
"class": "comestible",
"weight": 0.5, "weight": 0.5,
"value": 1, "value": 1,
"flags": ["FOOD", "CONSUMABLE"] "flags": ["FOOD", "CONSUMABLE", "STACKABLE"]
} }
] ]

View file

@ -62,7 +62,6 @@
"renderable": { "glyph": "@", "fg": "#034efc", "bg": "#000000", "order": 1 }, "renderable": { "glyph": "@", "fg": "#034efc", "bg": "#000000", "order": 1 },
"flags": ["NEUTRAL", "RANDOM_PATH", "IS_HUMAN"], "flags": ["NEUTRAL", "RANDOM_PATH", "IS_HUMAN"],
"level": 2, "level": 2,
"vision_range": 16,
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }], "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }],
"equipped": ["equip_shortsword", "equip_body_leather"], "equipped": ["equip_shortsword", "equip_body_leather"],
"quips": ["You wont catch me down the mine.", "Staying out of trouble?"] "quips": ["You wont catch me down the mine.", "Staying out of trouble?"]
@ -73,7 +72,6 @@
"renderable": { "glyph": "r", "fg": "#aa6000", "bg": "#000000", "order": 1 }, "renderable": { "glyph": "r", "fg": "#aa6000", "bg": "#000000", "order": 1 },
"flags": [], "flags": [],
"bac": 6, "bac": 6,
"vision_range": 16,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }], "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }],
"loot": { "table": "food", "chance": 0.1 } "loot": { "table": "food", "chance": 0.1 }
}, },
@ -83,7 +81,6 @@
"renderable": { "glyph": "c", "fg": "#BB6000", "bg": "#000000", "order": 1 }, "renderable": { "glyph": "c", "fg": "#BB6000", "bg": "#000000", "order": 1 },
"flags": ["HERBIVORE"], "flags": ["HERBIVORE"],
"bac": 8, "bac": 8,
"vision_range": 16,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }] "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }]
}, },
{ {
@ -92,7 +89,6 @@
"renderable": { "glyph": "q", "fg": "#a57037", "bg": "#000000", "order": 1 }, "renderable": { "glyph": "q", "fg": "#a57037", "bg": "#000000", "order": 1 },
"flags": ["HERBIVORE"], "flags": ["HERBIVORE"],
"bac": 8, "bac": 8,
"vision_range": 16,
"attacks": [{ "name": "kicks", "hit_bonus": 0, "damage": "1d2" }] "attacks": [{ "name": "kicks", "hit_bonus": 0, "damage": "1d2" }]
}, },
{ {
@ -101,7 +97,6 @@
"renderable": { "glyph": "q", "fg": "#e7e7e7", "bg": "#000000", "order": 1 }, "renderable": { "glyph": "q", "fg": "#e7e7e7", "bg": "#000000", "order": 1 },
"flags": ["HERBIVORE", "SMALL_GROUP"], "flags": ["HERBIVORE", "SMALL_GROUP"],
"bac": 10, "bac": 10,
"vision_range": 16,
"attacks": [{ "name": "kicks", "hit_bonus": 0, "damage": "1d2" }] "attacks": [{ "name": "kicks", "hit_bonus": 0, "damage": "1d2" }]
}, },
{ {
@ -110,7 +105,6 @@
"renderable": { "glyph": "c", "fg": "#fae478", "bg": "#000000", "order": 1 }, "renderable": { "glyph": "c", "fg": "#fae478", "bg": "#000000", "order": 1 },
"flags": ["HERBIVORE"], "flags": ["HERBIVORE"],
"bac": 10, "bac": 10,
"vision_range": 16,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }] "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }]
}, },
{ {
@ -121,7 +115,6 @@
"level": 3, "level": 3,
"bac": 6, "bac": 6,
"speed": 16, "speed": 16,
"vision_range": 16,
"attacks": [ "attacks": [
{ "name": "kicks", "hit_bonus": 0, "damage": "1d6" }, { "name": "kicks", "hit_bonus": 0, "damage": "1d6" },
{ "name": "bites", "hit_bonus": 0, "damage": "1d2" } { "name": "bites", "hit_bonus": 0, "damage": "1d2" }
@ -136,7 +129,6 @@
"level": 5, "level": 5,
"bac": 5, "bac": 5,
"speed": 20, "speed": 20,
"vision_range": 16,
"attacks": [ "attacks": [
{ "name": "kicks", "hit_bonus": 0, "damage": "1d8" }, { "name": "kicks", "hit_bonus": 0, "damage": "1d8" },
{ "name": "bites", "hit_bonus": 0, "damage": "1d3" } { "name": "bites", "hit_bonus": 0, "damage": "1d3" }
@ -150,7 +142,6 @@
"level": 7, "level": 7,
"bac": 4, "bac": 4,
"speed": 24, "speed": 24,
"vision_range": 16,
"attacks": [ "attacks": [
{ "name": "kicks", "hit_bonus": 0, "damage": "1d10" }, { "name": "kicks", "hit_bonus": 0, "damage": "1d10" },
{ "name": "bites", "hit_bonus": 0, "damage": "1d4" } { "name": "bites", "hit_bonus": 0, "damage": "1d4" }
@ -163,7 +154,6 @@
"flags": ["SMALL_GROUP"], "flags": ["SMALL_GROUP"],
"level": 1, "level": 1,
"bac": 7, "bac": 7,
"vision_range": 16,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }], "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }],
"loot": { "table": "scrolls", "chance": 0.05 } "loot": { "table": "scrolls", "chance": 0.05 }
}, },
@ -175,7 +165,6 @@
"level": 2, "level": 2,
"bac": 6, "bac": 6,
"speed": 18, "speed": 18,
"vision_range": 16,
"quips": ["<woof!>", "<bark!>", "<grrr..>"], "quips": ["<woof!>", "<bark!>", "<grrr..>"],
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d6" }] "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d6" }]
}, },
@ -187,7 +176,6 @@
"level": 4, "level": 4,
"bac": 5, "bac": 5,
"speed": 16, "speed": 16,
"vision_range": 16,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d6" }] "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d6" }]
}, },
{ {
@ -198,7 +186,6 @@
"level": 6, "level": 6,
"bac": 4, "bac": 4,
"speed": 15, "speed": 15,
"vision_range": 16,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "2d4" }] "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "2d4" }]
}, },
{ {
@ -208,7 +195,6 @@
"flags": ["SMALL_GROUP", "IS_GNOME"], "flags": ["SMALL_GROUP", "IS_GNOME"],
"level": 1, "level": 1,
"speed": 6, "speed": 6,
"vision_range": 16,
"attacks": [{ "name": "claws", "hit_bonus": 0, "damage": "1d6" }], "attacks": [{ "name": "claws", "hit_bonus": 0, "damage": "1d6" }],
"loot": { "table": "wands", "chance": 0.05 } "loot": { "table": "wands", "chance": 0.05 }
}, },
@ -230,7 +216,6 @@
"flags": [], "flags": [],
"level": 1, "level": 1,
"speed": 9, "speed": 9,
"vision_range": 16,
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d4" }] "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d4" }]
}, },
{ {
@ -240,7 +225,6 @@
"flags": [], "flags": [],
"level": 1, "level": 1,
"speed": 6, "speed": 6,
"vision_range": 16,
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d4" }], "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d4" }],
"loot": { "table": "food", "chance": 0.05 } "loot": { "table": "food", "chance": 0.05 }
}, },
@ -286,7 +270,6 @@
"level": 2, "level": 2,
"bac": 10, "bac": 10,
"speed": 6, "speed": 6,
"vision_range": 16,
"attacks": [{ "name": "hacks", "hit_bonus": 0, "damage": "1d8" }], "attacks": [{ "name": "hacks", "hit_bonus": 0, "damage": "1d8" }],
"equipped": ["equip_feet_iron"], "equipped": ["equip_feet_iron"],
"loot": { "table": "equipment", "chance": 0.05 } "loot": { "table": "equipment", "chance": 0.05 }
@ -322,17 +305,62 @@
"level": 1, "level": 1,
"bac": 3, "bac": 3,
"speed": 12, "speed": 12,
"vision_range": 16,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }], "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }],
"loot": { "table": "scrolls", "chance": 0.05 } "loot": { "table": "scrolls", "chance": 0.05 }
}, },
{
"id": "ant_worker",
"name": "worker ant",
"renderable": { "glyph": "a", "fg": "#ca7631", "bg": "#000000", "order": 1 },
"flags": ["SMALL_GROUP"],
"level": 2,
"bac": 3,
"speed": 18,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d4" }],
"loot": { "table": "food", "chance": 0.05 }
},
{
"id": "ant_soldier",
"name": "soldier ant",
"renderable": { "glyph": "a", "fg": "#ca3f26", "bg": "#000000", "order": 1 },
"flags": ["SMALL_GROUP", "POISON_RES"],
"level": 3,
"bac": 3,
"speed": 18,
"attacks": [
{ "name": "bites", "hit_bonus": 0, "damage": "2d4" },
{ "name": "stings", "hit_bonus": 0, "damage": "3d4;poison" }
],
"loot": { "table": "food", "chance": 0.05 }
},
{
"id": "caterpillar_cave",
"name": "caterpillar",
"renderable": { "glyph": "a", "fg": "#6b6b6b", "bg": "#000000", "order": 1 },
"flags": ["SMALL_GROUP"],
"level": 1,
"bac": 3,
"speed": 9,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }],
"loot": { "table": "food", "chance": 0.05 }
},
{
"id": "caterpillar_giant",
"name": "giant caterpillar",
"renderable": { "glyph": "a", "fg": "#b9aeae", "bg": "#000000", "order": 1 },
"flags": ["SMALL_GROUP"],
"level": 2,
"bac": 7,
"speed": 9,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d6" }],
"loot": { "table": "food", "chance": 0.10 }
},
{ {
"id": "jackal", "id": "jackal",
"name": "jackal", "name": "jackal",
"renderable": { "glyph": "d", "fg": "#AA5500", "bg": "#000000", "order": 1 }, "renderable": { "glyph": "d", "fg": "#AA5500", "bg": "#000000", "order": 1 },
"flags": ["CARNIVORE", "SMALL_GROUP"], "flags": ["CARNIVORE", "SMALL_GROUP"],
"bac": 7, "bac": 7,
"vision_range": 16,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }] "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }]
}, },
{ {
@ -341,7 +369,6 @@
"renderable": { "glyph": "d", "fg": "#FF0000", "bg": "#000000", "order": 1 }, "renderable": { "glyph": "d", "fg": "#FF0000", "bg": "#000000", "order": 1 },
"flags": ["CARNIVORE"], "flags": ["CARNIVORE"],
"bac": 7, "bac": 7,
"vision_range": 16,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }] "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }]
}, },
{ {
@ -351,7 +378,6 @@
"flags": ["CARNIVORE", "SMALL_GROUP"], "flags": ["CARNIVORE", "SMALL_GROUP"],
"level": 1, "level": 1,
"bac": 7, "bac": 7,
"vision_range": 16,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d4" }] "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d4" }]
}, },
{ {
@ -361,7 +387,6 @@
"flags": ["CARNIVORE"], "flags": ["CARNIVORE"],
"level": 5, "level": 5,
"bac": 4, "bac": 4,
"vision_range": 16,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "2d4" }] "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "2d4" }]
}, },
{ {
@ -371,7 +396,6 @@
"flags": [], "flags": [],
"level": 2, "level": 2,
"speed": 9, "speed": 9,
"vision_range": 16,
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }], "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }],
"loot": { "table": "wands", "chance": 0.05 } "loot": { "table": "wands", "chance": 0.05 }
}, },
@ -382,7 +406,6 @@
"flags": ["SMALL_GROUP"], "flags": ["SMALL_GROUP"],
"level": 1, "level": 1,
"speed": 9, "speed": 9,
"vision_range": 16,
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d6" }], "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d6" }],
"loot": { "table": "equipment", "chance": 0.05 } "loot": { "table": "equipment", "chance": 0.05 }
}, },
@ -393,7 +416,6 @@
"flags": ["LARGE_GROUP"], "flags": ["LARGE_GROUP"],
"level": 2, "level": 2,
"speed": 9, "speed": 9,
"vision_range": 16,
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d6" }], "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d6" }],
"loot": { "table": "equipment", "chance": 0.05 } "loot": { "table": "equipment", "chance": 0.05 }
}, },
@ -404,23 +426,88 @@
"flags": ["MULTIATTACK"], "flags": ["MULTIATTACK"],
"level": 5, "level": 5,
"speed": 5, "speed": 5,
"vision_range": 16,
"attacks": [ "attacks": [
{ "name": "hits", "hit_bonus": 0, "damage": "2d4" }, { "name": "hits", "hit_bonus": 0, "damage": "2d4" },
{ "name": "hits", "hit_bonus": 0, "damage": "2d4" } { "name": "hits", "hit_bonus": 0, "damage": "2d4" }
], ],
"loot": { "table": "equipment", "chance": 0.05 } "loot": { "table": "equipment", "chance": 0.05 }
}, },
{
"id": "warg",
"name": "warg",
"renderable": { "glyph": "d", "fg": "#8b7164", "bg": "#000000", "order": 1 },
"flags": ["SMALL_GROUP"],
"level": 7,
"bac": 4,
"speed": 12,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "2d6" }],
"loot": { "table": "food", "chance": 0.05 }
},
{
"id": "jaguar",
"name": "jaguar",
"renderable": { "glyph": "f", "fg": "#d3b947", "bg": "#000000", "order": 1 },
"flags": ["MULTIATTACK"],
"level": 4,
"bac": 6,
"speed": 15,
"attacks": [
{ "name": "claws", "hit_bonus": 0, "damage": "1d4" },
{ "name": "claws", "hit_bonus": 0, "damage": "1d4" },
{ "name": "bites", "hit_bonus": 0, "damage": "1d8" }
],
"loot": { "table": "food", "chance": 0.05 }
},
{
"id": "lynx",
"name": "lynx",
"renderable": { "glyph": "f", "fg": "#b5d347", "bg": "#000000", "order": 1 },
"flags": ["MULTIATTACK"],
"level": 5,
"bac": 6,
"speed": 15,
"attacks": [
{ "name": "claws", "hit_bonus": 0, "damage": "1d4" },
{ "name": "claws", "hit_bonus": 0, "damage": "1d4" },
{ "name": "bites", "hit_bonus": 0, "damage": "1d10" }
],
"loot": { "table": "food", "chance": 0.05 }
},
{
"id": "panther",
"name": "panther",
"renderable": { "glyph": "f", "fg": "#58554e", "bg": "#000000", "order": 1 },
"flags": ["MULTIATTACK"],
"level": 5,
"bac": 6,
"speed": 15,
"attacks": [
{ "name": "claws", "hit_bonus": 0, "damage": "1d6" },
{ "name": "claws", "hit_bonus": 0, "damage": "1d6" },
{ "name": "bites", "hit_bonus": 0, "damage": "1d10" }
],
"loot": { "table": "food", "chance": 0.05 }
},
{ {
"id": "ogre", "id": "ogre",
"name": "ogre", "name": "ogre",
"renderable": { "glyph": "O", "fg": "#00FF00", "bg": "#000000", "order": 1 }, "renderable": { "glyph": "O", "fg": "#10A70d", "bg": "#000000", "order": 1 },
"flags": ["SMALL_GROUP"], "flags": ["SMALL_GROUP"],
"level": 5, "level": 5,
"bac": 5, "bac": 5,
"speed": 10, "speed": 10,
"vision_range": 16,
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "2d5" }], "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "2d5" }],
"loot": { "table": "food", "chance": 0.05 } "loot": { "table": "food", "chance": 0.05 }
},
{
"id": "treant_small",
"name": "treant sapling",
"renderable": { "glyph": "♠️", "fg": "#10570d", "bg": "#000000", "order": 1 },
"flags": ["LARGE_GROUP", "GREEN_BLOOD", "FIRE_WEAK"],
"level": 2,
"bac": 12,
"speed": 3,
"attacks": [{ "name": "lashes", "hit_bonus": 4, "damage": "1d8" }],
"loot": { "table": "scrolls", "chance": 0.05 }
} }
] ]

View file

@ -85,11 +85,14 @@
{ "id": "fox", "weight": 1, "difficulty": 1}, { "id": "fox", "weight": 1, "difficulty": 1},
{ "id": "jackal", "weight": 4, "difficulty": 1}, { "id": "jackal", "weight": 4, "difficulty": 1},
{ "id": "deer_little", "weight": 1, "difficulty": 1}, { "id": "deer_little", "weight": 1, "difficulty": 1},
{ "id": "treant_small", "weight": 1, "difficulty": 1},
{ "id": "zombie_kobold", "weight": 1, "difficulty": 1}, { "id": "zombie_kobold", "weight": 1, "difficulty": 1},
{ "id": "zombie_gnome", "weight": 1, "difficulty": 2}, { "id": "zombie_gnome", "weight": 1, "difficulty": 2},
{ "id": "kobold_large", "weight": 1, "difficulty": 2}, { "id": "kobold_large", "weight": 1, "difficulty": 2},
{ "id": "rat_giant", "weight": 2, "difficulty": 2}, { "id": "rat_giant", "weight": 2, "difficulty": 2},
{ "id": "coyote", "weight": 4, "difficulty": 2}, { "id": "coyote", "weight": 4, "difficulty": 2},
{ "id": "caterpillar_cave", "weight": 2, "difficulty": 2},
{ "id": "caterpillar_giant", "weight": 2, "difficulty": 3},
{ "id": "zombie_orc", "weight": 1, "difficulty": 3}, { "id": "zombie_orc", "weight": 1, "difficulty": 3},
{ "id": "zombie_dwarf", "weight": 1, "difficulty": 3}, { "id": "zombie_dwarf", "weight": 1, "difficulty": 3},
{ "id": "gnome", "weight": 1, "difficulty": 3}, { "id": "gnome", "weight": 1, "difficulty": 3},
@ -101,15 +104,41 @@
{ "id": "dwarf", "weight": 3, "difficulty": 4}, { "id": "dwarf", "weight": 3, "difficulty": 4},
{ "id": "orc_hill", "weight": 1, "difficulty": 4}, { "id": "orc_hill", "weight": 1, "difficulty": 4},
{ "id": "horse_little", "weight": 2, "difficulty": 4}, { "id": "horse_little", "weight": 2, "difficulty": 4},
{ "id": "ant_worker", "weight": 3, "difficulty": 4},
{ "id": "dog", "weight": 1, "difficulty": 5}, { "id": "dog", "weight": 1, "difficulty": 5},
{ "id": "wolf", "weight": 2, "difficulty": 6}, { "id": "wolf", "weight": 2, "difficulty": 6},
{ "id": "jaguar", "weight": 2, "difficulty": 6},
{ "id": "ant_soldier", "weight": 2, "difficulty": 6},
{ "id": "orc_captain", "weight": 1, "difficulty": 7}, { "id": "orc_captain", "weight": 1, "difficulty": 7},
{ "id": "dog_large", "weight": 1, "difficulty": 7}, { "id": "dog_large", "weight": 1, "difficulty": 7},
{ "id": "lynx", "weight": 1, "difficulty": 7},
{ "id": "panther", "weight": 1, "difficulty": 7},
{ "id": "horse", "weight": 2, "difficulty": 7}, { "id": "horse", "weight": 2, "difficulty": 7},
{ "id": "ogre", "weight": 1, "difficulty": 7}, { "id": "ogre", "weight": 1, "difficulty": 7},
{ "id": "warg", "weight": 2, "difficulty": 8},
{ "id": "horse_large", "weight": 2, "difficulty": 9} { "id": "horse_large", "weight": 2, "difficulty": 9}
] ]
}, },
{
"id": "squad_goblin",
"table": [
{ "id": "goblin", "weight": 3, "difficulty": 1}
]
},
{
"id": "squad_kobold",
"table": [
{ "id": "kobold", "weight": 3, "difficulty": 1},
{ "id": "kobold_large", "weight": 2, "difficulty": 2}
]
},
{
"id": "squad_orc",
"table": [
{ "id": "orc", "weight": 2, "difficulty": 2},
{ "id": "orc_hill", "weight": 1, "difficulty": 4}
]
},
{ {
"id": "traps", "id": "traps",
"table": [ "table": [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View file

@ -1,5 +1,5 @@
use crate::{ EntityMoved, Map, Position, TakingTurn, Telepath, Viewshed, WantsToApproach }; use crate::{ EntityMoved, Map, Position, TakingTurn, Telepath, Viewshed, WantsToApproach };
use rltk::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
pub struct ApproachAI {} pub struct ApproachAI {}
@ -37,11 +37,31 @@ impl<'a> System<'a> for ApproachAI {
&turns, &turns,
).join() { ).join() {
turn_done.push(entity); turn_done.push(entity);
let path = a_star_search( let target_idxs = if
map.xy_idx(pos.x, pos.y) as i32, let Some(paths) = get_adjacent_unblocked(&map, approach.idx as usize)
map.xy_idx(approach.idx % map.width, approach.idx / map.width) as i32, {
&mut *map paths
); } else {
continue;
};
let mut path: Option<NavigationPath> = None;
let idx = map.xy_idx(pos.x, pos.y);
for tar_idx in target_idxs {
let potential_path = a_star_search(idx, tar_idx, &mut *map);
if potential_path.success && potential_path.steps.len() > 1 {
if
path.is_none() ||
potential_path.steps.len() < path.as_ref().unwrap().steps.len()
{
path = Some(potential_path);
}
}
}
let path = if path.is_some() {
path.unwrap()
} else {
continue;
};
if path.success && path.steps.len() > 1 { if path.success && path.steps.len() > 1 {
let idx = map.xy_idx(pos.x, pos.y); let idx = map.xy_idx(pos.x, pos.y);
pos.x = (path.steps[1] as i32) % map.width; pos.x = (path.steps[1] as i32) % map.width;
@ -61,3 +81,25 @@ impl<'a> System<'a> for ApproachAI {
} }
} }
} }
/// Try to get an unblocked index within one tile of a given idx, or None.
pub fn get_adjacent_unblocked(map: &WriteExpect<Map>, idx: usize) -> Option<Vec<usize>> {
let mut adjacent = Vec::new();
let x = (idx as i32) % map.width;
let y = (idx as i32) / map.width;
for i in -1..2 {
for j in -1..2 {
if i == 0 && j == 0 {
continue;
}
let new_idx = (x + i + (y + j) * map.width) as usize;
if !crate::spatial::is_blocked(new_idx) {
adjacent.push(new_idx);
}
}
}
if adjacent.is_empty() {
return None;
}
return Some(adjacent);
}

View file

@ -1,7 +1,8 @@
use crate::{ Chasing, EntityMoved, Map, Position, TakingTurn, Telepath, Viewshed }; use crate::{ Chasing, EntityMoved, Map, Position, TakingTurn, Telepath, Viewshed };
use rltk::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
use super::approach_ai_system::get_adjacent_unblocked;
// If the target is beyond this distance, they're no longer being detected, // If the target is beyond this distance, they're no longer being detected,
// so stop following them. This is essentially a combined value of the sound // so stop following them. This is essentially a combined value of the sound
@ -25,8 +26,16 @@ impl<'a> System<'a> for ChaseAI {
); );
fn run(&mut self, data: Self::SystemData) { fn run(&mut self, data: Self::SystemData) {
let (mut turns, mut chasing, mut positions, mut map, mut viewsheds, mut telepaths, mut entity_moved, entities) = let (
data; mut turns,
mut chasing,
mut positions,
mut map,
mut viewsheds,
mut telepaths,
mut entity_moved,
entities,
) = data;
let mut targets: HashMap<Entity, (i32, i32)> = HashMap::new(); let mut targets: HashMap<Entity, (i32, i32)> = HashMap::new();
let mut end_chase: Vec<Entity> = Vec::new(); let mut end_chase: Vec<Entity> = Vec::new();
// For every chasing entity with a turn, look for a valid target position, // For every chasing entity with a turn, look for a valid target position,
@ -57,11 +66,30 @@ impl<'a> System<'a> for ChaseAI {
).join() { ).join() {
turn_done.push(entity); turn_done.push(entity);
let target_pos = targets[&entity]; let target_pos = targets[&entity];
let path = a_star_search( let target_idx = map.xy_idx(target_pos.0, target_pos.1);
map.xy_idx(pos.x, pos.y) as i32, let target_idxs = if let Some(paths) = get_adjacent_unblocked(&map, target_idx) {
map.xy_idx(target_pos.0, target_pos.1) as i32, paths
&mut *map } else {
); continue;
};
let mut path: Option<NavigationPath> = None;
let idx = map.xy_idx(pos.x, pos.y);
for tar_idx in target_idxs {
let potential_path = a_star_search(idx, tar_idx, &mut *map);
if potential_path.success && potential_path.steps.len() > 1 {
if
path.is_none() ||
potential_path.steps.len() < path.as_ref().unwrap().steps.len()
{
path = Some(potential_path);
}
}
}
let path = if path.is_some() {
path.unwrap()
} else {
continue;
};
if path.success && path.steps.len() > 1 && path.steps.len() < MAX_CHASE_DISTANCE { if path.success && path.steps.len() > 1 && path.steps.len() < MAX_CHASE_DISTANCE {
let idx = map.xy_idx(pos.x, pos.y); let idx = map.xy_idx(pos.x, pos.y);
pos.x = (path.steps[1] as i32) % map.width; pos.x = (path.steps[1] as i32) % map.width;

View file

@ -1,5 +1,16 @@
use crate::{ tile_walkable, EntityMoved, Map, MoveMode, Movement, Position, TakingTurn, Telepath, Viewshed }; use crate::{
tile_walkable,
EntityMoved,
Map,
MoveMode,
Movement,
Position,
TakingTurn,
Telepath,
Viewshed,
};
use specs::prelude::*; use specs::prelude::*;
use bracket_lib::prelude::*;
// Rolling a 1d8+x to decide where to move, where x are the number // Rolling a 1d8+x to decide where to move, where x are the number
// of dice rolls in which they will remian stationary. i.e. If this // of dice rolls in which they will remian stationary. i.e. If this
@ -16,7 +27,7 @@ impl<'a> System<'a> for DefaultAI {
WriteStorage<'a, Viewshed>, WriteStorage<'a, Viewshed>,
WriteStorage<'a, Telepath>, WriteStorage<'a, Telepath>,
WriteStorage<'a, EntityMoved>, WriteStorage<'a, EntityMoved>,
WriteExpect<'a, rltk::RandomNumberGenerator>, WriteExpect<'a, RandomNumberGenerator>,
Entities<'a>, Entities<'a>,
); );
@ -85,7 +96,9 @@ impl<'a> System<'a> for DefaultAI {
let idx = map.xy_idx(pos.x, pos.y); let idx = map.xy_idx(pos.x, pos.y);
pos.x = x; pos.x = x;
pos.y = y; pos.y = y;
entity_moved.insert(entity, EntityMoved {}).expect("Unable to insert EntityMoved"); entity_moved
.insert(entity, EntityMoved {})
.expect("Unable to insert EntityMoved");
crate::spatial::move_entity(entity, idx, dest_idx); crate::spatial::move_entity(entity, idx, dest_idx);
viewshed.dirty = true; viewshed.dirty = true;
if let Some(is_telepath) = telepaths.get_mut(entity) { if let Some(is_telepath) = telepaths.get_mut(entity) {
@ -102,7 +115,9 @@ impl<'a> System<'a> for DefaultAI {
if !crate::spatial::is_blocked(path[1] as usize) { if !crate::spatial::is_blocked(path[1] as usize) {
pos.x = (path[1] as i32) % map.width; pos.x = (path[1] as i32) % map.width;
pos.y = (path[1] as i32) / map.width; pos.y = (path[1] as i32) / map.width;
entity_moved.insert(entity, EntityMoved {}).expect("Unable to insert EntityMoved"); entity_moved
.insert(entity, EntityMoved {})
.expect("Unable to insert EntityMoved");
let new_idx = map.xy_idx(pos.x, pos.y); let new_idx = map.xy_idx(pos.x, pos.y);
crate::spatial::move_entity(entity, idx, new_idx); crate::spatial::move_entity(entity, idx, new_idx);
viewshed.dirty = true; viewshed.dirty = true;
@ -110,6 +125,21 @@ impl<'a> System<'a> for DefaultAI {
is_telepath.dirty = true; is_telepath.dirty = true;
} }
path.remove(0); path.remove(0);
} else {
// If the path is blocked, recalculate a new path to the same waypoint.
let path = a_star_search(
map.xy_idx(pos.x, pos.y) as i32,
map.xy_idx(
(path[path.len() - 1] as i32) % map.width,
(path[path.len() - 1] as i32) / map.width
) as i32,
&mut *map
);
if path.success && path.steps.len() > 1 {
move_mode.mode = Movement::RandomWaypoint {
path: Some(path.steps),
};
}
} }
} else { } else {
move_mode.mode = Movement::RandomWaypoint { path: None }; move_mode.mode = Movement::RandomWaypoint { path: None };
@ -119,7 +149,7 @@ impl<'a> System<'a> for DefaultAI {
let target_y = rng.roll_dice(1, map.height - 2); let target_y = rng.roll_dice(1, map.height - 2);
let idx = map.xy_idx(target_x, target_y); let idx = map.xy_idx(target_x, target_y);
if tile_walkable(map.tiles[idx]) { if tile_walkable(map.tiles[idx]) {
let path = rltk::a_star_search( let path = a_star_search(
map.xy_idx(pos.x, pos.y) as i32, map.xy_idx(pos.x, pos.y) as i32,
map.xy_idx(target_x, target_y) as i32, map.xy_idx(target_x, target_y) as i32,
&mut *map &mut *map

View file

@ -1,7 +1,7 @@
use crate::{ gamelog, Attributes, Burden, EquipmentChanged, Equipped, InBackpack, Item, Pools }; use crate::{ gamelog, Attributes, Burden, EquipmentChanged, Equipped, InBackpack, Item, Pools };
use specs::prelude::*; use specs::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
use crate::config::entity::CARRY_CAPACITY_PER_STRENGTH; use crate::data::entity::CARRY_CAPACITY_PER_STRENGTH;
pub struct EncumbranceSystem {} pub struct EncumbranceSystem {}

View file

@ -1,7 +1,21 @@
use crate::config::entity::*; use crate::data::entity::*;
use crate::{ Burden, BurdenLevel, Clock, Energy, Name, Position, RunState, TakingTurn, LOG_TICKS }; use crate::{
use rltk::prelude::*; Burden,
BurdenLevel,
Clock,
Energy,
Name,
Position,
RunState,
Map,
TakingTurn,
Confusion,
Intrinsics,
};
use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
use crate::config::CONFIG;
use crate::data::events::*;
pub struct EnergySystem {} pub struct EnergySystem {}
@ -10,6 +24,7 @@ const TURN_COST: i32 = NORMAL_SPEED * TURN_COST_MULTIPLIER;
impl<'a> System<'a> for EnergySystem { impl<'a> System<'a> for EnergySystem {
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
type SystemData = ( type SystemData = (
ReadExpect<'a, Map>,
ReadStorage<'a, Clock>, ReadStorage<'a, Clock>,
WriteStorage<'a, Energy>, WriteStorage<'a, Energy>,
ReadStorage<'a, Burden>, ReadStorage<'a, Burden>,
@ -21,10 +36,13 @@ impl<'a> System<'a> for EnergySystem {
ReadExpect<'a, Entity>, ReadExpect<'a, Entity>,
ReadStorage<'a, Name>, ReadStorage<'a, Name>,
ReadExpect<'a, Point>, ReadExpect<'a, Point>,
ReadStorage<'a, Confusion>,
ReadStorage<'a, Intrinsics>,
); );
fn run(&mut self, data: Self::SystemData) { fn run(&mut self, data: Self::SystemData) {
let ( let (
map,
clock, clock,
mut energies, mut energies,
burdens, burdens,
@ -36,6 +54,8 @@ impl<'a> System<'a> for EnergySystem {
player, player,
names, names,
player_pos, player_pos,
confusion,
intrinsics,
) = data; ) = data;
// If not ticking, do nothing. // If not ticking, do nothing.
if *runstate != RunState::Ticking { if *runstate != RunState::Ticking {
@ -47,28 +67,37 @@ impl<'a> System<'a> for EnergySystem {
for (entity, _clock, energy) in (&entities, &clock, &mut energies).join() { for (entity, _clock, energy) in (&entities, &clock, &mut energies).join() {
energy.current += NORMAL_SPEED; energy.current += NORMAL_SPEED;
if energy.current >= TURN_COST { if energy.current >= TURN_COST {
turns.insert(entity, TakingTurn {}).expect("Unable to insert turn for turn counter."); turns
.insert(entity, TakingTurn {})
.expect("Unable to insert turn for turn counter.");
energy.current -= TURN_COST; energy.current -= TURN_COST;
crate::gamelog::record_event("turns", 1); crate::gamelog::record_event(EVENT::Turn(1));
// Handle spawning mobs each turn // Handle spawning mobs each turn
if LOG_TICKS { if CONFIG.logging.log_ticks {
console::log(format!("===== TURN {} =====", crate::gamelog::get_event_count("turns"))); console::log(
format!(
"===== TURN {} =====",
crate::gamelog::get_event_count(EVENT::COUNT_TURN)
)
);
} }
} }
} }
// EVERYTHING ELSE // EVERYTHING ELSE
for (entity, energy, pos) in (&entities, &mut energies, &positions).join() { for (entity, energy, pos, _c) in (
let burden_modifier = if let Some(burden) = burdens.get(entity) { &entities,
match burden.level { &mut energies,
BurdenLevel::Burdened => 0.75, &positions,
BurdenLevel::Strained => 0.5, !&confusion,
BurdenLevel::Overloaded => 0.25, ).join() {
} let burden_modifier = get_burden_modifier(&burdens, entity);
} else { let overmap_mod = get_overmap_modifier(&map);
1.0 let intrinsic_speed = get_intrinsic_speed(&intrinsics, entity);
};
// Every entity has a POTENTIAL equal to their speed. // Every entity has a POTENTIAL equal to their speed.
let mut energy_potential: i32 = ((energy.speed as f32) * burden_modifier) as i32; let mut energy_potential: i32 = ((energy.speed as f32) *
burden_modifier *
overmap_mod *
intrinsic_speed) as i32;
// Increment current energy by NORMAL_SPEED for every // Increment current energy by NORMAL_SPEED for every
// whole number of NORMAL_SPEEDS in their POTENTIAL. // whole number of NORMAL_SPEEDS in their POTENTIAL.
while energy_potential >= NORMAL_SPEED { while energy_potential >= NORMAL_SPEED {
@ -89,26 +118,61 @@ impl<'a> System<'a> for EnergySystem {
// has enough energy, they take a turn and decrement their energy // has enough energy, they take a turn and decrement their energy
// by TURN_COST. If the current entity is the player, await input. // by TURN_COST. If the current entity is the player, await input.
if energy.current >= TURN_COST { if energy.current >= TURN_COST {
let mut my_turn = true;
energy.current -= TURN_COST; energy.current -= TURN_COST;
if entity == *player { if entity == *player {
*runstate = RunState::AwaitingInput; *runstate = RunState::AwaitingInput;
} else { } else if cull_turn_by_distance(&player_pos, pos) {
let distance = rltk::DistanceAlg::Pythagoras.distance2d(*player_pos, Point::new(pos.x, pos.y)); continue;
if distance > 20.0 {
my_turn = false;
}
} }
if my_turn { turns.insert(entity, TakingTurn {}).expect("Unable to insert turn.");
turns.insert(entity, TakingTurn {}).expect("Unable to insert turn."); if CONFIG.logging.log_ticks {
if LOG_TICKS { let name = if let Some(name) = names.get(entity) {
let name = if let Some(name) = names.get(entity) { &name.name } else { "Unknown entity" }; &name.name
console::log( } else {
format!("ENERGY SYSTEM: {} granted a turn. [leftover energy: {}].", name, energy.current) "Unknown entity"
); };
} console::log(
format!(
"ENERGY SYSTEM: {} granted a turn. [leftover energy: {}].",
name,
energy.current
)
);
} }
} }
} }
} }
} }
fn get_burden_modifier(burdens: &ReadStorage<Burden>, entity: Entity) -> f32 {
return if let Some(burden) = burdens.get(entity) {
match burden.level {
BurdenLevel::Burdened => SPEED_MOD_BURDENED,
BurdenLevel::Strained => SPEED_MOD_STRAINED,
BurdenLevel::Overloaded => SPEED_MOD_OVERLOADED,
}
} else {
1.0
};
}
fn get_overmap_modifier(map: &ReadExpect<Map>) -> f32 {
return if map.overmap { SPEED_MOD_OVERMAP_TRAVEL } else { 1.0 };
}
fn cull_turn_by_distance(player_pos: &Point, pos: &Position) -> bool {
let distance = DistanceAlg::Pythagoras.distance2d(*player_pos, Point::new(pos.x, pos.y));
if distance > 20.0 {
return true;
}
return false;
}
fn get_intrinsic_speed(intrinsics: &ReadStorage<Intrinsics>, entity: Entity) -> f32 {
if let Some(intrinsics) = intrinsics.get(entity) {
if intrinsics.list.contains(&crate::Intrinsic::Speed) {
return 4.0 / 3.0;
}
}
return 1.0;
}

View file

@ -1,5 +1,5 @@
use crate::{ EntityMoved, Map, Position, TakingTurn, Telepath, Viewshed, WantsToFlee }; use crate::{ EntityMoved, Map, Position, TakingTurn, Telepath, Viewshed, WantsToFlee };
use rltk::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
pub struct FleeAI {} pub struct FleeAI {}
@ -39,7 +39,13 @@ impl<'a> System<'a> for FleeAI {
turn_done.push(entity); turn_done.push(entity);
let my_idx = map.xy_idx(pos.x, pos.y); let my_idx = map.xy_idx(pos.x, pos.y);
map.populate_blocked(); map.populate_blocked();
let flee_map = DijkstraMap::new(map.width as usize, map.height as usize, &fleeing.indices, &*map, 100.0); let flee_map = DijkstraMap::new(
map.width as usize,
map.height as usize,
&fleeing.indices,
&*map,
100.0
);
let flee_target = DijkstraMap::find_highest_exit(&flee_map, my_idx, &*map); let flee_target = DijkstraMap::find_highest_exit(&flee_map, my_idx, &*map);
if let Some(flee_target) = flee_target { if let Some(flee_target) = flee_target {
if !crate::spatial::is_blocked(flee_target as usize) { if !crate::spatial::is_blocked(flee_target as usize) {
@ -50,7 +56,9 @@ impl<'a> System<'a> for FleeAI {
} }
pos.x = (flee_target as i32) % map.width; pos.x = (flee_target as i32) % map.width;
pos.y = (flee_target as i32) / map.width; pos.y = (flee_target as i32) / map.width;
entity_moved.insert(entity, EntityMoved {}).expect("Unable to insert EntityMoved"); entity_moved
.insert(entity, EntityMoved {})
.expect("Unable to insert EntityMoved");
} }
} }
} }

View file

@ -1,5 +1,5 @@
use crate::{ gamelog, gui::renderable_colour, Name, Quips, Renderable, TakingTurn, Viewshed }; use crate::{ gamelog, gui::renderable_colour, Name, Quips, Renderable, TakingTurn, Viewshed };
use rltk::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
pub struct QuipSystem {} pub struct QuipSystem {}
@ -19,8 +19,18 @@ impl<'a> System<'a> for QuipSystem {
fn run(&mut self, data: Self::SystemData) { fn run(&mut self, data: Self::SystemData) {
let (entities, mut quips, names, renderables, turns, player_pos, viewsheds, mut rng) = data; let (entities, mut quips, names, renderables, turns, player_pos, viewsheds, mut rng) = data;
for (entity, quip, name, viewshed, _turn) in (&entities, &mut quips, &names, &viewsheds, &turns).join() { for (entity, quip, name, viewshed, _turn) in (
if !quip.available.is_empty() && viewshed.visible_tiles.contains(&player_pos) && rng.roll_dice(1, 6) == 1 { &entities,
&mut quips,
&names,
&viewsheds,
&turns,
).join() {
if
!quip.available.is_empty() &&
viewshed.visible_tiles.contains(&player_pos) &&
rng.roll_dice(1, 6) == 1
{
let quip_index = if quip.available.len() == 1 { let quip_index = if quip.available.len() == 1 {
0 0
} else { } else {

View file

@ -9,8 +9,10 @@ use crate::{
Position, Position,
RandomNumberGenerator, RandomNumberGenerator,
TakingTurn, TakingTurn,
Intrinsics,
}; };
use specs::prelude::*; use specs::prelude::*;
use crate::data::events::*;
pub struct RegenSystem {} pub struct RegenSystem {}
@ -35,10 +37,24 @@ impl<'a> System<'a> for RegenSystem {
ReadStorage<'a, HasClass>, ReadStorage<'a, HasClass>,
ReadStorage<'a, Attributes>, ReadStorage<'a, Attributes>,
WriteExpect<'a, RandomNumberGenerator>, WriteExpect<'a, RandomNumberGenerator>,
ReadStorage<'a, Intrinsics>,
ReadExpect<'a, Entity>,
); );
fn run(&mut self, data: Self::SystemData) { fn run(&mut self, data: Self::SystemData) {
let (clock, entities, positions, mut pools, turns, player, classes, attributes, mut rng) = data; let (
clock,
entities,
positions,
mut pools,
turns,
player,
classes,
attributes,
mut rng,
intrinsics,
player_entity,
) = data;
let mut clock_turn = false; let mut clock_turn = false;
for (_e, _c, _t) in (&entities, &clock, &turns).join() { for (_e, _c, _t) in (&entities, &clock, &turns).join() {
clock_turn = true; clock_turn = true;
@ -47,27 +63,37 @@ impl<'a> System<'a> for RegenSystem {
return; return;
} }
// Monster HP regen // Monster HP regen
let current_turn = gamelog::get_event_count("turns"); let current_turn = gamelog::get_event_count(EVENT::COUNT_TURN);
if current_turn % MONSTER_HP_REGEN_TURN == 0 { if current_turn % MONSTER_HP_REGEN_TURN == 0 {
for (_e, _p, pool, _player) in (&entities, &positions, &mut pools, !&player).join() { for (_e, _p, pool, _player) in (&entities, &positions, &mut pools, !&player).join() {
try_hp_regen_tick(pool, MONSTER_HP_REGEN_PER_TICK); try_hp_regen_tick(pool, MONSTER_HP_REGEN_PER_TICK);
} }
} }
// Player HP regen // Player HP regen
let level = gamelog::get_event_count("player_level"); let level = gamelog::get_event_count(EVENT::COUNT_LEVEL);
if current_turn % get_player_hp_regen_turn(level) == 0 { if
current_turn % get_player_hp_regen_turn(level) == 0 ||
intrinsics.get(*player_entity).unwrap().list.contains(&crate::Intrinsic::Regeneration)
{
for (_e, _p, pool, _player) in (&entities, &positions, &mut pools, &player).join() { for (_e, _p, pool, _player) in (&entities, &positions, &mut pools, &player).join() {
try_hp_regen_tick(pool, get_player_hp_regen_per_tick(level)); try_hp_regen_tick(pool, get_player_hp_regen_per_tick(level));
} }
} }
// Both MP regen // Both MP regen
for (e, _p, pool) in (&entities, &positions, &mut pools).join() { for (e, _p, pool) in (&entities, &positions, &mut pools).join() {
let is_wizard = if let Some(class) = classes.get(e) { class.name == Class::Wizard } else { false }; let is_wizard = if let Some(class) = classes.get(e) {
class.name == Class::Wizard
} else {
false
};
let numerator = if is_wizard { WIZARD_MP_REGEN_MOD } else { NONWIZARD_MP_REGEN_MOD }; let numerator = if is_wizard { WIZARD_MP_REGEN_MOD } else { NONWIZARD_MP_REGEN_MOD };
let multiplier: f32 = (numerator as f32) / (MP_REGEN_DIVISOR as f32); let multiplier: f32 = (numerator as f32) / (MP_REGEN_DIVISOR as f32);
let mp_regen_tick = (((MP_REGEN_BASE - pool.level) as f32) * multiplier) as i32; let mp_regen_tick = (((MP_REGEN_BASE - pool.level) as f32) * multiplier) as i32;
if current_turn % mp_regen_tick == 0 { if current_turn % mp_regen_tick == 0 {
try_mana_regen_tick(pool, rng.roll_dice(1, get_mana_regen_per_tick(e, &attributes))); try_mana_regen_tick(
pool,
rng.roll_dice(1, get_mana_regen_per_tick(e, &attributes))
);
} }
} }
} }

View file

@ -7,9 +7,12 @@ use crate::{
Name, Name,
Renderable, Renderable,
TakingTurn, TakingTurn,
Item,
Prop,
}; };
use rltk::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
use crate::data::events::*;
pub struct TurnStatusSystem {} pub struct TurnStatusSystem {}
@ -23,10 +26,22 @@ impl<'a> System<'a> for TurnStatusSystem {
ReadStorage<'a, Name>, ReadStorage<'a, Name>,
ReadExpect<'a, Entity>, ReadExpect<'a, Entity>,
ReadStorage<'a, Renderable>, ReadStorage<'a, Renderable>,
ReadStorage<'a, Item>,
ReadStorage<'a, Prop>,
); );
fn run(&mut self, data: Self::SystemData) { fn run(&mut self, data: Self::SystemData) {
let (mut turns, clock, mut confusion, entities, names, player_entity, renderables) = data; let (
mut turns,
clock,
mut confusion,
entities,
names,
player_entity,
renderables,
items,
props,
) = data;
let mut clock_tick = false; let mut clock_tick = false;
for (_e, _c, _t) in (&entities, &clock, &turns).join() { for (_e, _c, _t) in (&entities, &clock, &turns).join() {
clock_tick = true; clock_tick = true;
@ -38,15 +53,19 @@ impl<'a> System<'a> for TurnStatusSystem {
let mut log = false; let mut log = false;
let mut not_my_turn: Vec<Entity> = Vec::new(); let mut not_my_turn: Vec<Entity> = Vec::new();
let mut not_confused: Vec<Entity> = Vec::new(); let mut not_confused: Vec<Entity> = Vec::new();
for (entity, _turn, confused, name) in (&entities, &mut turns, &mut confusion, &names).join() { for (entity, confused, name, _i, _p) in (
&entities,
&mut confusion,
&names,
!&items,
!&props,
).join() {
confused.turns -= 1; confused.turns -= 1;
if confused.turns < 1 { if confused.turns < 1 {
not_confused.push(entity); not_confused.push(entity);
if entity == *player_entity { if entity == *player_entity {
logger = logger logger = logger
.colour(renderable_colour(&renderables, entity))
.append("You") .append("You")
.colour(WHITE)
.append("snap out of it."); .append("snap out of it.");
log = true; log = true;
} else { } else {
@ -73,12 +92,10 @@ impl<'a> System<'a> for TurnStatusSystem {
not_my_turn.push(entity); not_my_turn.push(entity);
if entity == *player_entity { if entity == *player_entity {
logger = logger logger = logger
.colour(renderable_colour(&renderables, entity))
.append("You") .append("You")
.colour(WHITE)
.append("are confused!"); .append("are confused!");
log = true; log = true;
gamelog::record_event("player_confused", 1); gamelog::record_event(EVENT::PlayerConfused(1));
} else { } else {
logger = logger logger = logger
.append("The") .append("The")

View file

@ -12,7 +12,7 @@ use crate::{
WantsToApproach, WantsToApproach,
WantsToFlee, WantsToFlee,
}; };
use rltk::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
use std::collections::HashSet; use std::collections::HashSet;
@ -81,10 +81,16 @@ impl<'a> System<'a> for VisibleAI {
} }
reactions.sort_by(|(a, _, _), (b, _, _)| { reactions.sort_by(|(a, _, _), (b, _, _)| {
let (a_x, a_y) = (a % (map.width as usize), a / (map.width as usize)); let (a_x, a_y) = (a % (map.width as usize), a / (map.width as usize));
let dist_a = DistanceAlg::PythagorasSquared.distance2d(Point::new(a_x, a_y), Point::new(pos.x, pos.y)); let dist_a = DistanceAlg::PythagorasSquared.distance2d(
Point::new(a_x, a_y),
Point::new(pos.x, pos.y)
);
let dist_a_estimate = dist_a as i32; let dist_a_estimate = dist_a as i32;
let (b_x, b_y) = (b % (map.width as usize), b / (map.width as usize)); let (b_x, b_y) = (b % (map.width as usize), b / (map.width as usize));
let dist_b = DistanceAlg::PythagorasSquared.distance2d(Point::new(b_x, b_y), Point::new(pos.x, pos.y)); let dist_b = DistanceAlg::PythagorasSquared.distance2d(
Point::new(b_x, b_y),
Point::new(pos.x, pos.y)
);
let dist_b_estimate = dist_b as i32; let dist_b_estimate = dist_b as i32;
return dist_b_estimate.cmp(&dist_a_estimate); return dist_b_estimate.cmp(&dist_a_estimate);
}); });
@ -96,7 +102,9 @@ impl<'a> System<'a> for VisibleAI {
wants_to_approach wants_to_approach
.insert(entity, WantsToApproach { idx: reaction.0 as i32 }) .insert(entity, WantsToApproach { idx: reaction.0 as i32 })
.expect("Error inserting WantsToApproach"); .expect("Error inserting WantsToApproach");
chasing.insert(entity, Chasing { target: reaction.2 }).expect("Unable to insert Chasing"); chasing
.insert(entity, Chasing { target: reaction.2 })
.expect("Unable to insert Chasing");
continue; continue;
} }
} }
@ -108,7 +116,9 @@ impl<'a> System<'a> for VisibleAI {
} }
} }
if !flee.is_empty() { if !flee.is_empty() {
wants_to_flee.insert(entity, WantsToFlee { indices: flee }).expect("Unable to insert"); wants_to_flee
.insert(entity, WantsToFlee { indices: flee })
.expect("Unable to insert");
} }
} }
} }

View file

@ -1,26 +1,37 @@
use super::{ Hidden, Map, Mind, Position, Prop, Renderable }; use super::{ Hidden, Map, Mind, Position, Prop, Renderable };
use rltk::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
use std::ops::Mul; use std::ops::Mul;
const SHOW_BOUNDARIES: bool = false; const SHOW_BOUNDARIES: bool = false;
pub fn get_screen_bounds(ecs: &World, _ctx: &mut Rltk) -> (i32, i32, i32, i32, i32, i32) { pub fn get_screen_bounds(ecs: &World, _ctx: &mut BTerm) -> (i32, i32, i32, i32, i32, i32) {
let player_pos = ecs.fetch::<Point>(); let player_pos = ecs.fetch::<Point>();
let (x_chars, y_chars, x_offset, y_offset) = (69, 41, 1, 10); let map = ecs.fetch::<Map>();
let (x_chars, y_chars, mut x_offset, mut y_offset) = (69, 41, 1, 10);
let centre_x = (x_chars / 2) as i32; let centre_x = (x_chars / 2) as i32;
let centre_y = (y_chars / 2) as i32; let centre_y = (y_chars / 2) as i32;
let min_x = player_pos.x - centre_x; let min_x = if map.width < (x_chars as i32) {
let min_y = player_pos.y - centre_y; x_offset += ((x_chars as i32) - map.width) / 2;
0
} else {
(player_pos.x - centre_x).clamp(0, map.width - (x_chars as i32))
};
let min_y = if map.height < (y_chars as i32) {
y_offset += ((y_chars as i32) - map.height) / 2;
0
} else {
(player_pos.y - centre_y).clamp(0, map.height - (y_chars as i32))
};
let max_x = min_x + (x_chars as i32); let max_x = min_x + (x_chars as i32);
let max_y = min_y + (y_chars as i32); let max_y = min_y + (y_chars as i32);
(min_x, max_x, min_y, max_y, x_offset, y_offset) (min_x, max_x, min_y, max_y, x_offset, y_offset)
} }
pub fn render_camera(ecs: &World, ctx: &mut Rltk) { pub fn render_camera(ecs: &World, ctx: &mut BTerm) {
let map = ecs.fetch::<Map>(); let map = ecs.fetch::<Map>();
let (min_x, max_x, min_y, max_y, x_offset, y_offset) = get_screen_bounds(ecs, ctx); let (min_x, max_x, min_y, max_y, x_offset, y_offset) = get_screen_bounds(ecs, ctx);
@ -35,12 +46,19 @@ pub fn render_camera(ecs: &World, ctx: &mut Rltk) {
let (glyph, fg, bg) = crate::map::themes::get_tile_renderables_for_id( let (glyph, fg, bg) = crate::map::themes::get_tile_renderables_for_id(
idx, idx,
&*map, &*map,
Some(*ecs.fetch::<Point>()) Some(*ecs.fetch::<Point>()),
None
); );
ctx.set(x + x_offset, y + y_offset, fg, bg, glyph); ctx.set(x + x_offset, y + y_offset, fg, bg, glyph);
} }
} else if SHOW_BOUNDARIES { } else if SHOW_BOUNDARIES {
ctx.set(x + x_offset, y + y_offset, RGB::named(DARKSLATEGRAY), RGB::named(BLACK), rltk::to_cp437('#')); ctx.set(
x + x_offset,
y + y_offset,
RGB::named(DARKSLATEGRAY),
RGB::named(BLACK),
to_cp437('#')
);
} }
x += 1; x += 1;
} }
@ -66,12 +84,17 @@ pub fn render_camera(ecs: &World, ctx: &mut Rltk) {
if pos.x < max_x && pos.y < max_y && pos.x >= min_x && pos.y >= min_y { if pos.x < max_x && pos.y < max_y && pos.x >= min_x && pos.y >= min_y {
let mut draw = false; let mut draw = false;
let mut fg = render.fg; let mut fg = render.fg;
let mut bg = crate::map::themes::get_tile_renderables_for_id(idx, &*map, Some(*ecs.fetch::<Point>())).2; let mut bg = crate::map::themes::get_tile_renderables_for_id(
idx,
&*map,
Some(*ecs.fetch::<Point>()),
None
).2;
// Draw entities on visible tiles // Draw entities on visible tiles
if map.visible_tiles[idx] { if map.visible_tiles[idx] {
draw = true; draw = true;
} else { } else {
fg = fg.mul(crate::config::visuals::NON_VISIBLE_MULTIPLIER); fg = fg.mul(crate::data::visuals::NON_VISIBLE_MULTIPLIER);
// We don't darken BG, because get_tile_renderables_for_id handles this. // We don't darken BG, because get_tile_renderables_for_id handles this.
} }
@ -95,14 +118,20 @@ pub fn render_camera(ecs: &World, ctx: &mut Rltk) {
} }
} }
if draw { if draw {
ctx.set(entity_offset_x + x_offset, entity_offset_y + y_offset, fg, bg, render.glyph); ctx.set(
entity_offset_x + x_offset,
entity_offset_y + y_offset,
fg,
bg,
render.glyph
);
} }
} }
} }
} }
} }
pub fn render_debug_map(map: &Map, ctx: &mut Rltk) { pub fn render_debug_map(map: &Map, ctx: &mut BTerm) {
let player_pos = Point::new(map.width / 2, map.height / 2); let player_pos = Point::new(map.width / 2, map.height / 2);
let (x_chars, y_chars) = ctx.get_char_size(); let (x_chars, y_chars) = ctx.get_char_size();
@ -124,11 +153,16 @@ pub fn render_debug_map(map: &Map, ctx: &mut Rltk) {
if tx >= 0 && tx < map_width && ty >= 0 && ty < map_height { if tx >= 0 && tx < map_width && ty >= 0 && ty < map_height {
let idx = map.xy_idx(tx, ty); let idx = map.xy_idx(tx, ty);
if map.revealed_tiles[idx] { if map.revealed_tiles[idx] {
let (glyph, fg, bg) = crate::map::themes::get_tile_renderables_for_id(idx, &*map, None); let (glyph, fg, bg) = crate::map::themes::get_tile_renderables_for_id(
idx,
&*map,
None,
None
);
ctx.set(x, y, fg, bg, glyph); ctx.set(x, y, fg, bg, glyph);
} }
} else if SHOW_BOUNDARIES { } else if SHOW_BOUNDARIES {
ctx.set(x, y, RGB::named(rltk::GRAY), RGB::named(rltk::BLACK), rltk::to_cp437('·')); ctx.set(x, y, RGB::named(GRAY), RGB::named(BLACK), to_cp437('·'));
} }
x += 1; x += 1;
} }

View file

@ -1,12 +1,12 @@
use crate::gui::Ancestry; use crate::gui::Ancestry;
use crate::gui::Class; use crate::gui::Class;
use rltk::RGB; use bracket_lib::prelude::*;
use serde::{ Deserialize, Serialize }; use serde::{ Deserialize, Serialize };
use specs::error::NoError; use specs::error::NoError;
use specs::prelude::*; use specs::prelude::*;
use specs::saveload::{ ConvertSaveload, Marker }; use specs::saveload::{ ConvertSaveload, Marker };
use specs_derive::*; use specs_derive::*;
use std::collections::HashMap; use std::collections::{ HashMap, HashSet };
// Serialization helper code. We need to implement ConvertSaveload for each type that contains an // Serialization helper code. We need to implement ConvertSaveload for each type that contains an
// Entity. // Entity.
@ -21,7 +21,8 @@ pub struct SerializationHelper {
pub struct DMSerializationHelper { pub struct DMSerializationHelper {
pub map: super::map::MasterDungeonMap, pub map: super::map::MasterDungeonMap,
pub log: Vec<Vec<crate::gamelog::LogFragment>>, pub log: Vec<Vec<crate::gamelog::LogFragment>>,
pub events: HashMap<String, i32>, pub event_counts: HashMap<String, i32>,
pub events: HashMap<u32, Vec<String>>,
} }
#[derive(Component, ConvertSaveload, Clone)] #[derive(Component, ConvertSaveload, Clone)]
@ -39,12 +40,17 @@ pub struct OtherLevelPosition {
#[derive(Component, ConvertSaveload, Clone)] #[derive(Component, ConvertSaveload, Clone)]
pub struct Renderable { pub struct Renderable {
pub glyph: rltk::FontCharType, pub glyph: FontCharType,
pub fg: RGB, pub fg: RGB,
pub bg: RGB, pub bg: RGB,
pub render_order: i32, pub render_order: i32,
} }
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Bleeds {
pub colour: RGB,
}
#[derive(Component, Debug, Serialize, Deserialize, Clone)] #[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Player {} pub struct Player {}
@ -98,14 +104,14 @@ pub struct Mind {}
#[derive(Component, ConvertSaveload, Clone)] #[derive(Component, ConvertSaveload, Clone)]
pub struct Viewshed { pub struct Viewshed {
pub visible_tiles: Vec<rltk::Point>, pub visible_tiles: Vec<Point>,
pub range: i32, pub range: i32,
pub dirty: bool, pub dirty: bool,
} }
#[derive(Component, ConvertSaveload, Clone)] #[derive(Component, ConvertSaveload, Clone)]
pub struct Telepath { pub struct Telepath {
pub telepath_tiles: Vec<rltk::Point>, pub telepath_tiles: Vec<Point>,
pub range: i32, pub range: i32,
pub dirty: bool, pub dirty: bool,
} }
@ -237,16 +243,55 @@ pub enum BUC {
Blessed, Blessed,
} }
#[derive(Component, Debug, Serialize, Deserialize, Clone)] impl BUC {
pub fn noncursed(&self) -> bool {
match self {
BUC::Cursed => false,
_ => true,
}
}
}
#[derive(Component, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Clone)]
pub struct Beatitude { pub struct Beatitude {
pub buc: BUC, pub buc: BUC,
pub known: bool, pub known: bool,
} }
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]
pub enum ItemType {
Amulet,
Weapon,
Armour,
Comestible,
Scroll,
Spellbook,
Potion,
Ring,
Wand,
}
impl ItemType {
pub fn string(&self) -> &str {
match self {
ItemType::Amulet => "Amulets",
ItemType::Weapon => "Weapons",
ItemType::Armour => "Armour",
ItemType::Comestible => "Comestibles",
ItemType::Scroll => "Scrolls",
ItemType::Spellbook => "Spellbooks",
ItemType::Potion => "Potions",
ItemType::Ring => "Rings",
ItemType::Wand => "Wands",
}
}
}
#[derive(Component, Debug, Serialize, Deserialize, Clone)] #[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Item { pub struct Item {
pub weight: f32, // in lbs pub weight: f32, // in lbs
pub value: f32, // base pub value: f32, // base
pub category: ItemType,
} }
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
@ -310,6 +355,7 @@ pub enum WeaponAttribute {
#[derive(Component, Serialize, Deserialize, Clone)] #[derive(Component, Serialize, Deserialize, Clone)]
pub struct MeleeWeapon { pub struct MeleeWeapon {
pub damage_type: DamageType,
pub attribute: WeaponAttribute, pub attribute: WeaponAttribute,
pub damage_n_dice: i32, pub damage_n_dice: i32,
pub damage_die_type: i32, pub damage_die_type: i32,
@ -320,6 +366,7 @@ pub struct MeleeWeapon {
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct NaturalAttack { pub struct NaturalAttack {
pub name: String, pub name: String,
pub damage_type: DamageType,
pub damage_n_dice: i32, pub damage_n_dice: i32,
pub damage_die_type: i32, pub damage_die_type: i32,
pub damage_bonus: i32, pub damage_bonus: i32,
@ -359,8 +406,108 @@ pub struct ProvidesHealing {
pub modifier: i32, pub modifier: i32,
} }
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]
pub enum DamageType {
Physical,
Magic, // e.g. magic missiles, silvered weapons
Fire, // e.g. fireball
Cold, // e.g. cone of cold
Poison, // e.g. poison gas
Forced, // Bypasses any immunities. e.g. Hunger ticks.
}
impl DamageType {
pub fn is_magic(&self) -> bool {
match self {
DamageType::Magic | DamageType::Fire | DamageType::Cold => true,
_ => false,
}
}
}
#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)]
pub enum DamageModifier {
None,
Weakness,
Resistance,
Immune,
}
impl DamageModifier {
const NONE_MOD: f32 = 1.0;
const WEAK_MOD: f32 = 2.0;
const RESIST_MOD: f32 = 0.5;
const IMMUNE_MOD: f32 = 0.0;
pub fn multiplier(&self) -> f32 {
match self {
DamageModifier::None => Self::NONE_MOD,
DamageModifier::Weakness => Self::WEAK_MOD,
DamageModifier::Resistance => Self::RESIST_MOD,
DamageModifier::Immune => Self::IMMUNE_MOD,
}
}
}
#[derive(Component, Serialize, Deserialize, Debug, Clone)]
pub struct HasDamageModifiers {
pub modifiers: HashMap<DamageType, DamageModifier>,
}
impl HasDamageModifiers {
pub fn modifier(&self, damage_type: &DamageType) -> &DamageModifier {
self.modifiers.get(damage_type).unwrap_or(&DamageModifier::None)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Intrinsic {
Regeneration, // Regenerate 1 HP on every tick
Speed, // 4/3x speed multiplier
}
impl Intrinsic {
pub fn describe(&self) -> &str {
match self {
Intrinsic::Regeneration => "regenerates health",
Intrinsic::Speed => "is hasted",
}
}
}
#[derive(Component, Serialize, Deserialize, Debug, Clone)]
pub struct Intrinsics {
pub list: HashSet<Intrinsic>,
}
impl Intrinsics {
pub fn describe(&self) -> String {
let mut descriptions = Vec::new();
for intrinsic in &self.list {
descriptions.push(intrinsic.describe());
}
match descriptions.len() {
0 =>
unreachable!("describe() should never be called on an empty Intrinsics component."),
1 => format!("It {}.", descriptions[0]),
_ => {
let last = descriptions.pop().unwrap();
let joined = descriptions.join(", ");
format!("It {}, and {}.", joined, last)
}
}
}
}
#[derive(Component, Serialize, Deserialize, Debug, Clone)]
pub struct IntrinsicChanged {
pub gained: HashSet<Intrinsic>,
pub lost: HashSet<Intrinsic>,
}
#[derive(Component, Debug, ConvertSaveload, Clone)] #[derive(Component, Debug, ConvertSaveload, Clone)]
pub struct InflictsDamage { pub struct InflictsDamage {
pub damage_type: DamageType,
pub n_dice: i32, pub n_dice: i32,
pub sides: i32, pub sides: i32,
pub modifier: i32, pub modifier: i32,
@ -411,7 +558,7 @@ pub struct WantsToRemoveItem {
#[derive(Component, Debug, ConvertSaveload)] #[derive(Component, Debug, ConvertSaveload)]
pub struct WantsToUseItem { pub struct WantsToUseItem {
pub item: Entity, pub item: Entity,
pub target: Option<rltk::Point>, pub target: Option<Point>,
} }
#[derive(Component, Debug, Serialize, Deserialize, Clone)] #[derive(Component, Debug, Serialize, Deserialize, Clone)]
@ -440,8 +587,8 @@ pub struct Charges {
#[derive(Component, Serialize, Deserialize, Clone)] #[derive(Component, Serialize, Deserialize, Clone)]
pub struct SpawnParticleLine { pub struct SpawnParticleLine {
pub glyph: rltk::FontCharType, pub glyph: FontCharType,
pub tail_glyph: rltk::FontCharType, pub tail_glyph: FontCharType,
pub colour: RGB, pub colour: RGB,
pub lifetime_ms: f32, pub lifetime_ms: f32,
pub trail_colour: RGB, pub trail_colour: RGB,
@ -450,16 +597,16 @@ pub struct SpawnParticleLine {
#[derive(Component, Serialize, Deserialize, Clone)] #[derive(Component, Serialize, Deserialize, Clone)]
pub struct SpawnParticleSimple { pub struct SpawnParticleSimple {
pub glyph: rltk::FontCharType, pub glyph: FontCharType,
pub colour: RGB, pub colour: RGB,
pub lifetime_ms: f32, pub lifetime_ms: f32,
} }
#[derive(Component, Serialize, Deserialize, Clone)] #[derive(Component, Serialize, Deserialize, Clone)]
pub struct SpawnParticleBurst { pub struct SpawnParticleBurst {
pub glyph: rltk::FontCharType, pub glyph: FontCharType,
pub head_glyph: rltk::FontCharType, pub head_glyph: FontCharType,
pub tail_glyph: rltk::FontCharType, pub tail_glyph: FontCharType,
pub colour: RGB, pub colour: RGB,
pub lerp: RGB, pub lerp: RGB,
pub lifetime_ms: f32, pub lifetime_ms: f32,
@ -501,3 +648,20 @@ pub struct EntityMoved {}
#[derive(Component, Debug, Serialize, Deserialize, Clone)] #[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct MultiAttack {} pub struct MultiAttack {}
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Stackable {}
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct WantsToRemoveKey {}
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct WantsToDelete {}
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Key {
pub idx: usize,
}
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct WantsToAssignKey {}

View file

@ -1,21 +0,0 @@
// DEFAULT THEME
#[allow(dead_code)]
pub const WALL_GLYPH: char = '#';
pub const FLOOR_GLYPH: char = '.';
pub const DOWN_STAIR_GLYPH: char = '>';
pub const UP_STAIR_GLYPH: char = '<';
pub const WOOD_FLOOR_GLYPH: char = '.';
pub const FENCE_GLYPH: char = '=';
pub const BRIDGE_GLYPH: char = '.';
pub const GRAVEL_GLYPH: char = ';';
pub const ROAD_GLYPH: char = '.';
pub const GRASS_GLYPH: char = '"';
pub const FOLIAGE_GLYPH: char = ':';
pub const HEAVY_FOLIAGE_GLYPH: char = ';';
pub const SAND_GLYPH: char = '.';
pub const SHALLOW_WATER_GLYPH: char = '~';
pub const DEEP_WATER_GLYPH: char = '≈';
pub const BARS_GLYPH: char = '#';
// FOREST THEME
pub const FOREST_WALL_GLYPH: char = '♣';

View file

@ -1,10 +1,140 @@
pub mod entity; use bracket_lib::prelude::*;
pub mod visuals; use toml::Value;
pub mod glyphs; use serde::{ Serialize, Deserialize };
pub mod messages;
pub mod char_create;
// DEBUG/LOGGING lazy_static! {
pub const SHOW_MAPGEN: bool = false; // Shows the step-by-step map gen process. pub static ref CONFIG: Config = try_load_configuration();
pub const LOG_SPAWNING: bool = true; // Logs spawning of entities. }
pub const LOG_TICKS: bool = false; // Logs hunger/energy ticks.
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub logging: LogConfig,
pub visuals: VisualConfig,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LogConfig {
pub show_mapgen: bool,
pub log_combat: bool,
pub log_spawning: bool,
pub log_ticks: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VisualConfig {
pub with_scanlines: bool,
pub with_screen_burn: bool,
pub with_darken_by_distance: bool,
pub use_bitset_walls: bool,
pub use_coloured_tile_bg: bool,
pub add_colour_variance: bool,
}
impl Default for Config {
fn default() -> Self {
Config {
logging: LogConfig {
show_mapgen: false,
log_combat: false,
log_spawning: false,
log_ticks: false,
},
visuals: VisualConfig {
with_scanlines: false,
with_screen_burn: false,
with_darken_by_distance: true,
use_bitset_walls: true,
use_coloured_tile_bg: true,
add_colour_variance: true,
},
}
}
}
impl Config {
pub fn load_from_file(filename: &str) -> Config {
if let Ok(contents) = std::fs::read_to_string(filename) {
let parsed_config: Result<Value, _> = toml::from_str(&contents);
if let Ok(parsed_config) = parsed_config {
let mut config = Config::default();
let mut requires_write = false;
requires_write |= config.logging.apply_values(&parsed_config);
requires_write |= config.visuals.apply_values(&parsed_config);
if requires_write {
if let Err(write_err) = config.save_to_file(filename) {
console::log(format!("Error writing config: {:?}", write_err));
}
}
return config;
}
}
let config = Config::default();
if let Err(write_err) = config.save_to_file(filename) {
console::log(format!("Error writing config: {:?}", write_err));
}
return config;
}
pub fn save_to_file(&self, filename: &str) -> Result<(), Box<dyn std::error::Error>> {
let toml_string = toml::to_string(self)?;
std::fs::write(filename, toml_string)?;
Ok(())
}
}
macro_rules! apply_bool_value {
($config:expr, $parsed_config:expr, $changed:expr, $field:ident) => {
if let Some(value) = $parsed_config.get(stringify!($field)).and_then(|v| v.as_bool()) {
if $config.$field != value {
$config.$field = value;
$changed = true;
}
}
};
}
trait Section {
fn apply_values(&mut self, parsed_config: &Value) -> bool;
}
impl Section for LogConfig {
fn apply_values(&mut self, parsed_config: &Value) -> bool {
if let Some(section) = parsed_config.get("logging") {
let mut missing = false;
apply_bool_value!(self, section, missing, log_spawning);
apply_bool_value!(self, section, missing, log_ticks);
missing
} else {
true
}
}
}
impl Section for VisualConfig {
fn apply_values(&mut self, parsed_config: &Value) -> bool {
if let Some(section) = parsed_config.get("visuals") {
let mut missing = false;
apply_bool_value!(self, section, missing, with_scanlines);
apply_bool_value!(self, section, missing, with_screen_burn);
apply_bool_value!(self, section, missing, with_darken_by_distance);
apply_bool_value!(self, section, missing, use_bitset_walls);
apply_bool_value!(self, section, missing, use_coloured_tile_bg);
apply_bool_value!(self, section, missing, add_colour_variance);
missing
} else {
true
}
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn try_load_configuration() -> Config {
let config: Config = Config::load_from_file("config.toml");
return config;
}
#[cfg(target_arch = "wasm32")]
pub fn try_load_configuration() -> Config {
let config = Config::default();
return config;
}

View file

@ -1,39 +0,0 @@
// POST-PROCESSING
pub const WITH_SCANLINES: bool = false; // Adds scanlines to the screen.
pub const WITH_SCREEN_BURN: bool = false; // Requires WITH_SCANLINES.
pub const WITH_DARKEN_BY_DISTANCE: bool = true; // If further away tiles should get darkened, instead of a harsh transition to non-visible.
pub const MAX_COLOUR_OFFSET_PERCENT: i32 = 30;
pub const WITH_SCANLINES_BRIGHTEN_AMOUNT: f32 = 0.1; // 0.0 = no brightening, 1.0 = full brightening.
pub const NON_VISIBLE_MULTIPLIER: f32 = 0.3; // 0.0 = black, 1.0 = full colour.
pub const NON_VISIBLE_MULTIPLIER_IF_SCANLINES: f32 = 0.8; // as above, but when using scanlines. should be higher.
pub const MAX_DARKENING: f32 = 0.45; // 0.0 = black, 1.0 = full colour - only used if WITH_DARKEN_BY_DISTANCE is true.
pub const MAX_DARKENING_IF_SCANLINES: f32 = 0.9; // as above, but when using scanlines. should be higher.
pub const START_DARKEN_AT_N_TILES: f32 = 8.0; // start darkening at this distance (should always be less than entity::DEFAULT_VIEWSHED_STANDARD).
pub const SHORT_PARTICLE_LIFETIME: f32 = 100.0; // in ms
pub const DEFAULT_PARTICLE_LIFETIME: f32 = 200.0;
pub const LONG_PARTICLE_LIFETIME: f32 = 300.0;
// THEMES
pub const BLOODSTAIN_COLOUR: (u8, u8, u8) = (153, 0, 0);
// DEFAULT THEME
pub const DEFAULT_BG_COLOUR: (u8, u8, u8) = (29, 50, 50);
pub const WALL_COLOUR: (u8, u8, u8) = (229, 191, 94);
pub const FLOOR_COLOUR: (u8, u8, u8) = (25, 204, 122);
pub const STAIR_COLOUR: (u8, u8, u8) = (200, 200, 0);
pub const WOOD_FLOOR_COLOUR: (u8, u8, u8) = (41, 30, 20);
pub const FENCE_FG_COLOUR: (u8, u8, u8) = (110, 24, 0);
pub const FENCE_COLOUR: (u8, u8, u8) = (45, 30, 10);
pub const BRIDGE_COLOUR: (u8, u8, u8) = (42, 48, 37);
pub const GRAVEL_COLOUR: (u8, u8, u8) = (26, 26, 53);
pub const ROAD_COLOUR: (u8, u8, u8) = (8, 38, 40);
pub const GRASS_COLOUR: (u8, u8, u8) = (9, 65, 6);
pub const FOLIAGE_COLOUR: (u8, u8, u8) = (5, 60, 5);
pub const HEAVY_FOLIAGE_COLOUR: (u8, u8, u8) = (5, 60, 5);
pub const SAND_COLOUR: (u8, u8, u8) = (70, 70, 21);
pub const SHALLOW_WATER_COLOUR: (u8, u8, u8) = (24, 47, 99);
pub const DEEP_WATER_COLOUR: (u8, u8, u8) = (18, 33, 63);
pub const BARS_COLOUR: (u8, u8, u8) = (100, 100, 100);
// FOREST THEME
pub const FOREST_WALL_COLOUR: (u8, u8, u8) = (0, 153, 0);

View file

@ -11,9 +11,12 @@ use super::{
Position, Position,
Renderable, Renderable,
RunState, RunState,
WantsToRemoveKey,
WantsToDelete,
}; };
use rltk::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
use crate::data::events;
pub fn delete_the_dead(ecs: &mut World) { pub fn delete_the_dead(ecs: &mut World) {
let mut dead: Vec<Entity> = Vec::new(); let mut dead: Vec<Entity> = Vec::new();
@ -39,7 +42,7 @@ pub fn delete_the_dead(ecs: &mut World) {
.append("The") .append("The")
.colour(renderable_colour(&renderables, entity)) .colour(renderable_colour(&renderables, entity))
.append(&victim_name.name) .append(&victim_name.name)
.colour(rltk::WHITE) .colour(WHITE)
.append("is destroyed!") .append("is destroyed!")
.log(); .log();
} else { } else {
@ -48,7 +51,7 @@ pub fn delete_the_dead(ecs: &mut World) {
.append("The") .append("The")
.colour(renderable_colour(&renderables, entity)) .colour(renderable_colour(&renderables, entity))
.append(&victim_name.name) .append(&victim_name.name)
.colour(rltk::WHITE) .colour(WHITE)
.append("dies!") .append("dies!")
.log(); .log();
} }
@ -64,7 +67,17 @@ pub fn delete_the_dead(ecs: &mut World) {
} }
} }
} }
let (items_to_delete, loot_to_spawn) = handle_dead_entity_items(ecs, &dead); let (mut items_to_delete, loot_to_spawn) = handle_dead_entity_items(ecs, &dead);
{
let entities = ecs.entities();
let removekeys = ecs.read_storage::<WantsToRemoveKey>();
let delete = ecs.read_storage::<WantsToDelete>();
// Add items marked for deletion to the list, but only if they've already had their
// key assignments handled, to ensurew we don't leave any dangling references behind.
for (e, _d, _r) in (&entities, &delete, !&removekeys).join() {
items_to_delete.push(e);
}
}
for loot in loot_to_spawn { for loot in loot_to_spawn {
crate::raws::spawn_named_entity( crate::raws::spawn_named_entity(
&crate::raws::RAWS.lock().unwrap(), &crate::raws::RAWS.lock().unwrap(),
@ -80,12 +93,16 @@ pub fn delete_the_dead(ecs: &mut World) {
} }
// For everything that died, increment the event log, and delete. // For everything that died, increment the event log, and delete.
for victim in dead { for victim in dead {
gamelog::record_event("death_count", 1); gamelog::record_event(events::EVENT::Turn(1));
// TODO: Delete stuff from inventory? This should be handled elsewhere.
ecs.delete_entity(victim).expect("Unable to delete."); ecs.delete_entity(victim).expect("Unable to delete.");
} }
} }
fn handle_dead_entity_items(ecs: &mut World, dead: &Vec<Entity>) -> (Vec<Entity>, Vec<(String, Position)>) { fn handle_dead_entity_items(
ecs: &mut World,
dead: &Vec<Entity>
) -> (Vec<Entity>, Vec<(String, Position)>) {
let mut to_drop: Vec<(Entity, Position)> = Vec::new(); let mut to_drop: Vec<(Entity, Position)> = Vec::new();
let mut to_spawn: Vec<(String, Position)> = Vec::new(); let mut to_spawn: Vec<(String, Position)> = Vec::new();
let entities = ecs.entities(); let entities = ecs.entities();

View file

@ -44,6 +44,6 @@ pub const WIZARD_STARTING_FOOD: &str = "1d2+1";
pub const WIZARD_STARTING_WEAPON: &str = "equip_dagger"; pub const WIZARD_STARTING_WEAPON: &str = "equip_dagger";
pub const WIZARD_STARTING_ARMOUR: &str = "equip_back_protection"; pub const WIZARD_STARTING_ARMOUR: &str = "equip_back_protection";
pub const WIZARD_MAX_SCROLL_LVL: i32 = 3; pub const WIZARD_MAX_SCROLL_LVL: i32 = 3;
pub const WIZARD_SCROLL_AMOUNT: &str = "1d3"; pub const WIZARD_SCROLL_AMOUNT: &str = "1d3+1";
pub const WIZARD_POTION_AMOUNT: &str = "1d3-1"; pub const WIZARD_POTION_AMOUNT: &str = "1d3";
pub const VILLAGER_STARTING_FOOD: &str = "1d3+2"; pub const VILLAGER_STARTING_FOOD: &str = "1d3+2";

View file

@ -1,6 +1,10 @@
pub const DEFAULT_VIEWSHED_STANDARD: i32 = 16; // Standard viewshed radius for almost all entities. pub const DEFAULT_VIEWSHED_STANDARD: i32 = 16; // Standard viewshed radius for almost all entities.
pub const CARRY_CAPACITY_PER_STRENGTH: i32 = 8; // How much weight can be carried per point of strength. pub const CARRY_CAPACITY_PER_STRENGTH: i32 = 5; // How much weight can be carried per point of strength.
pub const NORMAL_SPEED: i32 = 12; // Normal speed for almost all entities. pub const NORMAL_SPEED: i32 = 12; // Normal speed for almost all entities.
pub const SPEED_MOD_BURDENED: f32 = 0.75;
pub const SPEED_MOD_STRAINED: f32 = 0.5;
pub const SPEED_MOD_OVERLOADED: f32 = 0.25;
pub const SPEED_MOD_OVERMAP_TRAVEL: f32 = 0.33;
pub const TURN_COST_MULTIPLIER: i32 = 4; // How many ticks for NORMAL_SPEED to get a turn. pub const TURN_COST_MULTIPLIER: i32 = 4; // How many ticks for NORMAL_SPEED to get a turn.
pub const ATTR_BONUS_0: i32 = 10; // At this value, the attribute bonus is 0. pub const ATTR_BONUS_0: i32 = 10; // At this value, the attribute bonus is 0.
pub const ATTR_NEEDED_PER_POINT: i32 = 2; // How many points +- ATTR_BONUS_0 are needed per +- 1 bonus. pub const ATTR_NEEDED_PER_POINT: i32 = 2; // How many points +- ATTR_BONUS_0 are needed per +- 1 bonus.

27
src/data/events.rs Normal file
View file

@ -0,0 +1,27 @@
use serde::{ Deserialize, Serialize };
#[derive(Serialize, Deserialize, Clone)]
pub enum EVENT {
Turn(i32),
Level(i32),
ChangedFloor(String),
PlayerConfused(i32),
KickedSomething(i32),
BrokeDoor(i32),
LookedForHelp(i32),
Killed(String),
PlayerDied(String),
Discovered(String),
Identified(String),
}
impl EVENT {
pub const COUNT_TURN: &str = "turns";
pub const COUNT_KILLED: &str = "killed";
pub const COUNT_LEVEL: &str = "level";
pub const COUNT_CHANGED_FLOOR: &str = "changed_floor";
pub const COUNT_BROKE_DOOR: &str = "BrokeDoor";
pub const COUNT_PLAYER_CONFUSED: &str = "PlayerConfused";
pub const COUNT_KICK: &str = "kick";
pub const COUNT_LOOKED_FOR_HELP: &str = "LookedForHelp";
}

34
src/data/ids.rs Normal file
View file

@ -0,0 +1,34 @@
use super::names::*;
use super::visuals::*;
use bracket_lib::prelude::*;
pub const ID_OVERMAP: i32 = 1;
pub const ID_TOWN: i32 = 10;
pub const ID_TOWN2: i32 = ID_TOWN + 1;
pub const ID_TOWN3: i32 = ID_TOWN + 2;
pub const ID_INFINITE: i32 = 1000;
pub fn get_local_desc(id: i32) -> String {
let str = match id {
ID_TOWN => NAME_STARTER_TOWN,
ID_INFINITE => NAME_DUNGEON_RANDOM,
_ => "an unnamed overmap tile",
};
return str.to_string();
}
pub fn get_local_col(id: i32) -> RGB {
let col = match id {
ID_TOWN => TO_TOWN_COLOUR,
ID_TOWN2 => GRASS_COLOUR,
ID_OVERMAP => TO_OVERMAP_COLOUR,
_ => (255, 255, 255),
};
return RGB::from_u8(col.0, col.1, col.2);
}
pub fn rgb_to_u8(col: RGB) -> (u8, u8, u8) {
return ((col.r * 255.0) as u8, (col.g * 255.0) as u8, (col.b * 255.0) as u8);
}

View file

@ -25,7 +25,28 @@ pub const NUTRITION_BLESSED: &str = "Delicious";
pub const LEVELUP_PLAYER: &str = "Welcome to experience level"; pub const LEVELUP_PLAYER: &str = "Welcome to experience level";
pub const YOU_PICKUP_ITEM: &str = "You pick up the"; pub const YOU_PICKUP_ITEM: &str = "You pick up the";
pub const NO_MORE_KEYS: &str = "Your backpack cannot accomodate any more items";
pub const YOU_DROP_ITEM: &str = "You drop the"; pub const YOU_DROP_ITEM: &str = "You drop the";
pub const YOU_EQUIP_ITEM: &str = "You equip the"; pub const YOU_EQUIP_ITEM: &str = "You equip the";
pub const YOU_REMOVE_ITEM: &str = "You unequip your"; pub const YOU_REMOVE_ITEM: &str = "You unequip your";
pub const YOU_REMOVE_ITEM_CURSED: &str = "You can't remove the"; pub const YOU_REMOVE_ITEM_CURSED: &str = "You can't remove the";
/// Prefixes death message.
pub const PLAYER_DIED: &str = "You died!";
/// Death message specifiers. Appended after PlayerDied.
pub const PLAYER_DIED_SUICIDE: &str = "You killed yourself";
pub const PLAYER_DIED_NAMED_ATTACKER: &str = "You were killed by";
pub const PLAYER_DIED_UNKNOWN: &str = "You were killed"; // Ultimately, this should never be used. Slowly include specific messages for any death.
/// Death message addendums. Appended at end of death message.
pub const PLAYER_DIED_ADDENDUM_FIRST: &str = " ";
pub const PLAYER_DIED_ADDENDUM_MID: &str = ", ";
pub const PLAYER_DIED_ADDENDUM_LAST: &str = ", and ";
pub const STATUS_CONFUSED_STRING: &str = "confused";
pub const STATUS_BLIND_STRING: &str = "blinded";
// Results in something like: "You died! You were killed by a kobold captain, whilst confused."
// Dungeon features
pub const FEATURE_TREANTS: &str = "You feel an unusual freshness in the air.";
pub const FEATURE_BARRACKS_GOBLIN: &str = "You hear an order being barked, and ignored.";
pub const FEATURE_BARRACKS_KOBOLD: &str = "You hear someone being reprimanded for disobedience.";
pub const FEATURE_BARRACKS_ORC: &str = "You hear someone barking orders.";

7
src/data/mod.rs Normal file
View file

@ -0,0 +1,7 @@
pub mod entity;
pub mod visuals;
pub mod messages;
pub mod char_create;
pub mod events;
pub mod ids;
pub mod names;

8
src/data/names.rs Normal file
View file

@ -0,0 +1,8 @@
pub const NAME_OVERMAP: &str = "the travel map";
pub const SHORTNAME_OVERMAP: &str = "Travel";
pub const NAME_DUNGEON_RANDOM: &str = "the dungeon";
pub const SHORTNAME_DUNGEON_RANDOM: &str = "D";
pub const NAME_STARTER_TOWN: &str = "the port town of Saff";
pub const SHORTNAME_STARTER_TOWN: &str = "Saff";
pub const NAME_FOREST_BUILDER: &str = "the woods outside of town";
pub const SHORTNAME_FOREST_BUILDER: &str = "Woods";

92
src/data/visuals.rs Normal file
View file

@ -0,0 +1,92 @@
use bracket_lib::prelude::*;
// POST-PROCESSING
pub const WITH_DARKEN_BY_DISTANCE: bool = true; // If further away tiles should get darkened, instead of a harsh transition to non-visible.
pub const BRIGHTEN_FG_COLOUR_BY: i32 = 16;
pub const GLOBAL_OFFSET_MIN_CLAMP: f32 = -0.5;
pub const GLOBAL_OFFSET_MAX_CLAMP: f32 = 1.0;
pub const WITH_SCANLINES_BRIGHTEN_AMOUNT: f32 = 0.1; // 0.0 = no brightening, 1.0 = full brightening.
pub const NON_VISIBLE_MULTIPLIER: f32 = 0.3; // 0.0 = black, 1.0 = full colour.
pub const NON_VISIBLE_MULTIPLIER_IF_SCANLINES: f32 = 0.8; // as above, but when using scanlines. should be higher.
pub const MAX_DARKENING: f32 = 0.45; // 0.0 = black, 1.0 = full colour - only used if WITH_DARKEN_BY_DISTANCE is true.
pub const MAX_DARKENING_IF_SCANLINES: f32 = 0.9; // as above, but when using scanlines. should be higher.
pub const START_DARKEN_AT_N_TILES: f32 = 8.0; // start darkening at this distance (should always be less than entity::DEFAULT_VIEWSHED_STANDARD).
pub const SHORT_PARTICLE_LIFETIME: f32 = 100.0; // in ms
pub const DEFAULT_PARTICLE_LIFETIME: f32 = 200.0;
pub const LONG_PARTICLE_LIFETIME: f32 = 300.0;
pub const TARGETING_CURSOR_COL: (u8, u8, u8) = GOLDENROD;
pub const TARGETING_LINE_COL: (u8, u8, u8) = LIGHTGOLDENROD;
pub const TARGETING_AOE_COL: (u8, u8, u8) = (20, 20, 20);
pub const TARGETING_VALID_COL: (u8, u8, u8) = (10, 10, 10);
// THEMES
pub const BLOODSTAIN_COLOUR: (u8, u8, u8) = (153, 0, 0);
// DEFAULT THEME
pub const DEFAULT_BG_COLOUR: (u8, u8, u8) = (29, 50, 50);
pub const DEFAULT_BG_OFFSETS: (i32, i32, i32) = (10, 10, 10);
pub const WALL_COLOUR: (u8, u8, u8) = (229, 191, 94);
pub const WALL_OFFSETS: (i32, i32, i32) = (48, 48, 48);
pub const FLOOR_COLOUR: (u8, u8, u8) = (25, 204, 122);
pub const FLOOR_OFFSETS: (i32, i32, i32) = (10, 10, 10);
pub const STAIR_COLOUR: (u8, u8, u8) = (200, 200, 0);
pub const STAIR_OFFSETS: (i32, i32, i32) = (10, 10, 10);
pub const WOOD_FLOOR_COLOUR: (u8, u8, u8) = (41, 30, 20);
pub const WOOD_FLOOR_OFFSETS: (i32, i32, i32) = (10, 10, 10);
pub const FENCE_FG_COLOUR: (u8, u8, u8) = (110, 24, 0);
pub const FENCE_COLOUR: (u8, u8, u8) = (45, 30, 10);
pub const FENCE_OFFSETS: (i32, i32, i32) = (10, 10, 10);
pub const BRIDGE_COLOUR: (u8, u8, u8) = (42, 48, 37);
pub const BRIDGE_OFFSETS: (i32, i32, i32) = (10, 10, 10);
pub const GRAVEL_COLOUR: (u8, u8, u8) = (26, 26, 53);
pub const GRAVEL_OFFSETS: (i32, i32, i32) = (10, 10, 10);
pub const ROAD_COLOUR: (u8, u8, u8) = (8, 38, 40);
pub const ROAD_OFFSETS: (i32, i32, i32) = (10, 10, 10);
pub const GRASS_COLOUR: (u8, u8, u8) = (9, 65, 6);
pub const GRASS_OFFSETS: (i32, i32, i32) = (3, 20, 10);
pub const FOLIAGE_COLOUR: (u8, u8, u8) = (5, 60, 5);
pub const FOLIAGE_OFFSETS: (i32, i32, i32) = (10, 10, 10);
pub const HEAVY_FOLIAGE_COLOUR: (u8, u8, u8) = (5, 60, 5);
pub const HEAVY_FOLIAGE_OFFSETS: (i32, i32, i32) = (10, 10, 10);
pub const SAND_COLOUR: (u8, u8, u8) = (70, 70, 21);
pub const SAND_OFFSETS: (i32, i32, i32) = (10, 10, 10);
pub const SHALLOW_WATER_COLOUR: (u8, u8, u8) = (24, 47, 99);
pub const SHALLOW_WATER_OFFSETS: (i32, i32, i32) = (3, 10, 45);
pub const DEEP_WATER_COLOUR: (u8, u8, u8) = (18, 33, 63);
pub const DEEP_WATER_OFFSETS: (i32, i32, i32) = (5, 10, 32);
pub const BARS_COLOUR: (u8, u8, u8) = (100, 100, 100);
pub const IMPASSABLE_MOUNTAIN_COLOUR: (u8, u8, u8) = (20, 23, 20);
pub const IMPASSABLE_MOUNTAIN_OFFSETS: (i32, i32, i32) = (4, 4, 4);
// FOREST THEME
pub const FOREST_WALL_COLOUR: (u8, u8, u8) = (0, 153, 0);
// DEFAULT THEME
#[allow(dead_code)]
pub const WALL_GLYPH: char = '#';
pub const FLOOR_GLYPH: char = '.';
pub const DOWN_STAIR_GLYPH: char = '>';
pub const UP_STAIR_GLYPH: char = '<';
pub const WOOD_FLOOR_GLYPH: char = '.';
pub const FENCE_GLYPH: char = '=';
pub const BRIDGE_GLYPH: char = '.';
pub const GRAVEL_GLYPH: char = ';';
pub const ROAD_GLYPH: char = '.';
pub const GRASS_GLYPH: char = '"';
pub const FOLIAGE_GLYPH: char = ':';
pub const HEAVY_FOLIAGE_GLYPH: char = ';';
pub const SAND_GLYPH: char = '.';
pub const SHALLOW_WATER_GLYPH: char = '~';
pub const DEEP_WATER_GLYPH: char = '≈';
pub const BARS_GLYPH: char = '#';
pub const IMPASSABLE_MOUNTAIN_GLYPH: char = '▲';
// FOREST THEME
pub const FOREST_WALL_GLYPH: char = '♣';
// Overmap/transition stuff
pub const TO_OVERMAP_GLYPH: char = '<';
pub const TO_OVERMAP_COLOUR: (u8, u8, u8) = (205, 127, 50);
pub const TO_TOWN_GLYPH: char = 'o';
pub const TO_TOWN_COLOUR: (u8, u8, u8) = (205, 127, 50);

View file

@ -9,19 +9,42 @@ use crate::{
Map, Map,
Player, Player,
Pools, Pools,
Name,
Blind,
HungerClock,
HungerState,
Bleeds,
HasDamageModifiers,
}; };
use crate::config::visuals::{ DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME }; use crate::gui::with_article;
use crate::config::messages::LEVELUP_PLAYER; use crate::data::visuals::{ DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME };
use rltk::prelude::*; use crate::data::messages::LEVELUP_PLAYER;
use crate::data::events::*;
use crate::data::messages::*;
use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) { pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) {
let mut pools = ecs.write_storage::<Pools>(); let mut pools = ecs.write_storage::<Pools>();
if let Some(target_pool) = pools.get_mut(target) { if let Some(target_pool) = pools.get_mut(target) {
if !target_pool.god { if !target_pool.god {
if let EffectType::Damage { amount } = damage.effect_type { if let EffectType::Damage { amount, damage_type } = damage.effect_type {
target_pool.hit_points.current -= amount; let mult = if
add_effect(None, EffectType::Bloodstain, Targets::Entity { target }); let Some(modifiers) = ecs.read_storage::<HasDamageModifiers>().get(target)
{
modifiers.modifier(&damage_type).multiplier()
} else {
1.0
};
target_pool.hit_points.current -= ((amount as f32) * mult) as i32;
let bleeders = ecs.read_storage::<Bleeds>();
if let Some(bleeds) = bleeders.get(target) {
add_effect(
None,
EffectType::Bloodstain { colour: bleeds.colour },
Targets::Entity { target }
);
}
add_effect( add_effect(
None, None,
EffectType::Particle { EffectType::Particle {
@ -49,7 +72,10 @@ pub fn heal_damage(ecs: &mut World, heal: &EffectSpawner, target: Entity) {
if let Some(pool) = pools.get_mut(target) { if let Some(pool) = pools.get_mut(target) {
if let EffectType::Healing { amount, increment_max } = &heal.effect_type { if let EffectType::Healing { amount, increment_max } = &heal.effect_type {
let before = pool.hit_points.current; let before = pool.hit_points.current;
pool.hit_points.current = i32::min(pool.hit_points.max, pool.hit_points.current + amount); pool.hit_points.current = i32::min(
pool.hit_points.max,
pool.hit_points.current + amount
);
if pool.hit_points.current - before < *amount && *increment_max { if pool.hit_points.current - before < *amount && *increment_max {
// If the heal was not fully effective, and healing source was noncursed, increase max HP by 1. // If the heal was not fully effective, and healing source was noncursed, increase max HP by 1.
pool.hit_points.max += 1; pool.hit_points.max += 1;
@ -78,44 +104,61 @@ pub fn add_confusion(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
} }
} }
pub fn bloodstain(ecs: &mut World, target: usize) { pub fn bloodstain(ecs: &mut World, target: usize, colour: RGB) {
let mut map = ecs.fetch_mut::<Map>(); let mut map = ecs.fetch_mut::<Map>();
// If the current tile isn't bloody, bloody it. // If the current tile isn't bloody, bloody it.
if !map.bloodstains.contains(&target) { if !map.bloodstains.contains_key(&target) {
map.bloodstains.insert(target); map.bloodstains.insert(target, colour);
return; return;
} }
let mut spread: i32 = target as i32; if map.bloodstains.get(&target).unwrap() == &colour {
let mut attempts: i32 = 0; let mut spread: i32 = target as i32;
// Otherwise, roll to move one tile in any direction. let mut attempts: i32 = 0;
// If this tile isn't bloody, bloody it. If not, loop. // Otherwise, roll to move one tile in any direction.
loop { // If this tile isn't bloody, bloody it. If not, loop.
let mut rng = ecs.write_resource::<RandomNumberGenerator>(); loop {
attempts += 1; let mut rng = ecs.write_resource::<RandomNumberGenerator>();
spread = match rng.roll_dice(1, 8) { attempts += 1;
1 => spread + 1, spread = match rng.roll_dice(1, 8) {
2 => spread - 1, 1 => spread + 1,
3 => spread + 1 + map.width, 2 => spread - 1,
4 => spread - 1 + map.width, 3 => spread + 1 + map.width,
5 => spread + 1 - map.width, 4 => spread - 1 + map.width,
6 => spread - 1 - map.width, 5 => spread + 1 - map.width,
7 => spread + map.width, 6 => spread - 1 - map.width,
_ => spread - map.width, 7 => spread + map.width,
}; _ => spread - map.width,
// - If we're in bounds and the tile is unbloodied, bloody it and return. };
// - If we ever leave bounds, return. // - If we're in bounds and the tile is unbloodied, bloody it and return.
// - Roll a dice on each failed attempt, with an increasing change to return (soft-capping max spread) // - If we ever leave bounds, return.
if spread > 0 && spread < map.height * map.width { // - Roll a dice on each failed attempt, with an increasing change to return (soft-capping max spread)
if !map.bloodstains.contains(&(spread as usize)) { if spread > 0 && spread < map.height * map.width {
map.bloodstains.insert(spread as usize); if !map.bloodstains.contains_key(&(spread as usize)) {
map.bloodstains.insert(spread as usize, colour);
return;
}
// If bloodied with the same colour, return
if map.bloodstains.get(&(spread as usize)).unwrap() == &colour {
if rng.roll_dice(1, 10 - attempts) == 1 {
return;
}
// If bloodied but a *different* colour, lerp this blood and current blood.
} else {
let new_col = map.bloodstains
.get(&(spread as usize))
.unwrap()
.lerp(colour, 0.5);
map.bloodstains.insert(spread as usize, new_col);
}
} else {
return; return;
} }
if rng.roll_dice(1, 10 - attempts) == 1 {
return;
}
} else {
return;
} }
} else {
let curr_blood = map.bloodstains.get(&target).unwrap();
let new_colour = curr_blood.lerp(colour, 0.5);
map.bloodstains.insert(target, new_colour);
return;
} }
} }
@ -133,17 +176,67 @@ fn get_next_level_requirement(level: i32) -> i32 {
return -1; return -1;
} }
fn get_death_message(ecs: &World, source: Entity) -> String {
let player = ecs.fetch::<Entity>();
let mut result: String = format!("{} ", PLAYER_DIED);
// If we killed ourselves,
if source == *player {
result.push_str(format!("{}", PLAYER_DIED_SUICIDE).as_str());
} else if let Some(name) = ecs.read_storage::<Name>().get(source) {
result.push_str(
format!("{} {}", PLAYER_DIED_NAMED_ATTACKER, with_article(name.name.clone())).as_str()
);
} else {
result.push_str(format!("{}", PLAYER_DIED_UNKNOWN).as_str());
}
// Status effects
{
let mut addendums: Vec<&str> = Vec::new();
if let Some(_confused) = ecs.read_storage::<Confusion>().get(*player) {
addendums.push(STATUS_CONFUSED_STRING);
}
if let Some(_blind) = ecs.read_storage::<Blind>().get(*player) {
addendums.push(STATUS_BLIND_STRING);
}
if !addendums.is_empty() {
result.push_str(" whilst");
for (i, addendum) in addendums.iter().enumerate() {
if i == 0 {
result.push_str(format!("{}{}", PLAYER_DIED_ADDENDUM_FIRST, addendum).as_str());
} else if i == addendums.len() {
result.push_str(format!("{}{}", PLAYER_DIED_ADDENDUM_LAST, addendum).as_str());
} else {
result.push_str(format!("{}{}", PLAYER_DIED_ADDENDUM_MID, addendum).as_str());
}
}
}
}
return result;
}
/// Handles EntityDeath effects. /// Handles EntityDeath effects.
pub fn entity_death(ecs: &mut World, effect: &EffectSpawner, target: Entity) { pub fn entity_death(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
let mut xp_gain = 0; let mut xp_gain = 0;
let mut pools = ecs.write_storage::<Pools>(); let mut pools = ecs.write_storage::<Pools>();
let attributes = ecs.read_storage::<Attributes>(); let attributes = ecs.read_storage::<Attributes>();
let names = ecs.read_storage::<Name>();
let player = ecs.fetch::<Entity>();
// If the target has a position, remove it from the SpatialMap. // If the target has a position, remove it from the SpatialMap.
if let Some(pos) = targeting::entity_position(ecs, target) { if let Some(pos) = targeting::entity_position(ecs, target) {
crate::spatial::remove_entity(target, pos as usize); crate::spatial::remove_entity(target, pos as usize);
} }
// If the target was killed by a source, cont. // If the target was killed by a source, cont.
if let Some(source) = effect.source { if let Some(source) = effect.source {
// If the target was the player, game over, and record source of death.
if target == *player {
gamelog::record_event(EVENT::PlayerDied(get_death_message(ecs, source)));
return;
} else {
// If the player was the source, record the kill.
if let Some(tar_name) = names.get(target) {
gamelog::record_event(EVENT::Killed(tar_name.name.clone()));
}
}
// Calc XP value of target. // Calc XP value of target.
if let Some(xp_value) = ecs.read_storage::<GrantsXP>().get(target) { if let Some(xp_value) = ecs.read_storage::<GrantsXP>().get(target) {
xp_gain += xp_value.amount; xp_gain += xp_value.amount;
@ -161,8 +254,13 @@ pub fn entity_death(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
source_pools.level += 1; source_pools.level += 1;
// If it was the PLAYER that levelled up: // If it was the PLAYER that levelled up:
if ecs.read_storage::<Player>().get(source).is_some() { if ecs.read_storage::<Player>().get(source).is_some() {
gamelog::record_event("player_level", 1); gamelog::record_event(EVENT::Level(1));
gamelog::Logger::new().append(LEVELUP_PLAYER).append_n(source_pools.level).append("!").log(); gamelog::Logger
::new()
.append(LEVELUP_PLAYER)
.append_n(source_pools.level)
.append("!")
.log();
let player_pos = ecs.fetch::<Point>(); let player_pos = ecs.fetch::<Point>();
let map = ecs.fetch_mut::<Map>(); let map = ecs.fetch_mut::<Map>();
for i in 0..5 { for i in 0..5 {
@ -188,7 +286,12 @@ pub fn entity_death(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
lifespan: LONG_PARTICLE_LIFETIME, lifespan: LONG_PARTICLE_LIFETIME,
delay: (i as f32) * 100.0, delay: (i as f32) * 100.0,
}, },
Targets::Tile { target: map.xy_idx(player_pos.x + (i - 2), player_pos.y - i) } Targets::Tile {
target: map.xy_idx(
player_pos.x + (i - 2),
player_pos.y - i
),
}
); );
add_effect( add_effect(
None, None,
@ -199,7 +302,12 @@ pub fn entity_death(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
lifespan: LONG_PARTICLE_LIFETIME, lifespan: LONG_PARTICLE_LIFETIME,
delay: (i as f32) * 100.0, delay: (i as f32) * 100.0,
}, },
Targets::Tile { target: map.xy_idx(player_pos.x - (i - 2), player_pos.y - i) } Targets::Tile {
target: map.xy_idx(
player_pos.x - (i - 2),
player_pos.y - i
),
}
); );
} }
} }
@ -224,5 +332,17 @@ pub fn entity_death(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
source_pools.mana.current += mana_gained; source_pools.mana.current += mana_gained;
} }
} }
} else {
if target == *player {
if let Some(hc) = ecs.read_storage::<HungerClock>().get(target) {
if hc.state == HungerState::Starving {
gamelog::record_event(EVENT::PlayerDied("You starved to death!".to_string()));
}
} else {
gamelog::record_event(
EVENT::PlayerDied("You died from unknown causes!".to_string())
);
}
}
} }
} }

11
src/effects/intrinsics.rs Normal file
View file

@ -0,0 +1,11 @@
use super::{ EffectSpawner, EffectType };
use specs::prelude::*;
pub fn add_intrinsic(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
let intrinsic = if let EffectType::AddIntrinsic { intrinsic } = &effect.effect_type {
intrinsic
} else {
unreachable!("add_intrinsic() called with the wrong EffectType")
};
add_intr!(ecs, target, *intrinsic);
}

View file

@ -1,15 +1,17 @@
use super::BUC; use super::BUC;
use crate::spatial; use crate::spatial;
use rltk::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::sync::Mutex; use std::sync::Mutex;
use crate::components::*;
mod damage; mod damage;
mod hunger; mod hunger;
mod particles; mod particles;
mod targeting; mod targeting;
mod triggers; mod triggers;
mod intrinsics;
pub use targeting::aoe_tiles; pub use targeting::aoe_tiles;
@ -24,6 +26,7 @@ lazy_static! {
pub enum EffectType { pub enum EffectType {
Damage { Damage {
amount: i32, amount: i32,
damage_type: DamageType,
}, },
Healing { Healing {
amount: i32, amount: i32,
@ -32,7 +35,9 @@ pub enum EffectType {
Confusion { Confusion {
turns: i32, turns: i32,
}, },
Bloodstain, Bloodstain {
colour: RGB,
},
Particle { Particle {
glyph: FontCharType, glyph: FontCharType,
fg: RGB, fg: RGB,
@ -47,6 +52,9 @@ pub enum EffectType {
ModifyNutrition { ModifyNutrition {
amount: i32, amount: i32,
}, },
AddIntrinsic {
intrinsic: Intrinsic,
},
TriggerFire { TriggerFire {
trigger: Entity, trigger: Entity,
}, },
@ -119,9 +127,11 @@ fn target_applicator(ecs: &mut World, effect: &EffectSpawner) {
// Otherwise, just match the effect and enact it directly. // Otherwise, just match the effect and enact it directly.
match &effect.target { match &effect.target {
Targets::Tile { target } => affect_tile(ecs, effect, *target), Targets::Tile { target } => affect_tile(ecs, effect, *target),
Targets::TileList { targets } => targets.iter().for_each(|target| affect_tile(ecs, effect, *target)), Targets::TileList { targets } =>
targets.iter().for_each(|target| affect_tile(ecs, effect, *target)),
Targets::Entity { target } => affect_entity(ecs, effect, *target), Targets::Entity { target } => affect_entity(ecs, effect, *target),
Targets::EntityList { targets } => targets.iter().for_each(|target| affect_entity(ecs, effect, *target)), Targets::EntityList { targets } =>
targets.iter().for_each(|target| affect_entity(ecs, effect, *target)),
} }
} }
@ -134,7 +144,6 @@ fn affect_tile(ecs: &mut World, effect: &EffectSpawner, target: usize) {
} }
match &effect.effect_type { match &effect.effect_type {
EffectType::Bloodstain => damage::bloodstain(ecs, target),
EffectType::Particle { .. } => particles::particle_to_tile(ecs, target as i32, &effect), EffectType::Particle { .. } => particles::particle_to_tile(ecs, target as i32, &effect),
_ => {} _ => {}
} }
@ -148,6 +157,7 @@ fn tile_effect_hits_entities(effect: &EffectType) -> bool {
EffectType::Healing { .. } => true, EffectType::Healing { .. } => true,
EffectType::ModifyNutrition { .. } => true, EffectType::ModifyNutrition { .. } => true,
EffectType::Confusion { .. } => true, EffectType::Confusion { .. } => true,
EffectType::AddIntrinsic { .. } => true,
_ => false, _ => false,
} }
} }
@ -158,9 +168,9 @@ fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
EffectType::Damage { .. } => damage::inflict_damage(ecs, effect, target), EffectType::Damage { .. } => damage::inflict_damage(ecs, effect, target),
EffectType::Healing { .. } => damage::heal_damage(ecs, effect, target), EffectType::Healing { .. } => damage::heal_damage(ecs, effect, target),
EffectType::Confusion { .. } => damage::add_confusion(ecs, effect, target), EffectType::Confusion { .. } => damage::add_confusion(ecs, effect, target),
EffectType::Bloodstain { .. } => { EffectType::Bloodstain { colour } => {
if let Some(pos) = targeting::entity_position(ecs, target) { if let Some(pos) = targeting::entity_position(ecs, target) {
damage::bloodstain(ecs, pos) damage::bloodstain(ecs, pos, *colour);
} }
} }
EffectType::Particle { .. } => { EffectType::Particle { .. } => {
@ -170,6 +180,7 @@ fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
} }
EffectType::EntityDeath => damage::entity_death(ecs, effect, target), EffectType::EntityDeath => damage::entity_death(ecs, effect, target),
EffectType::ModifyNutrition { .. } => hunger::modify_nutrition(ecs, effect, target), EffectType::ModifyNutrition { .. } => hunger::modify_nutrition(ecs, effect, target),
EffectType::AddIntrinsic { .. } => intrinsics::add_intrinsic(ecs, effect, target),
_ => {} _ => {}
} }
} }

View file

@ -1,6 +1,6 @@
use super::{ add_effect, targeting, EffectSpawner, EffectType, Targets }; use super::{ add_effect, targeting, EffectSpawner, EffectType, Targets };
use crate::{ Map, ParticleBuilder, SpawnParticleBurst, SpawnParticleLine, SpawnParticleSimple }; use crate::{ Map, ParticleBuilder, SpawnParticleBurst, SpawnParticleLine, SpawnParticleSimple };
use rltk::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
pub fn particle_to_tile(ecs: &mut World, target: i32, effect: &EffectSpawner) { pub fn particle_to_tile(ecs: &mut World, target: i32, effect: &EffectSpawner) {
@ -8,9 +8,24 @@ pub fn particle_to_tile(ecs: &mut World, target: i32, effect: &EffectSpawner) {
let map = ecs.fetch::<Map>(); let map = ecs.fetch::<Map>();
let mut particle_builder = ecs.fetch_mut::<ParticleBuilder>(); let mut particle_builder = ecs.fetch_mut::<ParticleBuilder>();
if delay <= 0.0 { if delay <= 0.0 {
particle_builder.request(target % map.width, target / map.width, fg, bg, glyph, lifespan); particle_builder.request(
target % map.width,
target / map.width,
fg,
bg,
glyph,
lifespan
);
} else { } else {
particle_builder.delay(target % map.width, target / map.width, fg, bg, glyph, lifespan, delay); particle_builder.delay(
target % map.width,
target / map.width,
fg,
bg,
glyph,
lifespan,
delay
);
} }
} }
} }
@ -113,9 +128,14 @@ pub fn handle_line_particles(ecs: &World, entity: Entity, target: &Targets) {
if let Some(part) = ecs.read_storage::<SpawnParticleLine>().get(entity) { if let Some(part) = ecs.read_storage::<SpawnParticleLine>().get(entity) {
if let Some(start_pos) = targeting::find_item_position(ecs, entity) { if let Some(start_pos) = targeting::find_item_position(ecs, entity) {
match target { match target {
Targets::Tile { target } => spawn_line_particles(ecs, start_pos, *target as i32, part), Targets::Tile { target } =>
spawn_line_particles(ecs, start_pos, *target as i32, part),
Targets::TileList { targets } => { Targets::TileList { targets } => {
targets.iter().for_each(|target| spawn_line_particles(ecs, start_pos, *target as i32, part)) targets
.iter()
.for_each(|target|
spawn_line_particles(ecs, start_pos, *target as i32, part)
)
} }
Targets::Entity { target } => { Targets::Entity { target } => {
if let Some(end_pos) = targeting::entity_position(ecs, *target) { if let Some(end_pos) = targeting::entity_position(ecs, *target) {

View file

@ -1,5 +1,5 @@
use crate::{ Equipped, InBackpack, Map, Position }; use crate::{ Equipped, InBackpack, Map, Position };
use rltk::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
pub fn entity_position(ecs: &World, target: Entity) -> Option<usize> { pub fn entity_position(ecs: &World, target: Entity) -> Option<usize> {
@ -10,8 +10,8 @@ pub fn entity_position(ecs: &World, target: Entity) -> Option<usize> {
return None; return None;
} }
pub fn aoe_tiles(map: &Map, target: rltk::Point, radius: i32) -> Vec<usize> { pub fn aoe_tiles(map: &Map, target: Point, radius: i32) -> Vec<usize> {
let mut blast_tiles = rltk::field_of_view(target, radius, &*map); let mut blast_tiles = 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); 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(); let mut result = Vec::new();
for t in blast_tiles.iter() { for t in blast_tiles.iter() {

View file

@ -1,4 +1,4 @@
use super::{ add_effect, get_noncursed, particles, spatial, EffectType, Entity, Targets, World }; use super::{ add_effect, particles, spatial, EffectType, Entity, Targets, World };
use crate::{ use crate::{
gamelog, gamelog,
gui::item_colour_ecs, gui::item_colour_ecs,
@ -30,11 +30,14 @@ use crate::{
SingleActivation, SingleActivation,
BUC, BUC,
GrantsSpell, GrantsSpell,
KnownSpell,
KnownSpells, KnownSpells,
Position,
Viewshed,
WantsToRemoveKey,
WantsToDelete,
}; };
use crate::config::messages::*; use crate::data::messages::*;
use rltk::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
pub fn item_trigger(source: Option<Entity>, item: Entity, target: &Targets, ecs: &mut World) { pub fn item_trigger(source: Option<Entity>, item: Entity, target: &Targets, ecs: &mut World) {
// Check if the item has charges, etc. // Check if the item has charges, etc.
@ -45,8 +48,10 @@ pub fn item_trigger(source: Option<Entity>, item: Entity, target: &Targets, ecs:
gamelog::Logger::new().append(NOCHARGES_DIDNOTHING).log(); gamelog::Logger::new().append(NOCHARGES_DIDNOTHING).log();
return; return;
} }
gamelog::Logger::new().colour(rltk::YELLOW).append(NOCHARGES_WREST); gamelog::Logger::new().colour(YELLOW).append(NOCHARGES_WREST);
ecs.write_storage::<Consumable>().insert(item, Consumable {}).expect("Could not insert consumable"); ecs.write_storage::<Consumable>()
.insert(item, Consumable {})
.expect("Could not insert consumable");
} }
has_charges.uses -= 1; has_charges.uses -= 1;
} }
@ -54,7 +59,10 @@ pub fn item_trigger(source: Option<Entity>, item: Entity, target: &Targets, ecs:
let did_something = event_trigger(source, item, target, ecs); let did_something = event_trigger(source, item, target, ecs);
// If it's a consumable, delete it // If it's a consumable, delete it
if did_something && ecs.read_storage::<Consumable>().get(item).is_some() { if did_something && ecs.read_storage::<Consumable>().get(item).is_some() {
ecs.entities().delete(item).expect("Failed to delete item"); let mut removekey = ecs.write_storage::<WantsToRemoveKey>();
removekey.insert(item, WantsToRemoveKey {}).expect("Unable to insert WantsToRemoveKey");
let mut delete = ecs.write_storage::<WantsToDelete>();
delete.insert(item, WantsToDelete {}).expect("Unable to insert WantsToDelete");
} }
} }
@ -81,7 +89,12 @@ struct EventInfo {
// It does almost no sanity-checking to make sure the logs only appear if the effect is taking // It does almost no sanity-checking to make sure the logs only appear if the effect is taking
// place on the player -- once monsters can use an item, their item usage will make logs for // place on the player -- once monsters can use an item, their item usage will make logs for
// the player saying they were the one who used the item. This will need refactoring then. // the player saying they were the one who used the item. This will need refactoring then.
fn event_trigger(source: Option<Entity>, entity: Entity, target: &Targets, ecs: &mut World) -> bool { fn event_trigger(
source: Option<Entity>,
entity: Entity,
target: &Targets,
ecs: &mut World
) -> bool {
let buc = if let Some(beatitude) = ecs.read_storage::<Beatitude>().get(entity) { let buc = if let Some(beatitude) = ecs.read_storage::<Beatitude>().get(entity) {
beatitude.buc.clone() beatitude.buc.clone()
} else { } else {
@ -145,7 +158,11 @@ fn handle_restore_nutrition(
return (logger, false); return (logger, false);
} }
fn handle_magic_mapper(ecs: &mut World, event: &mut EventInfo, mut logger: gamelog::Logger) -> (gamelog::Logger, bool) { fn handle_magic_mapper(
ecs: &mut World,
event: &mut EventInfo,
mut logger: gamelog::Logger
) -> (gamelog::Logger, bool) {
if ecs.read_storage::<MagicMapper>().get(event.entity).is_some() { if ecs.read_storage::<MagicMapper>().get(event.entity).is_some() {
let mut runstate = ecs.fetch_mut::<RunState>(); let mut runstate = ecs.fetch_mut::<RunState>();
let cursed = if event.buc == BUC::Cursed { true } else { false }; let cursed = if event.buc == BUC::Cursed { true } else { false };
@ -157,9 +174,17 @@ fn handle_magic_mapper(ecs: &mut World, event: &mut EventInfo, mut logger: gamel
return (logger, false); return (logger, false);
} }
fn handle_grant_spell(ecs: &mut World, event: &mut EventInfo, mut logger: gamelog::Logger) -> (gamelog::Logger, bool) { fn handle_grant_spell(
if let Some(granted_spell) = ecs.read_storage::<GrantsSpell>().get(event.entity) { ecs: &mut World,
if let Some(known_spells) = ecs.write_storage::<KnownSpells>().get_mut(event.source.unwrap()) { event: &mut EventInfo,
logger: gamelog::Logger
) -> (gamelog::Logger, bool) {
if let Some(_granted_spell) = ecs.read_storage::<GrantsSpell>().get(event.entity) {
if
let Some(_known_spells) = ecs
.write_storage::<KnownSpells>()
.get_mut(event.source.unwrap())
{
// TODO: Check if the player knows *this* spell, and add it if not. // TODO: Check if the player knows *this* spell, and add it if not.
} else { } else {
// TODO: Grant the KnownSpells component, and then add the spell. // TODO: Grant the KnownSpells component, and then add the spell.
@ -168,7 +193,11 @@ fn handle_grant_spell(ecs: &mut World, event: &mut EventInfo, mut logger: gamelo
return (logger, false); return (logger, false);
} }
fn handle_healing(ecs: &mut World, event: &mut EventInfo, mut logger: gamelog::Logger) -> (gamelog::Logger, bool) { fn handle_healing(
ecs: &mut World,
event: &mut EventInfo,
mut logger: gamelog::Logger
) -> (gamelog::Logger, bool) {
if let Some(healing_item) = ecs.read_storage::<ProvidesHealing>().get(event.entity) { if let Some(healing_item) = ecs.read_storage::<ProvidesHealing>().get(event.entity) {
let mut rng = ecs.write_resource::<RandomNumberGenerator>(); let mut rng = ecs.write_resource::<RandomNumberGenerator>();
let buc_mod = match event.buc { let buc_mod = match event.buc {
@ -176,22 +205,25 @@ fn handle_healing(ecs: &mut World, event: &mut EventInfo, mut logger: gamelog::L
BUC::Cursed => -1, BUC::Cursed => -1,
_ => 0, _ => 0,
}; };
let roll = rng.roll_dice(healing_item.n_dice + buc_mod, healing_item.sides) + healing_item.modifier; let roll =
rng.roll_dice(healing_item.n_dice + buc_mod, healing_item.sides) +
healing_item.modifier;
add_effect( add_effect(
event.source, event.source,
EffectType::Healing { amount: roll, increment_max: get_noncursed(&event.buc) }, EffectType::Healing { amount: roll, increment_max: event.buc.noncursed() },
event.target.clone() event.target.clone()
); );
for target in get_entity_targets(&event.target) { for target in get_entity_targets(&event.target) {
if ecs.read_storage::<Prop>().get(target).is_some() || ecs.read_storage::<Item>().get(target).is_some() { if
ecs.read_storage::<Prop>().get(target).is_some() ||
ecs.read_storage::<Item>().get(target).is_some()
{
continue; continue;
} }
let renderables = ecs.read_storage::<Renderable>(); let renderables = ecs.read_storage::<Renderable>();
if ecs.read_storage::<Player>().get(target).is_some() { if ecs.read_storage::<Player>().get(target).is_some() {
logger = logger logger = logger
.colour(renderable_colour(&renderables, target))
.append("You") .append("You")
.colour(WHITE)
.append(HEAL_PLAYER_HIT) .append(HEAL_PLAYER_HIT)
.buc(event.buc.clone(), None, Some(HEAL_PLAYER_HIT_BLESSED)); .buc(event.buc.clone(), None, Some(HEAL_PLAYER_HIT_BLESSED));
} else { } else {
@ -209,40 +241,55 @@ fn handle_healing(ecs: &mut World, event: &mut EventInfo, mut logger: gamelog::L
return (logger, false); return (logger, false);
} }
fn handle_damage(ecs: &mut World, event: &mut EventInfo, mut logger: gamelog::Logger) -> (gamelog::Logger, bool) { fn handle_damage(
ecs: &mut World,
event: &mut EventInfo,
mut logger: gamelog::Logger
) -> (gamelog::Logger, bool) {
if let Some(damage_item) = ecs.read_storage::<InflictsDamage>().get(event.entity) { if let Some(damage_item) = ecs.read_storage::<InflictsDamage>().get(event.entity) {
let mut rng = ecs.write_resource::<RandomNumberGenerator>(); let mut rng = ecs.write_resource::<RandomNumberGenerator>();
let roll = rng.roll_dice(damage_item.n_dice, damage_item.sides) + damage_item.modifier; let roll = rng.roll_dice(damage_item.n_dice, damage_item.sides) + damage_item.modifier;
add_effect(event.source, EffectType::Damage { amount: roll }, event.target.clone()); add_effect(
event.source,
EffectType::Damage { amount: roll, damage_type: damage_item.damage_type },
event.target.clone()
);
for target in get_entity_targets(&event.target) { for target in get_entity_targets(&event.target) {
if ecs.read_storage::<Prop>().get(target).is_some() { if ecs.read_storage::<Prop>().get(target).is_some() {
continue; continue;
} }
let renderables = ecs.read_storage::<Renderable>(); let renderables = ecs.read_storage::<Renderable>();
let positions = ecs.read_storage::<Position>();
let target_pos = positions.get(target).unwrap_or(&(Position { x: 0, y: 0 }));
let viewsheds = ecs.read_storage::<Viewshed>();
let player_viewshed = viewsheds.get(*ecs.fetch::<Entity>()).unwrap();
if ecs.read_storage::<Player>().get(target).is_some() { if ecs.read_storage::<Player>().get(target).is_some() {
logger = logger logger = logger
.colour(renderable_colour(&renderables, target))
.append("You") .append("You")
.colour(WHITE)
.append(DAMAGE_PLAYER_HIT); .append(DAMAGE_PLAYER_HIT);
} else if ecs.read_storage::<Item>().get(target).is_some() { event.log = true;
if ecs.read_storage::<Destructible>().get(target).is_some() { } else if
player_viewshed.visible_tiles.contains(&Point::new(target_pos.x, target_pos.y))
{
if ecs.read_storage::<Item>().get(target).is_some() {
if ecs.read_storage::<Destructible>().get(target).is_some() {
logger = logger
.append("The")
.colour(renderable_colour(&renderables, target))
.append(obfuscate_name_ecs(ecs, target).0)
.colour(WHITE)
.append(DAMAGE_ITEM_HIT);
}
} else {
logger = logger logger = logger
.append("The") .append("The")
.colour(renderable_colour(&renderables, target)) .colour(renderable_colour(&renderables, target))
.append(obfuscate_name_ecs(ecs, target).0) .append(obfuscate_name_ecs(ecs, target).0)
.colour(WHITE) .colour(WHITE)
.append(DAMAGE_ITEM_HIT); .append(DAMAGE_OTHER_HIT);
} }
} else { event.log = true;
logger = logger
.append("The")
.colour(renderable_colour(&renderables, target))
.append(obfuscate_name_ecs(ecs, target).0)
.colour(WHITE)
.append(DAMAGE_OTHER_HIT);
} }
event.log = true;
} }
return (logger, true); return (logger, true);
} }
@ -250,9 +297,17 @@ fn handle_damage(ecs: &mut World, event: &mut EventInfo, mut logger: gamelog::Lo
} }
#[allow(unused_mut)] #[allow(unused_mut)]
fn handle_confusion(ecs: &mut World, event: &mut EventInfo, mut logger: gamelog::Logger) -> (gamelog::Logger, bool) { fn handle_confusion(
ecs: &mut World,
event: &mut EventInfo,
mut logger: gamelog::Logger
) -> (gamelog::Logger, bool) {
if let Some(confusion) = ecs.read_storage::<Confusion>().get(event.entity) { if let Some(confusion) = ecs.read_storage::<Confusion>().get(event.entity) {
add_effect(event.source, EffectType::Confusion { turns: confusion.turns }, event.target.clone()); add_effect(
event.source,
EffectType::Confusion { turns: confusion.turns },
event.target.clone()
);
return (logger, true); return (logger, true);
} }
return (logger, false); return (logger, false);
@ -263,7 +318,11 @@ fn select_single(ecs: &World, runstate: RunState) {
*new_runstate = runstate; *new_runstate = runstate;
} }
fn handle_identify(ecs: &mut World, event: &mut EventInfo, mut logger: gamelog::Logger) -> (gamelog::Logger, bool) { fn handle_identify(
ecs: &mut World,
event: &mut EventInfo,
mut logger: gamelog::Logger
) -> (gamelog::Logger, bool) {
if let Some(_i) = ecs.read_storage::<ProvidesIdentify>().get(event.entity) { if let Some(_i) = ecs.read_storage::<ProvidesIdentify>().get(event.entity) {
let mut rng = ecs.write_resource::<RandomNumberGenerator>(); let mut rng = ecs.write_resource::<RandomNumberGenerator>();
let mut dm = ecs.fetch_mut::<MasterDungeonMap>(); let mut dm = ecs.fetch_mut::<MasterDungeonMap>();
@ -294,7 +353,10 @@ fn handle_identify(ecs: &mut World, event: &mut EventInfo, mut logger: gamelog::
.get(event.source.unwrap()) .get(event.source.unwrap())
.map(|b| b.known) .map(|b| b.known)
.unwrap_or(true); .unwrap_or(true);
return in_this_backpack && (has_obfuscated_name || !already_identified || !known_beatitude); let result =
in_this_backpack &&
(has_obfuscated_name || !already_identified || !known_beatitude);
return result;
}) { }) {
to_identify.push((e, name.name.clone())); to_identify.push((e, name.name.clone()));
} }
@ -304,14 +366,20 @@ fn handle_identify(ecs: &mut World, event: &mut EventInfo, mut logger: gamelog::
beatitude.known = true; beatitude.known = true;
} }
} }
logger = logger.append(IDENTIFY_ALL).buc(event.buc.clone(), None, Some(IDENTIFY_ALL_BLESSED)); logger = logger
.append(IDENTIFY_ALL)
.buc(event.buc.clone(), None, Some(IDENTIFY_ALL_BLESSED));
event.log = true; event.log = true;
return (logger, true); return (logger, true);
} }
return (logger, false); return (logger, false);
} }
fn handle_remove_curse(ecs: &mut World, event: &mut EventInfo, mut logger: gamelog::Logger) -> (gamelog::Logger, bool) { fn handle_remove_curse(
ecs: &mut World,
event: &mut EventInfo,
mut logger: gamelog::Logger
) -> (gamelog::Logger, bool) {
if let Some(_r) = ecs.read_storage::<ProvidesRemoveCurse>().get(event.entity) { if let Some(_r) = ecs.read_storage::<ProvidesRemoveCurse>().get(event.entity) {
let mut to_decurse: Vec<Entity> = Vec::new(); let mut to_decurse: Vec<Entity> = Vec::new();
match event.buc { match event.buc {
@ -329,7 +397,9 @@ fn handle_remove_curse(ecs: &mut World, event: &mut EventInfo, mut logger: gamel
&ecs.read_storage::<Beatitude>(), &ecs.read_storage::<Beatitude>(),
) )
.join() .join()
.filter(|(_e, _i, bp, b)| bp.owner == event.source.unwrap() && b.buc == BUC::Cursed) { .filter(
|(_e, _i, bp, b)| bp.owner == event.source.unwrap() && b.buc == BUC::Cursed
) {
to_decurse.push(entity); to_decurse.push(entity);
} }
} }
@ -358,7 +428,9 @@ fn handle_remove_curse(ecs: &mut World, event: &mut EventInfo, mut logger: gamel
} }
let mut beatitudes = ecs.write_storage::<Beatitude>(); let mut beatitudes = ecs.write_storage::<Beatitude>();
for e in to_decurse { for e in to_decurse {
beatitudes.insert(e, Beatitude { buc: BUC::Uncursed, known: true }).expect("Unable to insert beatitude"); beatitudes
.insert(e, Beatitude { buc: BUC::Uncursed, known: true })
.expect("Unable to insert beatitude");
} }
logger = logger.append(REMOVECURSE).buc(event.buc.clone(), None, Some(REMOVECURSE_BLESSED)); logger = logger.append(REMOVECURSE).buc(event.buc.clone(), None, Some(REMOVECURSE_BLESSED));
event.log = true; event.log = true;

View file

@ -1,6 +1,6 @@
use super::{ append_entry, LogFragment }; use super::{ append_entry, LogFragment };
use crate::BUC; use crate::BUC;
use rltk::prelude::*; use bracket_lib::prelude::*;
pub struct Logger { pub struct Logger {
current_colour: RGB, current_colour: RGB,
@ -10,7 +10,7 @@ pub struct Logger {
impl Logger { impl Logger {
/// Creates a blank builder for making message log entries. /// Creates a blank builder for making message log entries.
pub fn new() -> Self { pub fn new() -> Self {
Logger { current_colour: RGB::named(rltk::WHITE), fragments: Vec::new() } Logger { current_colour: RGB::named(WHITE), fragments: Vec::new() }
} }
/// Sets the colour of the current message logger. /// Sets the colour of the current message logger.
@ -42,11 +42,23 @@ impl Logger {
pub fn buc<T: ToString>(mut self, buc: BUC, cursed: Option<T>, blessed: Option<T>) -> Self { pub fn buc<T: ToString>(mut self, buc: BUC, cursed: Option<T>, blessed: Option<T>) -> Self {
if buc == BUC::Cursed && cursed.is_some() { if buc == BUC::Cursed && cursed.is_some() {
self.fragments.push(LogFragment { colour: RGB::named(SALMON), text: cursed.unwrap().to_string() }); self.fragments.push(LogFragment {
self.fragments.push(LogFragment { colour: self.current_colour, text: ". ".to_string() }); colour: RGB::named(SALMON),
text: cursed.unwrap().to_string(),
});
self.fragments.push(LogFragment {
colour: self.current_colour,
text: ". ".to_string(),
});
} else if buc == BUC::Blessed && blessed.is_some() { } else if buc == BUC::Blessed && blessed.is_some() {
self.fragments.push(LogFragment { colour: RGB::named(CYAN), text: blessed.unwrap().to_string() }); self.fragments.push(LogFragment {
self.fragments.push(LogFragment { colour: self.current_colour, text: ". ".to_string() }); colour: RGB::named(CYAN),
text: blessed.unwrap().to_string(),
});
self.fragments.push(LogFragment {
colour: self.current_colour,
text: ". ".to_string(),
});
} }
return self; return self;
} }
@ -55,47 +67,4 @@ impl Logger {
pub fn log(self) { pub fn log(self) {
return append_entry(self.fragments); return append_entry(self.fragments);
} }
/// Appends text in YELLOW to the current message logger.
#[allow(unused)]
pub fn npc_name<T: ToString>(mut self, text: T) -> Self {
let mut text_with_space = text.to_string();
text_with_space.push_str(" ");
self.fragments.push(LogFragment { colour: RGB::named(rltk::YELLOW), text: text_with_space });
return self;
}
/// Appends text in YELLOW to the current message logger, with no space.
pub fn npc_name_n<T: ToString>(mut self, text: T) -> Self {
self.fragments.push(LogFragment { colour: RGB::named(rltk::YELLOW), text: text.to_string() });
return self;
}
/// Appends text in CYAN to the current message logger.
pub fn item_name<T: ToString>(mut self, text: T) -> Self {
let mut text_with_space = text.to_string();
text_with_space.push_str(" ");
self.fragments.push(LogFragment { colour: RGB::named(rltk::CYAN), text: text_with_space });
return self;
}
/// Appends text in CYAN to the current message logger, with no space.
pub fn item_name_n<T: ToString>(mut self, text: T) -> Self {
self.fragments.push(LogFragment { colour: RGB::named(rltk::CYAN), text: text.to_string() });
return self;
}
/// Appends text in RED to the current message logger.
#[allow(dead_code)]
pub fn damage(mut self, damage: i32) -> Self {
self.fragments.push(LogFragment { colour: RGB::named(rltk::RED), text: format!("{} ", damage).to_string() });
return self;
}
/// Appends text in RED to the current message logger, with no space.
#[allow(dead_code)]
pub fn damage_n(mut self, damage: i32) -> Self {
self.fragments.push(LogFragment { colour: RGB::named(rltk::RED), text: format!("{}", damage).to_string() });
return self;
}
} }

View file

@ -1,18 +1,56 @@
use std::collections::HashMap; use std::collections::{ HashSet, HashMap };
use std::sync::Mutex; use std::sync::Mutex;
use crate::data::events::EVENT;
use crate::data::names::*;
lazy_static! { lazy_static! {
static ref EVENTS: Mutex<HashMap<String, i32>> = Mutex::new(HashMap::new()); /// A count of each event that has happened over the run. i.e. "turns", "descended", "ascended"
pub static ref EVENT_COUNTER: Mutex<HashMap<String, i32>> = Mutex::new(HashMap::new());
// A record of events that happened on a given turn. i.e. "Advanced to level 2".
pub static ref EVENTS: Mutex<HashMap<u32, Vec<String>>> = Mutex::new(HashMap::new());
// A record of floors visited, and monsters killed. Used to determine if an event is significant.
static ref VISITED: Mutex<HashSet<String>> = Mutex::new({
let mut set = HashSet::new();
set.insert(NAME_OVERMAP.to_string());
set
});
static ref KILLED: Mutex<HashSet<String>> = Mutex::new(HashSet::new());
} }
pub fn clear_events() { /// Makes a copy of event counts (FOR SERIALIZATION)
pub fn clone_event_counts() -> HashMap<String, i32> {
EVENT_COUNTER.lock().unwrap().clone()
}
/// Makes a copy of events (FOR SERIALIZATION)
pub fn clone_events() -> HashMap<u32, Vec<String>> {
EVENTS.lock().unwrap().clone()
}
/// Fetches event counter into mutex (FOR DESERIALIZATION)
pub fn restore_event_counter(events: HashMap<String, i32>) {
EVENT_COUNTER.lock().unwrap().clear();
events.iter().for_each(|(k, v)| {
EVENT_COUNTER.lock().unwrap().insert(k.to_string(), *v);
});
}
/// Fetches events into mutex (FOR DESERIALIZATION)
pub fn restore_events(events: HashMap<u32, Vec<String>>) {
EVENTS.lock().unwrap().clear(); EVENTS.lock().unwrap().clear();
events.iter().for_each(|(k, v)| {
EVENTS.lock().unwrap().insert(*k, v.to_vec());
});
}
/// Wipes all events - for starting a new game.
pub fn clear_events() {
let (mut events, mut event_counts) = (EVENTS.lock().unwrap(), EVENT_COUNTER.lock().unwrap());
events.clear();
event_counts.clear();
} }
#[allow(unused_mut)] #[allow(unused_mut)]
pub fn record_event<T: ToString>(event: T, n: i32) { /// Increments the event counter by n for a given event.
fn modify_event_count<T: ToString>(event: T, n: i32) {
let event_name = event.to_string(); let event_name = event.to_string();
let mut events_lock = EVENTS.lock(); let mut events_lock = EVENT_COUNTER.lock();
let mut events = events_lock.as_mut().unwrap(); let mut events = events_lock.as_mut().unwrap();
if let Some(e) = events.get_mut(&event_name) { if let Some(e) = events.get_mut(&event_name) {
*e += n; *e += n;
@ -20,10 +58,10 @@ pub fn record_event<T: ToString>(event: T, n: i32) {
events.insert(event_name, n); events.insert(event_name, n);
} }
} }
/// Returns how many times an event has taken place.
pub fn get_event_count<T: ToString>(event: T) -> i32 { pub fn get_event_count<T: ToString>(event: T) -> i32 {
let event_name = event.to_string(); let event_name = event.to_string();
let events_lock = EVENTS.lock(); let events_lock = EVENT_COUNTER.lock();
let events = events_lock.unwrap(); let events = events_lock.unwrap();
if let Some(e) = events.get(&event_name) { if let Some(e) = events.get(&event_name) {
*e *e
@ -31,14 +69,77 @@ pub fn get_event_count<T: ToString>(event: T) -> i32 {
0 0
} }
} }
/// Records an event on the current turn.
pub fn record_event(event: EVENT) {
let mut new_event: String = "unknown event".to_string();
let mut significant_event = true;
match event {
EVENT::Turn(n) => {
modify_event_count(EVENT::COUNT_TURN, n);
significant_event = false;
}
// If de-levelling is ever implemented, this needs refactoring (along with a lot of stuff).
EVENT::Level(n) => {
modify_event_count(EVENT::COUNT_LEVEL, n);
let new_lvl = get_event_count(EVENT::COUNT_LEVEL);
if new_lvl == 1 {
new_event = format!("You embarked on your first adventure!");
} else {
new_event = format!("Advanced to level {}", new_lvl);
}
}
EVENT::ChangedFloor(n) => {
modify_event_count(EVENT::COUNT_CHANGED_FLOOR, 1);
if VISITED.lock().unwrap().contains(&n) {
significant_event = false;
} else {
VISITED.lock().unwrap().insert(n.clone());
new_event = format!("Visited {} for the first time", n);
}
}
EVENT::KickedSomething(n) => {
modify_event_count(EVENT::COUNT_KICK, n);
significant_event = false;
}
EVENT::BrokeDoor(n) => {
modify_event_count(EVENT::COUNT_BROKE_DOOR, n);
significant_event = false;
}
EVENT::PlayerConfused(n) => {
modify_event_count(EVENT::COUNT_PLAYER_CONFUSED, n);
significant_event = false;
}
EVENT::LookedForHelp(n) => {
modify_event_count(EVENT::COUNT_LOOKED_FOR_HELP, n);
significant_event = false;
}
EVENT::Killed(name) => {
modify_event_count(EVENT::COUNT_KILLED, 1);
if KILLED.lock().unwrap().contains(&name) {
significant_event = false;
} else {
KILLED.lock().unwrap().insert(name.clone());
new_event = format!("Killed your first {}", name);
}
}
EVENT::Discovered(name) => {
new_event = format!("Discovered {}", name);
}
EVENT::Identified(name) => {
new_event = format!("Identified {}", crate::gui::with_article(name));
}
EVENT::PlayerDied(str) => {
// Generating the String is handled in the death effect, to avoid passing the ecs here.
new_event = format!("{}", str);
}
}
pub fn clone_events() -> HashMap<String, i32> { if significant_event {
EVENTS.lock().unwrap().clone() EVENTS.lock()
} .as_mut()
.unwrap()
pub fn load_events(events: HashMap<String, i32>) { .entry(get_event_count(EVENT::COUNT_TURN) as u32)
EVENTS.lock().unwrap().clear(); .or_insert_with(Vec::new)
events.iter().for_each(|(k, v)| { .push(new_event);
EVENTS.lock().unwrap().insert(k.to_string(), *v); }
});
} }

View file

@ -1,5 +1,5 @@
use super::{ events, LogFragment, Logger }; use super::{ events, LogFragment, Logger };
use rltk::prelude::*; use bracket_lib::prelude::*;
use std::sync::Mutex; use std::sync::Mutex;
lazy_static! { lazy_static! {
@ -19,7 +19,13 @@ pub fn clear_log() {
LOG.lock().unwrap().clear(); LOG.lock().unwrap().clear();
} }
pub fn print_log(console: &mut Box<dyn Console>, pos: Point, _descending: bool, len: usize, maximum_len: i32) { pub fn print_log(
console: &mut Box<dyn Console>,
pos: Point,
_descending: bool,
len: usize,
maximum_len: i32
) {
let mut y = pos.y; let mut y = pos.y;
let mut x = pos.x; let mut x = pos.x;
// Reverse the log, take the number we want to show, and iterate through them // Reverse the log, take the number we want to show, and iterate through them
@ -61,7 +67,13 @@ pub fn print_log(console: &mut Box<dyn Console>, pos: Point, _descending: bool,
} }
// Stay within bounds // Stay within bounds
if y > pos.y - (len as i32) { if y > pos.y - (len as i32) {
console.print_color(x, y, frag.colour.into(), RGB::named(rltk::BLACK).into(), part); console.print_color(
x,
y,
frag.colour.into(),
RGB::named(BLACK).into(),
part
);
} }
x += part.len() as i32; x += part.len() as i32;
} }
@ -80,7 +92,7 @@ pub fn setup_log() {
Logger::new() Logger::new()
.append("Welcome!") .append("Welcome!")
.colour(rltk::CYAN) .colour(CYAN)
.append_n("Press [?] at any time to view controls") .append_n("Press [?] at any time to view controls")
.period() .period()
.log(); .log();

View file

@ -1,5 +1,3 @@
use rltk::prelude::*;
mod builder; mod builder;
pub use builder::*; pub use builder::*;
mod logstore; mod logstore;
@ -9,6 +7,7 @@ mod events;
pub use events::*; pub use events::*;
use serde::{ Deserialize, Serialize }; use serde::{ Deserialize, Serialize };
use bracket_lib::prelude::*;
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct LogFragment { pub struct LogFragment {
pub colour: RGB, pub colour: RGB,

View file

@ -1,8 +1,8 @@
use super::{ Skill, Skills }; use super::{ Skill, Skills };
use crate::gui::{ Ancestry, Class }; use crate::gui::{ Ancestry, Class };
use crate::config::entity; use crate::data::entity;
use crate::config::char_create::*; use crate::data::char_create::*;
use rltk::prelude::*; use bracket_lib::prelude::*;
use std::cmp::max; use std::cmp::max;
/// Returns the attribute bonus for a given attribute score, where every 2 points above /// Returns the attribute bonus for a given attribute score, where every 2 points above
@ -12,13 +12,13 @@ pub fn attr_bonus(value: i32) -> i32 {
} }
/// Returns the number of HP gained per level for a given constitution score. /// Returns the number of HP gained per level for a given constitution score.
pub fn hp_per_level(rng: &mut rltk::RandomNumberGenerator, constitution: i32) -> i32 { pub fn hp_per_level(rng: &mut RandomNumberGenerator, constitution: i32) -> i32 {
return max(rng.roll_dice(1, entity::STANDARD_HIT_DIE) + attr_bonus(constitution), 1); return max(rng.roll_dice(1, entity::STANDARD_HIT_DIE) + attr_bonus(constitution), 1);
} }
#[allow(dead_code)] #[allow(dead_code)]
/// Returns a total HP roll for a player, based on a given constitution score and level. /// Returns a total HP roll for a player, based on a given constitution score and level.
pub fn player_hp_at_level(rng: &mut rltk::RandomNumberGenerator, constitution: i32, level: i32) -> i32 { pub fn player_hp_at_level(rng: &mut RandomNumberGenerator, constitution: i32, level: i32) -> i32 {
let mut total = entity::STANDARD_HIT_DIE + attr_bonus(constitution); let mut total = entity::STANDARD_HIT_DIE + attr_bonus(constitution);
for _i in 0..level { for _i in 0..level {
total += hp_per_level(rng, constitution); total += hp_per_level(rng, constitution);
@ -27,7 +27,7 @@ pub fn player_hp_at_level(rng: &mut rltk::RandomNumberGenerator, constitution: i
} }
/// Returns a total HP roll for an NPC, based on a given constitution score and level. /// Returns a total HP roll for an NPC, based on a given constitution score and level.
pub fn npc_hp_at_level(rng: &mut rltk::RandomNumberGenerator, constitution: i32, level: i32) -> i32 { pub fn npc_hp_at_level(rng: &mut RandomNumberGenerator, constitution: i32, level: i32) -> i32 {
if level == 0 { if level == 0 {
return rng.roll_dice(1, entity::STANDARD_HIT_DIE_0); return rng.roll_dice(1, entity::STANDARD_HIT_DIE_0);
} }
@ -39,12 +39,12 @@ pub fn npc_hp_at_level(rng: &mut rltk::RandomNumberGenerator, constitution: i32,
} }
/// Returns the number of mana gained per level for a given intelligence score. /// Returns the number of mana gained per level for a given intelligence score.
pub fn mana_per_level(rng: &mut rltk::RandomNumberGenerator, intelligence: i32) -> i32 { pub fn mana_per_level(rng: &mut RandomNumberGenerator, intelligence: i32) -> i32 {
return max(rng.roll_dice(1, entity::STANDARD_MANA_DIE) + attr_bonus(intelligence), 1); return max(rng.roll_dice(1, entity::STANDARD_MANA_DIE) + attr_bonus(intelligence), 1);
} }
/// Returns the number of mana gained per level for a given intelligence score. /// Returns the number of mana gained per level for a given intelligence score.
pub fn mana_at_level(rng: &mut rltk::RandomNumberGenerator, intelligence: i32, level: i32) -> i32 { pub fn mana_at_level(rng: &mut RandomNumberGenerator, intelligence: i32, level: i32) -> i32 {
let mut total = entity::MINIMUM_MANA; let mut total = entity::MINIMUM_MANA;
for _i in 0..level { for _i in 0..level {
total += mana_per_level(rng, intelligence); total += mana_per_level(rng, intelligence);
@ -63,7 +63,7 @@ pub fn skill_bonus(skill: Skill, skills: &Skills) -> i32 {
/// Roll 4d6 and drop the lowest, for rolling d20-style stats /// Roll 4d6 and drop the lowest, for rolling d20-style stats
#[allow(unused)] #[allow(unused)]
pub fn roll_4d6(rng: &mut rltk::RandomNumberGenerator) -> i32 { pub fn roll_4d6(rng: &mut RandomNumberGenerator) -> i32 {
let mut rolls: Vec<i32> = Vec::new(); let mut rolls: Vec<i32> = Vec::new();
for _i in 0..4 { for _i in 0..4 {
rolls.push(rng.roll_dice(1, 6)); rolls.push(rng.roll_dice(1, 6));

View file

@ -1,6 +1,15 @@
use super::{ gamesystem::attr_bonus, gamesystem::get_attribute_rolls, Attributes, Pools, Renderable, RunState, State }; use super::{
use crate::config::entity; gamesystem::attr_bonus,
use crate::config::char_create::*; gamesystem::get_attribute_rolls,
gamesystem::mana_at_level,
Attributes,
Pools,
Renderable,
RunState,
State,
};
use crate::data::entity;
use crate::data::char_create::*;
use crate::{ use crate::{
raws, raws,
Attribute, Attribute,
@ -15,7 +24,7 @@ use crate::{
Telepath, Telepath,
BUC, BUC,
}; };
use rltk::prelude::*; use bracket_lib::prelude::*;
use serde::{ Deserialize, Serialize }; use serde::{ Deserialize, Serialize };
use specs::prelude::*; use specs::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
@ -102,14 +111,14 @@ pub enum CharCreateResult {
} }
/// Handles the player character creation screen. /// Handles the player character creation screen.
pub fn character_creation(gs: &mut State, ctx: &mut Rltk) -> CharCreateResult { pub fn character_creation(gs: &mut State, ctx: &mut BTerm) -> CharCreateResult {
let runstate = gs.ecs.fetch::<RunState>(); let runstate = gs.ecs.fetch::<RunState>();
let mut x = 2; let mut x = 2;
let mut y = 11; let mut y = 11;
let column_width = 20; let column_width = 20;
ctx.print_color(x, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), CHAR_CREATE_HEADER); ctx.print_color(x, y, RGB::named(WHITE), RGB::named(BLACK), CHAR_CREATE_HEADER);
y += 2; y += 2;
if let RunState::CharacterCreation { ancestry, class } = *runstate { if let RunState::CharacterCreation { ancestry, class } = *runstate {
@ -248,7 +257,9 @@ pub fn setup_player_ancestry(ecs: &mut World, ancestry: Ancestry) {
let player_skills = if let Some(skills) = skills.get_mut(*player) { let player_skills = if let Some(skills) = skills.get_mut(*player) {
skills skills
} else { } else {
skills.insert(*player, Skills { skills: HashMap::new() }).expect("Unable to insert skills component"); skills
.insert(*player, Skills { skills: HashMap::new() })
.expect("Unable to insert skills component");
skills.get_mut(*player).unwrap() skills.get_mut(*player).unwrap()
}; };
let mut ancestries = ecs.write_storage::<HasAncestry>(); let mut ancestries = ecs.write_storage::<HasAncestry>();
@ -258,9 +269,9 @@ pub fn setup_player_ancestry(ecs: &mut World, ancestry: Ancestry) {
Ancestry::Dwarf => { Ancestry::Dwarf => {
renderables renderables
.insert(*player, Renderable { .insert(*player, Renderable {
glyph: rltk::to_cp437(DWARF_GLYPH), glyph: to_cp437(DWARF_GLYPH),
fg: RGB::named(DWARF_COLOUR), fg: RGB::named(DWARF_COLOUR),
bg: RGB::named(rltk::BLACK), bg: RGB::named(BLACK),
render_order: 0, render_order: 0,
}) })
.expect("Unable to insert renderable component"); .expect("Unable to insert renderable component");
@ -269,33 +280,43 @@ pub fn setup_player_ancestry(ecs: &mut World, ancestry: Ancestry) {
Ancestry::Elf => { Ancestry::Elf => {
renderables renderables
.insert(*player, Renderable { .insert(*player, Renderable {
glyph: rltk::to_cp437(ELF_GLYPH), glyph: to_cp437(ELF_GLYPH),
fg: RGB::named(ELF_COLOUR), fg: RGB::named(ELF_COLOUR),
bg: RGB::named(rltk::BLACK), bg: RGB::named(BLACK),
render_order: 0, render_order: 0,
}) })
.expect("Unable to insert renderable component"); .expect("Unable to insert renderable component");
let mut telepaths = ecs.write_storage::<Telepath>(); let mut telepaths = ecs.write_storage::<Telepath>();
telepaths telepaths
.insert(*player, Telepath { telepath_tiles: Vec::new(), range: ELF_TELEPATH_RANGE, dirty: true }) .insert(*player, Telepath {
telepath_tiles: Vec::new(),
range: ELF_TELEPATH_RANGE,
dirty: true,
})
.expect("Unable to insert telepath component"); .expect("Unable to insert telepath component");
let mut speeds = ecs.write_storage::<Energy>(); let mut speeds = ecs.write_storage::<Energy>();
speeds speeds
.insert(*player, Energy { current: 0, speed: entity::NORMAL_SPEED + ELF_SPEED_BONUS }) .insert(*player, Energy {
current: 0,
speed: entity::NORMAL_SPEED + ELF_SPEED_BONUS,
})
.expect("Unable to insert energy component"); .expect("Unable to insert energy component");
} }
Ancestry::Catfolk => { Ancestry::Catfolk => {
renderables renderables
.insert(*player, Renderable { .insert(*player, Renderable {
glyph: rltk::to_cp437(CATFOLK_GLYPH), glyph: to_cp437(CATFOLK_GLYPH),
fg: RGB::named(CATFOLK_COLOUR), fg: RGB::named(CATFOLK_COLOUR),
bg: RGB::named(rltk::BLACK), bg: RGB::named(BLACK),
render_order: 0, render_order: 0,
}) })
.expect("Unable to insert renderable component"); .expect("Unable to insert renderable component");
let mut speeds = ecs.write_storage::<Energy>(); let mut speeds = ecs.write_storage::<Energy>();
speeds speeds
.insert(*player, Energy { current: 0, speed: entity::NORMAL_SPEED + CATFOLK_SPEED_BONUS }) .insert(*player, Energy {
current: 0,
speed: entity::NORMAL_SPEED + CATFOLK_SPEED_BONUS,
})
.expect("Unable to insert energy component"); .expect("Unable to insert energy component");
} }
_ => {} _ => {}
@ -332,10 +353,17 @@ pub fn setup_player_class(ecs: &mut World, class: Class, ancestry: Ancestry) {
.expect("Unable to insert attributes component"); .expect("Unable to insert attributes component");
let mut pools = ecs.write_storage::<Pools>(); let mut pools = ecs.write_storage::<Pools>();
let starting_mp = mana_at_level(&mut rng, int, 1);
pools pools
.insert(player, Pools { .insert(player, Pools {
hit_points: Pool { current: 8 + attr_bonus(con), max: entity::STANDARD_HIT_DIE + attr_bonus(con) }, hit_points: Pool {
mana: Pool { current: 1 + attr_bonus(int), max: entity::MINIMUM_MANA_PLAYER + attr_bonus(int) }, current: 8 + attr_bonus(con),
max: entity::STANDARD_HIT_DIE + attr_bonus(con),
},
mana: Pool {
current: starting_mp,
max: starting_mp,
},
xp: 0, xp: 0,
level: 1, level: 1,
bac: entity::STANDARD_BAC, bac: entity::STANDARD_BAC,
@ -371,7 +399,10 @@ pub fn setup_player_class(ecs: &mut World, class: Class, ancestry: Ancestry) {
} }
} }
fn get_starting_inventory(class: Class, rng: &mut RandomNumberGenerator) -> (Vec<String>, Vec<String>) { fn get_starting_inventory(
class: Class,
rng: &mut RandomNumberGenerator
) -> (Vec<String>, Vec<String>) {
let mut equipped: Vec<String> = Vec::new(); let mut equipped: Vec<String> = Vec::new();
let mut carried: Vec<String> = Vec::new(); let mut carried: Vec<String> = Vec::new();
let starting_food: &str; let starting_food: &str;
@ -392,8 +423,20 @@ fn get_starting_inventory(class: Class, rng: &mut RandomNumberGenerator) -> (Vec
Class::Wizard => { Class::Wizard => {
starting_food = WIZARD_STARTING_FOOD; starting_food = WIZARD_STARTING_FOOD;
equipped = vec![WIZARD_STARTING_WEAPON.to_string(), WIZARD_STARTING_ARMOUR.to_string()]; equipped = vec![WIZARD_STARTING_WEAPON.to_string(), WIZARD_STARTING_ARMOUR.to_string()];
pick_random_table_item(rng, &mut carried, "scrolls", WIZARD_SCROLL_AMOUNT, Some(WIZARD_MAX_SCROLL_LVL)); pick_random_table_item(
pick_random_table_item(rng, &mut carried, "potions", WIZARD_POTION_AMOUNT, Some(WIZARD_MAX_SCROLL_LVL)); rng,
&mut carried,
"scrolls",
WIZARD_SCROLL_AMOUNT,
Some(WIZARD_MAX_SCROLL_LVL)
);
pick_random_table_item(
rng,
&mut carried,
"potions",
WIZARD_POTION_AMOUNT,
Some(WIZARD_MAX_SCROLL_LVL)
);
} }
Class::Villager => { Class::Villager => {
starting_food = VILLAGER_STARTING_FOOD; starting_food = VILLAGER_STARTING_FOOD;
@ -408,11 +451,11 @@ fn pick_random_table_item(
rng: &mut RandomNumberGenerator, rng: &mut RandomNumberGenerator,
push_to: &mut Vec<String>, push_to: &mut Vec<String>,
table: &'static str, table: &'static str,
dice: &'static str, dice_str: &'static str,
difficulty: Option<i32> difficulty: Option<i32>
) { ) {
let (n, d, m) = raws::parse_dice_string(dice); let dice = parse_dice_string(dice_str).expect("Error parsing dice");
for _i in 0..rng.roll_dice(n, d) + m { for _i in 0..rng.roll_dice(dice.n_dice, dice.die_type) + dice.bonus {
push_to.push(raws::table_by_name(&raws::RAWS.lock().unwrap(), table, difficulty).roll(rng)); push_to.push(raws::table_by_name(&raws::RAWS.lock().unwrap(), table, difficulty).roll(rng));
} }
} }

View file

@ -1,5 +1,5 @@
use super::State; use super::State;
use rltk::prelude::*; use bracket_lib::prelude::*;
#[derive(PartialEq, Copy, Clone)] #[derive(PartialEq, Copy, Clone)]
pub enum CheatMenuResult { pub enum CheatMenuResult {
@ -12,13 +12,13 @@ pub enum CheatMenuResult {
GodMode, GodMode,
} }
pub fn show_cheat_menu(_gs: &mut State, ctx: &mut Rltk) -> CheatMenuResult { pub fn show_cheat_menu(_gs: &mut State, ctx: &mut BTerm) -> CheatMenuResult {
let (x_offset, y_offset) = (1, 10); let (x_offset, y_offset) = (1, 10);
ctx.print_color( ctx.print_color(
1 + x_offset, 1 + x_offset,
1 + y_offset, 1 + y_offset,
RGB::named(rltk::RED), RGB::named(RED),
RGB::named(rltk::BLACK), RGB::named(BLACK),
"DEBUG MENU! [aA-zZ][Esc.]" "DEBUG MENU! [aA-zZ][Esc.]"
); );
let x = 1 + x_offset; let x = 1 + x_offset;
@ -26,26 +26,26 @@ pub fn show_cheat_menu(_gs: &mut State, ctx: &mut Rltk) -> CheatMenuResult {
let count = 5; let count = 5;
let width = 19; let width = 19;
ctx.draw_box(x, y, width, (count + 1) as i32, RGB::named(rltk::RED), RGB::named(rltk::BLACK)); ctx.draw_box(x, y, width, (count + 1) as i32, RGB::named(RED), RGB::named(BLACK));
y += 1; y += 1;
// Asc // Asc
ctx.set(x_offset + 2, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), rltk::to_cp437('a')); ctx.set(x_offset + 2, y, RGB::named(YELLOW), RGB::named(BLACK), to_cp437('a'));
ctx.print(x_offset + 4, y, "ASCEND A FLOOR"); ctx.print(x_offset + 4, y, "ASCEND A FLOOR");
y += 1; y += 1;
// Desc // Desc
ctx.set(x_offset + 2, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), rltk::to_cp437('d')); ctx.set(x_offset + 2, y, RGB::named(YELLOW), RGB::named(BLACK), to_cp437('d'));
ctx.print(x_offset + 4, y, "DESCEND A FLOOR"); ctx.print(x_offset + 4, y, "DESCEND A FLOOR");
y += 1; y += 1;
// Heal // Heal
ctx.set(x_offset + 2, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), rltk::to_cp437('h')); ctx.set(x_offset + 2, y, RGB::named(YELLOW), RGB::named(BLACK), to_cp437('h'));
ctx.print(x_offset + 4, y, "HEAL TO FULL"); ctx.print(x_offset + 4, y, "HEAL TO FULL");
y += 1; y += 1;
// Reveal map // Reveal map
ctx.set(x_offset + 2, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), rltk::to_cp437('m')); ctx.set(x_offset + 2, y, RGB::named(YELLOW), RGB::named(BLACK), to_cp437('m'));
ctx.print(x_offset + 4, y, "MAGIC MAP REVEAL"); ctx.print(x_offset + 4, y, "MAGIC MAP REVEAL");
y += 1; y += 1;
// Godmode // Godmode
ctx.set(x_offset + 2, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), rltk::to_cp437('g')); ctx.set(x_offset + 2, y, RGB::named(YELLOW), RGB::named(BLACK), to_cp437('g'));
ctx.print(x_offset + 4, y, "GOD MODE"); ctx.print(x_offset + 4, y, "GOD MODE");
// Match keys // Match keys
match ctx.key { match ctx.key {

54
src/gui/farlook.rs Normal file
View file

@ -0,0 +1,54 @@
use super::{ State, RunState, tooltip::draw_tooltips, camera::get_screen_bounds };
use bracket_lib::prelude::*;
#[derive(PartialEq, Copy, Clone)]
pub enum FarlookResult {
NoResponse {
x: i32,
y: i32,
},
Cancel,
}
pub fn show_farlook(gs: &mut State, ctx: &mut BTerm) -> FarlookResult {
let runstate = gs.ecs.fetch::<RunState>();
let (_min_x, _max_x, _min_y, _max_y, x_offset, y_offset) = get_screen_bounds(&gs.ecs, ctx);
ctx.print_color(
1 + x_offset,
1 + y_offset,
RGB::named(WHITE),
RGB::named(BLACK),
"Look at what? [move keys][Esc.]"
);
if let RunState::Farlook { x, y } = *runstate {
let (screen_x, screen_y) = (69, 41);
let x = x.clamp(x_offset, x_offset - 1 + (screen_x as i32));
let y = y.clamp(y_offset, y_offset - 1 + (screen_y as i32));
ctx.set(x, y, RGB::named(WHITE), RGB::named(BLACK), to_cp437('X'));
draw_tooltips(&gs.ecs, ctx, Some((x, y)));
return match ctx.key {
None => FarlookResult::NoResponse { x, y },
Some(key) =>
match key {
VirtualKeyCode::Escape | VirtualKeyCode::X => FarlookResult::Cancel,
VirtualKeyCode::Numpad9 => FarlookResult::NoResponse { x: x + 1, y: y - 1 },
VirtualKeyCode::Numpad8 => FarlookResult::NoResponse { x, y: y - 1 },
VirtualKeyCode::Numpad7 => FarlookResult::NoResponse { x: x - 1, y: y - 1 },
VirtualKeyCode::Numpad6 => FarlookResult::NoResponse { x: x + 1, y },
VirtualKeyCode::Numpad4 => FarlookResult::NoResponse { x: x - 1, y },
VirtualKeyCode::Numpad3 => FarlookResult::NoResponse { x: x + 1, y: y + 1 },
VirtualKeyCode::Numpad2 => FarlookResult::NoResponse { x, y: y + 1 },
VirtualKeyCode::Numpad1 => FarlookResult::NoResponse { x: x - 1, y: y + 1 },
_ => FarlookResult::NoResponse { x, y },
}
};
} else {
let ppos = gs.ecs.fetch::<Point>();
// TODO: PPOS + offsets (should get these from screen_bounds())
return FarlookResult::NoResponse { x: ppos.x + x_offset, y: ppos.x + y_offset };
}
}

View file

@ -3,10 +3,10 @@ use super::{
item_colour_ecs, item_colour_ecs,
obfuscate_name_ecs, obfuscate_name_ecs,
print_options, print_options,
renderable_colour, unique_ecs,
check_key,
letter_to_option,
ItemMenuResult, ItemMenuResult,
UniqueInventoryItem,
BUC,
}; };
use crate::{ use crate::{
gamelog, gamelog,
@ -19,14 +19,15 @@ use crate::{
Name, Name,
ObfuscatedName, ObfuscatedName,
Renderable, Renderable,
State, Key,
states::state::*,
}; };
use rltk::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
use std::collections::BTreeMap; use std::collections::HashMap;
/// Handles the Identify menu. /// Handles the Identify menu.
pub fn identify(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option<Entity>) { pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<Entity>) {
let player_entity = gs.ecs.fetch::<Entity>(); let player_entity = gs.ecs.fetch::<Entity>();
let equipped = gs.ecs.read_storage::<Equipped>(); let equipped = gs.ecs.read_storage::<Equipped>();
let backpack = gs.ecs.read_storage::<InBackpack>(); let backpack = gs.ecs.read_storage::<InBackpack>();
@ -37,37 +38,41 @@ pub fn identify(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option<Entit
let names = gs.ecs.read_storage::<Name>(); let names = gs.ecs.read_storage::<Name>();
let renderables = gs.ecs.read_storage::<Renderable>(); let renderables = gs.ecs.read_storage::<Renderable>();
let beatitudes = gs.ecs.read_storage::<Beatitude>(); let beatitudes = gs.ecs.read_storage::<Beatitude>();
let keys = gs.ecs.read_storage::<Key>();
let build_identify_iterator = || { let build_identify_iterator = || {
(&entities, &items, &renderables, &names).join().filter(|(item_entity, _i, _r, n)| { (&entities, &items, &renderables, &names, &keys)
// If not owned by the player, return false. .join()
let mut keep = false; .filter(|(item_entity, _i, _r, n, _k)| {
if let Some(bp) = backpack.get(*item_entity) { // If not owned by the player, return false.
if bp.owner == *player_entity { let mut keep = false;
keep = true; if let Some(bp) = backpack.get(*item_entity) {
if bp.owner == *player_entity {
keep = true;
}
} }
} // If not equipped by the player, return false.
// If not equipped by the player, return false. if let Some(equip) = equipped.get(*item_entity) {
if let Some(equip) = equipped.get(*item_entity) { if equip.owner == *player_entity {
if equip.owner == *player_entity { keep = true;
keep = true; }
} }
} if !keep {
if !keep { return false;
return false; }
} // If not obfuscated, or already identified, return false.
// If not obfuscated, or already identified, return false. if
if (!obfuscated.get(*item_entity).is_some() ||
(!obfuscated.get(*item_entity).is_some() || dm.identified_items.contains(&n.name)) && dm.identified_items.contains(&n.name)) &&
beatitudes beatitudes
.get(*item_entity) .get(*item_entity)
.map(|beatitude| beatitude.known) .map(|beatitude| beatitude.known)
.unwrap_or(true) .unwrap_or(true)
{ {
return false; return false;
} }
return true; return true;
}) })
}; };
// Build list of items to display // Build list of items to display
@ -90,32 +95,15 @@ pub fn identify(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option<Entit
.log(); .log();
return (ItemMenuResult::Selected, Some(build_identify_iterator().nth(0).unwrap().0)); return (ItemMenuResult::Selected, Some(build_identify_iterator().nth(0).unwrap().0));
} }
let mut player_inventory: super::PlayerInventory = BTreeMap::new(); let mut player_inventory: super::PlayerInventory = HashMap::new();
for (entity, _i, renderable, name) in build_identify_iterator() { for (entity, _i, _r, _n, key) in build_identify_iterator() {
let (singular, plural) = obfuscate_name_ecs(&gs.ecs, entity); let unique_item = unique_ecs(&gs.ecs, entity);
let beatitude_status = if let Some(beatitude) = gs.ecs.read_storage::<Beatitude>().get(entity) {
match beatitude.buc {
BUC::Blessed => 1,
BUC::Uncursed => 2,
BUC::Cursed => 3,
}
} else {
0
};
let unique_item = UniqueInventoryItem {
display_name: super::DisplayName { singular: singular.clone(), plural: plural.clone() },
rgb: item_colour_ecs(&gs.ecs, entity),
renderables: renderable_colour(&renderables, entity),
glyph: renderable.glyph,
beatitude_status: beatitude_status,
name: name.name.clone(),
};
player_inventory player_inventory
.entry(unique_item) .entry(unique_item)
.and_modify(|(e, count)| { .and_modify(|slot| {
*count += 1; slot.count += 1;
}) })
.or_insert((entity, 1)); .or_insert(super::InventorySlot { item: entity, count: 1, idx: key.idx });
} }
// Get display args // Get display args
let width = get_max_inventory_width(&player_inventory); let width = get_max_inventory_width(&player_inventory);
@ -125,12 +113,12 @@ pub fn identify(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option<Entit
ctx.print_color( ctx.print_color(
1 + x_offset, 1 + x_offset,
1 + y_offset, 1 + y_offset,
RGB::named(rltk::WHITE), RGB::named(WHITE),
RGB::named(rltk::BLACK), RGB::named(BLACK),
"Identify which item? [aA-zZ][Esc.]" "Identify which item? [aA-zZ][Esc.]"
); );
ctx.draw_box(x, y, width + 2, count + 1, RGB::named(WHITE), RGB::named(BLACK)); ctx.draw_box(x, y, width + 2, count + 1, RGB::named(WHITE), RGB::named(BLACK));
print_options(&player_inventory, x + 1, y + 1, ctx); print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx);
// Input // Input
match ctx.key { match ctx.key {
None => (ItemMenuResult::NoResponse, None), None => (ItemMenuResult::NoResponse, None),
@ -138,21 +126,17 @@ pub fn identify(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option<Entit
match key { match key {
VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None), VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None),
_ => { _ => {
let selection = rltk::letter_to_option(key); let selection = letter_to_option::letter_to_option(key, ctx.shift);
if selection > -1 && selection < (count as i32) { if selection != -1 && check_key(selection as usize) {
let item = player_inventory // Get the first entity with a Key {} component that has an idx matching "selection".
.iter() let entities = gs.ecs.entities();
.nth(selection as usize) let keyed_items = gs.ecs.read_storage::<Key>();
.unwrap().1.0; let backpack = gs.ecs.read_storage::<InBackpack>();
gamelog::Logger for (e, key, _b) in (&entities, &keyed_items, &backpack).join() {
::new() if key.idx == (selection as usize) {
.append("You identify the") return (ItemMenuResult::Selected, Some(e));
.colour(item_colour_ecs(&gs.ecs, item)) }
.append_n(obfuscate_name_ecs(&gs.ecs, item).0) }
.colour(WHITE)
.append("!")
.log();
return (ItemMenuResult::Selected, Some(item));
} }
(ItemMenuResult::NoResponse, None) (ItemMenuResult::NoResponse, None)
} }

View file

@ -1,4 +1,4 @@
use rltk::prelude::*; use bracket_lib::prelude::*;
pub fn letter_to_option(key: VirtualKeyCode, shift: bool) -> i32 { pub fn letter_to_option(key: VirtualKeyCode, shift: bool) -> i32 {
if shift { if shift {

File diff suppressed because it is too large Load diff

View file

@ -3,17 +3,31 @@ use super::{
item_colour_ecs, item_colour_ecs,
obfuscate_name_ecs, obfuscate_name_ecs,
print_options, print_options,
renderable_colour, unique_ecs,
check_key,
letter_to_option,
ItemMenuResult, ItemMenuResult,
UniqueInventoryItem, InventorySlot,
}; };
use crate::{ gamelog, Beatitude, Entity, Equipped, InBackpack, Item, Name, Renderable, State, BUC }; use crate::{
use rltk::prelude::*; gamelog,
Beatitude,
Entity,
Equipped,
InBackpack,
Item,
Name,
Renderable,
states::state::*,
BUC,
Key,
};
use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
use std::collections::BTreeMap; use std::collections::HashMap;
/// Handles the Remove Curse menu. /// Handles the Remove Curse menu.
pub fn remove_curse(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option<Entity>) { pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<Entity>) {
let player_entity = gs.ecs.fetch::<Entity>(); let player_entity = gs.ecs.fetch::<Entity>();
let equipped = gs.ecs.read_storage::<Equipped>(); let equipped = gs.ecs.read_storage::<Equipped>();
let backpack = gs.ecs.read_storage::<InBackpack>(); let backpack = gs.ecs.read_storage::<InBackpack>();
@ -22,35 +36,38 @@ pub fn remove_curse(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option<E
let beatitudes = gs.ecs.read_storage::<Beatitude>(); let beatitudes = gs.ecs.read_storage::<Beatitude>();
let names = gs.ecs.read_storage::<Name>(); let names = gs.ecs.read_storage::<Name>();
let renderables = gs.ecs.read_storage::<Renderable>(); let renderables = gs.ecs.read_storage::<Renderable>();
let keys = gs.ecs.read_storage::<Key>();
let build_cursed_iterator = || { let build_cursed_iterator = || {
(&entities, &items, &beatitudes, &renderables, &names).join().filter(|(item_entity, _i, b, _r, _n)| { (&entities, &items, &beatitudes, &renderables, &names, &keys)
// Set all items to FALSE initially. .join()
let mut keep = false; .filter(|(item_entity, _i, b, _r, _n, _k)| {
// If found in the player's backpack, set to TRUE // Set all items to FALSE initially.
if let Some(bp) = backpack.get(*item_entity) { let mut keep = false;
if bp.owner == *player_entity { // If found in the player's backpack, set to TRUE
keep = true; if let Some(bp) = backpack.get(*item_entity) {
if bp.owner == *player_entity {
keep = true;
}
} }
} // If found in the player's equipslot, set to TRUE
// If found in the player's equipslot, set to TRUE if let Some(equip) = equipped.get(*item_entity) {
if let Some(equip) = equipped.get(*item_entity) { if equip.owner == *player_entity {
if equip.owner == *player_entity { keep = true;
keep = true; }
} }
} // If it's not OUR item, RETURN FALSE.
// If it's not OUR item, RETURN FALSE. if !keep {
if !keep { return false;
return false; }
} // If it's identified as noncursed, RETURN FALSE.
// If it's identified as noncursed, RETURN FALSE. if b.known && b.buc != BUC::Cursed {
if b.known && b.buc != BUC::Cursed { return false;
return false; }
} // Otherwise, return: returns any items that are unidentified,
// Otherwise, return: returns any items that are unidentified, // or identified as being cursed.
// or identified as being cursed. return true;
return true; })
})
}; };
// Build list of items to display // Build list of items to display
@ -73,32 +90,19 @@ pub fn remove_curse(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option<E
.log(); .log();
return (ItemMenuResult::Selected, Some(item)); return (ItemMenuResult::Selected, Some(item));
} }
let mut player_inventory: super::PlayerInventory = BTreeMap::new(); let mut player_inventory: super::PlayerInventory = HashMap::new();
for (entity, _i, _b, renderable, name) in build_cursed_iterator() { for (entity, _i, _b, _r, _n, key) in build_cursed_iterator() {
let (singular, plural) = obfuscate_name_ecs(&gs.ecs, entity); let unique_item = unique_ecs(&gs.ecs, entity);
let beatitude_status = if let Some(beatitude) = gs.ecs.read_storage::<Beatitude>().get(entity) {
match beatitude.buc {
BUC::Blessed => 1,
BUC::Uncursed => 2,
BUC::Cursed => 3,
}
} else {
0
};
let unique_item = UniqueInventoryItem {
display_name: super::DisplayName { singular: singular.clone(), plural: plural.clone() },
rgb: item_colour_ecs(&gs.ecs, entity),
renderables: renderable_colour(&renderables, entity),
glyph: renderable.glyph,
beatitude_status: beatitude_status,
name: name.name.clone(),
};
player_inventory player_inventory
.entry(unique_item) .entry(unique_item)
.and_modify(|(e, count)| { .and_modify(|slot| {
*count += 1; slot.count += 1;
}) })
.or_insert((entity, 1)); .or_insert(InventorySlot {
item: entity,
count: 1,
idx: key.idx,
});
} }
// Get display args // Get display args
let width = get_max_inventory_width(&player_inventory); let width = get_max_inventory_width(&player_inventory);
@ -108,12 +112,12 @@ pub fn remove_curse(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option<E
ctx.print_color( ctx.print_color(
1 + x_offset, 1 + x_offset,
1 + y_offset, 1 + y_offset,
RGB::named(rltk::WHITE), RGB::named(WHITE),
RGB::named(rltk::BLACK), RGB::named(BLACK),
"Decurse which item? [aA-zZ][Esc.]" "Decurse which item? [aA-zZ][Esc.]"
); );
ctx.draw_box(x, y, width + 2, count + 1, RGB::named(WHITE), RGB::named(BLACK)); ctx.draw_box(x, y, width + 2, count + 1, RGB::named(WHITE), RGB::named(BLACK));
print_options(&player_inventory, x + 1, y + 1, ctx); print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx);
// Input // Input
match ctx.key { match ctx.key {
None => (ItemMenuResult::NoResponse, None), None => (ItemMenuResult::NoResponse, None),
@ -121,21 +125,17 @@ pub fn remove_curse(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option<E
match key { match key {
VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None), VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None),
_ => { _ => {
let selection = rltk::letter_to_option(key); let selection = letter_to_option::letter_to_option(key, ctx.shift);
if selection > -1 && selection < (count as i32) { if selection != -1 && check_key(selection as usize) {
let item = player_inventory // Get the first entity with a Key {} component that has an idx matching "selection".
.iter() let entities = gs.ecs.entities();
.nth(selection as usize) let keyed_items = gs.ecs.read_storage::<Key>();
.unwrap().1.0; let backpack = gs.ecs.read_storage::<InBackpack>();
gamelog::Logger for (e, key, _b) in (&entities, &keyed_items, &backpack).join() {
::new() if key.idx == (selection as usize) {
.append("You decurse the") return (ItemMenuResult::Selected, Some(e));
.colour(item_colour_ecs(&gs.ecs, item)) }
.append_n(obfuscate_name_ecs(&gs.ecs, item).0) }
.colour(WHITE)
.append("!")
.log();
return (ItemMenuResult::Selected, Some(item));
} }
(ItemMenuResult::NoResponse, None) (ItemMenuResult::NoResponse, None)
} }

View file

@ -1,5 +1,18 @@
use super::{ camera::get_screen_bounds, Attributes, Hidden, Map, Name, Pools, Position, Renderable, Rltk, World, RGB }; use super::{
use rltk::prelude::*; camera::get_screen_bounds,
Attributes,
Hidden,
Map,
Name,
Pools,
Position,
Renderable,
World,
RGB,
};
use crate::TileType;
use crate::data::ids::*;
use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
struct Tooltip { struct Tooltip {
@ -31,8 +44,15 @@ impl Tooltip {
fn height(&self) -> i32 { fn height(&self) -> i32 {
return (self.lines.len() as i32) + 2i32; return (self.lines.len() as i32) + 2i32;
} }
fn render(&self, ctx: &mut Rltk, x: i32, y: i32) { fn render(&self, ctx: &mut BTerm, x: i32, y: i32) {
ctx.draw_box(x, y, self.width() - 1, self.height() - 1, RGB::named(WHITE), RGB::named(BLACK)); ctx.draw_box(
x,
y,
self.width() - 1,
self.height() - 1,
RGB::named(WHITE),
RGB::named(BLACK)
);
for (i, s) in self.lines.iter().enumerate() { for (i, s) in self.lines.iter().enumerate() {
ctx.print_color(x + 1, y + (i as i32) + 1, s.1, RGB::named(BLACK), &s.0); ctx.print_color(x + 1, y + (i as i32) + 1, s.1, RGB::named(BLACK), &s.0);
} }
@ -40,7 +60,7 @@ impl Tooltip {
} }
#[rustfmt::skip] #[rustfmt::skip]
pub fn draw_tooltips(ecs: &World, ctx: &mut Rltk) { pub fn draw_tooltips(ecs: &World, ctx: &mut BTerm, xy: Option<(i32, i32)>) {
let (min_x, _max_x, min_y, _max_y, x_offset, y_offset) = get_screen_bounds(ecs, ctx); let (min_x, _max_x, min_y, _max_y, x_offset, y_offset) = get_screen_bounds(ecs, ctx);
let map = ecs.fetch::<Map>(); let map = ecs.fetch::<Map>();
let names = ecs.read_storage::<Name>(); let names = ecs.read_storage::<Name>();
@ -52,7 +72,7 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut Rltk) {
let entities = ecs.entities(); let entities = ecs.entities();
let player_entity = ecs.fetch::<Entity>(); let player_entity = ecs.fetch::<Entity>();
let mouse_pos = ctx.mouse_pos(); let mouse_pos = if xy.is_none() { ctx.mouse_pos() } else { xy.unwrap() };
let mut mouse_pos_adjusted = mouse_pos; let mut mouse_pos_adjusted = mouse_pos;
mouse_pos_adjusted.0 += min_x - x_offset; mouse_pos_adjusted.0 += min_x - x_offset;
mouse_pos_adjusted.1 += min_y - y_offset; mouse_pos_adjusted.1 += min_y - y_offset;
@ -70,10 +90,33 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut Rltk) {
} }
let mut tooltips: Vec<Tooltip> = Vec::new(); let mut tooltips: Vec<Tooltip> = Vec::new();
match map.tiles[map.xy_idx(mouse_pos_adjusted.0, mouse_pos_adjusted.1)] {
TileType::ToLocal(n) => {
let name = get_local_desc(n);
let mut tip = Tooltip::new();
tip.add(format!("You see {}.", name), get_local_col(n));
tooltips.push(tip);
}
TileType::ToOvermap(n) => {
let name = get_local_desc(n);
let mut tip = Tooltip::new();
tip.add(format!("You see an exit from {}.", name), get_local_col(n));
tooltips.push(tip);
}
_ => {}
}
for (entity, position, renderable, _name, _hidden) in (&entities, &positions, &renderables, &names, !&hidden).join() { for (entity, position, renderable, _name, _hidden) in (&entities, &positions, &renderables, &names, !&hidden).join() {
if position.x == mouse_pos_adjusted.0 && position.y == mouse_pos_adjusted.1 { if position.x == mouse_pos_adjusted.0 && position.y == mouse_pos_adjusted.1 {
let mut tip = Tooltip::new(); let mut tip = Tooltip::new();
tip.add(crate::gui::obfuscate_name_ecs(ecs, entity).0, renderable.fg); tip.add(crate::gui::obfuscate_name_ecs(ecs, entity).0, renderable.fg);
let intrinsics = ecs.read_storage::<crate::components::Intrinsics>();
if let Some(intrinsics) = intrinsics.get(entity) {
if !intrinsics.list.is_empty() {
tip.add(intrinsics.describe(), RGB::named(WHITE));
}
}
// Attributes // Attributes
let attr = attributes.get(entity); let attr = attributes.get(entity);
if let Some(a) = attr { if let Some(a) = attr {
@ -124,12 +167,12 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut Rltk) {
if tooltips.is_empty() { return ; } if tooltips.is_empty() { return ; }
let white = RGB::named(rltk::WHITE); let white = RGB::named(WHITE);
let arrow; let arrow;
let arrow_x; let arrow_x;
let arrow_y = mouse_pos.1; let arrow_y = mouse_pos.1;
if mouse_pos.0 < 35 { if mouse_pos.0 > 35 {
// Render to the left // Render to the left
arrow = to_cp437('→'); arrow = to_cp437('→');
arrow_x = mouse_pos.0 - 1; arrow_x = mouse_pos.0 - 1;
@ -138,7 +181,7 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut Rltk) {
arrow = to_cp437('←'); arrow = to_cp437('←');
arrow_x = mouse_pos.0 + 1; arrow_x = mouse_pos.0 + 1;
} }
ctx.set(arrow_x, arrow_y, white, RGB::named(rltk::BLACK), arrow); ctx.set(arrow_x, arrow_y, white, RGB::named(BLACK), arrow);
let mut total_height = 0; let mut total_height = 0;
for t in tooltips.iter() { for t in tooltips.iter() {
@ -151,7 +194,7 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut Rltk) {
} }
for t in tooltips.iter() { for t in tooltips.iter() {
let x = if mouse_pos.0 < 35 { let x = if mouse_pos.0 > 35 {
mouse_pos.0 - (1 + t.width()) mouse_pos.0 - (1 + t.width())
} else { } else {
mouse_pos.0 + (1 + 1) mouse_pos.0 + (1 + 1)

View file

@ -5,10 +5,12 @@ use super::{
HungerClock, HungerClock,
HungerState, HungerState,
TakingTurn, TakingTurn,
LOG_TICKS, DamageType,
Intrinsics,
}; };
use rltk::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
use crate::config::CONFIG;
/// HungerSystem is in charge of ticking down the hunger clock for entities with a hunger clock, /// HungerSystem is in charge of ticking down the hunger clock for entities with a hunger clock,
/// every time the turn clock ticks. /// every time the turn clock ticks.
@ -52,10 +54,11 @@ impl<'a> System<'a> for HungerSystem {
ReadExpect<'a, Entity>, ReadExpect<'a, Entity>,
ReadStorage<'a, Clock>, ReadStorage<'a, Clock>,
ReadStorage<'a, TakingTurn>, ReadStorage<'a, TakingTurn>,
ReadStorage<'a, Intrinsics>,
); );
fn run(&mut self, data: Self::SystemData) { fn run(&mut self, data: Self::SystemData) {
let (entities, mut hunger_clock, player_entity, turn_clock, turns) = data; let (entities, mut hunger_clock, player_entity, turn_clock, turns, intrinsics) = data;
// If the turn clock isn't taking a turn this tick, don't bother ticking hunger. // If the turn clock isn't taking a turn this tick, don't bother ticking hunger.
let mut ticked = false; let mut ticked = false;
@ -71,16 +74,32 @@ impl<'a> System<'a> for HungerSystem {
if hunger_clock.duration >= MAX_SATIATION { if hunger_clock.duration >= MAX_SATIATION {
hunger_clock.duration = MAX_SATIATION; hunger_clock.duration = MAX_SATIATION;
} else { } else {
hunger_clock.duration -= BASE_CLOCK_DECREMENT_PER_TURN; let mut modifier = 0;
let intrinsic_regen = if let Some(i) = intrinsics.get(entity) {
i.list.contains(&crate::Intrinsic::Regeneration)
} else {
false
};
if intrinsic_regen {
modifier += 1;
}
hunger_clock.duration -= BASE_CLOCK_DECREMENT_PER_TURN + modifier;
} }
let initial_state = hunger_clock.state; let initial_state = hunger_clock.state;
hunger_clock.state = get_hunger_state(hunger_clock.duration); hunger_clock.state = get_hunger_state(hunger_clock.duration);
if hunger_clock.state == HungerState::Starving { if hunger_clock.state == HungerState::Starving {
add_effect(None, EffectType::Damage { amount: 1 }, Targets::Entity { target: entity }); add_effect(
None,
EffectType::Damage { amount: 1, damage_type: DamageType::Forced },
Targets::Entity { target: entity }
);
} }
if LOG_TICKS && entity == *player_entity { if CONFIG.logging.log_ticks && entity == *player_entity {
rltk::console::log( console::log(
format!("HUNGER SYSTEM: Ticked for player entity. [clock: {}]", hunger_clock.duration) format!(
"HUNGER SYSTEM: Ticked for player entity. [clock: {}]",
hunger_clock.duration
)
); );
} }
if hunger_clock.state == initial_state { if hunger_clock.state == initial_state {

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
gamelog, gamelog,
gui::obfuscate_name, gui::obfuscate_name,
gui::item_colour,
Beatitude, Beatitude,
Charges, Charges,
EquipmentChanged, EquipmentChanged,
@ -11,9 +12,11 @@ use crate::{
ObfuscatedName, ObfuscatedName,
Position, Position,
WantsToPickupItem, WantsToPickupItem,
WantsToAssignKey,
}; };
use specs::prelude::*; use specs::prelude::*;
use crate::config::messages; use crate::data::messages;
use bracket_lib::prelude::*;
pub struct ItemCollectionSystem {} pub struct ItemCollectionSystem {}
@ -31,6 +34,7 @@ impl<'a> System<'a> for ItemCollectionSystem {
ReadStorage<'a, Beatitude>, ReadStorage<'a, Beatitude>,
ReadExpect<'a, MasterDungeonMap>, ReadExpect<'a, MasterDungeonMap>,
ReadStorage<'a, Charges>, ReadStorage<'a, Charges>,
ReadStorage<'a, WantsToAssignKey>,
); );
fn run(&mut self, data: Self::SystemData) { fn run(&mut self, data: Self::SystemData) {
@ -46,20 +50,17 @@ impl<'a> System<'a> for ItemCollectionSystem {
beatitudes, beatitudes,
dm, dm,
wands, wands,
wants_key,
) = data; ) = data;
let mut to_remove: Vec<Entity> = Vec::new();
for pickup in wants_pickup.join() { // For every item that wants to be picked up that *isn't* waiting on a key assignment.
positions.remove(pickup.item); for (pickup, _key) in (&wants_pickup, !&wants_key).join() {
backpack.insert(pickup.item, InBackpack { owner: pickup.collected_by }).expect("Unable to pickup item.");
equipment_changed
.insert(pickup.collected_by, EquipmentChanged {})
.expect("Unable to insert EquipmentChanged.");
if pickup.collected_by == *player_entity { if pickup.collected_by == *player_entity {
gamelog::Logger gamelog::Logger
::new() ::new()
.append(messages::YOU_PICKUP_ITEM) .append(messages::YOU_PICKUP_ITEM)
.item_name_n( .colour(item_colour(pickup.item, &beatitudes))
.append_n(
format!( format!(
"{}", "{}",
obfuscate_name( obfuscate_name(
@ -73,11 +74,21 @@ impl<'a> System<'a> for ItemCollectionSystem {
).0 ).0
) )
) )
.colour(WHITE)
.period() .period()
.log(); .log();
} }
positions.remove(pickup.item);
backpack
.insert(pickup.item, InBackpack { owner: pickup.collected_by })
.expect("Unable to pickup item");
equipment_changed
.insert(pickup.collected_by, EquipmentChanged {})
.expect("Unable to insert EquipmentChanged");
to_remove.push(pickup.collected_by);
}
for item in to_remove.iter() {
wants_pickup.remove(*item);
} }
wants_pickup.clear();
} }
} }

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
gamelog, gamelog,
gui::obfuscate_name, gui::obfuscate_name,
gui::item_colour,
Beatitude, Beatitude,
Charges, Charges,
EquipmentChanged, EquipmentChanged,
@ -11,9 +12,11 @@ use crate::{
ObfuscatedName, ObfuscatedName,
Position, Position,
WantsToDropItem, WantsToDropItem,
WantsToRemoveKey,
}; };
use specs::prelude::*; use specs::prelude::*;
use crate::config::messages; use crate::data::messages;
use bracket_lib::prelude::*;
pub struct ItemDropSystem {} pub struct ItemDropSystem {}
@ -32,6 +35,7 @@ impl<'a> System<'a> for ItemDropSystem {
ReadStorage<'a, ObfuscatedName>, ReadStorage<'a, ObfuscatedName>,
ReadExpect<'a, MasterDungeonMap>, ReadExpect<'a, MasterDungeonMap>,
ReadStorage<'a, Charges>, ReadStorage<'a, Charges>,
WriteStorage<'a, WantsToRemoveKey>,
); );
fn run(&mut self, data: Self::SystemData) { fn run(&mut self, data: Self::SystemData) {
@ -48,10 +52,13 @@ impl<'a> System<'a> for ItemDropSystem {
obfuscated_names, obfuscated_names,
dm, dm,
wands, wands,
mut keys,
) = data; ) = data;
for (entity, to_drop) in (&entities, &wants_drop).join() { for (entity, to_drop) in (&entities, &wants_drop).join() {
equipment_changed.insert(entity, EquipmentChanged {}).expect("Unable to insert EquipmentChanged."); equipment_changed
.insert(entity, EquipmentChanged {})
.expect("Unable to insert EquipmentChanged.");
let mut dropper_pos: Position = Position { x: 0, y: 0 }; let mut dropper_pos: Position = Position { x: 0, y: 0 };
{ {
let dropped_pos = positions.get(entity).unwrap(); let dropped_pos = positions.get(entity).unwrap();
@ -64,10 +71,14 @@ impl<'a> System<'a> for ItemDropSystem {
backpack.remove(to_drop.item); backpack.remove(to_drop.item);
if entity == *player_entity { if entity == *player_entity {
keys.insert(to_drop.item, WantsToRemoveKey {}).expect(
"Unable to insert WantsToRemoveKey"
);
gamelog::Logger gamelog::Logger
::new() ::new()
.append(messages::YOU_DROP_ITEM) .append(messages::YOU_DROP_ITEM)
.item_name_n( .colour(item_colour(to_drop.item, &beatitudes))
.append_n(
format!( format!(
"{}", "{}",
obfuscate_name( obfuscate_name(
@ -81,6 +92,7 @@ impl<'a> System<'a> for ItemDropSystem {
).0 ).0
) )
) )
.colour(WHITE)
.period() .period()
.log(); .log();
} }

View file

@ -16,7 +16,8 @@ use crate::{
BUC, BUC,
}; };
use specs::prelude::*; use specs::prelude::*;
use crate::config::messages; use crate::data::messages;
use bracket_lib::prelude::*;
pub struct ItemEquipSystem {} pub struct ItemEquipSystem {}
@ -66,7 +67,11 @@ impl<'a> System<'a> for ItemEquipSystem {
// Remove any items target has in item's slot // Remove any items target has in item's slot
let mut can_equip = true; let mut can_equip = true;
let mut to_unequip: Vec<Entity> = Vec::new(); let mut to_unequip: Vec<Entity> = Vec::new();
for (item_entity, already_equipped, _name) in (&entities, &equipped, &names).join() { for (item_entity, already_equipped, _name) in (
&entities,
&equipped,
&names,
).join() {
if already_equipped.owner == target && already_equipped.slot == target_slot { if already_equipped.owner == target && already_equipped.slot == target_slot {
if let Some(beatitude) = beatitudes.get(item_entity) { if let Some(beatitude) = beatitudes.get(item_entity) {
if beatitude.buc == BUC::Cursed { if beatitude.buc == BUC::Cursed {
@ -85,7 +90,7 @@ impl<'a> System<'a> for ItemEquipSystem {
None None
).0 ).0
) )
.colour(rltk::WHITE) .colour(WHITE)
.append("!"); .append("!");
identified_beatitude identified_beatitude
.insert(item_entity, IdentifiedBeatitude {}) .insert(item_entity, IdentifiedBeatitude {})
@ -101,15 +106,25 @@ impl<'a> System<'a> for ItemEquipSystem {
} }
for item in to_unequip.iter() { for item in to_unequip.iter() {
equipped.remove(*item); equipped.remove(*item);
backpack.insert(*item, InBackpack { owner: target }).expect("Unable to insert backpack"); backpack
.insert(*item, InBackpack { owner: target })
.expect("Unable to insert backpack");
if target == *player_entity { if target == *player_entity {
logger = logger logger = logger
.append(messages::YOU_REMOVE_ITEM) .append(messages::YOU_REMOVE_ITEM)
.colour(item_colour(*item, &beatitudes)) .colour(item_colour(*item, &beatitudes))
.append_n( .append_n(
obfuscate_name(*item, &names, &magic_items, &obfuscated_names, &beatitudes, &dm, None).0 obfuscate_name(
*item,
&names,
&magic_items,
&obfuscated_names,
&beatitudes,
&dm,
None
).0
) )
.colour(rltk::WHITE) .colour(WHITE)
.period(); .period();
} }
} }
@ -134,7 +149,7 @@ impl<'a> System<'a> for ItemEquipSystem {
None None
).0 ).0
) )
.colour(rltk::WHITE) .colour(WHITE)
.period(); .period();
logger.log(); logger.log();
identified_items identified_items

View file

@ -1,5 +1,16 @@
use crate::{ Beatitude, IdentifiedBeatitude, IdentifiedItem, Item, MasterDungeonMap, Name, ObfuscatedName, Player }; use crate::{
Beatitude,
IdentifiedBeatitude,
IdentifiedItem,
Item,
MasterDungeonMap,
Name,
ObfuscatedName,
Player,
};
use specs::prelude::*; use specs::prelude::*;
use crate::data::events::*;
use crate::gamelog;
pub struct ItemIdentificationSystem {} pub struct ItemIdentificationSystem {}
@ -32,6 +43,9 @@ impl<'a> System<'a> for ItemIdentificationSystem {
for (_p, id) in (&player, &identified).join() { for (_p, id) in (&player, &identified).join() {
let tag = crate::raws::get_id_from_name(id.name.clone()); let tag = crate::raws::get_id_from_name(id.name.clone());
if !dm.identified_items.contains(&id.name) && crate::raws::is_tag_magic(&tag) { if !dm.identified_items.contains(&id.name) && crate::raws::is_tag_magic(&tag) {
if gamelog::get_event_count(EVENT::COUNT_TURN) != 1 {
gamelog::record_event(EVENT::Identified(id.name.clone()));
}
dm.identified_items.insert(id.name.clone()); dm.identified_items.insert(id.name.clone());
for (entity, _item, name) in (&entities, &items, &names).join() { for (entity, _item, name) in (&entities, &items, &names).join() {
if name.name == id.name { if name.name == id.name {

View file

@ -0,0 +1,153 @@
use crate::{
gamelog,
gui::unique,
Beatitude,
Charges,
MagicItem,
MasterDungeonMap,
Name,
ObfuscatedName,
Stackable,
Renderable,
WantsToAssignKey,
WantsToRemoveKey,
Key,
};
use specs::prelude::*;
use crate::data::messages;
use bracket_lib::prelude::*;
use crate::invkeys::*;
pub struct KeyHandling {}
const DEBUG_KEYHANDLING: bool = true;
impl<'a> System<'a> for KeyHandling {
#[allow(clippy::type_complexity)]
type SystemData = (
Entities<'a>,
WriteStorage<'a, WantsToAssignKey>,
WriteStorage<'a, WantsToRemoveKey>,
WriteStorage<'a, Key>,
ReadStorage<'a, Stackable>,
ReadStorage<'a, Name>,
ReadStorage<'a, ObfuscatedName>,
ReadStorage<'a, Renderable>,
ReadStorage<'a, Beatitude>,
ReadStorage<'a, MagicItem>,
ReadStorage<'a, Charges>,
ReadExpect<'a, MasterDungeonMap>,
);
fn run(&mut self, data: Self::SystemData) {
let (
entities,
mut wants_keys,
mut wants_removekey,
mut keys,
stackable,
names,
obfuscated_names,
renderables,
beatitudes,
magic_items,
wands,
dm,
) = data;
// For every entity that wants to be picked up, that still needs a key assigned.
for (e, _wants_key) in (&entities, &wants_keys).join() {
if DEBUG_KEYHANDLING {
console::log(&format!("KEYHANDLING: Assigning key to {:?}", e));
}
let (stacks, mut handled, unique) = (
if let Some(_) = stackable.get(e) { true } else { false },
false,
unique(
e,
&names,
&obfuscated_names,
&renderables,
&beatitudes,
&magic_items,
Some(&wands),
&dm
),
);
if stacks {
console::log(&format!("KEYHANDLING: Item is stackable."));
let maybe_key = item_exists(&unique);
if maybe_key.is_some() {
console::log(&format!("KEYHANDLING: Existing stack found for this item."));
let key = maybe_key.unwrap();
keys.insert(e, Key { idx: key }).expect("Unable to insert Key.");
console::log(&format!("KEYHANDLING: Assigned key idx {} to item.", key));
handled = true;
}
}
if !handled {
console::log(
&format!("KEYHANDLING: Item is not stackable, or no existing stack found.")
);
if let Some(idx) = assign_next_available() {
console::log(
&format!("KEYHANDLING: Assigned next available index {} to item.", idx)
);
keys.insert(e, Key { idx }).expect("Unable to insert Key.");
register_stackable(stacks, unique, idx);
} else {
console::log(&format!("KEYHANDLING: No more keys available."));
gamelog::Logger
::new()
.append(messages::NO_MORE_KEYS)
.colour(WHITE)
.period()
.log();
}
}
}
for (e, _wants_key) in (&entities, &wants_removekey).join() {
let idx = keys.get(e).unwrap().idx;
if DEBUG_KEYHANDLING {
console::log(&format!("KEYHANDLING: Removing key from {:?}", e));
}
// If the item is *not* stackable, then we can just remove the key and clear the index.
if let None = stackable.get(e) {
console::log(
&format!("KEYHANDLING: Item is not stackable, clearing index {}.", idx)
);
clear_idx(idx);
keys.remove(e);
continue;
}
// If the item *is* stackable, then we need to check if there are any other items that
// share this key assignment, before clearing the index.
console::log(
&format!(
"KEYHANDLING: Item is stackable, checking if any other items share this key."
)
);
let mut sole_item_with_key = true;
for (entity, key) in (&entities, &keys).join() {
if entity != e && key.idx == idx {
console::log(&format!("KEYHANDLING: Another item shares index {}", idx));
sole_item_with_key = false;
break;
}
}
// If no other items shared this key, free up the index.
if sole_item_with_key {
console::log(
&format!("KEYHANDLING: No other items found, clearing index {}.", idx)
);
clear_idx(idx);
}
// Either way, remove the key component from this item, because we're dropping it.
console::log(&format!("KEYHANDLING: Removing key component from item."));
keys.remove(e);
}
wants_removekey.clear();
wants_keys.clear();
}
}

View file

@ -4,6 +4,7 @@ mod equip_system;
mod identification_system; mod identification_system;
mod remove_system; mod remove_system;
mod use_system; mod use_system;
mod keyhandling;
pub use self::{ pub use self::{
collection_system::ItemCollectionSystem, collection_system::ItemCollectionSystem,
@ -12,4 +13,5 @@ pub use self::{
identification_system::ItemIdentificationSystem, identification_system::ItemIdentificationSystem,
remove_system::ItemRemoveSystem, remove_system::ItemRemoveSystem,
use_system::ItemUseSystem, use_system::ItemUseSystem,
keyhandling::KeyHandling,
}; };

View file

@ -11,9 +11,9 @@ use crate::{
WantsToRemoveItem, WantsToRemoveItem,
BUC, BUC,
}; };
use rltk::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
use crate::config::messages; use crate::data::messages;
pub struct ItemRemoveSystem {} pub struct ItemRemoveSystem {}
@ -99,7 +99,9 @@ impl<'a> System<'a> for ItemRemoveSystem {
.log(); .log();
} }
} }
backpack.insert(to_remove.item, InBackpack { owner: entity }).expect("Unable to insert backpack"); backpack
.insert(to_remove.item, InBackpack { owner: entity })
.expect("Unable to insert backpack");
} }
wants_remove.clear(); wants_remove.clear();

59
src/invkeys.rs Normal file
View file

@ -0,0 +1,59 @@
use std::sync::Mutex;
use std::collections::HashMap;
use crate::gui::UniqueInventoryItem;
lazy_static! {
pub static ref INVKEYS: Mutex<HashMap<UniqueInventoryItem, usize>> = Mutex::new(HashMap::new());
pub static ref ASSIGNEDKEYS: Mutex<Vec<bool>> = Mutex::new(vec![false; 52]);
}
/// For (de)serialization.
pub fn clone_invkeys() -> HashMap<UniqueInventoryItem, usize> {
let invkeys = INVKEYS.lock().unwrap();
invkeys.clone()
}
pub fn restore_invkeys(invkeys: HashMap<UniqueInventoryItem, usize>) {
INVKEYS.lock().unwrap().clear();
INVKEYS.lock().unwrap().extend(invkeys);
}
pub fn check_key(idx: usize) -> bool {
let lock = ASSIGNEDKEYS.lock().unwrap();
lock[idx]
}
pub fn item_exists(item: &UniqueInventoryItem) -> Option<usize> {
let invkeys = INVKEYS.lock().unwrap();
use bracket_lib::prelude::*;
console::log(&format!("{:?}", item));
if invkeys.contains_key(item) {
Some(*invkeys.get(item).unwrap())
} else {
None
}
}
pub fn assign_next_available() -> Option<usize> {
let mut lock = ASSIGNEDKEYS.lock().unwrap();
for (i, key) in lock.iter_mut().enumerate() {
if !*key {
*key = true;
return Some(i);
}
}
None
}
pub fn register_stackable(stacks: bool, item: UniqueInventoryItem, idx: usize) {
if stacks {
let mut invkeys = INVKEYS.lock().unwrap();
invkeys.insert(item, idx);
}
}
pub fn clear_idx(idx: usize) {
let mut lock = ASSIGNEDKEYS.lock().unwrap();
lock[idx] = false;
let mut invkeys = INVKEYS.lock().unwrap();
invkeys.retain(|_k, v| *v != idx);
}

47
src/lib.rs Normal file
View file

@ -0,0 +1,47 @@
// src/lib.rs
// 31-Aug-2023
use bracket_lib::prelude::*;
use specs::prelude::*;
extern crate serde;
#[macro_use]
extern crate lazy_static;
#[macro_use]
pub mod macros;
pub mod camera;
pub mod components;
pub mod raws;
pub mod map;
pub mod player;
pub mod gamelog;
pub mod gui;
pub mod map_builders;
pub mod saveload_system;
pub mod spawner;
pub mod visibility_system;
pub mod damage_system;
pub mod hunger_system;
pub mod melee_combat_system;
pub mod trigger_system;
pub mod inventory;
pub mod particle_system;
pub mod ai;
pub mod data;
pub mod config;
pub mod effects;
pub mod gamesystem;
pub mod random_table;
pub mod rex_assets;
pub mod spatial;
pub mod morgue;
pub mod states;
pub mod invkeys;
pub use components::*;
use particle_system::ParticleBuilder;
pub use map::*;
pub use states::runstate::RunState;
pub use states::state::State;

93
src/macros/mod.rs Normal file
View file

@ -0,0 +1,93 @@
// macros/mod.rs
#[macro_export]
/// Used to check if the player has a given component.
macro_rules! player_has_component {
($ecs:expr, $component:ty) => {
{
let player = $ecs.fetch::<Entity>();
let component = $ecs.read_storage::<$component>();
if let Some(player_component) = component.get(*player) {
true
} else {
false
}
}
};
}
#[macro_export]
/// Used to check if a given entity has a given Intrinsic.
macro_rules! has {
($ecs:expr, $entity:expr, $intrinsic:expr) => {
{
let intrinsics = $ecs.read_storage::<crate::Intrinsics>();
if let Some(has_intrinsics) = intrinsics.get($entity) {
has_intrinsics.list.contains(&$intrinsic)
} else {
false
}
}
};
}
#[macro_export]
/// Used to check if the player has a given Intrinsic.
macro_rules! player_has {
($ecs:expr, $intrinsic:expr) => {
{
let player = $ecs.fetch::<Entity>();
let intrinsics = $ecs.read_storage::<crate::Intrinsics>();
if let Some(player_intrinsics) = intrinsics.get(*player) {
player_intrinsics.list.contains(&$intrinsic)
} else {
false
}
}
};
}
#[macro_export]
/// Handles adding an Intrinsic to the player, and adding it to the IntrinsicChanged component.
macro_rules! add_intr {
($ecs:expr, $entity:expr, $intrinsic:expr) => {
{
let mut intrinsics = $ecs.write_storage::<crate::Intrinsics>();
if let Some(player_intrinsics) = intrinsics.get_mut($entity) {
if !player_intrinsics.list.contains(&$intrinsic) {
player_intrinsics.list.insert($intrinsic);
let mut intrinsic_changed = $ecs.write_storage::<crate::IntrinsicChanged>();
if let Some(this_intrinsic_changed) = intrinsic_changed.get_mut($entity) {
this_intrinsic_changed.gained.insert($intrinsic);
} else {
intrinsic_changed.insert($entity, crate::IntrinsicChanged {
gained: {
let mut m = std::collections::HashSet::new();
m.insert($intrinsic);
m
},
lost: std::collections::HashSet::new()
}).expect("Failed to insert IntrinsicChanged component.");
}
}
} else {
intrinsics.insert($entity, crate::Intrinsics {
list: {
let mut m = std::collections::HashSet::new();
m.insert($intrinsic);
m
}
}).expect("Failed to insert Intrinsics component.");
let mut intrinsic_changed = $ecs.write_storage::<crate::IntrinsicChanged>();
intrinsic_changed.insert($entity, crate::IntrinsicChanged {
gained: {
let mut m = std::collections::HashSet::new();
m.insert($intrinsic);
m
},
lost: std::collections::HashSet::new()
}).expect("Failed to insert IntrinsicChanged component.");
}
}
};
}

View file

@ -1,636 +1,34 @@
use rltk::{ GameState, Point, RandomNumberGenerator, Rltk }; use rust_rl::*;
use specs::prelude::*; use specs::prelude::*;
use specs::saveload::{ SimpleMarker, SimpleMarkerAllocator }; use specs::saveload::{ SimpleMarker, SimpleMarkerAllocator };
extern crate serde; use bracket_lib::prelude::*;
pub mod camera;
mod components;
pub mod raws;
pub use components::*;
mod map;
pub use map::*;
mod player;
use player::*;
mod rect;
pub use rect::Rect;
mod gamelog;
mod gui;
pub mod map_builders;
mod saveload_system;
mod spawner;
mod visibility_system;
use visibility_system::VisibilitySystem;
mod damage_system;
mod hunger_system;
mod melee_combat_system;
mod trigger_system;
use melee_combat_system::MeleeCombatSystem;
mod inventory;
mod particle_system;
use particle_system::ParticleBuilder;
mod ai;
mod config;
mod effects;
mod gamesystem;
mod random_table;
mod rex_assets;
mod spatial;
#[macro_use]
extern crate lazy_static;
//Consts
pub use config::{ SHOW_MAPGEN, LOG_SPAWNING, LOG_TICKS };
#[derive(PartialEq, Copy, Clone)]
pub enum RunState {
AwaitingInput, // Player's turn
PreRun,
Ticking, // Tick systems
ShowCheatMenu, // Teleport, godmode, etc. - for debugging
ShowInventory,
ShowDropItem,
ShowRemoveItem,
ShowTargeting {
range: i32,
item: Entity,
aoe: i32,
},
ShowRemoveCurse,
ShowIdentify,
ActionWithDirection {
function: fn(i: i32, j: i32, ecs: &mut World) -> RunState,
},
MainMenu {
menu_selection: gui::MainMenuSelection,
},
CharacterCreation {
ancestry: gui::Ancestry,
class: gui::Class,
},
SaveGame,
GameOver,
NextLevel,
PreviousLevel,
HelpScreen,
MagicMapReveal {
row: i32,
cursed: bool,
}, // Animates magic mapping effect
MapGeneration,
}
pub struct State {
pub ecs: World,
mapgen_next_state: Option<RunState>,
mapgen_history: Vec<Map>,
mapgen_index: usize,
mapgen_timer: f32,
}
impl State {
fn generate_world_map(&mut self, new_id: i32, offset: i32) {
// Visualisation stuff
self.mapgen_index = 0;
self.mapgen_timer = 0.0;
self.mapgen_history.clear();
let map_building_info = map::level_transition(&mut self.ecs, new_id, offset);
if let Some(history) = map_building_info {
self.mapgen_history = history;
} else {
map::dungeon::thaw_entities(&mut self.ecs);
}
}
fn run_systems(&mut self) {
let mut hunger_clock = hunger_system::HungerSystem {};
let mut particle_system = particle_system::ParticleSpawnSystem {};
// Order is *very* important here, to ensure effects take place in the right order,
// and that the player/AI are making decisions based on the most up-to-date info.
self.resolve_entity_decisions(); // Push Player messages of intent to effects queue, and run it.
self.refresh_indexes(); // Get up-to-date map and viewsheds prior to AI turn.
self.run_ai(); // Get AI decision-making.
self.resolve_entity_decisions(); // Push AI messages of intent to effects queue, and run it.
hunger_clock.run_now(&self.ecs); // Tick the hunger clock (on the turn clock's turn)
particle_system.run_now(&self.ecs); // Spawn/delete particles (turn independent)
self.ecs.maintain();
}
fn resolve_entity_decisions(&mut self) {
let mut trigger_system = trigger_system::TriggerSystem {};
let mut inventory_system = inventory::ItemCollectionSystem {};
let mut item_equip_system = inventory::ItemEquipSystem {};
let mut item_use_system = inventory::ItemUseSystem {};
let mut item_drop_system = inventory::ItemDropSystem {};
let mut item_remove_system = inventory::ItemRemoveSystem {};
let mut item_id_system = inventory::ItemIdentificationSystem {};
let mut melee_system = MeleeCombatSystem {};
trigger_system.run_now(&self.ecs);
inventory_system.run_now(&self.ecs);
item_equip_system.run_now(&self.ecs);
item_use_system.run_now(&self.ecs);
item_drop_system.run_now(&self.ecs);
item_remove_system.run_now(&self.ecs);
item_id_system.run_now(&self.ecs);
melee_system.run_now(&self.ecs);
effects::run_effects_queue(&mut self.ecs);
}
fn refresh_indexes(&mut self) {
let mut mapindex = spatial::MapIndexingSystem {};
let mut vis = VisibilitySystem {};
mapindex.run_now(&self.ecs);
vis.run_now(&self.ecs);
}
fn run_ai(&mut self) {
let mut encumbrance_system = ai::EncumbranceSystem {}; // Must run first, as it affects energy regen.
let mut energy = ai::EnergySystem {}; // Figures out who deserves a turn.
let mut regen_system = ai::RegenSystem {}; // Restores HP on appropriate clock ticks.
let mut turn_status_system = ai::TurnStatusSystem {}; // Ticks stasuses. Should anyone now lose their turn? i.e. confusion
let mut quip_system = ai::QuipSystem {}; // Quipping is "free". It doesn't use up a turn.
let mut adjacent_ai = ai::AdjacentAI {}; // AdjacentAI -> DefaultAI are all exclusive. If one acts, the entity's turn is over.
let mut visible_ai = ai::VisibleAI {};
let mut approach_ai = ai::ApproachAI {};
let mut flee_ai = ai::FleeAI {};
let mut chase_ai = ai::ChaseAI {};
let mut default_move_ai = ai::DefaultAI {};
encumbrance_system.run_now(&self.ecs);
energy.run_now(&self.ecs);
regen_system.run_now(&self.ecs);
turn_status_system.run_now(&self.ecs);
quip_system.run_now(&self.ecs);
adjacent_ai.run_now(&self.ecs);
visible_ai.run_now(&self.ecs);
approach_ai.run_now(&self.ecs);
flee_ai.run_now(&self.ecs);
chase_ai.run_now(&self.ecs);
default_move_ai.run_now(&self.ecs);
}
fn goto_level(&mut self, offset: i32) {
// Build new map + place player
let current_id;
{
let worldmap_resource = self.ecs.fetch::<Map>();
current_id = worldmap_resource.id;
}
// Record the correct type of event
if offset > 0 {
gamelog::record_event("descended", 1);
} else if current_id == 1 {
gamelog::Logger::new().append("CHEAT MENU: YOU CAN'T DO THAT.").colour((255, 0, 0)).log();
return;
} else {
gamelog::record_event("ascended", 1);
}
// Freeze the current level
map::dungeon::freeze_entities(&mut self.ecs);
self.generate_world_map(current_id + offset, offset);
let mapname = self.ecs.fetch::<Map>().name.clone();
gamelog::Logger::new().append("You head to").npc_name_n(mapname).period().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 and build new map
{
let player_entity = spawner::player(&mut self.ecs, 0, 0);
let mut player_entity_writer = self.ecs.write_resource::<Entity>();
*player_entity_writer = player_entity;
}
// Replace map list
self.ecs.insert(map::dungeon::MasterDungeonMap::new());
self.generate_world_map(1, 0);
gamelog::setup_log();
gamelog::record_event("player_level", 1);
}
}
impl GameState for State {
fn tick(&mut self, ctx: &mut Rltk) {
let mut new_runstate;
{
let runstate = self.ecs.fetch::<RunState>();
new_runstate = *runstate;
}
// Clear screen
ctx.cls();
particle_system::particle_ticker(&mut self.ecs, ctx);
match new_runstate {
RunState::MainMenu { .. } => {}
RunState::CharacterCreation { .. } => {}
_ => {
// Draw map and ui
camera::render_camera(&self.ecs, ctx);
gui::draw_ui(&self.ecs, ctx);
}
}
match new_runstate {
RunState::PreRun => {
self.run_systems();
self.ecs.maintain();
new_runstate = RunState::AwaitingInput;
}
RunState::AwaitingInput => {
// We refresh the index, and run anything that might
// still be in the queue, just to make 100% sure that
// there are no lingering effects from the last tick.
self.refresh_indexes();
effects::run_effects_queue(&mut self.ecs);
// Sanity-checking that the player actually *should*
// be taking a turn before giving them one. If they
// don't have a turn component, go back to ticking.
let mut can_act = false;
{
let player_entity = self.ecs.fetch::<Entity>();
let turns = self.ecs.read_storage::<TakingTurn>();
if let Some(_) = turns.get(*player_entity) {
can_act = true;
}
}
if can_act {
new_runstate = player_input(self, ctx);
} else {
new_runstate = RunState::Ticking;
}
}
RunState::Ticking => {
while new_runstate == RunState::Ticking {
self.run_systems();
self.ecs.maintain();
try_spawn_interval(&mut self.ecs);
match *self.ecs.fetch::<RunState>() {
RunState::AwaitingInput => {
new_runstate = RunState::AwaitingInput;
}
RunState::MagicMapReveal { row, cursed } => {
new_runstate = RunState::MagicMapReveal { row: row, cursed: cursed };
}
RunState::ShowRemoveCurse => {
new_runstate = RunState::ShowRemoveCurse;
}
RunState::ShowIdentify => {
new_runstate = RunState::ShowIdentify;
}
_ => {
new_runstate = RunState::Ticking;
}
}
}
}
RunState::ShowCheatMenu => {
let result = gui::show_cheat_menu(self, ctx);
match result {
gui::CheatMenuResult::Cancel => {
new_runstate = RunState::AwaitingInput;
}
gui::CheatMenuResult::NoResponse => {}
gui::CheatMenuResult::Ascend => {
self.goto_level(-1);
self.mapgen_next_state = Some(RunState::PreRun);
new_runstate = RunState::MapGeneration;
}
gui::CheatMenuResult::Descend => {
self.goto_level(1);
self.mapgen_next_state = Some(RunState::PreRun);
new_runstate = RunState::MapGeneration;
}
gui::CheatMenuResult::Heal => {
let player = self.ecs.fetch::<Entity>();
let mut pools = self.ecs.write_storage::<Pools>();
let mut player_pools = pools.get_mut(*player).unwrap();
player_pools.hit_points.current = player_pools.hit_points.max;
new_runstate = RunState::AwaitingInput;
}
gui::CheatMenuResult::MagicMap => {
let mut map = self.ecs.fetch_mut::<Map>();
for v in map.revealed_tiles.iter_mut() {
*v = true;
}
new_runstate = RunState::AwaitingInput;
}
gui::CheatMenuResult::GodMode => {
let player = self.ecs.fetch::<Entity>();
let mut pools = self.ecs.write_storage::<Pools>();
let mut player_pools = pools.get_mut(*player).unwrap();
gamelog::Logger::new().item_name("TOGGLED GOD MODE!").log();
player_pools.god = !player_pools.god;
new_runstate = RunState::AwaitingInput;
}
}
}
RunState::ShowInventory => {
let result = gui::show_inventory(self, ctx);
match result.0 {
gui::ItemMenuResult::Cancel => {
new_runstate = RunState::AwaitingInput;
}
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let item_entity = result.1.unwrap();
let is_ranged = self.ecs.read_storage::<Ranged>();
let ranged_item = is_ranged.get(item_entity);
if let Some(ranged_item) = ranged_item {
let is_aoe = self.ecs.read_storage::<AOE>();
let aoe_item = is_aoe.get(item_entity);
if let Some(aoe_item) = aoe_item {
new_runstate = RunState::ShowTargeting {
range: ranged_item.range,
item: item_entity,
aoe: aoe_item.radius,
};
} else {
new_runstate = RunState::ShowTargeting {
range: ranged_item.range,
item: item_entity,
aoe: 0,
};
}
} else {
let mut intent = self.ecs.write_storage::<WantsToUseItem>();
intent
.insert(*self.ecs.fetch::<Entity>(), WantsToUseItem { item: item_entity, target: None })
.expect("Unable to insert intent.");
new_runstate = RunState::Ticking;
}
}
}
}
RunState::ShowDropItem => {
let result = gui::drop_item_menu(self, ctx);
match result.0 {
gui::ItemMenuResult::Cancel => {
new_runstate = RunState::AwaitingInput;
}
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let item_entity = result.1.unwrap();
let mut intent = self.ecs.write_storage::<WantsToDropItem>();
intent
.insert(*self.ecs.fetch::<Entity>(), WantsToDropItem { item: item_entity })
.expect("Unable to insert intent");
new_runstate = RunState::Ticking;
}
}
}
RunState::ShowRemoveItem => {
let result = gui::remove_item_menu(self, ctx);
match result.0 {
gui::ItemMenuResult::Cancel => {
new_runstate = RunState::AwaitingInput;
}
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let item_entity = result.1.unwrap();
let mut intent = self.ecs.write_storage::<WantsToRemoveItem>();
intent
.insert(*self.ecs.fetch::<Entity>(), WantsToRemoveItem { item: item_entity })
.expect("Unable to insert intent");
new_runstate = RunState::Ticking;
}
}
}
RunState::ShowTargeting { range, item, aoe } => {
let result = gui::ranged_target(self, ctx, range, aoe);
match result.0 {
gui::ItemMenuResult::Cancel => {
new_runstate = RunState::AwaitingInput;
}
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let mut intent = self.ecs.write_storage::<WantsToUseItem>();
intent
.insert(*self.ecs.fetch::<Entity>(), WantsToUseItem { item, target: result.1 })
.expect("Unable to insert intent.");
new_runstate = RunState::Ticking;
}
}
}
RunState::ShowRemoveCurse => {
let result = gui::remove_curse(self, ctx);
match result.0 {
gui::ItemMenuResult::Cancel => {
new_runstate = RunState::AwaitingInput;
}
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let item_entity = result.1.unwrap();
self.ecs
.write_storage::<Beatitude>()
.insert(item_entity, Beatitude { buc: BUC::Uncursed, known: true })
.expect("Unable to insert beatitude");
new_runstate = RunState::Ticking;
}
}
}
RunState::ShowIdentify => {
let result = gui::identify(self, ctx);
match result.0 {
gui::ItemMenuResult::Cancel => {
new_runstate = RunState::AwaitingInput;
}
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let item_entity = result.1.unwrap();
if let Some(name) = self.ecs.read_storage::<Name>().get(item_entity) {
let mut dm = self.ecs.fetch_mut::<MasterDungeonMap>();
dm.identified_items.insert(name.name.clone());
}
if let Some(beatitude) = self.ecs.write_storage::<Beatitude>().get_mut(item_entity) {
beatitude.known = true;
}
new_runstate = RunState::Ticking;
}
}
}
RunState::ActionWithDirection { function } => {
new_runstate = gui::get_input_direction(&mut self.ecs, ctx, function);
}
RunState::MainMenu { .. } => {
let result = gui::main_menu(self, ctx);
match result {
gui::MainMenuResult::NoSelection { selected } => {
new_runstate = RunState::MainMenu { menu_selection: selected };
}
gui::MainMenuResult::Selected { selected } =>
match selected {
gui::MainMenuSelection::NewGame => {
new_runstate = RunState::CharacterCreation {
ancestry: gui::Ancestry::Human,
class: gui::Class::Fighter,
};
}
gui::MainMenuSelection::LoadGame => {
saveload_system::load_game(&mut self.ecs);
new_runstate = RunState::AwaitingInput;
saveload_system::delete_save();
}
gui::MainMenuSelection::Quit => {
::std::process::exit(0);
}
}
}
}
RunState::CharacterCreation { .. } => {
let result = gui::character_creation(self, ctx);
match result {
gui::CharCreateResult::NoSelection { ancestry, class } => {
new_runstate = RunState::CharacterCreation { ancestry, class };
}
gui::CharCreateResult::Selected { ancestry, class } => {
if ancestry == gui::Ancestry::NULL {
new_runstate = RunState::MainMenu { menu_selection: gui::MainMenuSelection::NewGame };
} else {
gui::setup_player_ancestry(&mut self.ecs, ancestry);
gui::setup_player_class(&mut self.ecs, class, ancestry);
new_runstate = RunState::PreRun;
}
}
}
}
RunState::SaveGame => {
saveload_system::save_game(&mut self.ecs);
new_runstate = RunState::MainMenu { menu_selection: gui::MainMenuSelection::LoadGame };
}
RunState::GameOver => {
let result = gui::game_over(ctx);
match result {
gui::YesNoResult::NoSelection => {}
gui::YesNoResult::Yes => {
self.game_over_cleanup();
new_runstate = RunState::MapGeneration;
self.mapgen_next_state = Some(RunState::MainMenu {
menu_selection: gui::MainMenuSelection::NewGame,
});
}
}
}
RunState::NextLevel => {
self.goto_level(1);
self.mapgen_next_state = Some(RunState::PreRun);
new_runstate = RunState::MapGeneration;
}
RunState::PreviousLevel => {
self.goto_level(-1);
self.mapgen_next_state = Some(RunState::PreRun);
new_runstate = RunState::MapGeneration;
}
RunState::HelpScreen => {
let result = gui::show_help(ctx);
match result {
gui::YesNoResult::NoSelection => {}
gui::YesNoResult::Yes => {
gamelog::record_event("looked_for_help", 1);
new_runstate = RunState::AwaitingInput;
}
}
}
RunState::MagicMapReveal { row, cursed } => {
let mut map = self.ecs.fetch_mut::<Map>();
// Could probably toss this into a function somewhere, and/or
// have multiple simple animations for it.
for x in 0..map.width {
let idx;
if x % 2 == 0 {
idx = map.xy_idx(x as i32, row);
} else {
idx = map.xy_idx(x as i32, (map.height as i32) - 1 - row);
}
if !cursed {
map.revealed_tiles[idx] = true;
} else {
map.revealed_tiles[idx] = false;
}
}
// Dirtify viewshed only if cursed, so our currently visible tiles aren't removed too
if cursed {
let player_entity = self.ecs.fetch::<Entity>();
let mut viewshed_components = self.ecs.write_storage::<Viewshed>();
let viewshed = viewshed_components.get_mut(*player_entity);
if let Some(viewshed) = viewshed {
viewshed.dirty = true;
}
}
if (row as usize) == (map.height as usize) - 1 {
new_runstate = RunState::Ticking;
} else {
new_runstate = RunState::MagicMapReveal { row: row + 1, cursed: cursed };
}
}
RunState::MapGeneration => {
if !SHOW_MAPGEN {
new_runstate = self.mapgen_next_state.unwrap();
}
if self.mapgen_history.len() != 0 {
ctx.cls();
camera::render_debug_map(&self.mapgen_history[self.mapgen_index], ctx);
self.mapgen_timer += ctx.frame_time_ms;
if self.mapgen_timer > 300.0 {
self.mapgen_timer = 0.0;
self.mapgen_index += 1;
if self.mapgen_index >= self.mapgen_history.len() {
new_runstate = self.mapgen_next_state.unwrap();
}
}
}
}
}
{
let mut runwriter = self.ecs.write_resource::<RunState>();
*runwriter = new_runstate;
}
damage_system::delete_the_dead(&mut self.ecs);
let _ = rltk::render_draw_buffer(ctx);
}
}
const DISPLAYWIDTH: i32 = 105; const DISPLAYWIDTH: i32 = 105;
const DISPLAYHEIGHT: i32 = 56; const DISPLAYHEIGHT: i32 = 56;
fn main() -> rltk::BError { fn main() -> BError {
// Embedded resources for use in wasm build // Embedded resources for use in wasm build
const CURSES_14_16_BYTES: &[u8] = include_bytes!("../resources/curses14x16.png"); const CURSES_14_16_BYTES: &[u8] = include_bytes!("../resources/curses14x16.png");
rltk::embedding::EMBED.lock().add_resource("resources/curses14x16.png".to_string(), CURSES_14_16_BYTES); EMBED.lock().add_resource("resources/curses14x16.png".to_string(), CURSES_14_16_BYTES);
//rltk::link_resource!(CURSES14X16, "../resources/curses_14x16.png"); //link_resource!(CURSES14X16, "../resources/curses_14x16.png");
use rltk::RltkBuilder; let mut context = BTermBuilder::new()
let mut context = RltkBuilder::new()
.with_title("rust-rl") .with_title("rust-rl")
.with_dimensions(DISPLAYWIDTH, DISPLAYHEIGHT) .with_dimensions(DISPLAYWIDTH, DISPLAYHEIGHT)
.with_font("curses14x16.png", 14, 16) .with_font("curses14x16.png", 14, 16)
.with_tile_dimensions(14, 16) .with_tile_dimensions(14, 16)
.with_simple_console(DISPLAYWIDTH, DISPLAYHEIGHT, "curses14x16.png") .with_simple_console(DISPLAYWIDTH, DISPLAYHEIGHT, "curses14x16.png")
.build()?; .build()?;
if config::visuals::WITH_SCANLINES { if config::CONFIG.visuals.with_scanlines {
context.with_post_scanlines(config::visuals::WITH_SCREEN_BURN); context.with_post_scanlines(config::CONFIG.visuals.with_screen_burn);
} }
let mut gs = State { let mut gs = State {
ecs: World::new(), ecs: World::new(),
mapgen_next_state: Some(RunState::MainMenu { menu_selection: gui::MainMenuSelection::NewGame }), mapgen_next_state: Some(RunState::MainMenu {
menu_selection: gui::MainMenuSelection::NewGame,
}),
mapgen_index: 0, mapgen_index: 0,
mapgen_history: Vec::new(), mapgen_history: Vec::new(),
mapgen_timer: 0.0, mapgen_timer: 0.0,
@ -706,10 +104,19 @@ fn main() -> rltk::BError {
gs.ecs.register::<ProvidesIdentify>(); gs.ecs.register::<ProvidesIdentify>();
gs.ecs.register::<KnownSpells>(); gs.ecs.register::<KnownSpells>();
gs.ecs.register::<GrantsSpell>(); gs.ecs.register::<GrantsSpell>();
gs.ecs.register::<Bleeds>();
gs.ecs.register::<ParticleLifetime>(); gs.ecs.register::<ParticleLifetime>();
gs.ecs.register::<SpawnParticleSimple>(); gs.ecs.register::<SpawnParticleSimple>();
gs.ecs.register::<SpawnParticleBurst>(); gs.ecs.register::<SpawnParticleBurst>();
gs.ecs.register::<SpawnParticleLine>(); gs.ecs.register::<SpawnParticleLine>();
gs.ecs.register::<HasDamageModifiers>();
gs.ecs.register::<Intrinsics>();
gs.ecs.register::<IntrinsicChanged>();
gs.ecs.register::<Stackable>();
gs.ecs.register::<WantsToAssignKey>();
gs.ecs.register::<Key>();
gs.ecs.register::<WantsToRemoveKey>();
gs.ecs.register::<WantsToDelete>();
gs.ecs.register::<SimpleMarker<SerializeMe>>(); gs.ecs.register::<SimpleMarker<SerializeMe>>();
gs.ecs.register::<SerializationHelper>(); gs.ecs.register::<SerializationHelper>();
gs.ecs.register::<DMSerializationHelper>(); gs.ecs.register::<DMSerializationHelper>();
@ -718,11 +125,11 @@ fn main() -> rltk::BError {
raws::load_raws(); raws::load_raws();
// Insert calls // Insert calls
gs.ecs.insert(rltk::RandomNumberGenerator::new()); gs.ecs.insert(RandomNumberGenerator::new());
gs.ecs.insert(map::MasterDungeonMap::new()); // Master map list gs.ecs.insert(map::MasterDungeonMap::new()); // Master map list
gs.ecs.insert(Map::new(1, 64, 64, 0, "New Map")); // Map gs.ecs.insert(Map::new(true, 1, 64, 64, 0, "New Map", "N", 0)); // Map
gs.ecs.insert(Point::new(0, 0)); // Player pos gs.ecs.insert(Point::new(0, 0)); // Player pos
gs.ecs.insert(gui::Ancestry::Dwarf); // ancestry gs.ecs.insert(gui::Ancestry::Human); // ancestry
let player_entity = spawner::player(&mut gs.ecs, 0, 0); let player_entity = spawner::player(&mut gs.ecs, 0, 0);
gs.ecs.insert(player_entity); // Player entity gs.ecs.insert(player_entity); // Player entity
gs.ecs.insert(RunState::MapGeneration {}); // RunState gs.ecs.insert(RunState::MapGeneration {}); // RunState
@ -730,8 +137,8 @@ fn main() -> rltk::BError {
gs.ecs.insert(rex_assets::RexAssets::new()); gs.ecs.insert(rex_assets::RexAssets::new());
gamelog::setup_log(); gamelog::setup_log();
gamelog::record_event("player_level", 1); gamelog::record_event(data::events::EVENT::Level(1));
gs.generate_world_map(1, 0); gs.generate_world_map(1, TileType::Floor);
rltk::main_loop(context, gs) main_loop(context, gs)
} }

View file

@ -1,9 +1,10 @@
use super::{ Map, TileType }; use super::{ Map, TileType };
use crate::{ gamelog, map_builders, OtherLevelPosition, Position, Telepath, Viewshed }; use crate::{ gamelog, map_builders, OtherLevelPosition, Position, Telepath, Viewshed };
use rltk::prelude::*; use bracket_lib::prelude::*;
use serde::{ Deserialize, Serialize }; use serde::{ Deserialize, Serialize };
use specs::prelude::*; use specs::prelude::*;
use std::collections::{ HashMap, HashSet }; use std::collections::{ HashMap, HashSet };
use crate::data::events::*;
#[derive(Default, Serialize, Deserialize, Clone)] #[derive(Default, Serialize, Deserialize, Clone)]
pub struct MasterDungeonMap { pub struct MasterDungeonMap {
@ -60,7 +61,11 @@ impl MasterDungeonMap {
fn make_scroll_name(rng: &mut RandomNumberGenerator) -> String { fn make_scroll_name(rng: &mut RandomNumberGenerator) -> String {
let len = 4 + rng.roll_dice(1, 6); let len = 4 + rng.roll_dice(1, 6);
let space_at_i = if len > 6 && rng.roll_dice(1, 2) == 1 { rng.roll_dice(1, len - 6) + 3 } else { -1 }; let space_at_i = if len > 6 && rng.roll_dice(1, 2) == 1 {
rng.roll_dice(1, len - 6) + 3
} else {
-1
};
let offset = rng.roll_dice(1, 2) - 1; let offset = rng.roll_dice(1, 2) - 1;
let mut name = "".to_string(); let mut name = "".to_string();
for i in 0..len { for i in 0..len {
@ -141,7 +146,9 @@ const POTION_ADJECTIVES: &[&str] = &[
fn make_potion_name(rng: &mut RandomNumberGenerator, used_names: &mut HashSet<String>) -> String { fn make_potion_name(rng: &mut RandomNumberGenerator, used_names: &mut HashSet<String>) -> String {
loop { loop {
let mut name: String = let mut name: String =
POTION_ADJECTIVES[(rng.roll_dice(1, POTION_ADJECTIVES.len() as i32) as usize) - 1].to_string(); POTION_ADJECTIVES[
(rng.roll_dice(1, POTION_ADJECTIVES.len() as i32) as usize) - 1
].to_string();
name += " "; name += " ";
name += POTION_COLOURS[(rng.roll_dice(1, POTION_COLOURS.len() as i32) as usize) - 1]; name += POTION_COLOURS[(rng.roll_dice(1, POTION_COLOURS.len() as i32) as usize) - 1];
name += " potion"; name += " potion";
@ -177,7 +184,8 @@ const WAND_TYPES: &[&str] = &[
fn make_wand_name(rng: &mut RandomNumberGenerator, used_names: &mut HashSet<String>) -> String { fn make_wand_name(rng: &mut RandomNumberGenerator, used_names: &mut HashSet<String>) -> String {
loop { loop {
let mut name: String = WAND_TYPES[(rng.roll_dice(1, WAND_TYPES.len() as i32) as usize) - 1].to_string(); let mut name: String =
WAND_TYPES[(rng.roll_dice(1, WAND_TYPES.len() as i32) as usize) - 1].to_string();
name += " wand"; name += " wand";
if !used_names.contains(&name) { if !used_names.contains(&name) {
@ -187,41 +195,57 @@ fn make_wand_name(rng: &mut RandomNumberGenerator, used_names: &mut HashSet<Stri
} }
} }
pub fn level_transition(ecs: &mut World, new_id: i32, offset: i32) -> Option<Vec<Map>> { pub fn level_transition(ecs: &mut World, new_id: i32, dest_tile: TileType) -> Option<Vec<Map>> {
// Obtain master // Obtain master
let dungeon_master = ecs.read_resource::<MasterDungeonMap>(); let dungeon_master = ecs.read_resource::<MasterDungeonMap>();
if dungeon_master.get_map(new_id).is_some() { if dungeon_master.get_map(new_id).is_some() {
std::mem::drop(dungeon_master); std::mem::drop(dungeon_master);
transition_to_existing_map(ecs, new_id, offset); transition_to_existing_map(ecs, new_id, dest_tile);
return None; return None;
} else { } else {
std::mem::drop(dungeon_master); std::mem::drop(dungeon_master);
return Some(transition_to_new_map(ecs, new_id)); return Some(transition_to_new_map(ecs, new_id, dest_tile));
} }
} }
fn transition_to_existing_map(ecs: &mut World, new_id: i32, offset: i32) { fn transition_to_existing_map(ecs: &mut World, new_id: i32, dest_tile: TileType) {
let dungeon_master = ecs.read_resource::<MasterDungeonMap>(); let mut dungeon_master = ecs.write_resource::<MasterDungeonMap>();
// Unwrapping here panics if new_id isn't present. But this should // Unwrapping here panics if new_id isn't present. But this should
// never be called without new_id being present by level_transition. // never be called without new_id being present by level_transition.
let map = dungeon_master.get_map(new_id).unwrap(); let map = dungeon_master.get_map(new_id).unwrap();
let mut worldmap_resource = ecs.write_resource::<Map>(); let mut worldmap_resource = ecs.write_resource::<Map>();
let player_entity = ecs.fetch::<Entity>(); let player_entity = ecs.fetch::<Entity>();
// Find down stairs, place player
let w = map.width; let w = map.width;
let stair_type = if offset < 0 { TileType::DownStair } else { TileType::UpStair }; let mut possible_destinations: Vec<usize> = Vec::new();
for (idx, tt) in map.tiles.iter().enumerate() { for (idx, tt) in map.tiles.iter().enumerate() {
if *tt == stair_type { if *tt == dest_tile {
let mut player_position = ecs.write_resource::<Point>(); possible_destinations.push(idx);
*player_position = Point::new((idx as i32) % w, (idx as i32) / w);
let mut position_components = ecs.write_storage::<Position>();
let player_pos_component = position_components.get_mut(*player_entity);
if let Some(player_pos_component) = player_pos_component {
player_pos_component.x = (idx as i32) % w;
player_pos_component.y = (idx as i32) / w;
}
} }
} }
if possible_destinations.is_empty() {
console::log("WARNING: No destination tiles found on map transition.");
match dest_tile {
TileType::DownStair => console::log("DESTINATION: DownStair"),
TileType::UpStair => console::log("DESTINATION: UpStair"),
TileType::ToOvermap(id) => console::log(format!("DESTINATION: ToOvermap({})", id)),
TileType::ToLocal(id) => console::log(format!("DESTINATION: ToLocal({})", id)),
_ => console::log("DESTINATION: Unknown"),
}
possible_destinations.push(((map.width * map.height) as usize) / 2); // Centre of map
}
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
let idx =
possible_destinations[(rng.roll_dice(1, possible_destinations.len() as i32) as usize) - 1];
let mut player_position = ecs.write_resource::<Point>();
*player_position = Point::new((idx as i32) % w, (idx as i32) / w);
let mut position_components = ecs.write_storage::<Position>();
let player_pos_component = position_components.get_mut(*player_entity);
if let Some(player_pos_component) = player_pos_component {
player_pos_component.x = (idx as i32) % w;
player_pos_component.y = (idx as i32) / w;
}
dungeon_master.store_map(&worldmap_resource);
*worldmap_resource = map; *worldmap_resource = map;
// Dirtify viewsheds (forces refresh) // Dirtify viewsheds (forces refresh)
let mut viewshed_components = ecs.write_storage::<Viewshed>(); let mut viewshed_components = ecs.write_storage::<Viewshed>();
@ -236,25 +260,29 @@ fn transition_to_existing_map(ecs: &mut World, new_id: i32, offset: i32) {
} }
} }
fn transition_to_new_map(ecs: &mut World, new_id: i32) -> Vec<Map> { fn transition_to_new_map(ecs: &mut World, new_id: i32, _dest_tile: TileType) -> Vec<Map> {
let mut rng = ecs.write_resource::<rltk::RandomNumberGenerator>(); let mut rng = ecs.write_resource::<RandomNumberGenerator>();
// Might need this to fallback to 1, but if player // Might need this to fallback to 1, but if player
// level isn't found at all, there's a bigger concern // level isn't found at all, there's a bigger concern
// concern than just this function not working. // concern than just this function not working.
let player_level = gamelog::get_event_count("player_level"); let player_level = gamelog::get_event_count(EVENT::COUNT_LEVEL);
let mut builder = map_builders::level_builder(new_id, &mut rng, 100, 50, player_level); let mut builder = map_builders::level_builder(new_id, &mut rng, 100, 50, player_level);
builder.build_map(&mut rng); builder.build_map(&mut rng);
std::mem::drop(rng); std::mem::drop(rng);
if new_id > 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::UpStair;
}
}
let mapgen_history = builder.build_data.history.clone(); let mapgen_history = builder.build_data.history.clone();
let player_start; let player_start;
let old_map: Map;
{ {
let mut worldmap_resource = ecs.write_resource::<Map>(); let mut worldmap_resource = ecs.write_resource::<Map>();
old_map = worldmap_resource.clone();
// If there is zero overmap involvement, place an upstair where we ended up.
// Otherwise, this should be hand-placed.
if !old_map.overmap && !builder.build_data.map.overmap {
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::UpStair;
}
}
*worldmap_resource = builder.build_data.map.clone(); *worldmap_resource = builder.build_data.map.clone();
// Unwrap so we get a CTD if there's no starting pos. // Unwrap so we get a CTD if there's no starting pos.
player_start = builder.build_data.starting_position.as_mut().unwrap().clone(); player_start = builder.build_data.starting_position.as_mut().unwrap().clone();
@ -284,6 +312,7 @@ fn transition_to_new_map(ecs: &mut World, new_id: i32) -> Vec<Map> {
} }
// Store newly minted map // Store newly minted map
let mut dungeon_master = ecs.write_resource::<MasterDungeonMap>(); let mut dungeon_master = ecs.write_resource::<MasterDungeonMap>();
dungeon_master.store_map(&old_map);
dungeon_master.store_map(&builder.build_data.map); dungeon_master.store_map(&builder.build_data.map);
return mapgen_history; return mapgen_history;
} }
@ -325,7 +354,9 @@ pub fn thaw_entities(ecs: &mut World) {
let mut pos_to_delete: Vec<Entity> = Vec::new(); let mut pos_to_delete: Vec<Entity> = Vec::new();
for (entity, pos) in (&entities, &other_positions).join() { for (entity, pos) in (&entities, &other_positions).join() {
if entity != *player_entity && pos.id == map_id { if entity != *player_entity && pos.id == map_id {
positions.insert(entity, Position { x: pos.x, y: pos.y }).expect("Failed to insert OtherLevelPosition"); positions
.insert(entity, Position { x: pos.x, y: pos.y })
.expect("Failed to insert OtherLevelPosition");
pos_to_delete.push(entity); pos_to_delete.push(entity);
} }
} }

View file

@ -1,7 +1,45 @@
use crate::{ gamelog, raws, spawner, Clock, Map, RandomNumberGenerator, TakingTurn, LOG_SPAWNING }; use crate::{
config::CONFIG,
gamelog,
raws,
spawner,
Clock,
Map,
RandomNumberGenerator,
TakingTurn,
};
use specs::prelude::*; use specs::prelude::*;
use bracket_lib::prelude::*;
use crate::data::events::*;
const TRY_SPAWN_CHANCE: i32 = 70; const TRY_SPAWN_CHANCE: i32 = 70;
const FEATURE_MESSAGE_CHANCE: i32 = 110;
pub fn maybe_map_message(ecs: &mut World) {
let mut maybe_message = false;
let map = ecs.fetch::<Map>();
if map.messages.is_empty() {
return;
}
// Scope for borrow checker (ECS)
{
let clock = ecs.read_storage::<Clock>();
let turns = ecs.read_storage::<TakingTurn>();
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
for (_c, _t) in (&clock, &turns).join() {
if rng.roll_dice(1, FEATURE_MESSAGE_CHANCE) == 1 {
maybe_message = true;
}
}
}
if maybe_message {
let mut logger = gamelog::Logger::new();
for message in map.messages.clone() {
logger = logger.append(message);
}
logger.log();
}
}
pub fn try_spawn_interval(ecs: &mut World) { pub fn try_spawn_interval(ecs: &mut World) {
let mut try_spawn = false; let mut try_spawn = false;
@ -14,7 +52,7 @@ pub fn try_spawn_interval(ecs: &mut World) {
} }
let clock = ecs.read_storage::<Clock>(); let clock = ecs.read_storage::<Clock>();
let turns = ecs.read_storage::<TakingTurn>(); let turns = ecs.read_storage::<TakingTurn>();
let mut rng = ecs.write_resource::<rltk::RandomNumberGenerator>(); let mut rng = ecs.write_resource::<RandomNumberGenerator>();
for (_c, _t) in (&clock, &turns).join() { for (_c, _t) in (&clock, &turns).join() {
if rng.roll_dice(1, TRY_SPAWN_CHANCE) == 1 { if rng.roll_dice(1, TRY_SPAWN_CHANCE) == 1 {
try_spawn = true; try_spawn = true;
@ -22,8 +60,8 @@ pub fn try_spawn_interval(ecs: &mut World) {
} }
} }
if try_spawn { if try_spawn {
if LOG_SPAWNING { if CONFIG.logging.log_spawning {
rltk::console::log("SPAWNINFO: Trying spawn."); console::log("SPAWNINFO: Trying spawn.");
} }
spawn_random_mob_in_free_nonvisible_tile(ecs); spawn_random_mob_in_free_nonvisible_tile(ecs);
} }
@ -32,12 +70,12 @@ pub fn try_spawn_interval(ecs: &mut World) {
fn spawn_random_mob_in_free_nonvisible_tile(ecs: &mut World) { fn spawn_random_mob_in_free_nonvisible_tile(ecs: &mut World) {
let map = ecs.fetch::<Map>(); let map = ecs.fetch::<Map>();
let mut available_tiles = populate_unblocked_nonvisible(&map); let mut available_tiles = populate_unblocked_nonvisible(&map);
let player_level = gamelog::get_event_count("player_level"); let player_level = gamelog::get_event_count(EVENT::COUNT_LEVEL);
rltk::console::log(player_level); console::log(player_level);
let difficulty = (map.difficulty + player_level) / 2; let difficulty = (map.difficulty + player_level) / 2;
if available_tiles.len() == 0 { if available_tiles.len() == 0 {
if LOG_SPAWNING { if CONFIG.logging.log_spawning {
rltk::console::log("SPAWNINFO: No free tiles; not spawning anything.."); console::log("SPAWNINFO: No free tiles; not spawning anything..");
} }
return; return;
} }
@ -55,8 +93,8 @@ fn spawn_random_mob_in_free_nonvisible_tile(ecs: &mut World) {
std::mem::drop(rng); std::mem::drop(rng);
// For every idx in the spawn list, spawn mob. // For every idx in the spawn list, spawn mob.
for idx in spawn_locations { for idx in spawn_locations {
if LOG_SPAWNING { if CONFIG.logging.log_spawning {
rltk::console::log(format!("SPAWNINFO: Spawning {} at {}, {}.", key, idx.0, idx.1)); console::log(format!("SPAWNINFO: Spawning {} at {}, {}.", key, idx.0, idx.1));
} }
raws::spawn_named_entity( raws::spawn_named_entity(
&raws::RAWS.lock().unwrap(), &raws::RAWS.lock().unwrap(),
@ -81,8 +119,12 @@ fn populate_unblocked_nonvisible(map: &Map) -> Vec<usize> {
} }
/// Picks a random index from a vector of indexes, and removes it from the vector. /// Picks a random index from a vector of indexes, and removes it from the vector.
fn get_random_idx_from_tiles(rng: &mut rltk::RandomNumberGenerator, area: &mut Vec<usize>) -> usize { fn get_random_idx_from_tiles(rng: &mut RandomNumberGenerator, area: &mut Vec<usize>) -> usize {
let idx = if area.len() == 1 { 0usize } else { (rng.roll_dice(1, area.len() as i32) - 1) as usize }; let idx = if area.len() == 1 {
0usize
} else {
(rng.roll_dice(1, area.len() as i32) - 1) as usize
};
area.remove(idx); area.remove(idx);
return area[idx]; return area[idx];
} }

View file

@ -1,14 +1,18 @@
use rltk::{ Algorithm2D, BaseMap, Point }; use bracket_lib::prelude::*;
use serde::{ Deserialize, Serialize }; use serde::{ Deserialize, Serialize };
use std::collections::HashSet; use std::collections::{ HashSet, HashMap };
mod tiletype; mod tiletype;
pub use tiletype::{ tile_cost, tile_opaque, tile_walkable, TileType }; pub use tiletype::{ tile_cost, tile_opaque, tile_walkable, TileType, get_dest, Destination };
mod interval_spawning_system; mod interval_spawning_system;
pub use interval_spawning_system::try_spawn_interval; pub use interval_spawning_system::{ maybe_map_message, try_spawn_interval };
pub mod dungeon; pub mod dungeon;
pub use dungeon::{ level_transition, MasterDungeonMap }; pub use dungeon::{ level_transition, MasterDungeonMap };
pub mod themes; pub mod themes;
use crate::config::visuals::MAX_COLOUR_OFFSET_PERCENT; use super::data::visuals::{
BRIGHTEN_FG_COLOUR_BY,
GLOBAL_OFFSET_MIN_CLAMP,
GLOBAL_OFFSET_MAX_CLAMP,
};
// FIXME: If the map size gets too small, entities stop being rendered starting from the right. // FIXME: If the map size gets too small, entities stop being rendered starting from the right.
// i.e. on a map size of 40*40, only entities to the left of the player are rendered. // i.e. on a map size of 40*40, only entities to the left of the player are rendered.
@ -16,6 +20,7 @@ use crate::config::visuals::MAX_COLOUR_OFFSET_PERCENT;
#[derive(Default, Serialize, Deserialize, Clone)] #[derive(Default, Serialize, Deserialize, Clone)]
pub struct Map { pub struct Map {
pub overmap: bool,
pub tiles: Vec<TileType>, pub tiles: Vec<TileType>,
pub width: i32, pub width: i32,
pub height: i32, pub height: i32,
@ -23,12 +28,15 @@ pub struct Map {
pub visible_tiles: Vec<bool>, pub visible_tiles: Vec<bool>,
pub lit_tiles: Vec<bool>, pub lit_tiles: Vec<bool>,
pub telepath_tiles: Vec<bool>, pub telepath_tiles: Vec<bool>,
pub colour_offset: Vec<(f32, f32, f32)>, pub colour_offset: Vec<((f32, f32, f32), (f32, f32, f32))>,
pub additional_fg_offset: rltk::RGB, pub additional_fg_offset: RGB,
pub id: i32, pub id: i32,
pub name: String, pub name: String,
pub short_name: String,
pub depth: i32,
pub messages: HashSet<String>,
pub difficulty: i32, pub difficulty: i32,
pub bloodstains: HashSet<usize>, pub bloodstains: HashMap<usize, RGB>,
pub view_blocked: HashSet<usize>, pub view_blocked: HashSet<usize>,
} }
@ -37,10 +45,20 @@ impl Map {
(y as usize) * (self.width as usize) + (x as usize) (y as usize) * (self.width as usize) + (x as usize)
} }
pub fn new<S: ToString>(new_id: i32, width: i32, height: i32, difficulty: i32, name: S) -> Map { pub fn new<S: ToString>(
overmap: bool,
new_id: i32,
width: i32,
height: i32,
difficulty: i32,
name: S,
short_name: S,
depth: i32
) -> Map {
let map_tile_count = (width * height) as usize; let map_tile_count = (width * height) as usize;
crate::spatial::set_size(map_tile_count); crate::spatial::set_size(map_tile_count);
let mut map = Map { let mut map = Map {
overmap: overmap,
tiles: vec![TileType::Wall; map_tile_count], tiles: vec![TileType::Wall; map_tile_count],
width: width, width: width,
height: height, height: height,
@ -48,30 +66,35 @@ impl Map {
visible_tiles: vec![false; map_tile_count], visible_tiles: vec![false; map_tile_count],
lit_tiles: vec![true; map_tile_count], // NYI: Light sources. Once those exist, we can set this to false. lit_tiles: vec![true; map_tile_count], // NYI: Light sources. Once those exist, we can set this to false.
telepath_tiles: vec![false; map_tile_count], telepath_tiles: vec![false; map_tile_count],
colour_offset: vec![(1.0, 1.0, 1.0); map_tile_count], colour_offset: vec![((0.0, 0.0, 0.0), (0.0, 0.0, 0.0)); map_tile_count],
additional_fg_offset: rltk::RGB::from_u8( additional_fg_offset: RGB::from_u8(
MAX_COLOUR_OFFSET_PERCENT as u8, BRIGHTEN_FG_COLOUR_BY as u8,
MAX_COLOUR_OFFSET_PERCENT as u8, BRIGHTEN_FG_COLOUR_BY as u8,
MAX_COLOUR_OFFSET_PERCENT as u8 BRIGHTEN_FG_COLOUR_BY as u8
), ),
id: new_id, id: new_id,
name: name.to_string(), name: name.to_string(),
short_name: short_name.to_string(),
messages: HashSet::new(),
depth: depth,
difficulty: difficulty, difficulty: difficulty,
bloodstains: HashSet::new(), bloodstains: HashMap::new(),
view_blocked: HashSet::new(), view_blocked: HashSet::new(),
}; };
const TWICE_OFFSET: i32 = MAX_COLOUR_OFFSET_PERCENT * 2; let mut rng = RandomNumberGenerator::new();
let mut rng = rltk::RandomNumberGenerator::new();
for idx in 0..map.colour_offset.len() { for idx in 0..map.colour_offset.len() {
let red_roll: f32 = map.colour_offset[idx].0 = (
((rng.roll_dice(1, TWICE_OFFSET - 1) + 1 - MAX_COLOUR_OFFSET_PERCENT) as f32) / 100f32 + 1.0; rng.range(GLOBAL_OFFSET_MIN_CLAMP, GLOBAL_OFFSET_MAX_CLAMP),
let green_roll: f32 = rng.range(GLOBAL_OFFSET_MIN_CLAMP, GLOBAL_OFFSET_MAX_CLAMP),
((rng.roll_dice(1, TWICE_OFFSET - 1) + 1 - MAX_COLOUR_OFFSET_PERCENT) as f32) / 100f32 + 1.0; rng.range(GLOBAL_OFFSET_MIN_CLAMP, GLOBAL_OFFSET_MAX_CLAMP),
let blue_roll: f32 = );
((rng.roll_dice(1, TWICE_OFFSET - 1) + 1 - MAX_COLOUR_OFFSET_PERCENT) as f32) / 100f32 + 1.0; map.colour_offset[idx].1 = (
map.colour_offset[idx] = (red_roll, green_roll, blue_roll); rng.range(GLOBAL_OFFSET_MIN_CLAMP, GLOBAL_OFFSET_MAX_CLAMP),
rng.range(GLOBAL_OFFSET_MIN_CLAMP, GLOBAL_OFFSET_MAX_CLAMP),
rng.range(GLOBAL_OFFSET_MIN_CLAMP, GLOBAL_OFFSET_MAX_CLAMP),
);
} }
return map; return map;
@ -115,12 +138,12 @@ impl BaseMap for Map {
let w = self.width as usize; let w = self.width as usize;
let p1 = Point::new(idx1 % w, idx1 / w); let p1 = Point::new(idx1 % w, idx1 / w);
let p2 = Point::new(idx2 % w, idx2 / w); let p2 = Point::new(idx2 % w, idx2 / w);
rltk::DistanceAlg::Pythagoras.distance2d(p1, p2) DistanceAlg::Pythagoras.distance2d(p1, p2)
} }
/// Evaluate every possible exit from a given tile in a cardinal direction, and return it as a vector. /// Evaluate every possible exit from a given tile in a cardinal direction, and return it as a vector.
fn get_available_exits(&self, idx: usize) -> rltk::SmallVec<[(usize, f32); 10]> { fn get_available_exits(&self, idx: usize) -> SmallVec<[(usize, f32); 10]> {
let mut exits = rltk::SmallVec::new(); let mut exits = SmallVec::new();
let x = (idx as i32) % self.width; let x = (idx as i32) % self.width;
let y = (idx as i32) / self.width; let y = (idx as i32) / self.width;
let w = self.width as usize; let w = self.width as usize;

View file

@ -1,31 +1,53 @@
use super::{ Map, Point, TileType }; use super::{ Map, Point, TileType };
use crate::config::glyphs::*; use crate::data::visuals::*;
use crate::config::visuals::*; use crate::config::CONFIG;
use rltk::prelude::*; use crate::data::ids::*;
use bracket_lib::prelude::*;
use std::ops::{ Add, Mul }; use std::ops::{ Add, Mul };
pub fn get_tile_renderables_for_id(idx: usize, map: &Map, other_pos: Option<Point>) -> (rltk::FontCharType, RGB, RGB) { /// Gets the renderables for a tile, with darkening/offset/post-processing/etc. Passing a val for "debug" will ignore viewshed.
let (glyph, mut fg, mut bg) = match map.id { pub fn get_tile_renderables_for_id(
2 => get_forest_theme_renderables(idx, map), idx: usize,
_ => get_default_theme_renderables(idx, map), map: &Map,
other_pos: Option<Point>,
debug: Option<bool>
) -> (FontCharType, RGB, RGB) {
let coloured_bg = CONFIG.visuals.use_coloured_tile_bg;
let (glyph, mut fg, mut bg, fg_offset, bg_offset) = match map.id {
ID_TOWN2 => get_forest_theme_renderables(idx, map, debug),
_ => get_default_theme_renderables(idx, map, debug),
}; };
// If one of the colours was left blank, make them the same. // If one of the colours was left blank, make them the same.
let mut same_col: bool = false;
if fg == RGB::new() { if fg == RGB::new() {
fg = bg; fg = bg;
same_col = true;
} else if bg == RGB::new() { } else if bg == RGB::new() {
bg = fg; bg = fg;
same_col = true;
} }
fg = fg.add(map.additional_fg_offset); if same_col && coloured_bg {
(fg, bg) = apply_colour_offset(fg, bg, map, idx); fg = fg.add(map.additional_fg_offset);
if WITH_SCANLINES && WITH_SCANLINES_BRIGHTEN_AMOUNT > 0.0 { }
(fg, bg) = brighten_by(fg, bg, WITH_SCANLINES_BRIGHTEN_AMOUNT); if CONFIG.visuals.add_colour_variance {
fg = apply_colour_offset(fg, map, idx, fg_offset, true);
bg = if coloured_bg { apply_colour_offset(bg, map, idx, bg_offset, false) } else { bg };
}
if CONFIG.visuals.with_scanlines && WITH_SCANLINES_BRIGHTEN_AMOUNT > 0.0 {
fg = brighten_by(fg, WITH_SCANLINES_BRIGHTEN_AMOUNT);
bg = if coloured_bg { brighten_by(bg, WITH_SCANLINES_BRIGHTEN_AMOUNT) } else { bg };
} }
bg = apply_bloodstain_if_necessary(bg, map, idx);
let (mut multiplier, mut nonvisible, mut darken) = (1.0, false, false); let (mut multiplier, mut nonvisible, mut darken) = (1.0, false, false);
if !map.visible_tiles[idx] { if !map.visible_tiles[idx] {
multiplier = if WITH_SCANLINES { NON_VISIBLE_MULTIPLIER_IF_SCANLINES } else { NON_VISIBLE_MULTIPLIER }; multiplier = if CONFIG.visuals.with_scanlines {
NON_VISIBLE_MULTIPLIER_IF_SCANLINES
} else {
NON_VISIBLE_MULTIPLIER
};
nonvisible = true; nonvisible = true;
} }
if other_pos.is_some() && WITH_DARKEN_BY_DISTANCE && !nonvisible { if other_pos.is_some() && WITH_DARKEN_BY_DISTANCE && !nonvisible {
@ -34,7 +56,7 @@ pub fn get_tile_renderables_for_id(idx: usize, map: &Map, other_pos: Option<Poin
other_pos.unwrap() other_pos.unwrap()
); );
multiplier = distance.clamp( multiplier = distance.clamp(
if WITH_SCANLINES { if CONFIG.visuals.with_scanlines {
NON_VISIBLE_MULTIPLIER_IF_SCANLINES NON_VISIBLE_MULTIPLIER_IF_SCANLINES
} else { } else {
NON_VISIBLE_MULTIPLIER NON_VISIBLE_MULTIPLIER
@ -44,101 +66,127 @@ pub fn get_tile_renderables_for_id(idx: usize, map: &Map, other_pos: Option<Poin
darken = true; darken = true;
} }
if nonvisible || darken { if nonvisible || darken {
(fg, bg) = (fg.mul(multiplier), bg.mul(multiplier)); fg = fg.mul(multiplier);
bg = if coloured_bg { bg.mul(multiplier) } else { bg };
} }
if !CONFIG.visuals.use_coloured_tile_bg {
bg = RGB::named(BLACK);
}
bg = apply_bloodstain_if_necessary(bg, map, idx);
return (glyph, fg, bg); return (glyph, fg, bg);
} }
#[rustfmt::skip] #[rustfmt::skip]
pub fn get_default_theme_renderables(idx: usize, map: &Map) -> (rltk::FontCharType, RGB, RGB) { pub fn get_default_theme_renderables(idx: usize, map: &Map, debug: Option<bool>) -> (FontCharType, RGB, RGB, (i32, i32, i32), (i32, i32, i32)) {
let glyph: rltk::FontCharType; let glyph: FontCharType;
#[allow(unused_assignments)] #[allow(unused_assignments)]
let mut fg: RGB = RGB::new(); let mut fg: RGB = RGB::new();
#[allow(unused_assignments)] #[allow(unused_assignments)]
let mut bg: RGB = RGB::new(); let mut bg: RGB = RGB::new();
let mut offsets: (i32, i32, i32) = (0, 0, 0);
let mut bg_offsets: (i32, i32, i32) = (-1, -1, -1);
match map.tiles[idx] { match map.tiles[idx] {
TileType::Floor => { glyph = rltk::to_cp437(FLOOR_GLYPH); fg = RGB::named(FLOOR_COLOUR); bg = RGB::named(DEFAULT_BG_COLOUR); } TileType::Floor => { glyph = to_cp437(FLOOR_GLYPH); fg = RGB::named(FLOOR_COLOUR); bg = RGB::named(DEFAULT_BG_COLOUR); offsets = FLOOR_OFFSETS; }
TileType::WoodFloor => { glyph = rltk::to_cp437(WOOD_FLOOR_GLYPH); bg = RGB::named(WOOD_FLOOR_COLOUR); } TileType::WoodFloor => { glyph = to_cp437(WOOD_FLOOR_GLYPH); bg = RGB::named(WOOD_FLOOR_COLOUR); offsets = WOOD_FLOOR_OFFSETS; }
TileType::Fence => { glyph = rltk::to_cp437(FENCE_GLYPH); fg = RGB::named(FENCE_FG_COLOUR); bg = RGB::named(FENCE_COLOUR); } TileType::Fence => { glyph = to_cp437(FENCE_GLYPH); fg = RGB::named(FENCE_FG_COLOUR); bg = RGB::named(FENCE_COLOUR); offsets = FENCE_OFFSETS; }
TileType::Wall => { let x = idx as i32 % map.width; let y = idx as i32 / map.width; glyph = wall_glyph(&*map, x, y); fg = RGB::named(WALL_COLOUR); bg = RGB::named(DEFAULT_BG_COLOUR); } TileType::Wall => { let x = idx as i32 % map.width; let y = idx as i32 / map.width; glyph = wall_glyph(&*map, x, y, debug); fg = RGB::named(WALL_COLOUR); bg = RGB::named(DEFAULT_BG_COLOUR); offsets = WALL_OFFSETS; bg_offsets = DEFAULT_BG_OFFSETS }
TileType::DownStair => { glyph = rltk::to_cp437(DOWN_STAIR_GLYPH); fg = RGB::named(STAIR_COLOUR); bg = RGB::named(DEFAULT_BG_COLOUR); } TileType::DownStair => { glyph = to_cp437(DOWN_STAIR_GLYPH); fg = RGB::named(STAIR_COLOUR); bg = RGB::named(DEFAULT_BG_COLOUR); offsets = STAIR_OFFSETS;}
TileType::UpStair => { glyph = rltk::to_cp437(UP_STAIR_GLYPH); fg = RGB::named(STAIR_COLOUR); bg = RGB::named(DEFAULT_BG_COLOUR); } TileType::UpStair => { glyph = to_cp437(UP_STAIR_GLYPH); fg = RGB::named(STAIR_COLOUR); bg = RGB::named(DEFAULT_BG_COLOUR); offsets = STAIR_OFFSETS; }
TileType::Bridge => { glyph = rltk::to_cp437(BRIDGE_GLYPH); bg = RGB::named(BRIDGE_COLOUR); } TileType::Bridge => { glyph = to_cp437(BRIDGE_GLYPH); bg = RGB::named(BRIDGE_COLOUR); offsets = BRIDGE_OFFSETS; }
TileType::Gravel => { glyph = rltk::to_cp437(GRAVEL_GLYPH); bg = RGB::named(GRAVEL_COLOUR); } TileType::Gravel => { glyph = to_cp437(GRAVEL_GLYPH); bg = RGB::named(GRAVEL_COLOUR); offsets = GRAVEL_OFFSETS;}
TileType::Road => { glyph = rltk::to_cp437(ROAD_GLYPH); bg = RGB::named(ROAD_COLOUR); } TileType::Road => { glyph = to_cp437(ROAD_GLYPH); bg = RGB::named(ROAD_COLOUR); offsets = ROAD_OFFSETS;}
TileType::Grass => { glyph = rltk::to_cp437(GRASS_GLYPH); bg = RGB::named(GRASS_COLOUR); } TileType::Grass => { glyph = to_cp437(GRASS_GLYPH); bg = RGB::named(GRASS_COLOUR); offsets = GRASS_OFFSETS; }
TileType::Foliage => { glyph = rltk::to_cp437(FOLIAGE_GLYPH); bg = RGB::named(FOLIAGE_COLOUR); } TileType::Foliage => { glyph = to_cp437(FOLIAGE_GLYPH); bg = RGB::named(FOLIAGE_COLOUR); offsets = FOLIAGE_OFFSETS; }
TileType::HeavyFoliage => { glyph = rltk::to_cp437(HEAVY_FOLIAGE_GLYPH); bg = RGB::named(HEAVY_FOLIAGE_COLOUR); } TileType::HeavyFoliage => { glyph = to_cp437(HEAVY_FOLIAGE_GLYPH); bg = RGB::named(HEAVY_FOLIAGE_COLOUR); offsets = HEAVY_FOLIAGE_OFFSETS; }
TileType::Sand => { glyph = rltk::to_cp437(SAND_GLYPH); bg = RGB::named(SAND_COLOUR); } TileType::Sand => { glyph = to_cp437(SAND_GLYPH); bg = RGB::named(SAND_COLOUR); offsets = SAND_OFFSETS; }
TileType::ShallowWater => { glyph = rltk::to_cp437(SHALLOW_WATER_GLYPH); bg = RGB::named(SHALLOW_WATER_COLOUR); } TileType::ShallowWater => { glyph = to_cp437(SHALLOW_WATER_GLYPH); bg = RGB::named(SHALLOW_WATER_COLOUR); offsets = SHALLOW_WATER_OFFSETS; }
TileType::DeepWater => { glyph = rltk::to_cp437(DEEP_WATER_GLYPH); bg = RGB::named(DEEP_WATER_COLOUR); } TileType::DeepWater => { glyph = to_cp437(DEEP_WATER_GLYPH); bg = RGB::named(DEEP_WATER_COLOUR); offsets = DEEP_WATER_OFFSETS; }
TileType::Bars => { glyph = rltk::to_cp437(BARS_GLYPH); fg = RGB::named(BARS_COLOUR); bg = RGB::named(FLOOR_COLOUR); } TileType::Bars => { glyph = to_cp437(BARS_GLYPH); fg = RGB::named(BARS_COLOUR); bg = RGB::named(FLOOR_COLOUR); }
TileType::ImpassableMountain => { glyph = to_cp437(IMPASSABLE_MOUNTAIN_GLYPH); bg = RGB::named(IMPASSABLE_MOUNTAIN_COLOUR); offsets = IMPASSABLE_MOUNTAIN_OFFSETS }
TileType::ToOvermap(_) => { glyph = to_cp437(TO_OVERMAP_GLYPH); fg = RGB::named(TO_OVERMAP_COLOUR); bg = RGB::named(GRASS_COLOUR); }
TileType::ToLocal(_) => { glyph = to_cp437(TO_TOWN_GLYPH); fg = RGB::named(TO_TOWN_COLOUR); bg = RGB::named(GRASS_COLOUR); }
} }
return (glyph, fg, bg); if bg_offsets == (-1, -1, -1) {
bg_offsets = offsets;
}
return (glyph, fg, bg, offsets, bg_offsets);
} }
#[rustfmt::skip] #[rustfmt::skip]
fn get_forest_theme_renderables(idx:usize, map: &Map) -> (rltk::FontCharType, RGB, RGB) { fn get_forest_theme_renderables(idx:usize, map: &Map, debug: Option<bool>) -> (FontCharType, RGB, RGB, (i32, i32, i32), (i32, i32, i32)) {
let glyph; let glyph;
#[allow(unused_assignments)] #[allow(unused_assignments)]
let mut fg = RGB::new(); let mut fg = RGB::new();
#[allow(unused_assignments)] #[allow(unused_assignments)]
let mut bg = RGB::new(); let mut bg = RGB::new();
let mut offsets: (i32, i32, i32) = (0, 0, 0);
let mut bg_offsets: (i32, i32, i32) = (-1, -1, -1);
match map.tiles[idx] { match map.tiles[idx] {
TileType::Wall => { glyph = rltk::to_cp437(FOREST_WALL_GLYPH); fg = RGB::named(FOREST_WALL_COLOUR); bg = RGB::named(GRASS_COLOUR) } TileType::Wall => { glyph = to_cp437(FOREST_WALL_GLYPH); fg = RGB::named(FOREST_WALL_COLOUR); bg = RGB::named(GRASS_COLOUR); offsets = GRASS_OFFSETS; }
TileType::Road => { glyph = rltk::to_cp437(ROAD_GLYPH); bg = RGB::named(ROAD_COLOUR); } TileType::Road => { glyph = to_cp437(ROAD_GLYPH); bg = RGB::named(ROAD_COLOUR); }
TileType::ShallowWater => { glyph = rltk::to_cp437(SHALLOW_WATER_GLYPH); bg = RGB::named(SHALLOW_WATER_COLOUR); } TileType::ShallowWater => { glyph = to_cp437(SHALLOW_WATER_GLYPH); bg = RGB::named(SHALLOW_WATER_COLOUR); offsets = SHALLOW_WATER_OFFSETS; }
_ => { (glyph, fg, _) = get_default_theme_renderables(idx, map); bg = RGB::named(GRASS_COLOUR) } _ => { (glyph, fg, _, offsets, _) = get_default_theme_renderables(idx, map, debug); bg = RGB::named(GRASS_COLOUR); bg_offsets = GRASS_OFFSETS; }
} }
if bg_offsets == (-1, -1, -1) {
(glyph, fg, bg) bg_offsets = offsets;
}
return (glyph, fg, bg, offsets, bg_offsets);
} }
fn is_revealed_and_wall(map: &Map, x: i32, y: i32) -> bool { fn is_revealed_and_wall(map: &Map, x: i32, y: i32, debug: Option<bool>) -> bool {
let idx = map.xy_idx(x, y); let idx = map.xy_idx(x, y);
map.tiles[idx] == TileType::Wall && map.revealed_tiles[idx] map.tiles[idx] == TileType::Wall &&
(if debug.is_none() { map.revealed_tiles[idx] } else { true })
} }
fn wall_glyph(map: &Map, x: i32, y: i32) -> rltk::FontCharType { fn wall_glyph(map: &Map, x: i32, y: i32, debug: Option<bool>) -> FontCharType {
if x < 1 || x > map.width - 2 || y < 1 || y > map.height - (2 as i32) { if
x < 1 ||
x > map.width - 2 ||
y < 1 ||
y > map.height - (2 as i32) ||
!CONFIG.visuals.use_bitset_walls
{
return 35; return 35;
} }
let mut mask: u8 = 0; let mut mask: u8 = 0;
let diagonals_matter: Vec<u8> = vec![7, 11, 13, 14, 15]; let diagonals_matter: Vec<u8> = vec![7, 11, 13, 14, 15];
if is_revealed_and_wall(map, x, y - 1) { if is_revealed_and_wall(map, x, y - 1, debug) {
// N // N
mask += 1; mask += 1;
} }
if is_revealed_and_wall(map, x, y + 1) { if is_revealed_and_wall(map, x, y + 1, debug) {
// S // S
mask += 2; mask += 2;
} }
if is_revealed_and_wall(map, x - 1, y) { if is_revealed_and_wall(map, x - 1, y, debug) {
// W // W
mask += 4; mask += 4;
} }
if is_revealed_and_wall(map, x + 1, y) { if is_revealed_and_wall(map, x + 1, y, debug) {
// E // E
mask += 8; mask += 8;
} }
if diagonals_matter.contains(&mask) { if diagonals_matter.contains(&mask) {
if is_revealed_and_wall(map, x + 1, y - 1) { if is_revealed_and_wall(map, x + 1, y - 1, debug) {
// Top right // Top right
mask += 16; mask += 16;
} }
if is_revealed_and_wall(map, x - 1, y - 1) { if is_revealed_and_wall(map, x - 1, y - 1, debug) {
// Top left // Top left
mask += 32; mask += 32;
} }
if is_revealed_and_wall(map, x + 1, y + 1) { if is_revealed_and_wall(map, x + 1, y + 1, debug) {
// Bottom right // Bottom right
mask += 64; mask += 64;
} }
if is_revealed_and_wall(map, x - 1, y + 1) { if is_revealed_and_wall(map, x - 1, y + 1, debug) {
// Bottom left // Bottom left
mask += 128; mask += 128;
} }
@ -238,39 +286,65 @@ fn wall_glyph(map: &Map, x: i32, y: i32) -> rltk::FontCharType {
} }
} }
fn apply_colour_offset(mut fg: RGB, mut bg: RGB, map: &Map, idx: usize) -> (RGB, RGB) { fn apply_colour_offset(
let offsets = map.colour_offset[idx]; mut rgb: RGB,
fg = multiply_by_float(fg.add(map.additional_fg_offset), offsets); map: &Map,
bg = multiply_by_float(bg, offsets); idx: usize,
return (fg, bg); offset: (i32, i32, i32),
fg: bool
) -> RGB {
let offset_mod = if fg { map.colour_offset[idx].0 } else { map.colour_offset[idx].1 };
let offset = (
(offset.0 as f32) * offset_mod.0,
(offset.1 as f32) * offset_mod.1,
(offset.2 as f32) * offset_mod.2,
);
rgb = add_i32_offsets(rgb, offset);
return rgb;
} }
fn apply_bloodstain_if_necessary(mut bg: RGB, map: &Map, idx: usize) -> RGB { fn apply_bloodstain_if_necessary(mut bg: RGB, map: &Map, idx: usize) -> RGB {
if map.bloodstains.contains(&idx) { if map.bloodstains.contains_key(&idx) {
bg = bg.add(RGB::named(BLOODSTAIN_COLOUR)); bg = bg.add(map.bloodstains[&idx]);
} }
return bg; return bg;
} }
pub fn multiply_by_float(rgb: rltk::RGB, offsets: (f32, f32, f32)) -> RGB { pub fn add_i32_offsets(rgb: RGB, offsets: (f32, f32, f32)) -> RGB {
let r = rgb.r + (offsets.0 as f32) / 255.0;
let g = rgb.g + (offsets.1 as f32) / 255.0;
let b = rgb.b + (offsets.2 as f32) / 255.0;
return RGB::from_f32(r, g, b);
}
pub fn multiply_by_float(rgb: RGB, offsets: (f32, f32, f32)) -> RGB {
let r = rgb.r * offsets.0; let r = rgb.r * offsets.0;
let g = rgb.g * offsets.1; let g = rgb.g * offsets.1;
let b = rgb.b * offsets.2; let b = rgb.b * offsets.2;
return rltk::RGB::from_f32(r, g, b); return RGB::from_f32(r, g, b);
} }
fn darken_by_distance(pos: Point, other_pos: Point) -> f32 { fn darken_by_distance(pos: Point, other_pos: Point) -> f32 {
let distance = DistanceAlg::Pythagoras.distance2d(pos, other_pos) as f32; // Get distance in tiles. let distance = DistanceAlg::Pythagoras.distance2d(pos, other_pos) as f32; // Get distance in tiles.
let interp_factor = let interp_factor =
(distance - START_DARKEN_AT_N_TILES) / (distance - START_DARKEN_AT_N_TILES) /
((crate::config::entity::DEFAULT_VIEWSHED_STANDARD as f32) - START_DARKEN_AT_N_TILES); ((crate::data::entity::DEFAULT_VIEWSHED_STANDARD as f32) - START_DARKEN_AT_N_TILES);
let interp_factor = interp_factor.max(0.0).min(1.0); // Clamp [0-1] let interp_factor = interp_factor.max(0.0).min(1.0); // Clamp [0-1]
return 1.0 - interp_factor * (1.0 - (if WITH_SCANLINES { MAX_DARKENING_IF_SCANLINES } else { MAX_DARKENING })); let result =
1.0 -
interp_factor *
(1.0 -
(if CONFIG.visuals.with_scanlines {
MAX_DARKENING_IF_SCANLINES
} else {
MAX_DARKENING
}));
return result;
} }
fn brighten_by(mut fg: RGB, mut bg: RGB, amount: f32) -> (RGB, RGB) { fn brighten_by(mut rgb: RGB, amount: f32) -> RGB {
fg = fg.add(RGB::from_f32(amount, amount, amount)); rgb = rgb.add(RGB::from_f32(amount, amount, amount));
bg = bg.add(RGB::from_f32(amount, amount, amount)); return rgb;
return (fg, bg);
} }

View file

@ -1,8 +1,9 @@
use serde::{ Deserialize, Serialize }; use serde::{ Deserialize, Serialize };
#[derive(PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)] #[derive(PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize, Debug)]
pub enum TileType { pub enum TileType {
// Walls (opaque) // Walls (opaque)
ImpassableMountain,
Wall, Wall,
// Impassable (transparent) // Impassable (transparent)
DeepWater, DeepWater,
@ -22,38 +23,54 @@ pub enum TileType {
// Stairs (changes floor) // Stairs (changes floor)
DownStair, DownStair,
UpStair, UpStair,
// To/From Overmap - ids are in src/data/ids.rs, are used in try_change_level() in src/player.rs
ToOvermap(i32),
ToLocal(i32),
} }
pub fn tile_walkable(tt: TileType) -> bool { pub fn tile_walkable(tt: TileType) -> bool {
match tt { match tt {
| TileType::Floor TileType::ImpassableMountain | TileType::Wall | TileType::DeepWater | TileType::Fence | TileType::Bars => false,
| TileType::WoodFloor _ => true,
| TileType::Gravel
| TileType::Road
| TileType::Grass
| TileType::Foliage
| TileType::HeavyFoliage
| TileType::Sand
| TileType::ShallowWater
| TileType::Bridge
| TileType::DownStair
| TileType::UpStair => true,
_ => false,
} }
} }
pub fn tile_opaque(tt: TileType) -> bool { pub fn tile_opaque(tt: TileType) -> bool {
match tt { match tt {
TileType::ImpassableMountain => true,
TileType::Wall => true, TileType::Wall => true,
_ => false, _ => false,
} }
} }
pub fn tile_cost(tt: TileType) -> f32 { pub fn tile_cost(tt: TileType) -> f32 {
match tt { match tt {
TileType::Road => 0.5, TileType::Road => 0.75,
TileType::Grass => 1.1, TileType::Grass => 1.2,
TileType::ShallowWater => 1.3, TileType::ShallowWater => 1.5,
_ => 1.0, _ => 1.0,
} }
} }
pub fn get_dest(this_tile: TileType, backtracking: bool) -> Destination {
let result = if !backtracking {
match this_tile {
// If on downstair, GOTO next level, and end up on an upstair
TileType::DownStair => Destination::NextLevel,
// If on overmap ToLocal tile, GOTO local map, and end up on an overmap ToOvermap tile with corresponding ID
TileType::ToLocal(id) => Destination::ToOvermap(id),
_ => Destination::None,
}
} else {
match this_tile {
TileType::UpStair => Destination::PreviousLevel,
TileType::ToOvermap(id) => Destination::ToLocal(id),
_ => Destination::None,
}
};
return result;
}
pub enum Destination {
PreviousLevel,
NextLevel,
ToOvermap(i32),
ToLocal(i32),
None,
}

View file

@ -1,5 +1,5 @@
use super::{ BuilderMap, MetaMapBuilder, Position }; use super::{ BuilderMap, MetaMapBuilder, Position };
use rltk::RandomNumberGenerator; use bracket_lib::prelude::*;
#[allow(dead_code)] #[allow(dead_code)]
pub enum XStart { pub enum XStart {
@ -21,7 +21,7 @@ pub struct AreaStartingPosition {
} }
impl MetaMapBuilder for AreaStartingPosition { impl MetaMapBuilder for AreaStartingPosition {
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
@ -64,15 +64,18 @@ impl AreaStartingPosition {
if crate::tile_walkable(*tiletype) { if crate::tile_walkable(*tiletype) {
available_floors.push(( available_floors.push((
idx, idx,
rltk::DistanceAlg::PythagorasSquared.distance2d( DistanceAlg::PythagorasSquared.distance2d(
rltk::Point::new((idx as i32) % build_data.map.width, (idx as i32) / build_data.map.width), Point::new(
rltk::Point::new(seed_x, seed_y) (idx as i32) % build_data.map.width,
(idx as i32) / build_data.map.width
),
Point::new(seed_x, seed_y)
), ),
)); ));
} }
} }
if available_floors.is_empty() { if available_floors.is_empty() {
panic!("No valid floors to start on"); unreachable!("No valid floors to start on.");
} }
available_floors.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); available_floors.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());

View file

@ -1,5 +1,5 @@
use super::{ BuilderMap, InitialMapBuilder, Rect, TileType }; use super::{ BuilderMap, InitialMapBuilder, Rect, TileType };
use rltk::RandomNumberGenerator; use bracket_lib::prelude::*;
pub struct BspDungeonBuilder { pub struct BspDungeonBuilder {
rects: Vec<Rect>, rects: Vec<Rect>,
@ -7,7 +7,7 @@ pub struct BspDungeonBuilder {
impl InitialMapBuilder for BspDungeonBuilder { impl InitialMapBuilder for BspDungeonBuilder {
#[allow(dead_code)] #[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
@ -21,7 +21,7 @@ impl BspDungeonBuilder {
fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
let mut rooms: Vec<Rect> = Vec::new(); let mut rooms: Vec<Rect> = Vec::new();
self.rects.clear(); 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 self.rects.push(Rect::with_size(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]; let first_room = self.rects[0];
self.add_subrects(first_room); // Divide the first room self.add_subrects(first_room); // Divide the first room
@ -49,10 +49,12 @@ impl BspDungeonBuilder {
let half_width = i32::max(width / 2, 1); let half_width = i32::max(width / 2, 1);
let half_height = i32::max(height / 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::with_size(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::with_size(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::with_size(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)); self.rects.push(
Rect::with_size(rect.x1 + half_width, rect.y1 + half_height, half_width, half_height)
);
} }
fn get_random_rect(&mut self, rng: &mut RandomNumberGenerator) -> Rect { fn get_random_rect(&mut self, rng: &mut RandomNumberGenerator) -> Rect {

View file

@ -1,5 +1,5 @@
use super::{ draw_corridor, BuilderMap, InitialMapBuilder, Rect, TileType }; use super::{ draw_corridor, BuilderMap, InitialMapBuilder, Rect, TileType };
use rltk::RandomNumberGenerator; use bracket_lib::prelude::*;
const MIN_ROOM_SIZE: i32 = 8; const MIN_ROOM_SIZE: i32 = 8;
@ -9,7 +9,7 @@ pub struct BspInteriorBuilder {
impl InitialMapBuilder for BspInteriorBuilder { impl InitialMapBuilder for BspInteriorBuilder {
#[allow(dead_code)] #[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
@ -23,7 +23,7 @@ impl BspInteriorBuilder {
fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
let mut rooms: Vec<Rect> = Vec::new(); let mut rooms: Vec<Rect> = Vec::new();
self.rects.clear(); 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 self.rects.push(Rect::with_size(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]; let first_room = self.rects[0];
self.add_subrects(first_room, rng); // Divide the first room self.add_subrects(first_room, rng); // Divide the first room
@ -36,7 +36,10 @@ impl BspInteriorBuilder {
for y in room.y1..room.y2 { for y in room.y1..room.y2 {
for x in room.x1..room.x2 { for x in room.x1..room.x2 {
let idx = build_data.map.xy_idx(x, y); let idx = build_data.map.xy_idx(x, y);
if idx > 0 && idx < ((build_data.map.width * build_data.map.height - 1) as usize) { if
idx > 0 &&
idx < ((build_data.map.width * build_data.map.height - 1) as usize)
{
build_data.map.tiles[idx] = TileType::Floor; build_data.map.tiles[idx] = TileType::Floor;
} }
} }
@ -50,8 +53,10 @@ impl BspInteriorBuilder {
let next_room = rooms[i + 1]; let next_room = rooms[i + 1];
let start_x = room.x1 + (rng.roll_dice(1, i32::abs(room.x1 - room.x2)) - 1); let start_x = room.x1 + (rng.roll_dice(1, i32::abs(room.x1 - room.x2)) - 1);
let start_y = room.y1 + (rng.roll_dice(1, i32::abs(room.y1 - room.y2)) - 1); let start_y = room.y1 + (rng.roll_dice(1, i32::abs(room.y1 - room.y2)) - 1);
let end_x = next_room.x1 + (rng.roll_dice(1, i32::abs(next_room.x1 - next_room.x2)) - 1); let end_x =
let end_y = next_room.y1 + (rng.roll_dice(1, i32::abs(next_room.y1 - next_room.y2)) - 1); next_room.x1 + (rng.roll_dice(1, i32::abs(next_room.x1 - next_room.x2)) - 1);
let end_y =
next_room.y1 + (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); draw_corridor(&mut build_data.map, start_x, start_y, end_x, end_y);
build_data.take_snapshot(); build_data.take_snapshot();
} }
@ -75,24 +80,24 @@ impl BspInteriorBuilder {
if split <= 2 { if split <= 2 {
// Horizontal split // Horizontal split
let h1 = Rect::new(rect.x1, rect.y1, half_width - 1, height); let h1 = Rect::with_size(rect.x1, rect.y1, half_width - 1, height);
self.rects.push(h1); self.rects.push(h1);
if half_width > MIN_ROOM_SIZE { if half_width > MIN_ROOM_SIZE {
self.add_subrects(h1, rng); self.add_subrects(h1, rng);
} }
let h2 = Rect::new(rect.x1 + half_width, rect.y1, half_width, height); let h2 = Rect::with_size(rect.x1 + half_width, rect.y1, half_width, height);
self.rects.push(h2); self.rects.push(h2);
if half_width > MIN_ROOM_SIZE { if half_width > MIN_ROOM_SIZE {
self.add_subrects(h2, rng); self.add_subrects(h2, rng);
} }
} else { } else {
// Vertical split // Vertical split
let v1 = Rect::new(rect.x1, rect.y1, width, half_height - 1); let v1 = Rect::with_size(rect.x1, rect.y1, width, half_height - 1);
self.rects.push(v1); self.rects.push(v1);
if half_height > MIN_ROOM_SIZE { if half_height > MIN_ROOM_SIZE {
self.add_subrects(v1, rng); self.add_subrects(v1, rng);
} }
let v2 = Rect::new(rect.x1, rect.y1 + half_height, width, half_height); let v2 = Rect::with_size(rect.x1, rect.y1 + half_height, width, half_height);
self.rects.push(v2); self.rects.push(v2);
if half_height > MIN_ROOM_SIZE { if half_height > MIN_ROOM_SIZE {
self.add_subrects(v2, rng); self.add_subrects(v2, rng);

View file

@ -1,18 +1,20 @@
use super::{ BuilderMap, InitialMapBuilder, MetaMapBuilder, TileType }; use super::{ BuilderMap, InitialMapBuilder, MetaMapBuilder, TileType };
use rltk::RandomNumberGenerator; use bracket_lib::prelude::*;
pub struct CellularAutomataBuilder {} pub struct CellularAutomataBuilder {
floor_tile: TileType,
}
impl InitialMapBuilder for CellularAutomataBuilder { impl InitialMapBuilder for CellularAutomataBuilder {
#[allow(dead_code)] #[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
impl MetaMapBuilder for CellularAutomataBuilder { impl MetaMapBuilder for CellularAutomataBuilder {
#[allow(dead_code)] #[allow(dead_code)]
fn build_map(&mut self, _rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.apply_iteration(build_data); self.apply_iteration(build_data);
} }
} }
@ -20,7 +22,10 @@ impl MetaMapBuilder for CellularAutomataBuilder {
impl CellularAutomataBuilder { impl CellularAutomataBuilder {
#[allow(dead_code)] #[allow(dead_code)]
pub fn new() -> Box<CellularAutomataBuilder> { pub fn new() -> Box<CellularAutomataBuilder> {
Box::new(CellularAutomataBuilder {}) Box::new(CellularAutomataBuilder { floor_tile: TileType::Floor })
}
pub fn floor(floor_tile: TileType) -> Box<CellularAutomataBuilder> {
Box::new(CellularAutomataBuilder { floor_tile })
} }
#[allow(clippy::map_entry)] #[allow(clippy::map_entry)]
@ -64,23 +69,35 @@ impl CellularAutomataBuilder {
if build_data.map.tiles[idx + (build_data.map.width as usize)] == TileType::Wall { if build_data.map.tiles[idx + (build_data.map.width as usize)] == TileType::Wall {
neighbors += 1; neighbors += 1;
} }
if build_data.map.tiles[idx - ((build_data.map.width as usize) - 1)] == TileType::Wall { if
build_data.map.tiles[idx - ((build_data.map.width as usize) - 1)] ==
TileType::Wall
{
neighbors += 1; neighbors += 1;
} }
if build_data.map.tiles[idx - ((build_data.map.width as usize) + 1)] == TileType::Wall { if
build_data.map.tiles[idx - ((build_data.map.width as usize) + 1)] ==
TileType::Wall
{
neighbors += 1; neighbors += 1;
} }
if build_data.map.tiles[idx + ((build_data.map.width as usize) - 1)] == TileType::Wall { if
build_data.map.tiles[idx + ((build_data.map.width as usize) - 1)] ==
TileType::Wall
{
neighbors += 1; neighbors += 1;
} }
if build_data.map.tiles[idx + ((build_data.map.width as usize) + 1)] == TileType::Wall { if
build_data.map.tiles[idx + ((build_data.map.width as usize) + 1)] ==
TileType::Wall
{
neighbors += 1; neighbors += 1;
} }
if neighbors > 4 || neighbors == 0 { if neighbors > 4 || neighbors == 0 {
newtiles[idx] = TileType::Wall; newtiles[idx] = TileType::Wall;
} else { } else {
newtiles[idx] = TileType::Floor; newtiles[idx] = self.floor_tile;
} }
} }
} }

View file

@ -1,10 +1,11 @@
use super::{ BuilderMap, MetaMapBuilder, TileType }; use super::{ BuilderMap, MetaMapBuilder, TileType };
use rltk::RandomNumberGenerator; use crate::tile_walkable;
use bracket_lib::prelude::*;
pub struct CullUnreachable {} pub struct CullUnreachable {}
impl MetaMapBuilder for CullUnreachable { impl MetaMapBuilder for CullUnreachable {
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
@ -20,7 +21,7 @@ impl CullUnreachable {
let start_idx = build_data.map.xy_idx(starting_pos.x, starting_pos.y); let start_idx = build_data.map.xy_idx(starting_pos.x, starting_pos.y);
build_data.map.populate_blocked(); build_data.map.populate_blocked();
let map_starts: Vec<usize> = vec![start_idx]; let map_starts: Vec<usize> = vec![start_idx];
let dijkstra_map = rltk::DijkstraMap::new( let dijkstra_map = DijkstraMap::new(
build_data.map.width as usize, build_data.map.width as usize,
build_data.map.height as usize, build_data.map.height as usize,
&map_starts, &map_starts,
@ -28,7 +29,7 @@ impl CullUnreachable {
1000.0 1000.0
); );
for (i, tile) in build_data.map.tiles.iter_mut().enumerate() { for (i, tile) in build_data.map.tiles.iter_mut().enumerate() {
if *tile == TileType::Floor { if tile_walkable(*tile) {
let distance_to_start = dijkstra_map.map[i]; let distance_to_start = dijkstra_map.map[i];
// We can't get to this tile - so we'll make it a wall // We can't get to this tile - so we'll make it a wall
if distance_to_start == std::f32::MAX { if distance_to_start == std::f32::MAX {

View file

@ -1,10 +1,11 @@
use super::{ BuilderMap, MetaMapBuilder, TileType }; use super::{ BuilderMap, MetaMapBuilder, TileType };
use rltk::RandomNumberGenerator; use crate::tile_walkable;
use bracket_lib::prelude::*;
pub struct DistantExit {} pub struct DistantExit {}
impl MetaMapBuilder for DistantExit { impl MetaMapBuilder for DistantExit {
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
@ -20,7 +21,7 @@ impl DistantExit {
let start_idx = build_data.map.xy_idx(starting_pos.x, starting_pos.y); let start_idx = build_data.map.xy_idx(starting_pos.x, starting_pos.y);
build_data.map.populate_blocked(); build_data.map.populate_blocked();
let map_starts: Vec<usize> = vec![start_idx]; let map_starts: Vec<usize> = vec![start_idx];
let dijkstra_map = rltk::DijkstraMap::new( let dijkstra_map = DijkstraMap::new(
build_data.map.width as usize, build_data.map.width as usize,
build_data.map.height as usize, build_data.map.height as usize,
&map_starts, &map_starts,
@ -29,7 +30,7 @@ impl DistantExit {
); );
let mut exit_tile = (0, 0.0f32); let mut exit_tile = (0, 0.0f32);
for (i, tile) in build_data.map.tiles.iter_mut().enumerate() { for (i, tile) in build_data.map.tiles.iter_mut().enumerate() {
if *tile == TileType::Floor { if tile_walkable(*tile) {
let distance_to_start = dijkstra_map.map[i]; let distance_to_start = dijkstra_map.map[i];
if distance_to_start != std::f32::MAX { if distance_to_start != std::f32::MAX {
// If it is further away than our current exit candidate, move the exit // If it is further away than our current exit candidate, move the exit

View file

@ -1,5 +1,5 @@
use super::{ paint, BuilderMap, InitialMapBuilder, MetaMapBuilder, Position, Symmetry, TileType }; use super::{ paint, BuilderMap, InitialMapBuilder, MetaMapBuilder, Position, Symmetry, TileType };
use rltk::RandomNumberGenerator; use bracket_lib::prelude::*;
#[derive(PartialEq, Copy, Clone)] #[derive(PartialEq, Copy, Clone)]
#[allow(dead_code)] #[allow(dead_code)]
@ -18,14 +18,14 @@ pub struct DLABuilder {
impl InitialMapBuilder for DLABuilder { impl InitialMapBuilder for DLABuilder {
#[allow(dead_code)] #[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
impl MetaMapBuilder for DLABuilder { impl MetaMapBuilder for DLABuilder {
#[allow(dead_code)] #[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
@ -94,7 +94,10 @@ impl DLABuilder {
#[allow(clippy::map_entry)] #[allow(clippy::map_entry)]
fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
// Carve a starting seed // Carve a starting seed
let starting_position = Position { x: build_data.map.width / 2, y: build_data.map.height / 2 }; 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); let start_idx = build_data.map.xy_idx(starting_position.x, starting_position.y);
build_data.take_snapshot(); build_data.take_snapshot();
build_data.map.tiles[start_idx] = TileType::Floor; build_data.map.tiles[start_idx] = TileType::Floor;
@ -189,10 +192,10 @@ impl DLABuilder {
let mut prev_y = digger_y; let mut prev_y = digger_y;
let mut digger_idx = build_data.map.xy_idx(digger_x, digger_y); let mut digger_idx = build_data.map.xy_idx(digger_x, digger_y);
let mut path = rltk::line2d( let mut path = line2d(
rltk::LineAlg::Bresenham, LineAlg::Bresenham,
rltk::Point::new(digger_x, digger_y), Point::new(digger_x, digger_y),
rltk::Point::new(starting_position.x, starting_position.y) Point::new(starting_position.x, starting_position.y)
); );
while build_data.map.tiles[digger_idx] == TileType::Wall && !path.is_empty() { while build_data.map.tiles[digger_idx] == TileType::Wall && !path.is_empty() {

View file

@ -1,11 +1,11 @@
use super::{ BuilderMap, MetaMapBuilder, TileType }; use super::{ BuilderMap, MetaMapBuilder, TileType };
use rltk::RandomNumberGenerator; use bracket_lib::prelude::*;
pub struct DoorPlacement {} pub struct DoorPlacement {}
impl MetaMapBuilder for DoorPlacement { impl MetaMapBuilder for DoorPlacement {
#[allow(dead_code)] #[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.doors(rng, build_data); self.doors(rng, build_data);
} }
} }
@ -30,7 +30,11 @@ impl DoorPlacement {
// There are no corridors - scan for possible places // There are no corridors - scan for possible places
let tiles = build_data.map.tiles.clone(); let tiles = build_data.map.tiles.clone();
for (i, tile) in tiles.iter().enumerate() { for (i, tile) in tiles.iter().enumerate() {
if *tile == TileType::Floor && self.door_possible(build_data, i) && rng.roll_dice(1, 6) == 1 { if
*tile == TileType::Floor &&
self.door_possible(build_data, i) &&
rng.roll_dice(1, 6) == 1
{
build_data.spawn_list.push((i, "door".to_string())); build_data.spawn_list.push((i, "door".to_string()));
} }
} }

View file

@ -1,5 +1,5 @@
use super::{ paint, BuilderMap, InitialMapBuilder, MetaMapBuilder, Position, Symmetry, TileType }; use super::{ paint, BuilderMap, InitialMapBuilder, MetaMapBuilder, Position, Symmetry, TileType };
use rltk::RandomNumberGenerator; use bracket_lib::prelude::*;
#[derive(PartialEq, Copy, Clone)] #[derive(PartialEq, Copy, Clone)]
#[allow(dead_code)] #[allow(dead_code)]
@ -22,14 +22,14 @@ pub struct DrunkardsWalkBuilder {
impl InitialMapBuilder for DrunkardsWalkBuilder { impl InitialMapBuilder for DrunkardsWalkBuilder {
#[allow(dead_code)] #[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
impl MetaMapBuilder for DrunkardsWalkBuilder { impl MetaMapBuilder for DrunkardsWalkBuilder {
#[allow(dead_code)] #[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
@ -107,7 +107,10 @@ impl DrunkardsWalkBuilder {
fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
// Set a central starting point // Set a central starting point
let starting_position = Position { x: build_data.map.width / 2, y: build_data.map.height / 2 }; 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); let start_idx = build_data.map.xy_idx(starting_position.x, starting_position.y);
build_data.map.tiles[start_idx] = TileType::Floor; build_data.map.tiles[start_idx] = TileType::Floor;
@ -144,7 +147,13 @@ impl DrunkardsWalkBuilder {
if build_data.map.tiles[drunk_idx] == TileType::Wall { if build_data.map.tiles[drunk_idx] == TileType::Wall {
did_something = true; did_something = true;
} }
paint(&mut build_data.map, self.settings.symmetry, self.settings.brush_size, drunk_x, drunk_y); paint(
&mut build_data.map,
self.settings.symmetry,
self.settings.brush_size,
drunk_x,
drunk_y
);
build_data.map.tiles[drunk_idx] = TileType::DownStair; build_data.map.tiles[drunk_idx] = TileType::DownStair;
let stagger_direction = rng.roll_dice(1, 4); let stagger_direction = rng.roll_dice(1, 4);

View file

@ -1,13 +1,15 @@
use super::{ BuilderMap, MetaMapBuilder, TileType }; use super::{ BuilderMap, MetaMapBuilder, TileType };
use rltk::RandomNumberGenerator; use crate::tile_walkable;
use bracket_lib::prelude::*;
pub struct FillEdges { pub struct FillEdges {
fill_with: TileType, fill_with: TileType,
only_walkable: bool,
} }
impl MetaMapBuilder for FillEdges { impl MetaMapBuilder for FillEdges {
#[allow(dead_code)] #[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.fill_edges(rng, build_data); self.fill_edges(rng, build_data);
} }
} }
@ -15,21 +17,32 @@ impl MetaMapBuilder for FillEdges {
impl FillEdges { impl FillEdges {
#[allow(dead_code)] #[allow(dead_code)]
pub fn wall() -> Box<FillEdges> { pub fn wall() -> Box<FillEdges> {
return Box::new(FillEdges { fill_with: TileType::Wall }); return Box::new(FillEdges { fill_with: TileType::Wall, only_walkable: false });
}
pub fn overmap_transition(id: i32) -> Box<FillEdges> {
return Box::new(FillEdges { fill_with: TileType::ToOvermap(id), only_walkable: true });
} }
fn fill_edges(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { fn fill_edges(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
// Get map edges as possible points to fill
let mut possible_idxs: Vec<usize> = Vec::new();
for x in 0..build_data.map.width { for x in 0..build_data.map.width {
let mut idx = build_data.map.xy_idx(x, 0); let mut idx = build_data.map.xy_idx(x, 0);
build_data.map.tiles[idx] = self.fill_with; possible_idxs.push(idx);
idx = build_data.map.xy_idx(x, build_data.map.height - 1); idx = build_data.map.xy_idx(x, build_data.map.height - 1);
build_data.map.tiles[idx] = self.fill_with; possible_idxs.push(idx);
} }
for y in 0..build_data.map.height { for y in 0..build_data.map.height {
let mut idx = build_data.map.xy_idx(0, y); let mut idx = build_data.map.xy_idx(0, y);
build_data.map.tiles[idx] = self.fill_with; possible_idxs.push(idx);
idx = build_data.map.xy_idx(build_data.map.width - 1, y); idx = build_data.map.xy_idx(build_data.map.width - 1, y);
build_data.map.tiles[idx] = self.fill_with; possible_idxs.push(idx);
}
// For every possible point, first check if we only want to fill walkable tiles (and if its walkable if so)
for idx in possible_idxs {
if !self.only_walkable || tile_walkable(build_data.map.tiles[idx]) {
build_data.map.tiles[idx] = self.fill_with;
}
} }
} }
} }

View file

@ -0,0 +1,38 @@
use super::{ BuilderMap, MetaMapBuilder, TileType };
use bracket_lib::prelude::*;
pub struct Foliage {
start_tile: TileType,
percent: i32,
}
impl MetaMapBuilder for Foliage {
#[allow(dead_code)]
fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.apply(rng, build_data);
}
}
impl Foliage {
#[allow(dead_code)]
pub fn percent(start_tile: TileType, percent: i32) -> Box<Foliage> {
return Box::new(Foliage { start_tile, percent });
}
fn apply(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
for tile in build_data.map.tiles.iter_mut() {
if *tile == self.start_tile {
if rng.roll_dice(1, 100) < self.percent {
match rng.roll_dice(1, 2) {
1 => {
*tile = TileType::Foliage;
}
_ => {
*tile = TileType::HeavyFoliage;
}
};
}
}
}
}
}

View file

@ -9,25 +9,39 @@ use super::{
VoronoiSpawning, VoronoiSpawning,
XStart, XStart,
YStart, YStart,
Foliage,
}; };
use rltk::prelude::*; use bracket_lib::prelude::*;
use crate::data::names::*;
pub fn forest_builder( pub fn forest_builder(
new_id: i32, new_id: i32,
_rng: &mut rltk::RandomNumberGenerator, _rng: &mut RandomNumberGenerator,
width: i32, width: i32,
height: i32, height: i32,
difficulty: i32, difficulty: i32,
initial_player_level: i32 initial_player_level: i32
) -> BuilderChain { ) -> BuilderChain {
let mut chain = BuilderChain::new(new_id, width, height, difficulty, "the woods", initial_player_level); let mut chain = BuilderChain::new(
chain.start_with(CellularAutomataBuilder::new()); false,
new_id,
width,
height,
difficulty,
NAME_FOREST_BUILDER,
SHORTNAME_FOREST_BUILDER,
0,
initial_player_level
);
chain.start_with(CellularAutomataBuilder::floor(TileType::Grass));
// Change ~30% of the floor to some sort of foliage.
chain.with(AreaStartingPosition::new(XStart::CENTRE, YStart::CENTRE)); chain.with(AreaStartingPosition::new(XStart::CENTRE, YStart::CENTRE));
chain.with(CullUnreachable::new()); chain.with(CullUnreachable::new());
chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTRE)); chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::CENTRE));
// Setup an exit and spawn mobs // Setup an exit and spawn mobs
chain.with(VoronoiSpawning::new());
chain.with(RoadExit::new()); chain.with(RoadExit::new());
chain.with(Foliage::percent(TileType::Grass, 30));
chain.with(VoronoiSpawning::new());
return chain; return chain;
} }
@ -52,14 +66,17 @@ impl RoadExit {
available_floors.push(( available_floors.push((
idx, idx,
DistanceAlg::PythagorasSquared.distance2d( DistanceAlg::PythagorasSquared.distance2d(
Point::new((idx as i32) % build_data.map.width, (idx as i32) / build_data.map.width), Point::new(
(idx as i32) % build_data.map.width,
(idx as i32) / build_data.map.width
),
Point::new(seed_x, seed_y) Point::new(seed_x, seed_y)
), ),
)); ));
} }
} }
if available_floors.is_empty() { if available_floors.is_empty() {
panic!("No valid floors to start on."); unreachable!("No valid floors to start on.");
} }
available_floors.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); 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_x = (available_floors[0].0 as i32) % build_data.map.width;
@ -80,7 +97,11 @@ impl RoadExit {
fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
let starting_pos = build_data.starting_position.as_ref().unwrap().clone(); 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 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.height / 2); let (end_x, end_y) = self.find_exit(
build_data,
build_data.map.width - 2,
build_data.height / 2
);
let end_idx = build_data.map.xy_idx(end_x, end_y); let end_idx = build_data.map.xy_idx(end_x, end_y);
build_data.map.populate_blocked(); build_data.map.populate_blocked();
@ -108,9 +129,9 @@ impl RoadExit {
let stream_idx = build_data.map.xy_idx(stream_x, stream_y) as usize; let stream_idx = build_data.map.xy_idx(stream_x, stream_y) as usize;
let stream = a_star_search(stairs_idx, stream_idx, &mut build_data.map); let stream = a_star_search(stairs_idx, stream_idx, &mut build_data.map);
for tile in stream.steps.iter() { for tile in stream.steps.iter() {
if build_data.map.tiles[*tile as usize] == TileType::Floor { // Maybe only turn grass to water here, and turn the road into a bridge.
build_data.map.tiles[*tile as usize] = TileType::ShallowWater; // i.e. if build_data.map.tiles[*tile as usize] == TileType::Grass
} build_data.map.tiles[*tile as usize] = TileType::ShallowWater;
} }
build_data.map.tiles[stairs_idx] = TileType::DownStair; build_data.map.tiles[stairs_idx] = TileType::DownStair;
build_data.take_snapshot(); build_data.take_snapshot();

View file

@ -1,11 +1,11 @@
use super::{ BuilderMap, InitialMapBuilder, Map, TileType }; use super::{ BuilderMap, InitialMapBuilder, Map, TileType };
use rltk::RandomNumberGenerator; use bracket_lib::prelude::*;
pub struct MazeBuilder {} pub struct MazeBuilder {}
impl InitialMapBuilder for MazeBuilder { impl InitialMapBuilder for MazeBuilder {
#[allow(dead_code)] #[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
@ -75,7 +75,14 @@ struct Grid<'a> {
impl<'a> Grid<'a> { impl<'a> Grid<'a> {
fn new(width: i32, height: i32, rng: &mut RandomNumberGenerator) -> Grid { fn new(width: i32, height: i32, rng: &mut RandomNumberGenerator) -> Grid {
let mut grid = Grid { width, height, cells: Vec::new(), backtrace: Vec::new(), current: 0, rng }; let mut grid = Grid {
width,
height,
cells: Vec::new(),
backtrace: Vec::new(),
current: 0,
rng,
};
for row in 0..height { for row in 0..height {
for column in 0..width { for column in 0..width {
@ -122,7 +129,9 @@ impl<'a> Grid<'a> {
if neighbors.len() == 1 { if neighbors.len() == 1 {
return Some(neighbors[0]); return Some(neighbors[0]);
} else { } else {
return Some(neighbors[(self.rng.roll_dice(1, neighbors.len() as i32) - 1) as usize]); return Some(
neighbors[(self.rng.roll_dice(1, neighbors.len() as i32) - 1) as usize]
);
} }
} }
None None
@ -141,7 +150,9 @@ impl<'a> Grid<'a> {
// __lower_part__ __higher_part_ // __lower_part__ __higher_part_
// / \ / \ // / \ / \
// --------cell1------ | cell2----------- // --------cell1------ | cell2-----------
let (lower_part, higher_part) = self.cells.split_at_mut(std::cmp::max(self.current, next)); 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 cell1 = &mut lower_part[std::cmp::min(self.current, next)];
let cell2 = &mut higher_part[0]; let cell2 = &mut higher_part[0];
cell1.remove_walls(cell2); cell1.remove_walls(cell2);

View file

@ -1,4 +1,6 @@
use super::{ spawner, Map, Position, Rect, TileType, SHOW_MAPGEN }; use super::{ spawner, Map, Position, Rect, TileType };
use bracket_lib::prelude::*;
mod bsp_dungeon; mod bsp_dungeon;
use bsp_dungeon::BspDungeonBuilder; use bsp_dungeon::BspDungeonBuilder;
mod bsp_interior; mod bsp_interior;
@ -35,6 +37,9 @@ mod voronoi_spawning;
use common::*; use common::*;
use specs::prelude::*; use specs::prelude::*;
use voronoi_spawning::VoronoiSpawning; use voronoi_spawning::VoronoiSpawning;
use super::config::CONFIG;
use super::data::ids::*;
use super::data::names::*;
//use wfc::WaveFunctionCollapseBuilder; //use wfc::WaveFunctionCollapseBuilder;
mod room_exploder; mod room_exploder;
use room_exploder::RoomExploder; use room_exploder::RoomExploder;
@ -62,6 +67,10 @@ mod town;
use town::town_builder; use town::town_builder;
mod forest; mod forest;
use forest::forest_builder; use forest::forest_builder;
mod foliage;
use foliage::Foliage;
mod room_themer;
use room_themer::ThemeRooms;
// Shared data to be passed around build chain // Shared data to be passed around build chain
pub struct BuilderMap { pub struct BuilderMap {
@ -78,7 +87,7 @@ pub struct BuilderMap {
impl BuilderMap { impl BuilderMap {
fn take_snapshot(&mut self) { fn take_snapshot(&mut self) {
if SHOW_MAPGEN { if CONFIG.logging.show_mapgen {
let mut snapshot = self.map.clone(); let mut snapshot = self.map.clone();
for v in snapshot.revealed_tiles.iter_mut() { for v in snapshot.revealed_tiles.iter_mut() {
*v = true; *v = true;
@ -96,11 +105,14 @@ pub struct BuilderChain {
impl BuilderChain { impl BuilderChain {
pub fn new<S: ToString>( pub fn new<S: ToString>(
overmap: bool,
new_id: i32, new_id: i32,
width: i32, width: i32,
height: i32, height: i32,
difficulty: i32, difficulty: i32,
name: S, name: S,
short_name: S,
depth: i32,
initial_player_level: i32 initial_player_level: i32
) -> BuilderChain { ) -> BuilderChain {
BuilderChain { BuilderChain {
@ -108,7 +120,7 @@ impl BuilderChain {
builders: Vec::new(), builders: Vec::new(),
build_data: BuilderMap { build_data: BuilderMap {
spawn_list: Vec::new(), spawn_list: Vec::new(),
map: Map::new(new_id, width, height, difficulty, name), map: Map::new(overmap, new_id, width, height, difficulty, name, short_name, depth),
starting_position: None, starting_position: None,
rooms: None, rooms: None,
corridors: None, corridors: None,
@ -125,7 +137,7 @@ impl BuilderChain {
None => { None => {
self.starter = Some(starter); self.starter = Some(starter);
} }
Some(_) => panic!("You can only have one starting builder."), Some(_) => unreachable!("You can only have one starting builder."),
}; };
} }
@ -133,9 +145,9 @@ impl BuilderChain {
self.builders.push(metabuilder); self.builders.push(metabuilder);
} }
pub fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator) { pub fn build_map(&mut self, rng: &mut RandomNumberGenerator) {
match &mut self.starter { match &mut self.starter {
None => panic!("Cannot run a map builder chain without a starting build system"), None => unreachable!("Cannot run a map builder chain without a starting build system"),
Some(starter) => { Some(starter) => {
// Build the starting map // Build the starting map
starter.build_map(rng, &mut self.build_data); starter.build_map(rng, &mut self.build_data);
@ -154,19 +166,21 @@ impl BuilderChain {
spawned_entities.push(&entity.1); spawned_entities.push(&entity.1);
spawner::spawn_entity(ecs, &(&entity.0, &entity.1)); spawner::spawn_entity(ecs, &(&entity.0, &entity.1));
} }
rltk::console::log(format!("DEBUGINFO: SPAWNED ENTITIES = {:?}", spawned_entities)); if CONFIG.logging.log_spawning {
console::log(format!("DEBUGINFO: SPAWNED ENTITIES = {:?}", spawned_entities));
}
} }
} }
pub trait InitialMapBuilder { pub trait InitialMapBuilder {
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap); fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap);
} }
pub trait MetaMapBuilder { pub trait MetaMapBuilder {
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap); fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap);
} }
fn random_start_position(rng: &mut rltk::RandomNumberGenerator) -> (XStart, YStart) { fn random_start_position(rng: &mut RandomNumberGenerator) -> (XStart, YStart) {
let x; let x;
let xroll = rng.roll_dice(1, 3); let xroll = rng.roll_dice(1, 3);
match xroll { match xroll {
@ -198,7 +212,7 @@ fn random_start_position(rng: &mut rltk::RandomNumberGenerator) -> (XStart, YSta
(x, y) (x, y)
} }
fn random_room_builder(rng: &mut rltk::RandomNumberGenerator, builder: &mut BuilderChain) { fn random_room_builder(rng: &mut RandomNumberGenerator, builder: &mut BuilderChain, end: bool) {
let build_roll = rng.roll_dice(1, 3); let build_roll = rng.roll_dice(1, 3);
// Start with a room builder. // Start with a room builder.
match build_roll { match build_roll {
@ -260,10 +274,12 @@ fn random_room_builder(rng: &mut rltk::RandomNumberGenerator, builder: &mut Buil
} }
// Decide where to put the exit - in a room or far away, anywhere. // Decide where to put the exit - in a room or far away, anywhere.
let exit_roll = rng.roll_dice(1, 2); if !end {
match exit_roll { let exit_roll = rng.roll_dice(1, 2);
1 => builder.with(RoomBasedStairs::new()), match exit_roll {
_ => builder.with(DistantExit::new()), 1 => builder.with(RoomBasedStairs::new()),
_ => builder.with(DistantExit::new()),
}
} }
// Decide whether to spawn entities only in rooms, or with voronoi noise. // Decide whether to spawn entities only in rooms, or with voronoi noise.
@ -272,9 +288,16 @@ fn random_room_builder(rng: &mut rltk::RandomNumberGenerator, builder: &mut Buil
1 => builder.with(RoomBasedSpawner::new()), 1 => builder.with(RoomBasedSpawner::new()),
_ => builder.with(VoronoiSpawning::new()), _ => builder.with(VoronoiSpawning::new()),
} }
builder.with(ThemeRooms::grass(5, 5 * 5)); // 5% chance of an overgrown treant room. Must be 5*5 tiles minimum.
builder.with(ThemeRooms::barracks(5, 6 * 6)); // 5% chance of a squad barracks. Must be 6*6 tiles minimum.
} }
fn random_shape_builder(rng: &mut rltk::RandomNumberGenerator, builder: &mut BuilderChain) -> bool { fn random_shape_builder(
rng: &mut RandomNumberGenerator,
builder: &mut BuilderChain,
end: bool
) -> bool {
// Pick an initial builder // Pick an initial builder
let builder_roll = rng.roll_dice(1, 16); let builder_roll = rng.roll_dice(1, 16);
let mut want_doors = true; let mut want_doors = true;
@ -295,7 +318,10 @@ fn random_shape_builder(rng: &mut rltk::RandomNumberGenerator, builder: &mut Bui
11 => builder.start_with(DLABuilder::insectoid()), 11 => builder.start_with(DLABuilder::insectoid()),
12 => builder.start_with(VoronoiBuilder::pythagoras()), 12 => builder.start_with(VoronoiBuilder::pythagoras()),
13 => builder.start_with(VoronoiBuilder::manhattan()), 13 => builder.start_with(VoronoiBuilder::manhattan()),
_ => builder.start_with(PrefabBuilder::constant(prefab_builder::prefab_levels::WFC_POPULATED)), _ =>
builder.start_with(
PrefabBuilder::constant(prefab_builder::prefab_levels::WFC_POPULATED)
),
} }
// 'Select' the centre by placing a starting position, and cull everywhere unreachable. // 'Select' the centre by placing a starting position, and cull everywhere unreachable.
@ -308,27 +334,73 @@ fn random_shape_builder(rng: &mut rltk::RandomNumberGenerator, builder: &mut Bui
// Place the exit and spawn mobs // Place the exit and spawn mobs
builder.with(VoronoiSpawning::new()); builder.with(VoronoiSpawning::new());
builder.with(DistantExit::new()); if !end {
builder.with(DistantExit::new());
}
return want_doors; return want_doors;
} }
fn overmap_builder() -> BuilderChain {
let mut builder = BuilderChain::new(
true,
ID_OVERMAP,
69,
41,
0,
NAME_OVERMAP,
SHORTNAME_OVERMAP,
0,
1
);
builder.start_with(PrefabBuilder::overmap());
builder.with(Foliage::percent(TileType::Grass, 30));
return builder;
}
pub enum BuildType {
Room = 1,
Shape = 2,
Any = 3,
}
pub fn random_builder( pub fn random_builder(
new_id: i32, new_id: i32,
rng: &mut rltk::RandomNumberGenerator, rng: &mut RandomNumberGenerator,
width: i32, width: i32,
height: i32, height: i32,
difficulty: i32, difficulty: i32,
initial_player_level: i32 depth: i32,
initial_player_level: i32,
end: bool,
build_type: BuildType
) -> BuilderChain { ) -> BuilderChain {
rltk::console::log(format!("DEBUGINFO: Building random (ID:{}, DIFF:{})", new_id, difficulty)); console::log(format!("DEBUGINFO: Building random (ID:{}, DIFF:{})", new_id, difficulty));
let mut builder = BuilderChain::new(new_id, width, height, difficulty, "the dungeon", initial_player_level); let mut builder = BuilderChain::new(
let type_roll = rng.roll_dice(1, 2); false,
new_id,
width,
height,
difficulty,
NAME_DUNGEON_RANDOM,
SHORTNAME_DUNGEON_RANDOM,
depth,
initial_player_level
);
let mut want_doors = true; let mut want_doors = true;
match type_roll { match build_type {
1 => random_room_builder(rng, &mut builder), BuildType::Room => random_room_builder(rng, &mut builder, end),
_ => { BuildType::Shape => {
want_doors = random_shape_builder(rng, &mut builder); want_doors = random_shape_builder(rng, &mut builder, end);
}
BuildType::Any => {
let roll = rng.roll_dice(1, 2);
match roll {
1 => random_room_builder(rng, &mut builder, end),
_ => {
want_doors = random_shape_builder(rng, &mut builder, end);
}
}
} }
} }
@ -366,17 +438,55 @@ pub fn random_builder(
} }
pub fn level_builder( pub fn level_builder(
new_id: i32, id: i32,
rng: &mut rltk::RandomNumberGenerator, rng: &mut RandomNumberGenerator,
width: i32, width: i32,
height: i32, height: i32,
initial_player_level: i32 initial_player_level: i32
) -> BuilderChain { ) -> BuilderChain {
// TODO: With difficulty and ID/depth decoupled, this can be used for branches later. match id {
let difficulty = new_id; ID_OVERMAP => overmap_builder(),
match new_id { ID_TOWN => town_builder(id, rng, width, height, 0, initial_player_level),
1 => town_builder(new_id, rng, width, height, 0, initial_player_level), ID_TOWN2 => forest_builder(id, rng, width, height, 1, initial_player_level),
2 => forest_builder(new_id, rng, width, height, 1, initial_player_level), ID_TOWN3 =>
_ => random_builder(new_id, rng, width, height, difficulty, initial_player_level), random_builder(
id,
rng,
width,
height,
2,
1,
initial_player_level,
true,
BuildType::Room
),
_ if id >= ID_INFINITE =>
random_builder(
id,
rng,
width,
height,
4 + diff(ID_INFINITE, id),
1 + diff(ID_INFINITE, id),
initial_player_level,
false,
BuildType::Room
),
_ => // This should be unreachable!() eventually. Right now it's reachable with the debug/cheat menu. It should not be in normal gameplay.
random_builder(
id,
rng,
width,
height,
1,
404,
initial_player_level,
false,
BuildType::Room
),
} }
} }
fn diff(branch_id: i32, lvl_id: i32) -> i32 {
return lvl_id - branch_id;
}

View file

@ -1,16 +1,21 @@
use super::{spawner, BuilderMap, InitialMapBuilder, MetaMapBuilder, Position, TileType}; use super::{ spawner, BuilderMap, InitialMapBuilder, MetaMapBuilder, Position, TileType };
use rltk::RandomNumberGenerator; use bracket_lib::prelude::*;
pub mod prefab_levels; pub mod prefab_levels;
pub mod prefab_sections; pub mod prefab_sections;
pub mod prefab_vaults; pub mod prefab_vaults;
use std::collections::HashSet; use std::collections::HashSet;
use crate::data::ids::*;
#[derive(PartialEq, Copy, Clone)] #[derive(PartialEq, Copy, Clone)]
#[allow(dead_code)] #[allow(dead_code)]
pub enum PrefabMode { pub enum PrefabMode {
RexLevel { template: &'static str }, Overmap,
Constant { level: prefab_levels::PrefabLevel }, Constant {
Sectional { section: prefab_sections::PrefabSection }, level: prefab_levels::PrefabLevel,
},
Sectional {
section: prefab_sections::PrefabSection,
},
RoomVaults, RoomVaults,
} }
@ -20,14 +25,14 @@ pub struct PrefabBuilder {
} }
impl MetaMapBuilder for PrefabBuilder { impl MetaMapBuilder for PrefabBuilder {
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
impl InitialMapBuilder for PrefabBuilder { impl InitialMapBuilder for PrefabBuilder {
#[allow(dead_code)] #[allow(dead_code)]
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
@ -39,8 +44,8 @@ impl PrefabBuilder {
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn rex_level(template: &'static str) -> Box<PrefabBuilder> { pub fn overmap() -> Box<PrefabBuilder> {
Box::new(PrefabBuilder { mode: PrefabMode::RexLevel { template } }) Box::new(PrefabBuilder { mode: PrefabMode::Overmap })
} }
#[allow(dead_code)] #[allow(dead_code)]
@ -60,24 +65,42 @@ impl PrefabBuilder {
fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
match self.mode { match self.mode {
PrefabMode::RexLevel { template } => self.load_rex_map(&template, rng, build_data), PrefabMode::Overmap =>
PrefabMode::Constant { level } => self.load_ascii_map(&level, rng, build_data), self.load_ascii_map(&prefab_levels::OVERMAP, rng, build_data, true),
PrefabMode::Constant { level } => self.load_ascii_map(&level, rng, build_data, false),
PrefabMode::Sectional { section } => self.apply_sectional(&section, rng, build_data), PrefabMode::Sectional { section } => self.apply_sectional(&section, rng, build_data),
PrefabMode::RoomVaults => self.apply_room_vaults(rng, build_data), PrefabMode::RoomVaults => self.apply_room_vaults(rng, build_data),
} }
build_data.take_snapshot(); build_data.take_snapshot();
} }
fn char_to_map(&mut self, ch: char, idx: usize, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { fn char_to_map(
&mut self,
ch: char,
idx: usize,
rng: &mut RandomNumberGenerator,
build_data: &mut BuilderMap
) {
let difficulty = (build_data.map.difficulty + build_data.initial_player_level) / 2; let difficulty = (build_data.map.difficulty + build_data.initial_player_level) / 2;
match ch { match ch {
' ' => build_data.map.tiles[idx] = TileType::Floor, ' ' => {
'#' => build_data.map.tiles[idx] = TileType::Wall, build_data.map.tiles[idx] = TileType::Floor;
'>' => build_data.map.tiles[idx] = TileType::DownStair, }
'≈' => build_data.map.tiles[idx] = TileType::Floor, // Placeholder for vines/brush '.' => {
build_data.map.tiles[idx] = TileType::Grass;
}
'#' => {
build_data.map.tiles[idx] = TileType::Wall;
}
'>' => {
build_data.map.tiles[idx] = TileType::DownStair;
}
'≈' => {
build_data.map.tiles[idx] = TileType::DeepWater;
} // Placeholder for vines/brush
'@' => { '@' => {
let x = idx as i32 % build_data.map.width; let x = (idx as i32) % build_data.map.width;
let y = 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.map.tiles[idx] = TileType::Floor;
build_data.starting_position = Some(Position { x: x as i32, y: y as i32 }); build_data.starting_position = Some(Position { x: x as i32, y: y as i32 });
} }
@ -107,7 +130,10 @@ impl PrefabBuilder {
} }
'!' => { '!' => {
build_data.map.tiles[idx] = TileType::Floor; build_data.map.tiles[idx] = TileType::Floor;
build_data.spawn_list.push((idx, spawner::potion_table(Some(difficulty)).roll(rng))); build_data.spawn_list.push((
idx,
spawner::potion_table(Some(difficulty)).roll(rng),
));
} }
'/' => { '/' => {
build_data.map.tiles[idx] = TileType::Floor; build_data.map.tiles[idx] = TileType::Floor;
@ -115,27 +141,85 @@ impl PrefabBuilder {
} }
'?' => { '?' => {
build_data.map.tiles[idx] = TileType::Floor; build_data.map.tiles[idx] = TileType::Floor;
build_data.spawn_list.push((idx, spawner::scroll_table(Some(difficulty)).roll(rng))); build_data.spawn_list.push((
idx,
spawner::scroll_table(Some(difficulty)).roll(rng),
));
} }
')' => { ')' => {
build_data.map.tiles[idx] = TileType::Floor; build_data.map.tiles[idx] = TileType::Floor;
build_data.spawn_list.push((idx, spawner::equipment_table(Some(difficulty)).roll(rng))); build_data.spawn_list.push((
idx,
spawner::equipment_table(Some(difficulty)).roll(rng),
));
} }
_ => { _ => {
rltk::console::log(format!("Unknown glyph '{}' when loading prefab", (ch as u8) as char)); console::log(format!("Unknown glyph '{}' when loading prefab", ch as u8 as char));
}
}
}
fn overmap_char_to_map(
&mut self,
ch: char,
idx: usize,
_rng: &mut RandomNumberGenerator,
build_data: &mut BuilderMap
) {
match ch {
' ' => {
build_data.map.tiles[idx] = TileType::Floor;
}
'.' => {
build_data.map.tiles[idx] = TileType::Grass;
}
'#' => {
build_data.map.tiles[idx] = TileType::Wall;
}
'>' => {
build_data.map.tiles[idx] = TileType::DownStair;
}
'~' => {
build_data.map.tiles[idx] = TileType::ShallowWater;
}
'≈' => {
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::Grass;
build_data.starting_position = Some(Position { x: x as i32, y: y as i32 });
}
'^' => {
build_data.map.tiles[idx] = TileType::ImpassableMountain;
}
'1' => {
build_data.map.tiles[idx] = TileType::ToLocal(ID_TOWN);
}
'2' => {
build_data.map.tiles[idx] = TileType::ToLocal(ID_INFINITE);
}
_ => {
console::log(format!("Unknown glyph '{}' when loading overmap", ch as u8 as char));
} }
} }
} }
#[allow(dead_code)] #[allow(dead_code)]
fn load_rex_map(&mut self, path: &str, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { fn load_rex_map(
let xp_file = rltk::rex::XpFile::from_resource(path).unwrap(); &mut self,
path: &str,
rng: &mut RandomNumberGenerator,
build_data: &mut BuilderMap
) {
let xp_file = rex::XpFile::from_resource(path).unwrap();
for layer in &xp_file.layers { for layer in &xp_file.layers {
for y in 0..layer.height { for y in 0..layer.height {
for x in 0..layer.width { for x in 0..layer.width {
let cell = layer.get(x, y).unwrap(); let cell = layer.get(x, y).unwrap();
if x < build_data.map.width as usize && y < build_data.map.height as usize { 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); 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 // 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, rng, build_data); self.char_to_map(cell.ch as u8 as char, idx, rng, build_data);
@ -146,9 +230,12 @@ impl PrefabBuilder {
} }
fn read_ascii_to_vec(template: &str) -> Vec<char> { fn read_ascii_to_vec(template: &str) -> Vec<char> {
let mut string_vec: Vec<char> = template.chars().filter(|a| *a != '\r' && *a != '\n').collect(); let mut string_vec: Vec<char> = template
.chars()
.filter(|a| *a != '\r' && *a != '\n')
.collect();
for c in string_vec.iter_mut() { for c in string_vec.iter_mut() {
if *c as u8 == 160u8 { if (*c as u8) == 160u8 {
*c = ' '; *c = ' ';
} }
} }
@ -161,16 +248,21 @@ impl PrefabBuilder {
level: &prefab_levels::PrefabLevel, level: &prefab_levels::PrefabLevel,
rng: &mut RandomNumberGenerator, rng: &mut RandomNumberGenerator,
build_data: &mut BuilderMap, build_data: &mut BuilderMap,
overmap: bool
) { ) {
let string_vec = PrefabBuilder::read_ascii_to_vec(level.template); let string_vec = PrefabBuilder::read_ascii_to_vec(level.template);
let mut i = 0; let mut i = 0;
for ty in 0..level.height { for ty in 0..level.height {
for tx in 0..level.width { for tx in 0..level.width {
if tx < build_data.map.width as usize && ty < build_data.map.height as usize { 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); let idx = build_data.map.xy_idx(tx as i32, ty as i32);
if i < string_vec.len() { if i < string_vec.len() {
self.char_to_map(string_vec[i], idx, rng, build_data); if overmap {
self.overmap_char_to_map(string_vec[i], idx, rng, build_data);
} else {
self.char_to_map(string_vec[i], idx, rng, build_data);
}
} }
} }
i += 1; i += 1;
@ -182,14 +274,14 @@ impl PrefabBuilder {
&mut self, &mut self,
mut filter: F, mut filter: F,
_rng: &mut RandomNumberGenerator, _rng: &mut RandomNumberGenerator,
build_data: &mut BuilderMap, build_data: &mut BuilderMap
) where )
F: FnMut(i32, i32) -> bool, where F: FnMut(i32, i32) -> bool
{ {
let width = build_data.map.width; let width = build_data.map.width;
build_data.spawn_list.retain(|(idx, _name)| { build_data.spawn_list.retain(|(idx, _name)| {
let x = *idx as i32 % width; let x = (*idx as i32) % width;
let y = *idx as i32 / width; let y = (*idx as i32) / width;
filter(x, y) filter(x, y)
}); });
build_data.take_snapshot(); build_data.take_snapshot();
@ -200,7 +292,7 @@ impl PrefabBuilder {
&mut self, &mut self,
section: &prefab_sections::PrefabSection, section: &prefab_sections::PrefabSection,
rng: &mut RandomNumberGenerator, rng: &mut RandomNumberGenerator,
build_data: &mut BuilderMap, build_data: &mut BuilderMap
) { ) {
use prefab_sections::*; use prefab_sections::*;
@ -209,36 +301,52 @@ impl PrefabBuilder {
// Place the new section // Place the new section
let chunk_x; let chunk_x;
match section.placement.0 { match section.placement.0 {
HorizontalPlacement::Left => chunk_x = 0, HorizontalPlacement::Left => {
HorizontalPlacement::Center => chunk_x = (build_data.map.width / 2) - (section.width as i32 / 2), chunk_x = 0;
HorizontalPlacement::Right => chunk_x = (build_data.map.width - 1) - section.width as i32, }
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; let chunk_y;
match section.placement.1 { match section.placement.1 {
VerticalPlacement::Top => chunk_y = 0, VerticalPlacement::Top => {
VerticalPlacement::Center => chunk_y = (build_data.map.height / 2) - (section.height as i32 / 2), chunk_y = 0;
VerticalPlacement::Bottom => chunk_y = (build_data.map.height - 1) - section.height as i32, }
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 // Build the map
self.apply_previous_iteration( self.apply_previous_iteration(
|x, y| { |x, y| {
x < chunk_x x < chunk_x ||
|| x > (chunk_x + section.width as i32) x > chunk_x + (section.width as i32) ||
|| y < chunk_y y < chunk_y ||
|| y > (chunk_y + section.height as i32) y > chunk_y + (section.height as i32)
}, },
rng, rng,
build_data, build_data
); );
let mut i = 0; let mut i = 0;
for ty in 0..section.height { for ty in 0..section.height {
for tx in 0..section.width { 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 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); let idx = build_data.map.xy_idx((tx as i32) + chunk_x, (ty as i32) + chunk_y);
if i < string_vec.len() { if i < string_vec.len() {
self.char_to_map(string_vec[i], idx, rng, build_data); self.char_to_map(string_vec[i], idx, rng, build_data);
} }
@ -274,7 +382,7 @@ impl PrefabBuilder {
FLUFF2_6X3, FLUFF2_6X3,
HOUSE_NOTRAP_7X7, HOUSE_NOTRAP_7X7,
HOUSE_TRAP_7X7, HOUSE_TRAP_7X7,
ORC_HOUSE_8X8, ORC_HOUSE_8X8
]; ];
// Filter the vault list down to ones that are applicable to the current id // Filter the vault list down to ones that are applicable to the current id
@ -313,8 +421,12 @@ impl PrefabBuilder {
let roll = rng.roll_dice(1, 4); let roll = rng.roll_dice(1, 4);
match roll { match roll {
1 => {} 1 => {}
2 => flip_x = true, 2 => {
3 => flip_y = true, flip_x = true;
}
3 => {
flip_y = true;
}
_ => { _ => {
flip_x = true; flip_x = true;
flip_y = true; flip_y = true;
@ -328,14 +440,15 @@ impl PrefabBuilder {
let mut idx = 0usize; let mut idx = 0usize;
loop { loop {
let x = (idx % build_data.map.width as usize) as i32; let x = (idx % (build_data.map.width as usize)) as i32;
let y = (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 // Check that we won't overflow the map
if x > 1 if
&& (x + vault.width as i32) < build_data.map.width - 2 x > 1 &&
&& y > 1 x + (vault.width as i32) < build_data.map.width - 2 &&
&& (y + vault.height as i32) < build_data.map.height - 2 y > 1 &&
y + (vault.height as i32) < build_data.map.height - 2
{ {
let mut possible = true; let mut possible = true;
for ty in 0..vault.height as i32 { for ty in 0..vault.height as i32 {
@ -379,7 +492,10 @@ impl PrefabBuilder {
let idx = e.0 as i32; let idx = e.0 as i32;
let x = idx % width; let x = idx % width;
let y = idx / height; let y = idx / height;
x < chunk_x || x > chunk_x + vault.width as i32 || y < chunk_y || y > chunk_y + vault.height as i32 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 string_vec = PrefabBuilder::read_ascii_to_vec(vault.template);
@ -390,17 +506,22 @@ impl PrefabBuilder {
let mut y_: i32 = tile_y as i32; let mut y_: i32 = tile_y as i32;
// Handle flipping // Handle flipping
if flip_x { if flip_x {
x_ = vault.width as i32 - 1 - x_; x_ = (vault.width as i32) - 1 - x_;
} }
if flip_y { if flip_y {
y_ = vault.height as i32 - 1 - y_; y_ = (vault.height as i32) - 1 - y_;
} }
if x_ < 0 || y_ < 0 { if x_ < 0 || y_ < 0 {
// If either of these go below 0, we run the risk of CTD, so just panic. // If either of these go below 0, we run the risk of CTD, so just panic.
// Something went wrong with flipping/rotating/defining a vault. // Something went wrong with flipping/rotating/defining a vault.
panic!( panic!(
"X or Y went below 0 when trying to place a vault! DEBUGINFO == [H: {}, W: {}; FLIPPED X: {}, FLIPPED Y: {}; X_: {}, Y_: {}]", "X or Y went below 0 when trying to place a vault! DEBUGINFO == [H: {}, W: {}; FLIPPED X: {}, FLIPPED Y: {}; X_: {}, Y_: {}]",
vault.width, vault.height, flip_x, flip_y, x_, y_ vault.width,
vault.height,
flip_x,
flip_y,
x_,
y_
); );
} }
let idx = build_data.map.xy_idx(x_ + chunk_x, y_ + chunk_y); let idx = build_data.map.xy_idx(x_ + chunk_x, y_ + chunk_y);

View file

@ -7,9 +7,11 @@ pub struct PrefabLevel {
#[allow(dead_code)] #[allow(dead_code)]
pub const WFC_POPULATED: PrefabLevel = PrefabLevel { template: LEVEL_MAP, width: 80, height: 43 }; pub const WFC_POPULATED: PrefabLevel = PrefabLevel { template: LEVEL_MAP, width: 80, height: 43 };
pub const OVERMAP: PrefabLevel = PrefabLevel { template: OVERMAP_TEMPLATE, width: 69, height: 41 };
#[allow(dead_code)] #[allow(dead_code)]
const LEVEL_MAP: &str = " const LEVEL_MAP: &str =
"
################################################################################ ################################################################################
#          ########################################################    ######### #          ########################################################    #########
#    @     ######    #########       ####     ###################        ####### #    @     ######    #########       ####     ###################        #######
@ -53,3 +55,47 @@ const LEVEL_MAP: &str = "
#!%^   ###  ###     ############### ########      ##### g     ####      # g#   # #!%^   ###  ###     ############### ########      ##### g     ####      # g#   #
# %^##  ^   ###     ############### ########      #####       ################## # %^##  ^   ###     ############### ########      #####       ##################
################################################################################"; ################################################################################";
const OVERMAP_TEMPLATE: &str =
"
^^^^^^^^^^^^^^^^^^^^^^^^^^^........
^^^^^^^^^^^^^^^^^^^^^^^^^^...........
^^^^^^^^^^^^^^^....^^^^^^.............
^^^^^^^^^^^^^^.................................
^^^^^^^^^^^^^.....................................
^^^^^^^^^^^.....................................
^^^^^^^^^.......................................
^^^^^^^^........................................
^^^^^^^..........................................
^^^^...........................................
^^.......................................
^^..............................................
^..................................................
^...................................................
^......................................................
^.........................................................
^.............................................................
^..............................................................
^..............................................................
^^.............................................................
^^.............................................................
^^.............................................................
^^^............................................................
^^^^............................................................
^^^^^...........................................................
^^^^^.^^........................................................
^^^^..^^^.......................................................
^^^...^^^......................................................
^^^2.^^^^................................................
^^^^^^^^................................................
^^^^^^^.............................................
^^^^^^^...............................
^^^^^^^.............................
^^^^^^^^...........................
^^^^^^^^......@.....................
^^^^^^^^^...............................
^^^^^^^^^........1..............
^^^^^^^^^^..................
^^^^^^^^^^.............
^^^^^^^^^^........
^^^^^^^^^^^^.";

View file

@ -1,10 +1,10 @@
use super::{ spawner, BuilderMap, MetaMapBuilder }; use super::{ spawner, BuilderMap, MetaMapBuilder };
use rltk::RandomNumberGenerator; use bracket_lib::prelude::*;
pub struct RoomBasedSpawner {} pub struct RoomBasedSpawner {}
impl MetaMapBuilder for RoomBasedSpawner { impl MetaMapBuilder for RoomBasedSpawner {
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
@ -27,7 +27,7 @@ impl RoomBasedSpawner {
); );
} }
} else { } else {
panic!("RoomBasedSpawner only works after rooms have been created"); unreachable!("RoomBasedSpawner tried to run without any rooms.");
} }
} }
} }

View file

@ -1,10 +1,10 @@
use super::{ BuilderMap, MetaMapBuilder, TileType }; use super::{ BuilderMap, MetaMapBuilder, TileType };
use rltk::RandomNumberGenerator; use bracket_lib::prelude::*;
pub struct RoomBasedStairs {} pub struct RoomBasedStairs {}
impl MetaMapBuilder for RoomBasedStairs { impl MetaMapBuilder for RoomBasedStairs {
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
@ -17,12 +17,12 @@ impl RoomBasedStairs {
fn build(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { fn build(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
if let Some(rooms) = &build_data.rooms { if let Some(rooms) = &build_data.rooms {
let stairs_position = rooms[rooms.len() - 1].centre(); let stairs_position = rooms[rooms.len() - 1].center();
let stairs_idx = build_data.map.xy_idx(stairs_position.0, stairs_position.1); let stairs_idx = build_data.map.xy_idx(stairs_position.x, stairs_position.y);
build_data.map.tiles[stairs_idx] = TileType::DownStair; build_data.map.tiles[stairs_idx] = TileType::DownStair;
build_data.take_snapshot(); build_data.take_snapshot();
} else { } else {
panic!("RoomBasedStairs only works after rooms have been created"); unreachable!("RoomBasedStairs tried to run without any rooms.");
} }
} }
} }

View file

@ -1,10 +1,10 @@
use super::{ BuilderMap, MetaMapBuilder, Position }; use super::{ BuilderMap, MetaMapBuilder, Position };
use rltk::RandomNumberGenerator; use bracket_lib::prelude::*;
pub struct RoomBasedStartingPosition {} pub struct RoomBasedStartingPosition {}
impl MetaMapBuilder for RoomBasedStartingPosition { impl MetaMapBuilder for RoomBasedStartingPosition {
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
@ -17,10 +17,10 @@ impl RoomBasedStartingPosition {
fn build(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { fn build(&mut self, _rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
if let Some(rooms) = &build_data.rooms { if let Some(rooms) = &build_data.rooms {
let start_pos = rooms[0].centre(); let start_pos = rooms[0].center();
build_data.starting_position = Some(Position { x: start_pos.0, y: start_pos.1 }); build_data.starting_position = Some(Position { x: start_pos.x, y: start_pos.y });
} else { } else {
panic!("RoomBasedStartingPosition only works after rooms have been created"); unreachable!("RoomBasedStartingPosition tried to run without any rooms.");
} }
} }
} }

View file

@ -1,10 +1,10 @@
use super::{ BuilderMap, MetaMapBuilder, Rect, TileType }; use super::{ BuilderMap, MetaMapBuilder, Rect, TileType };
use rltk::RandomNumberGenerator; use bracket_lib::prelude::*;
pub struct RoomCornerRounder {} pub struct RoomCornerRounder {}
impl MetaMapBuilder for RoomCornerRounder { impl MetaMapBuilder for RoomCornerRounder {
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
@ -42,7 +42,7 @@ impl RoomCornerRounder {
if let Some(rooms_builder) = &build_data.rooms { if let Some(rooms_builder) = &build_data.rooms {
rooms = rooms_builder.clone(); rooms = rooms_builder.clone();
} else { } else {
panic!("RoomCornerRounding requires a builder with rooms."); unreachable!("RoomCornerRounding tried to run without any rooms.");
} }
for room in rooms.iter() { for room in rooms.iter() {

View file

@ -1,10 +1,10 @@
use super::{ BuilderMap, MetaMapBuilder, Rect, TileType }; use super::{ BuilderMap, MetaMapBuilder, Rect, TileType };
use rltk::RandomNumberGenerator; use bracket_lib::prelude::*;
pub struct RoomDrawer {} pub struct RoomDrawer {}
impl MetaMapBuilder for RoomDrawer { impl MetaMapBuilder for RoomDrawer {
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
@ -28,13 +28,17 @@ impl RoomDrawer {
fn circle(&mut self, build_data: &mut BuilderMap, room: &Rect) { 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 radius = (i32::min(room.x2 - room.x1, room.y2 - room.y1) as f32) / 2.0;
let center = room.centre(); let center = room.center();
let center_pt = rltk::Point::new(center.0, center.1); let center_pt = Point::new(center.x, center.y);
for y in room.y1..=room.y2 { for y in room.y1..=room.y2 {
for x in room.x1..=room.x2 { for x in room.x1..=room.x2 {
let idx = build_data.map.xy_idx(x, y); let idx = build_data.map.xy_idx(x, y);
let distance = rltk::DistanceAlg::Pythagoras.distance2d(center_pt, rltk::Point::new(x, y)); let distance = DistanceAlg::Pythagoras.distance2d(center_pt, Point::new(x, y));
if idx > 0 && idx < ((build_data.map.width * build_data.map.height - 1) as usize) && distance <= radius { if
idx > 0 &&
idx < ((build_data.map.width * build_data.map.height - 1) as usize) &&
distance <= radius
{
build_data.map.tiles[idx] = TileType::Floor; build_data.map.tiles[idx] = TileType::Floor;
} }
} }
@ -46,7 +50,7 @@ impl RoomDrawer {
if let Some(rooms_builder) = &build_data.rooms { if let Some(rooms_builder) = &build_data.rooms {
rooms = rooms_builder.clone(); rooms = rooms_builder.clone();
} else { } else {
panic!("RoomDrawer require a builder with rooms"); unreachable!("RoomDrawer tried to run without any rooms.");
} }
for room in rooms.iter() { for room in rooms.iter() {

View file

@ -1,10 +1,10 @@
use super::{ paint, BuilderMap, MetaMapBuilder, Rect, Symmetry, TileType }; use super::{ paint, BuilderMap, MetaMapBuilder, Rect, Symmetry, TileType };
use rltk::RandomNumberGenerator; use bracket_lib::prelude::*;
pub struct RoomExploder {} pub struct RoomExploder {}
impl MetaMapBuilder for RoomExploder { impl MetaMapBuilder for RoomExploder {
fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data: &mut BuilderMap) { fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data); self.build(rng, build_data);
} }
} }
@ -20,15 +20,15 @@ impl RoomExploder {
if let Some(rooms_builder) = &build_data.rooms { if let Some(rooms_builder) = &build_data.rooms {
rooms = rooms_builder.clone(); rooms = rooms_builder.clone();
} else { } else {
panic!("RoomExploder requires a builder with rooms."); unreachable!("RoomExploder tried to run without any rooms.");
} }
for room in rooms.iter() { for room in rooms.iter() {
let start = room.centre(); let start = room.center();
let n_diggers = rng.roll_dice(1, 20) - 5; let n_diggers = rng.roll_dice(1, 20) - 5;
if n_diggers > 0 { if n_diggers > 0 {
for _i in 0..n_diggers { for _i in 0..n_diggers {
let mut drunk_x = start.0; let mut drunk_x = start.x;
let mut drunk_y = start.1; let mut drunk_y = start.y;
let mut drunk_life = 20; let mut drunk_life = 20;
let mut did_something = false; let mut did_something = false;

Some files were not shown because too many files have changed in this diff Show more