Compare commits

..

109 commits

Author SHA1 Message Date
Llywelwyn
6d4ab70c7b prompt() helper fn, help screen with placeholder text 2023-10-25 00:22:54 +01:00
Llywelwyn
e3a8c545fb .bin 2023-10-24 11:12:25 +01:00
Llywelwyn
17bebf03a4 .bin saves instead of json. it wasn't readable anyway 2023-10-24 11:11:50 +01:00
Llywelwyn
d1fe86f645 draw equipped items on player avatar - framework 2023-10-14 21:06:28 +01:00
Llywelwyn
6ba7778df0 slight volume variation on SFX to reduce uniformity 2023-10-14 21:05:54 +01:00
Llywelwyn
be7ff6b895 initial hit sounds 2023-10-14 21:04:24 +01:00
Llywelwyn
7ac2e14471 wav to ogg for ambient tracks 2023-10-12 15:33:26 +01:00
Llywelwyn
b105a415d5 door sounds, ambient tracks 2023-10-12 15:22:13 +01:00
Llywelwyn
0c74531c5b ambient track storing/replacing 2023-10-12 09:28:16 +01:00
Llywelwyn
e8d376fecf cleans up particles, and SFX/ambience w/ placeholders 2023-10-12 08:05:17 +01:00
Llywelwyn
465cd51a60 bloodsplosion 2023-10-11 17:33:51 +01:00
Llywelwyn
1de0c20b76 rm deprecated log 2023-10-11 15:35:39 +01:00
Llywelwyn
6c359a0780 as last commit - should have been included 2023-10-11 09:48:09 +01:00
Llywelwyn
13322eb2a1 adds viewport_to_px and viewport_to_idx
used for converting a viewport (x, y) value to the correct pixel location on the screen, and to the correct map index
2023-10-11 09:47:56 +01:00
Llywelwyn
f862f00f0b fixes up farlooking - sprite, etc. 2023-10-11 09:46:41 +01:00
Llywelwyn
deb9988935 removes some unneeded vision ranges 2023-10-11 08:48:44 +01:00
Llywelwyn
098617fbf1 log scales - drawn to rendertexture, and then *that* is drawn to draw 2023-10-11 08:48:27 +01:00
Llywelwyn
72a6ff6d14 up/downstair sprites 2023-10-10 19:07:38 +01:00
Llywelwyn
3c9eb2de27 draws entity hp bars above heads again 2023-10-10 19:07:30 +01:00
Llywelwyn
074e2465d7 draw now uses a projection - log text still needs tweaking
notan::text doesn't seem to use projections, so the log will need to be converted to notan::draw, or a work-around will need to be found.
2023-10-10 19:07:14 +01:00
Llywelwyn
59f7e9a8f7 clamps colour offsets to +-50%, rather than -50% to +100% 2023-10-07 11:05:37 +01:00
Llywelwyn
0ee2a80715 tile variants, and applying colour offsets again 2023-10-07 10:36:36 +01:00
Llywelwyn
396d548bf2 .col() and .offset() impls for TileTypes - and some more sprites in 2023-10-07 00:56:02 +01:00
Llywelwyn
40b048fd65 tweaks to make zoomfactor better - centred @ 2x zoom 2023-10-06 10:33:25 +01:00
Llywelwyn
af1040b970 player avatar - currently bare. need a component for avatar_sprite! 2023-10-06 10:06:04 +01:00
Llywelwyn
44b0674b5a the oryx-ening - zoom factor, and renderables refactor
currently extremely unfinished - half the sprites are gnomes, and tiles have no colours
2023-10-06 09:22:11 +01:00
Llywelwyn
8bb6a54a39 cleans up draw_targeting() with some bitflags 2023-10-05 10:27:43 +01:00
Llywelwyn
95c642d4ef rudimentary ShowTargeting - it needs a visual improvement, but it's back 2023-10-05 09:55:16 +01:00
Llywelwyn
a92f60bb15 makes keyhandling debug msgs optional 2023-10-05 04:38:34 +01:00
Llywelwyn
bc4f960d88 fixes missing borrow 2023-10-05 04:38:22 +01:00
Llywelwyn
a9dd729a2b removes mapgen debug msg 2023-10-05 04:38:09 +01:00
Llywelwyn
bb775761de Merge branch 'master' into notan
keeping things up to date. lets not have any annoying conflicts to fix later.
2023-10-05 04:30:23 +01:00
Llywelwyn
139e718fd1 more "cleanup" 2023-10-05 04:24:20 +01:00
Llywelwyn
9c1298df6b itemtypes, and filtering items by itemtype/buc/etc. 2023-10-05 03:54:31 +01:00
Llywelwyn
f3af75bf44 ensure key assignments are handled for consumed/deleted items 2023-10-05 01:05:00 +01:00
Llywelwyn
d11971126c static keys - items in inventory will save their assigned key
precursor to cleaning up/modularising inventory display, instead of needing to iterate through every item held to find unique copies, we can just check if the button pressed corresponds to any entity's Key {} index
2023-10-05 00:52:34 +01:00
Llywelwyn
a7b4f621fb better font support 2023-10-03 01:56:38 +01:00
Llywelwyn
71576f36c3 fixes reversed menu controls 2023-10-01 00:11:21 +01:00
Llywelwyn
a99f16b8db Icingdeath 2023-09-30 09:16:22 +01:00
Llywelwyn
0d834f5e23 draw showinventory 2023-09-30 08:53:16 +01:00
Llywelwyn
b524cc3b08 showinventory runstate (draw nyi) 2023-09-30 08:13:20 +01:00
Llywelwyn
56f6cb6ae8 attributes pad left so they stay aligned 2023-09-30 06:49:24 +01:00
Llywelwyn
c80cb01816 charcreation runstate 2023-09-30 06:40:35 +01:00
Llywelwyn
f4d4b414d5 draw_charcreation() 2023-09-30 06:30:48 +01:00
Llywelwyn
ec8793180d mainmenu draw 2023-09-30 03:24:30 +01:00
Llywelwyn
855304abc0 main menu runstate 2023-09-30 01:16:14 +01:00
Llywelwyn
ac0da55d14 temporarily making the gui code a mess, and fixing it later 2023-09-29 03:39:58 +01:00
Llywelwyn
d66bc0d746 de-sprite hp bar 2023-09-29 03:15:33 +01:00
Llywelwyn
719cedf526 viewport-sized dungeons 2023-09-29 02:53:14 +01:00
Llywelwyn
c757466df1 attribute exercising 2023-09-27 22:12:53 +01:00
Llywelwyn
4d614daad5 better attributes 2023-09-27 19:21:22 +01:00
Llywelwyn
1e5f565824 exercising/abusing attributes 2023-09-27 18:08:40 +01:00
Llywelwyn
046837d9a1 returning the fade2black with distance 2023-09-27 03:39:22 +01:00
Llywelwyn
cfbe4098b7 map memory 2023-09-27 02:32:51 +01:00
Llywelwyn
054468bbae hp bars 2023-09-26 22:44:44 +01:00
Llywelwyn
e723f27375 scroll sprites 2023-09-26 22:26:57 +01:00
Llywelwyn
0d230c7721 sanity check to ensure .swap() isn't being called for no reason 2023-09-26 22:11:48 +01:00
Llywelwyn
ae9f6b6ac6 proper alt sprite and render order swaps 2023-09-26 22:08:05 +01:00
Llywelwyn
06d5674199 trapdoors 2023-09-26 21:09:47 +01:00
Llywelwyn
bd450e806b improved sprites - spriteinfo and methods 2023-09-26 20:40:17 +01:00
Llywelwyn
849c400497 hp/mp bars 2023-09-26 18:27:41 +01:00
Llywelwyn
4094d3535c hunger to ui 2023-09-26 18:05:59 +01:00
Llywelwyn
d6ba6c628c sprites for entities, with text glyph fallback 2023-09-26 17:23:25 +01:00
Llywelwyn
2c4b4ca143 added gameover, added class/ancestry defaults 2023-09-25 22:04:49 +01:00
Llywelwyn
65ec5c1b15 cleanup 2023-09-25 20:50:30 +01:00
Llywelwyn
db29b60551 mapgen fix 2023-09-25 20:44:05 +01:00
Llywelwyn
e258767405 finalised gamelog 2023-09-25 20:38:52 +01:00
Llywelwyn
8de3648bae notan::text message log tests 2023-09-25 17:40:48 +01:00
Llywelwyn
093f9df86e action w/ direction, and mapgen ctd fix 2023-09-25 16:06:45 +01:00
Llywelwyn
030bda215a icon, wider log 2023-09-25 06:15:20 +01:00
Llywelwyn
c499528616 rem deprecated 2023-09-25 05:18:25 +01:00
Llywelwyn
b459966f95 alt log solution 2023-09-25 05:10:21 +01:00
Llywelwyn
aa63431d57 Notan ver 2023-09-25 04:51:38 +01:00
Llywelwyn
cdf0257598 rudimentary reimplementation of the log 2023-09-25 04:05:57 +01:00
Llywelwyn
1cd9f75ecc curses16x16 to spritesheet 2023-09-25 03:34:53 +01:00
Llywelwyn
6cc659fd23 scuffed entity chars 2023-09-25 03:11:05 +01:00
Llywelwyn
0586c2cdad map and turns 2023-09-25 02:16:06 +01:00
Llywelwyn
1038573a3a backpack 2023-09-25 01:47:40 +01:00
Llywelwyn
4e24a9b50a equip list - placeholder 2023-09-25 01:41:01 +01:00
Llywelwyn
df211c5c10 ui bulk 2023-09-25 01:30:05 +01:00
Llywelwyn
f3e58ad761 started on new ui fn 2023-09-25 00:36:11 +01:00
Llywelwyn
627d33b2d9 pixelfont 2023-09-25 00:05:26 +01:00
Llywelwyn
7f9ba34afa fixes cheatmenu, reimpl particle ticker 2023-09-24 23:56:29 +01:00
Llywelwyn
0d4c0c9558 data to consts, cheatmenu 2023-09-24 23:46:27 +01:00
Llywelwyn
a2fb893f49 mapgen runstate 2023-09-24 22:20:49 +01:00
Llywelwyn
7f02a5a30f draw_bg() 2023-09-24 21:24:11 +01:00
Llywelwyn
e482b29fc6 reimpl farlook (tooltips NYI) 2023-09-24 19:16:47 +01:00
Llywelwyn
e8aa7494a4 fixing the sea of red - entity rendering for things in view 2023-09-24 16:55:47 +01:00
Llywelwyn
643ecfcf3e drawing entities, and map memory 2023-09-24 16:15:24 +01:00
Llywelwyn
d9489e7ddc camera rendering, and breaking things temporarily 2023-09-24 14:23:48 +01:00
Llywelwyn
1299524c91 reimplementing gameloop with notan's App, instead of bracket-lib BTerm 2023-09-24 00:41:02 +01:00
Llywelwyn
683ab95531 removes ctx arg from get_screen_bounds() 2023-09-24 00:01:17 +01:00
Llywelwyn
be1c7aa1c7 draw "tiles" 2023-09-23 23:33:50 +01:00
Llywelwyn
2967cddf7b init 2023-09-23 23:15:36 +01:00
Llywelwyn
a573971d49 include dep 2023-09-23 22:29:42 +01:00
Llywelwyn
2d55f83bb3 del spritesheet test grid 2023-09-23 20:54:49 +01:00
Llywelwyn
670b365def refining the sprites 2023-09-23 19:35:51 +01:00
Llywelwyn
d58614b106 optional sprites for entities 2023-09-23 19:01:39 +01:00
Llywelwyn
cee4d02ce2 sprites init 2023-09-23 18:02:51 +01:00
Llywelwyn
8337f202cf tile clipping fixes 2023-09-23 11:58:34 +01:00
Llywelwyn
141d4b63d2 fixes infini-dungeon difficulty 2023-09-23 11:14:32 +01:00
Llywelwyn
ddcfd72318 fixing up some post-merge errors 2023-09-23 10:43:32 +01:00
Llywelwyn
441b22439f Merge branch 'master' into switching_to_draw_batches 2023-09-23 10:12:26 +01:00
Llywelwyn
a29a7f5be4 formatting 2023-09-03 22:47:59 +01:00
Llywelwyn
ae3e061ce8 back to curses -- still needs tweaking
box drawing glyphs are misaligned, etc
2023-09-03 08:42:10 +01:00
Llywelwyn
ebcce3183b fixes off-by-one pixels with a small gutter
not sure why this fixes it - but it does. needs some testing.
2023-09-03 06:56:05 +01:00
Llywelwyn
1bea2c0026 testing resizing
strange artifacts with this size of font. seems to be an OpenGL issue (on specific hardware?) - issue is present on two different devices, with different screen resolutions, but disappears when running the WASM build. might be easiest to return to something which doesn't have this issue.
2023-09-03 06:40:27 +01:00
Llywelwyn
4e0ed95a22 infinite font variations for testing - huge wip 2023-09-03 05:10:17 +01:00
Llywelwyn
2a3c59ad33 first steps - extreme wip
1. need to finish curses12x24 first of all
2. bind everything to the viewport, and make scalable
2023-09-03 01:45:18 +01:00
112 changed files with 16545 additions and 2234 deletions

View file

@ -12,7 +12,7 @@ env:
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

3
.gitignore vendored
View file

@ -3,6 +3,7 @@ target
wasm/index.css
wasm/index.html
docs/gifs/*
resources/archived resources
# VSCode/IDE config files
Cargo.lock
@ -12,7 +13,7 @@ Cargo.lock
.prettierrc.json
# Save files, morgue files
savegame.json
savegame.bin
morgue
# A default user config will be written on first run, if not present

View file

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

View file

@ -2,18 +2,18 @@
#### 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:
check out the page in the header for the wasm version, pick [a release of your choice](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)
this year for roguelikedev does the complete tutorial, i followed along with thebracket's [_roguelike tutorial - in rust_](https://bfnightly.bracketproductions.com). the notes i made during the sprint are being kept below for posterity - further changes since then are noted in [changelog.txt](https://github.com/Llywelwyn/rust-rl/blob/9150ed39e45bee536060cdc769d274e639021012/changelog.txt), and in the release notes.
i'm also working on translating over my progress into blog entries on my site @ [llyw.co.uk](https://llyw.co.uk/), with a larger focus on some of the more interesting implementation details.
---
<details>
<summary>boring details about the sprint where this project started</summary>
<details>
<summary>week 1</summary>
@ -157,4 +157,3 @@ check out the page in the header for the wasm version, pick [a release](https://
![squares](https://github.com/Llywelwyn/rust-rl/assets/82828093/b752e1cb-340d-475d-84ae-68fdb4977a80)
</details>
</details>

View file

@ -49,7 +49,3 @@ Complex example, with negative AC:
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.
- 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

@ -2,7 +2,7 @@
{
"id": "potion_health",
"name": { "name": "potion of health", "plural": "potions of health" },
"renderable": { "glyph": "!", "fg": "#FF00FF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "!", "sprite": "potion", "fg": "#FF00FF", "order": 4 },
"class": "potion",
"weight": 1,
"value": 50,
@ -13,7 +13,7 @@
{
"id": "potion_health_weak",
"name": { "name": "potion of lesser health", "plural": "potions of lesser health" },
"renderable": { "glyph": "!", "fg": "#FF00FF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "!", "sprite": "potion", "fg": "#FF00FF", "order": 4 },
"class": "potion",
"weight": 1,
"value": 25,
@ -24,75 +24,75 @@
{
"id": "scroll_identify",
"name": { "name": "scroll of identify", "plural": "scrolls of identify" },
"renderable": { "glyph": "?", "fg": "#0FFFFF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "?", "sprite": "scroll_writing", "fg": "#0FFFFF", "order": 4 },
"class": "scroll",
"weight": 0.5,
"value": 100,
"flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE", "IDENTIFY"],
"flags": ["CONSUMABLE", "DESTRUCTIBLE", "IDENTIFY"],
"magic": { "class": "uncommon", "naming": "scroll" }
},
{
"id": "scroll_removecurse",
"name": { "name": "scroll of remove curse", "plural": "scrolls of remove curse" },
"renderable": { "glyph": "?", "fg": "#0FFFFF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "?", "sprite": "scroll_writing", "fg": "#0FFFFF", "order": 4 },
"class": "scroll",
"weight": 0.5,
"value": 200,
"flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE", "REMOVE_CURSE"],
"flags": ["CONSUMABLE", "DESTRUCTIBLE", "REMOVE_CURSE"],
"magic": { "class": "rare", "naming": "scroll" }
},
{
"id": "scroll_health",
"name": { "name": "scroll of healing word", "plural": "scrolls of healing word" },
"renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "?", "sprite": "scroll_writing", "fg": "#00FFFF", "order": 4 },
"class": "scroll",
"weight": 0.5,
"value": 50,
"flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"],
"flags": ["CONSUMABLE", "DESTRUCTIBLE"],
"effects": { "particle_line": "*;-;#53f06d;75.0;#f9ff9f;100.0", "ranged": "12", "heal": "1d4+2" },
"magic": { "class": "uncommon", "naming": "scroll" }
},
{
"id": "scroll_mass_health",
"name": { "name": "scroll of mass healing word", "plural": "scrolls of mass healing word" },
"renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "?", "sprite": "scroll_writing", "fg": "#00FFFF", "order": 4 },
"class": "scroll",
"weight": 0.5,
"value": 200,
"flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"],
"flags": ["CONSUMABLE", "DESTRUCTIBLE"],
"effects": { "particle": "*;#53f06d;200.0", "ranged": "12", "aoe": "3", "heal": "1d4+2" },
"magic": { "class": "rare", "naming": "scroll" }
},
{
"id": "scroll_magicmissile",
"name": { "name": "scroll of magic missile", "plural": "scrolls of magic missile" },
"renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "?", "sprite": "scroll_writing", "fg": "#00FFFF", "order": 4 },
"class": "scroll",
"weight": 0.5,
"value": 50,
"flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"],
"flags": ["CONSUMABLE", "DESTRUCTIBLE"],
"effects": { "particle_line": "*;-;#00b7ff;75.0;#f4fc83;100.0", "ranged": "12", "damage": "3d4+3;magic" },
"magic": { "class": "uncommon", "naming": "scroll" }
},
{
"id": "scroll_embers",
"name": { "name": "scroll of embers", "plural": "scrolls of embers" },
"renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "?", "sprite": "scroll_writing", "fg": "#00FFFF", "order": 4 },
"class": "scroll",
"weight": 0.5,
"value": 100,
"flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"],
"flags": ["CONSUMABLE", "DESTRUCTIBLE"],
"effects": { "particle": "*;#FFA500;200.0", "ranged": "10", "damage": "4d6;fire", "aoe": "2" },
"magic": { "class": "uncommon", "naming": "scroll" }
},
{
"id": "scroll_fireball",
"name": { "name": "scroll of fireball", "plural": "scrolls of fireball" },
"renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "?", "sprite": "scroll_writing", "fg": "#00FFFF", "order": 4 },
"class": "scroll",
"weight": 0.5,
"value": 200,
"flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"],
"flags": ["CONSUMABLE", "DESTRUCTIBLE"],
"effects": {
"particle_burst": "▓;*;~;#FFA500;#000000;500.0;#ffd381;60.0",
"ranged": "10",
@ -104,40 +104,40 @@
{
"id": "scroll_confusion",
"name": { "name": "scroll of confusion", "plural": "scrolls of confusion" },
"renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "?", "sprite": "scroll_writing", "fg": "#00FFFF", "order": 4 },
"class": "scroll",
"weight": 0.5,
"value": 100,
"flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"],
"flags": ["CONSUMABLE", "DESTRUCTIBLE"],
"effects": { "particle_line": "*;-;#ad56a6;75.0;#cacaca;100.0", "ranged": "10", "confusion": "4" },
"magic": { "class": "uncommon", "naming": "scroll" }
},
{
"id": "scroll_mass_confusion",
"name": { "name": "scroll of mass confusion", "plural": "scrolls of mass confusion" },
"renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "?", "sprite": "scroll_writing", "fg": "#00FFFF", "order": 4 },
"class": "scroll",
"weight": 0.5,
"value": 200,
"flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"],
"flags": ["CONSUMABLE", "DESTRUCTIBLE"],
"effects": { "particle": "*;#ad56a6;200.0", "ranged": "10", "aoe": "3", "confusion": "3" },
"magic": { "class": "veryrare", "naming": "scroll" }
},
{
"id": "scroll_magicmap",
"name": { "name": "scroll of magic mapping", "plural": "scrolls of magic mapping" },
"renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "?", "sprite": "scroll_writing", "fg": "#00FFFF", "order": 4 },
"class": "scroll",
"weight": 0.5,
"value": 50,
"flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE", "MAGICMAP"],
"flags": ["CONSUMABLE", "DESTRUCTIBLE", "MAGICMAP"],
"effects": {},
"magic": { "class": "common", "naming": "scroll" }
},
{
"id": "equip_dagger",
"name": { "name": "dagger", "plural": "daggers" },
"renderable": { "glyph": ")", "fg": "#808080", "bg": "#000000", "order": 2 },
"renderable": { "glyph": ")", "sprite": "dagger", "fg": "#808080", "order": 4 },
"class": "weapon",
"weight": 1,
"value": 2,
@ -147,7 +147,8 @@
{
"id": "equip_shortsword",
"name": { "name": "shortsword", "plural": "shortswords" },
"renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 },
"renderable": { "glyph": ")", "sprite": "shortsword", "fg": "#C0C0C0", "order": 4 },
"avatar": "a_shortsword",
"class": "weapon",
"weight": 2,
"value": 10,
@ -157,7 +158,7 @@
{
"id": "equip_rapier",
"name": { "name": "rapier", "plural": "rapiers" },
"renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 },
"renderable": { "glyph": ")", "sprite": "gnome", "fg": "#C0C0C0", "order": 4 },
"class": "weapon",
"weight": 2,
"value": 10,
@ -167,7 +168,7 @@
{
"id": "equip_pitchfork",
"name": { "name": "pitchfork", "plural": "pitchforks" },
"renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 },
"renderable": { "glyph": ")", "sprite": "trident", "fg": "#C0C0C0", "order": 4 },
"class": "weapon",
"weight": 2,
"value": 5,
@ -177,7 +178,7 @@
{
"id": "equip_sickle",
"name": { "name": "sickle", "plural": "sickles" },
"renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 },
"renderable": { "glyph": ")", "sprite": "gnome", "fg": "#C0C0C0", "order": 4 },
"class": "weapon",
"weight": 2,
"value": 5,
@ -187,7 +188,7 @@
{
"id": "equip_handaxe",
"name": { "name": "handaxe", "plural": "handaxes" },
"renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 },
"renderable": { "glyph": ")", "sprite": "handaxe", "fg": "#C0C0C0", "order": 4 },
"class": "weapon",
"weight": 2,
"value": 5,
@ -197,17 +198,28 @@
{
"id": "equip_longsword",
"name": { "name": "longsword", "plural": "longswords" },
"renderable": { "glyph": ")", "fg": "#FFF8DC", "bg": "#000000", "order": 2 },
"renderable": { "glyph": ")", "sprite": "longsword", "fg": "#FFF8DC", "order": 4 },
"avatar": "a_longsword",
"class": "weapon",
"weight": 3,
"value": 15,
"flags": ["EQUIP_MELEE"],
"equip": { "flag": "STRENGTH", "damage": "1d8", "to_hit": 0 }
},
{
"id": "artifact_icingdeath",
"name": { "name": "Icingdeath", "plural": "Icingdeath" },
"renderable": { "glyph": ")", "sprite": "scimitar", "fg": "#37aecc", "order": 4 },
"class": "weapon",
"weight": 3,
"value": 300,
"flags": ["EQUIP_MELEE"],
"equip": { "flag": "FINESSE", "damage": "1d8+3;cold", "to_hit": 3 }
},
{
"id": "equip_smallshield",
"name": { "name": "buckler", "plural": "bucklers" },
"renderable": { "glyph": "[", "fg": "#808080", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "shield_small", "fg": "#808080", "order": 4 },
"class": "armour",
"weight": 2,
"value": 5,
@ -217,7 +229,8 @@
{
"id": "equip_mediumshield",
"name": { "name": "medium shield", "plural": "medium shields" },
"renderable": { "glyph": "[", "fg": "#C0C0C0", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "shield_round", "fg": "#C0C0C0", "order": 4 },
"avatar": "a_medshield",
"class": "armour",
"weight": 6,
"value": 10,
@ -227,7 +240,7 @@
{
"id": "equip_largeshield",
"name": { "name": "large shield", "plural": "large shields" },
"renderable": { "glyph": "[", "fg": "#FFF8DC", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "shield_large", "fg": "#FFF8DC", "order": 4 },
"class": "armour",
"weight": 12,
"value": 35,
@ -237,7 +250,7 @@
{
"id": "equip_body_weakleather",
"name": { "name": "leather jacket", "plural": "leather jackets" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "body_leather", "fg": "#aa6000", "order": 4 },
"class": "armour",
"weight": 8,
"value": 5,
@ -247,7 +260,7 @@
{
"id": "equip_body_leather",
"name": { "name": "leather chestpiece", "plural": "leather chestpiece" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "body_leather", "fg": "#aa6000", "order": 4 },
"class": "armour",
"weight": 10,
"value": 10,
@ -257,7 +270,7 @@
{
"id": "equip_body_studdedleather",
"name": { "name": "studded leather chestpiece", "plural": "studded leather chestpieces" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "body_leather", "fg": "#aa6000", "order": 4 },
"class": "armour",
"weight": 13,
"value": 45,
@ -267,7 +280,7 @@
{
"id": "equip_body_ringmail_o",
"name": { "name": "orcish ring mail", "plural": "orcish ring mail" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "body_leather", "fg": "#aa6000", "order": 4 },
"class": "armour",
"weight": 45,
"value": 50,
@ -277,7 +290,7 @@
{
"id": "equip_body_ringmail",
"name": { "name": "ring mail", "plural": "ring mail" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "body_leather", "fg": "#aa6000", "order": 4 },
"class": "armour",
"weight": 45,
"value": 70,
@ -287,7 +300,7 @@
{
"id": "equip_head_leather",
"name": { "name": "leather cap", "plural": "leather caps" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "body_leather", "fg": "#aa6000", "order": 4 },
"class": "armour",
"weight": 2,
"value": 10,
@ -297,7 +310,7 @@
{
"id": "equip_head_elvish",
"name": { "name": "elvish leather helm", "plural": "elvish leather helms" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "body_leather", "fg": "#aa6000", "order": 4 },
"class": "armour",
"weight": 2,
"value": 25,
@ -307,7 +320,7 @@
{
"id": "equip_head_o",
"name": { "name": "orcish helm", "plural": "orcish helm" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "body_leather", "fg": "#aa6000", "order": 4 },
"class": "armour",
"weight": 6,
"value": 25,
@ -317,7 +330,7 @@
{
"id": "equip_head_iron",
"name": { "name": "iron helm", "plural": "iron helm" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "body_leather", "fg": "#aa6000", "order": 4 },
"class": "armour",
"weight": 10,
"value": 45,
@ -327,7 +340,7 @@
{
"id": "equip_feet_leather",
"name": { "name": "leather shoes", "plural": "leather shoes" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "body_leather", "fg": "#aa6000", "order": 4 },
"class": "armour",
"weight": 2,
"value": 10,
@ -336,7 +349,7 @@
{
"id": "equip_feet_elvish",
"name": { "name": "elvish leather shoes", "plural": "elvish leather shoes" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "body_leather", "fg": "#aa6000", "order": 4 },
"class": "armour",
"weight": 2,
"value": 25,
@ -346,7 +359,7 @@
{
"id": "equip_feet_o",
"name": { "name": "orcish boots", "plural": "orcish boots" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "body_leather", "fg": "#aa6000", "order": 4 },
"class": "armour",
"weight": 6,
"value": 25,
@ -356,7 +369,7 @@
{
"id": "equip_feet_iron",
"name": { "name": "iron boots", "plural": "iron boots" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "body_leather", "fg": "#aa6000", "order": 4 },
"class": "armour",
"weight": 10,
"value": 45,
@ -366,7 +379,7 @@
{
"id": "equip_neck_protection",
"name": { "name": "amulet of protection", "plural": "amulets of protection" },
"renderable": { "glyph": "\"", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "\"", "sprite": "body_leather", "fg": "#aa6000", "order": 4 },
"class": "amulet",
"weight": 1,
"value": 200,
@ -376,7 +389,7 @@
{
"id": "equip_back_protection",
"name": { "name": "cloak of protection", "plural": "cloaks of protection" },
"renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "[", "sprite": "body_leather", "fg": "#aa6000", "order": 4 },
"class": "armour",
"weight": 1,
"value": 200,
@ -386,7 +399,7 @@
{
"id": "wand_magicmissile",
"name": { "name": "wand of magic missile", "plural": "wands of magic missile" },
"renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "/", "sprite": "body_leather", "fg": "#00FFFF", "order": 4 },
"class": "wand",
"weight": 2,
"value": 100,
@ -397,7 +410,7 @@
{
"id": "wand_fireball",
"name": { "name": "wand of fireball", "plural": "wands of fireball" },
"renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "/", "sprite": "body_leather", "fg": "#00FFFF", "order": 4 },
"class": "wand",
"weight": 2,
"value": 300,
@ -408,7 +421,7 @@
{
"id": "wand_confusion",
"name": { "name": "wand of confusion", "plural": "wands of confusion" },
"renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "/", "sprite": "body_leather", "fg": "#00FFFF", "order": 4 },
"class": "wand",
"weight": 2,
"value": 200,
@ -419,7 +432,7 @@
{
"id": "wand_digging",
"name": { "name": "wand of digging", "plural": "wands of digging" },
"renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "/", "sprite": "body_leather", "fg": "#00FFFF", "order": 4 },
"class": "wand",
"weight": 2,
"value": 300,
@ -430,7 +443,7 @@
{
"id": "food_rations",
"name": { "name": "rations", "plural": "rations" },
"renderable": { "glyph": "%", "fg": "#FFA07A", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "%", "sprite": "meat", "fg": "#FFA07A", "order": 4 },
"class": "comestible",
"weight": 1,
"value": 1,
@ -439,7 +452,7 @@
{
"id": "food_apple",
"name": { "name": "apple", "plural": "apples" },
"renderable": { "glyph": "%", "fg": "#00FF00", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "%", "sprite": "body_leather", "fg": "#00FF00", "order": 4 },
"class": "comestible",
"weight": 0.5,
"value": 1,

View file

@ -2,7 +2,7 @@
{
"id": "npc_barkeep",
"name": "barkeep",
"renderable": { "glyph": "@", "fg": "#EE82EE", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "@", "sprite": "gnome", "fg": "#EE82EE", "order": 3 },
"flags": ["NEUTRAL", "IS_HUMAN"],
"vision_range": 4,
"quips": ["Drink?", "Something to eat?", "Don't go out on an empty stomach."]
@ -10,7 +10,7 @@
{
"id": "npc_townsperson",
"name": "townsperson",
"renderable": { "glyph": "@", "fg": "#9fa86c", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "@", "sprite": "gnome", "fg": "#9fa86c", "order": 3 },
"flags": ["NEUTRAL", "RANDOM_PATH", "IS_HUMAN"],
"vision_range": 4,
"quips": ["Hello!", "Good morning.", "<a quiet complaint about chores>"]
@ -18,7 +18,7 @@
{
"id": "npc_drunk",
"name": "drunk",
"renderable": { "glyph": "@", "fg": "#a0a83c", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "@", "sprite": "gnome", "fg": "#a0a83c", "order": 3 },
"flags": ["NEUTRAL", "IS_HUMAN"],
"vision_range": 4,
"quips": ["Hic!", "H-Hic'.", "Get me 'nother, would you?"]
@ -26,7 +26,7 @@
{
"id": "npc_fisher",
"name": "fisher",
"renderable": { "glyph": "@", "fg": "#3ca3a8", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "@", "sprite": "gnome", "fg": "#3ca3a8", "order": 3 },
"flags": ["NEUTRAL", "IS_HUMAN"],
"vision_range": 4,
"quips": ["Hey."]
@ -34,7 +34,7 @@
{
"id": "npc_dockworker",
"name": "dock worker",
"renderable": { "glyph": "@", "fg": "#68d8de", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "@", "sprite": "gnome", "fg": "#68d8de", "order": 3 },
"flags": ["NEUTRAL", "IS_HUMAN"],
"vision_range": 4,
"quips": ["No boat for a few days.", "Not much for us to do."]
@ -42,7 +42,7 @@
{
"id": "npc_priest",
"name": "priest",
"renderable": { "glyph": "@", "fg": "#FFFFFF", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "@", "sprite": "gnome", "fg": "#FFFFFF", "order": 3 },
"flags": ["NEUTRAL", "IS_HUMAN"],
"vision_range": 4,
"quips": ["Light's givings.", "<a quiet prayer>", "Bless you."]
@ -50,7 +50,7 @@
{
"id": "npc_miner",
"name": "miner",
"renderable": { "glyph": "@", "fg": "#946123", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "@", "sprite": "gnome", "fg": "#946123", "order": 3 },
"flags": ["NEUTRAL", "IS_HUMAN"],
"vision_range": 4,
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }],
@ -59,7 +59,7 @@
{
"id": "npc_guard",
"name": "smalltown guard",
"renderable": { "glyph": "@", "fg": "#034efc", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "@", "sprite": "gnome", "fg": "#034efc", "order": 3 },
"flags": ["NEUTRAL", "RANDOM_PATH", "IS_HUMAN"],
"level": 2,
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }],
@ -69,7 +69,7 @@
{
"id": "rat",
"name": "rat",
"renderable": { "glyph": "r", "fg": "#aa6000", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "r", "sprite": "rat", "fg": "#aa6000", "order": 3 },
"flags": [],
"bac": 6,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }],
@ -78,7 +78,7 @@
{
"id": "chicken",
"name": "chicken",
"renderable": { "glyph": "c", "fg": "#BB6000", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "c", "sprite": "gnome", "fg": "#BB6000", "order": 3 },
"flags": ["HERBIVORE"],
"bac": 8,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }]
@ -86,7 +86,7 @@
{
"id": "deer_little",
"name": "fawn",
"renderable": { "glyph": "q", "fg": "#a57037", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "q", "sprite": "gnome", "fg": "#a57037", "order": 3 },
"flags": ["HERBIVORE"],
"bac": 8,
"attacks": [{ "name": "kicks", "hit_bonus": 0, "damage": "1d2" }]
@ -94,7 +94,7 @@
{
"id": "sheep_little",
"name": "lamb",
"renderable": { "glyph": "q", "fg": "#e7e7e7", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "q", "sprite": "gnome", "fg": "#e7e7e7", "order": 3 },
"flags": ["HERBIVORE", "SMALL_GROUP"],
"bac": 10,
"attacks": [{ "name": "kicks", "hit_bonus": 0, "damage": "1d2" }]
@ -102,7 +102,7 @@
{
"id": "chicken_little",
"name": "chick",
"renderable": { "glyph": "c", "fg": "#fae478", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "c", "sprite": "gnome", "fg": "#fae478", "order": 3 },
"flags": ["HERBIVORE"],
"bac": 10,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }]
@ -110,7 +110,7 @@
{
"id": "horse_little",
"name": "pony",
"renderable": { "glyph": "u", "fg": "#b36c29", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "u", "sprite": "horse", "fg": "#b36c29", "order": 3 },
"flags": ["HERBIVORE", "MULTIATTACK"],
"level": 3,
"bac": 6,
@ -124,7 +124,7 @@
{
"id": "horse",
"name": "horse",
"renderable": { "glyph": "u", "fg": "#744d29", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "u", "sprite": "horse", "fg": "#744d29", "order": 3 },
"flags": ["MULTIATTACK"],
"level": 5,
"bac": 5,
@ -137,7 +137,7 @@
{
"id": "horse_large",
"name": "warhorse",
"renderable": { "glyph": "u", "fg": "#8a3520", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "u", "sprite": "horse", "fg": "#8a3520", "order": 3 },
"flags": ["MULTIATTACK"],
"level": 7,
"bac": 4,
@ -150,7 +150,7 @@
{
"id": "rat_giant",
"name": "giant rat",
"renderable": { "glyph": "r", "fg": "#bb8000", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "r", "sprite": "rat_large", "fg": "#bb8000", "order": 3 },
"flags": ["SMALL_GROUP"],
"level": 1,
"bac": 7,
@ -160,7 +160,7 @@
{
"id": "dog_little",
"name": "little dog",
"renderable": { "glyph": "d", "fg": "#FFFFFF", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "d", "sprite": "dog", "fg": "#FFFFFF", "order": 3 },
"flags": ["NEUTRAL"],
"level": 2,
"bac": 6,
@ -171,7 +171,7 @@
{
"id": "dog",
"name": "dog",
"renderable": { "glyph": "d", "fg": "#EEEEEE", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "d", "sprite": "dog", "fg": "#EEEEEE", "order": 3 },
"flags": [],
"level": 4,
"bac": 5,
@ -181,7 +181,7 @@
{
"id": "dog_large",
"name": "large dog",
"renderable": { "glyph": "d", "fg": "#DDDDDD", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "d", "sprite": "dog", "fg": "#DDDDDD", "order": 3 },
"flags": [],
"level": 6,
"bac": 4,
@ -191,7 +191,7 @@
{
"id": "gnome",
"name": "gnome",
"renderable": { "glyph": "G", "fg": "#AA5500", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "G", "sprite": "gnome", "fg": "#AA5500", "order": 3 },
"flags": ["SMALL_GROUP", "IS_GNOME"],
"level": 1,
"speed": 6,
@ -201,18 +201,17 @@
{
"id": "zombie_gnome",
"name": "gnome zombie",
"renderable": { "glyph": "z", "fg": "#AA5500", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "z", "sprite": "gnome", "fg": "#AA5500", "order": 3 },
"flags": ["MINDLESS"],
"level": 1,
"speed": 6,
"vision_range": 12,
"attacks": [{ "name": "claws", "hit_bonus": 0, "damage": "1d4" }],
"loot": { "table": "wands", "chance": 0.05 }
},
{
"id": "goblin",
"name": "goblin",
"renderable": { "glyph": "g", "fg": "#00FF00", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "g", "sprite": "goblin", "fg": "#00FF00", "order": 3 },
"flags": [],
"level": 1,
"speed": 9,
@ -221,7 +220,7 @@
{
"id": "kobold",
"name": "kobold",
"renderable": { "glyph": "k", "fg": "#AA5500", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "k", "sprite": "kobold", "fg": "#AA5500", "order": 3 },
"flags": [],
"level": 1,
"speed": 6,
@ -231,41 +230,38 @@
{
"id": "zombie_kobold",
"name": "kobold zombie",
"renderable": { "glyph": "z", "fg": "#AA5500", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "z", "sprite": "kobold", "fg": "#AA5500", "order": 3 },
"flags": ["MINDLESS"],
"level": 1,
"speed": 6,
"vision_range": 12,
"attacks": [{ "name": "claws", "hit_bonus": 0, "damage": "1d4" }],
"loot": { "table": "scrolls", "chance": 0.05 }
},
{
"id": "kobold_large",
"name": "large kobold",
"renderable": { "glyph": "k", "fg": "#70461b", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "k", "sprite": "kobold_large", "fg": "#70461b", "order": 3 },
"flags": [],
"level": 1,
"speed": 6,
"vision_range": 12,
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d6" }],
"loot": { "table": "food", "chance": 0.05 }
},
{
"id": "zombie_orc",
"name": "orc zombie",
"renderable": { "glyph": "z", "fg": "#dbd830", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "z", "sprite": "orc", "fg": "#dbd830", "order": 3 },
"flags": ["MINDLESS"],
"level": 2,
"bac": 9,
"speed": 6,
"vision_range": 12,
"attacks": [{ "name": "claws", "hit_bonus": 0, "damage": "1d6" }],
"loot": { "table": "potions", "chance": 0.05 }
},
{
"id": "dwarf",
"name": "dwarf",
"renderable": { "glyph": "h", "fg": "#d61b1b", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "h", "sprite": "dwarf", "fg": "#d61b1b", "order": 3 },
"flags": ["IS_DWARF"],
"level": 2,
"bac": 10,
@ -277,30 +273,28 @@
{
"id": "zombie_dwarf",
"name": "dwarf zombie",
"renderable": { "glyph": "z", "fg": "#d61b1b", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "z", "sprite": "dwarf", "fg": "#d61b1b", "order": 3 },
"flags": ["MINDLESS"],
"level": 2,
"bac": 9,
"speed": 6,
"vision_range": 12,
"attacks": [{ "name": "claws", "hit_bonus": 0, "damage": "1d6" }],
"loot": { "table": "equipment", "chance": 0.05 }
},
{
"id": "kobold_captain",
"name": "kobold captain",
"renderable": { "glyph": "k", "fg": "#9331ac", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "k", "sprite": "kobold_c", "fg": "#9331ac", "order": 3 },
"flags": [],
"level": 2,
"speed": 6,
"vision_range": 12,
"attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "2d4" }],
"loot": { "table": "food", "chance": 0.05 }
},
{
"id": "spider_cave",
"name": "cave spider",
"renderable": { "glyph": "s", "fg": "#6b6b6b", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "s", "sprite": "spider", "fg": "#6b6b6b", "order": 3 },
"flags": ["SMALL_GROUP"],
"level": 1,
"bac": 3,
@ -311,7 +305,7 @@
{
"id": "ant_worker",
"name": "worker ant",
"renderable": { "glyph": "a", "fg": "#ca7631", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "a", "sprite": "ant", "fg": "#ca7631", "order": 3 },
"flags": ["SMALL_GROUP"],
"level": 2,
"bac": 3,
@ -322,7 +316,7 @@
{
"id": "ant_soldier",
"name": "soldier ant",
"renderable": { "glyph": "a", "fg": "#ca3f26", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "a", "sprite": "ant", "fg": "#ca3f26", "order": 3 },
"flags": ["SMALL_GROUP", "POISON_RES"],
"level": 3,
"bac": 3,
@ -336,7 +330,7 @@
{
"id": "caterpillar_cave",
"name": "caterpillar",
"renderable": { "glyph": "a", "fg": "#6b6b6b", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "a", "sprite": "caterpillar", "fg": "#6b6b6b", "order": 3 },
"flags": ["SMALL_GROUP"],
"level": 1,
"bac": 3,
@ -347,7 +341,7 @@
{
"id": "caterpillar_giant",
"name": "giant caterpillar",
"renderable": { "glyph": "a", "fg": "#b9aeae", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "a", "sprite": "caterpillar", "fg": "#b9aeae", "order": 3 },
"flags": ["SMALL_GROUP"],
"level": 2,
"bac": 7,
@ -358,7 +352,7 @@
{
"id": "jackal",
"name": "jackal",
"renderable": { "glyph": "d", "fg": "#AA5500", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "d", "sprite": "dog", "fg": "#AA5500", "order": 3 },
"flags": ["CARNIVORE", "SMALL_GROUP"],
"bac": 7,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }]
@ -366,7 +360,7 @@
{
"id": "fox",
"name": "fox",
"renderable": { "glyph": "d", "fg": "#FF0000", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "d", "sprite": "dog", "fg": "#FF0000", "order": 3 },
"flags": ["CARNIVORE"],
"bac": 7,
"attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }]
@ -374,7 +368,7 @@
{
"id": "coyote",
"name": "coyote",
"renderable": { "glyph": "d", "fg": "#6E3215", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "d", "sprite": "dog", "fg": "#6E3215", "order": 3 },
"flags": ["CARNIVORE", "SMALL_GROUP"],
"level": 1,
"bac": 7,
@ -383,7 +377,7 @@
{
"id": "wolf",
"name": "wolf",
"renderable": { "glyph": "d", "fg": "#5E4225", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "d", "sprite": "dog", "fg": "#5E4225", "order": 3 },
"flags": ["CARNIVORE"],
"level": 5,
"bac": 4,
@ -392,7 +386,7 @@
{
"id": "goblin_chieftain",
"name": "goblin chieftain",
"renderable": { "glyph": "g", "fg": "#9331ac", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "g", "sprite": "goblin_c", "fg": "#9331ac", "order": 3 },
"flags": [],
"level": 2,
"speed": 9,
@ -402,7 +396,7 @@
{
"id": "orc",
"name": "orc",
"renderable": { "glyph": "o", "fg": "#00FF00", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "o", "sprite": "orc", "fg": "#00FF00", "order": 3 },
"flags": ["SMALL_GROUP"],
"level": 1,
"speed": 9,
@ -412,7 +406,7 @@
{
"id": "orc_hill",
"name": "hill orc",
"renderable": { "glyph": "o", "fg": "#dbd830", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "o", "sprite": "orc", "fg": "#dbd830", "order": 3 },
"flags": ["LARGE_GROUP"],
"level": 2,
"speed": 9,
@ -422,7 +416,7 @@
{
"id": "orc_captain",
"name": "orc captain",
"renderable": { "glyph": "o", "fg": "#9331ac", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "o", "sprite": "orc", "fg": "#9331ac", "order": 3 },
"flags": ["MULTIATTACK"],
"level": 5,
"speed": 5,
@ -435,7 +429,7 @@
{
"id": "warg",
"name": "warg",
"renderable": { "glyph": "d", "fg": "#8b7164", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "d", "sprite": "dog", "fg": "#8b7164", "order": 3 },
"flags": ["SMALL_GROUP"],
"level": 7,
"bac": 4,
@ -446,7 +440,7 @@
{
"id": "jaguar",
"name": "jaguar",
"renderable": { "glyph": "f", "fg": "#d3b947", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "f", "sprite": "cat_large", "fg": "#d3b947", "order": 3 },
"flags": ["MULTIATTACK"],
"level": 4,
"bac": 6,
@ -461,7 +455,7 @@
{
"id": "lynx",
"name": "lynx",
"renderable": { "glyph": "f", "fg": "#b5d347", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "f", "sprite": "cat_large", "fg": "#b5d347", "order": 3 },
"flags": ["MULTIATTACK"],
"level": 5,
"bac": 6,
@ -476,7 +470,7 @@
{
"id": "panther",
"name": "panther",
"renderable": { "glyph": "f", "fg": "#58554e", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "f", "sprite": "cat_large", "fg": "#58554e", "order": 3 },
"flags": ["MULTIATTACK"],
"level": 5,
"bac": 6,
@ -491,7 +485,7 @@
{
"id": "ogre",
"name": "ogre",
"renderable": { "glyph": "O", "fg": "#10A70d", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "O", "sprite": "ogre", "fg": "#10A70d", "order": 3 },
"flags": ["SMALL_GROUP"],
"level": 5,
"bac": 5,
@ -502,7 +496,7 @@
{
"id": "treant_small",
"name": "treant sapling",
"renderable": { "glyph": "♠️", "fg": "#10570d", "bg": "#000000", "order": 1 },
"renderable": { "glyph": "♠️", "sprite": "gnome", "fg": "#10570d", "order": 3 },
"flags": ["LARGE_GROUP", "GREEN_BLOOD", "FIRE_WEAK"],
"level": 2,
"bac": 12,

View file

@ -2,83 +2,89 @@
{
"id": "door",
"name": "door",
"renderable": { "glyph": "+", "fg": "#00FFFF", "bg": "#000000", "order": 2 },
"flags": ["DOOR"]
"renderable": { "glyph": "+", "sprite": "door", "alt": "door_open", "fg": "#00FFFF", "order": 5 },
"door": { "open": false, "locked": false, "blocks_vis": true, "blocks_move": true }
},
{
"id": "trapdoor",
"name": "trapdoor",
"renderable": { "glyph": "+", "sprite": "trapdoor", "alt": "trapdoor_open", "fg": "#00FFFF", "order": 5, "alt_order": 1 },
"door": { "open": false, "locked": false, "blocks_vis": false, "blocks_move": false }
},
{
"id": "prop_altar",
"name": "altar",
"renderable": { "glyph": "_", "fg": "#FFFFFF", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "_", "sprite": "altar", "fg": "#FFFFFF", "order": 5 },
"flags": ["ENTRY_TRIGGER"],
"effects": { "heal": "8d8" }
},
{
"id": "prop_keg",
"name": "keg",
"renderable": { "glyph": "φ", "fg": "#AAAAAA", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "φ", "sprite": "gnome", "fg": "#AAAAAA", "order": 5 },
"flags": []
},
{
"id": "prop_table",
"name": "table",
"renderable": { "glyph": "-", "fg": "#AAAAAA", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "-", "sprite": "table", "fg": "#a76d3d", "order": 5 },
"flags": []
},
{
"id": "prop_hay",
"name": "hay",
"renderable": { "glyph": "%", "fg": "#c7ad39", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "%", "sprite": "plants", "fg": "#e2b82f", "order": 5 },
"flags": []
},
{
"id": "prop_statue",
"name": "statue",
"renderable": { "glyph": "@", "fg": "#ffffff", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "@", "sprite": "altar", "fg": "#ffffff", "order": 5 },
"flags": []
},
{
"id": "prop_bed",
"name": "bed",
"renderable": { "glyph": "=", "fg": "#AAAAAA", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "=", "sprite": "bed", "fg": "#a55d33", "order": 5 },
"flags": []
},
{
"id": "prop_chair",
"name": "chair",
"renderable": { "glyph": "└", "fg": "#AAAAAA", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "└", "sprite": "chair_r", "fg": "#a76d3d", "order": 5 },
"flags": []
},
{
"id": "prop_candle",
"name": "candle",
"renderable": { "glyph": "Ä", "fg": "#FFA500", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "Ä", "sprite": "candelabra", "fg": "#FFA500", "order": 4 },
"flags": []
},
{
"id": "trap_bear",
"name": "bear trap",
"renderable": { "glyph": "^", "fg": "#e6e6e6", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "^", "sprite": "beartrap", "fg": "#e6e6e6", "order": 5 },
"flags": ["HIDDEN", "ENTRY_TRIGGER", "SINGLE_ACTIVATION"],
"effects": { "damage": "2d4" }
},
{
"id": "trap_mini_mine",
"name": "mini-mine",
"renderable": { "glyph": "^", "fg": "#ff1e00", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "^", "sprite": "minimine", "fg": "#ff1e00", "order": 5 },
"flags": ["ENTRY_TRIGGER", "SINGLE_ACTIVATION"],
"effects": { "damage": "2d4", "aoe": "3" }
},
{
"id": "trap_stonefall",
"name": "stonefall trap",
"renderable": { "glyph": "^", "fg": "#beb5a7", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "^", "sprite": "stones", "fg": "#beb5a7", "order": 5 },
"flags": ["HIDDEN", "ENTRY_TRIGGER", "SINGLE_ACTIVATION"],
"effects": { "damage": "2d10" }
},
{
"id": "trap_confusion",
"name": "magic trap",
"renderable": { "glyph": "^", "fg": "#df07df", "bg": "#000000", "order": 2 },
"renderable": { "glyph": "^", "sprite": "magic_trap", "fg": "#df07df", "order": 5 },
"flags": ["HIDDEN", "ENTRY_TRIGGER", "SINGLE_ACTIVATION"],
"effects": { "confusion": "3" }
}

2556
resources/atlas.json Normal file

File diff suppressed because it is too large Load diff

BIN
resources/atlas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
resources/sounds/hit.wav Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

7482
resources/td.json Normal file

File diff suppressed because it is too large Load diff

BIN
resources/td.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -45,6 +45,7 @@ impl<'a> System<'a> for ApproachAI {
continue;
};
let mut path: Option<NavigationPath> = None;
let mut curr_abs_diff = 100;
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);
@ -54,6 +55,17 @@ impl<'a> System<'a> for ApproachAI {
potential_path.steps.len() < path.as_ref().unwrap().steps.len()
{
path = Some(potential_path);
let (x1, y1) = (pos.x, pos.y);
let (x2, y2) = ((tar_idx as i32) % map.width, (tar_idx as i32) / map.width);
curr_abs_diff = i32::abs(x2 - x1) + i32::abs(y2 - y1);
} else if potential_path.steps.len() == path.as_ref().unwrap().steps.len() {
let (x1, y1) = (pos.x, pos.y);
let (x2, y2) = ((tar_idx as i32) % map.width, (tar_idx as i32) / map.width);
let abs_diff = i32::abs(x2 - x1) + i32::abs(y2 - y1);
if abs_diff < curr_abs_diff {
path = Some(potential_path);
curr_abs_diff = abs_diff;
}
}
}
}

View file

@ -1,7 +1,7 @@
use crate::{ gamelog, Attributes, Burden, EquipmentChanged, Equipped, InBackpack, Item, Pools };
use specs::prelude::*;
use std::collections::HashMap;
use crate::data::entity::CARRY_CAPACITY_PER_STRENGTH;
use crate::consts::entity::CARRY_CAPACITY_PER_STRENGTH;
pub struct EncumbranceSystem {}
@ -20,7 +20,17 @@ impl<'a> System<'a> for EncumbranceSystem {
);
fn run(&mut self, data: Self::SystemData) {
let (mut equip_dirty, entities, items, backpacks, wielded, mut pools, attributes, player, mut burdened) = data;
let (
mut equip_dirty,
entities,
items,
backpacks,
wielded,
mut pools,
attributes,
player,
mut burdened,
) = data;
if equip_dirty.is_empty() {
return;
}
@ -50,7 +60,7 @@ impl<'a> System<'a> for EncumbranceSystem {
pool.weight = *weight;
if let Some(attr) = attributes.get(*entity) {
let carry_capacity_lbs =
(attr.strength.base + attr.strength.modifiers) * CARRY_CAPACITY_PER_STRENGTH;
(attr.strength.base + attr.strength.bonuses) * CARRY_CAPACITY_PER_STRENGTH;
if (pool.weight as i32) > 3 * carry_capacity_lbs {
// Overloaded
burdened

View file

@ -1,4 +1,4 @@
use crate::data::entity::*;
use crate::consts::entity::*;
use crate::{
Burden,
BurdenLevel,
@ -15,7 +15,7 @@ use crate::{
use bracket_lib::prelude::*;
use specs::prelude::*;
use crate::config::CONFIG;
use crate::data::events::*;
use crate::consts::events::*;
pub struct EnergySystem {}

View file

@ -12,7 +12,7 @@ use crate::{
Intrinsics,
};
use specs::prelude::*;
use crate::data::events::*;
use crate::consts::events::*;
pub struct RegenSystem {}
@ -121,7 +121,8 @@ fn try_hp_regen_tick(pool: &mut Pools, amount: i32) {
fn get_mana_regen_per_tick(e: Entity, attributes: &ReadStorage<Attributes>) -> i32 {
let regen = if let Some(attributes) = attributes.get(e) {
(attributes.intelligence.bonus + attributes.wisdom.bonus) / 2 + MIN_MP_REGEN_PER_TURN
(attributes.intelligence.modifier() + attributes.wisdom.modifier()) / 2 +
MIN_MP_REGEN_PER_TURN
} else {
MIN_MP_REGEN_PER_TURN
};

View file

@ -12,7 +12,7 @@ use crate::{
};
use bracket_lib::prelude::*;
use specs::prelude::*;
use crate::data::events::*;
use crate::consts::events::*;
pub struct TurnStatusSystem {}
@ -65,7 +65,9 @@ impl<'a> System<'a> for TurnStatusSystem {
not_confused.push(entity);
if entity == *player_entity {
logger = logger
.colour(renderable_colour(&renderables, entity))
.append("You")
.colour(WHITE)
.append("snap out of it.");
log = true;
} else {
@ -81,8 +83,8 @@ impl<'a> System<'a> for TurnStatusSystem {
None,
EffectType::Particle {
glyph: to_cp437('!'),
sprite: "gnome".to_string(), // FIXME: REMOVE THE GNOMES
fg: RGB::named(LIGHT_BLUE),
bg: RGB::named(BLACK),
lifespan: 200.0,
delay: 0.0,
},
@ -92,7 +94,9 @@ impl<'a> System<'a> for TurnStatusSystem {
not_my_turn.push(entity);
if entity == *player_entity {
logger = logger
.colour(renderable_colour(&renderables, entity))
.append("You")
.colour(WHITE)
.append("are confused!");
log = true;
gamelog::record_event(EVENT::PlayerConfused(1));
@ -109,8 +113,8 @@ impl<'a> System<'a> for TurnStatusSystem {
None,
EffectType::Particle {
glyph: to_cp437('?'),
sprite: "gnome".to_string(), // FIXME: REMOVE THE GNOMES
fg: RGB::named(MAGENTA),
bg: RGB::named(BLACK),
lifespan: 200.0,
delay: 0.0,
},

View file

@ -1,14 +1,66 @@
use super::{ Hidden, Map, Mind, Position, Prop, Renderable };
use super::{ Hidden, Map, Mind, Position, Prop, Renderable, Pools };
use bracket_lib::prelude::*;
use specs::prelude::*;
use std::ops::Mul;
use super::consts::visuals::{ TILES_IN_VIEWPORT_H, TILES_IN_VIEWPORT_W };
use super::consts::prelude::*;
const SHOW_BOUNDARIES: bool = false;
pub fn get_screen_bounds(ecs: &World, _ctx: &mut BTerm) -> (i32, i32, i32, i32, i32, i32) {
let player_pos = ecs.fetch::<Point>();
pub struct Offsets {
pub x: i32,
pub y: i32,
}
pub fn get_offset() -> Offsets {
return Offsets { x: 1, y: 8 };
}
pub struct ScreenBounds {
pub min_x: i32,
pub max_x: i32,
pub min_y: i32,
pub max_y: i32,
pub x_offset: i32,
pub y_offset: i32,
}
pub struct ScreenBoundsf32 {
pub min_x: f32,
pub max_x: f32,
pub min_y: f32,
pub max_y: f32,
pub x_offset: f32,
pub y_offset: f32,
}
impl ScreenBounds {
pub fn to_px(&self) -> ScreenBoundsf32 {
ScreenBoundsf32 {
min_x: (self.min_x as f32) * TILESIZE.sprite_x,
max_x: (self.max_x as f32) * TILESIZE.sprite_x,
min_y: (self.min_y as f32) * TILESIZE.sprite_y,
max_y: (self.max_y as f32) * TILESIZE.sprite_y,
x_offset: (self.x_offset as f32) * TILESIZE.x,
y_offset: (self.y_offset as f32) * TILESIZE.x,
}
}
}
pub fn get_screen_bounds(ecs: &World, debug: bool) -> ScreenBounds {
let map = ecs.fetch::<Map>();
let (x_chars, y_chars, mut x_offset, mut y_offset) = (69, 41, 1, 10);
let player_pos = if !debug {
*ecs.fetch::<Point>()
} else {
Point::new(map.width / 2, map.height / 2)
};
let (x_chars, y_chars, mut x_offset, mut y_offset) = (
TILES_IN_VIEWPORT_W,
TILES_IN_VIEWPORT_H,
1,
8,
);
let centre_x = (x_chars / 2) as i32;
let centre_y = (y_chars / 2) as i32;
@ -28,33 +80,60 @@ pub fn get_screen_bounds(ecs: &World, _ctx: &mut BTerm) -> (i32, i32, i32, i32,
let max_x = min_x + (x_chars as i32);
let max_y = min_y + (y_chars as i32);
(min_x, max_x, min_y, max_y, x_offset, y_offset)
ScreenBounds { min_x, max_x, min_y, max_y, x_offset, y_offset }
}
use crate::consts::TILESIZE;
pub fn in_bounds(x: i32, y: i32, min_x: i32, min_y: i32, upper_x: i32, upper_y: i32) -> bool {
x >= min_x && x < upper_x && y >= min_y && y < upper_y
}
pub fn render_camera(ecs: &World, ctx: &mut BTerm) {
let map = ecs.fetch::<Map>();
let (min_x, max_x, min_y, max_y, x_offset, y_offset) = get_screen_bounds(ecs, ctx);
let bounds = get_screen_bounds(ecs, false);
// Render map
let mut y = 0;
for t_y in min_y..max_y {
for t_y in bounds.min_y..bounds.max_y {
let mut x = 0;
for t_x in min_x..max_x {
for t_x in bounds.min_x..bounds.max_x {
if t_x >= 0 && t_x < map.width && t_y >= 0 && t_y < map.height {
let idx = map.xy_idx(t_x, t_y);
if map.revealed_tiles[idx] {
let (glyph, fg, bg) = crate::map::themes::get_tile_renderables_for_id(
idx,
&*map,
Some(*ecs.fetch::<Point>()),
None
);
ctx.set(x + x_offset, y + y_offset, fg, bg, glyph);
if 1 == 2 {
let (glyph, fg, bg) = crate::map::themes::get_tile_renderables_for_id(
idx,
&*map,
Some(*ecs.fetch::<Point>()),
None
);
ctx.set(x + bounds.x_offset, y + bounds.y_offset, fg, bg, glyph);
} else {
ctx.set_active_console(0);
let (id, tint) = crate::map::themes::get_sprite_for_id(
idx,
&*map,
Some(*ecs.fetch::<Point>())
);
ctx.add_sprite(
Rect::with_size(
x * 16 + bounds.x_offset * 16,
y * 16 + bounds.y_offset * 16,
16,
16
),
0,
RGBA::named(WHITE),
0 // Ya
);
ctx.set_active_console(TILE_LAYER);
}
}
} else if SHOW_BOUNDARIES {
ctx.set(
x + x_offset,
y + y_offset,
x + bounds.x_offset,
y + bounds.y_offset,
RGB::named(DARKSLATEGRAY),
RGB::named(BLACK),
to_cp437('#')
@ -67,8 +146,11 @@ pub fn render_camera(ecs: &World, ctx: &mut BTerm) {
// Render entities
{
ctx.set_active_console(ENTITY_LAYER);
let positions = ecs.read_storage::<Position>();
let renderables = ecs.read_storage::<Renderable>();
let pools = ecs.read_storage::<Pools>();
let minds = ecs.read_storage::<Mind>();
let hidden = ecs.read_storage::<Hidden>();
let props = ecs.write_storage::<Prop>();
@ -79,22 +161,22 @@ pub fn render_camera(ecs: &World, ctx: &mut BTerm) {
data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order));
for (pos, render, ent, _hidden) in data.iter() {
let idx = map.xy_idx(pos.x, pos.y);
let entity_offset_x = pos.x - min_x;
let entity_offset_y = pos.y - min_y;
if pos.x < max_x && pos.y < max_y && pos.x >= min_x && pos.y >= min_y {
let entity_offset_x = pos.x - bounds.min_x;
let entity_offset_y = pos.y - bounds.min_y;
if
pos.x < bounds.max_x &&
pos.y < bounds.max_y &&
pos.x >= bounds.min_x &&
pos.y >= bounds.min_y
{
let mut draw = false;
let mut fg = render.fg;
let mut bg = crate::map::themes::get_tile_renderables_for_id(
idx,
&*map,
Some(*ecs.fetch::<Point>()),
None
).2;
let bg = BLACK;
// Draw entities on visible tiles
if map.visible_tiles[idx] {
draw = true;
} else {
fg = fg.mul(crate::data::visuals::NON_VISIBLE_MULTIPLIER);
fg = fg.mul(crate::consts::visuals::NON_VISIBLE_MULTIPLIER);
// We don't darken BG, because get_tile_renderables_for_id handles this.
}
@ -104,9 +186,6 @@ pub fn render_camera(ecs: &World, ctx: &mut BTerm) {
let has_mind = minds.get(*ent);
if let Some(_) = has_mind {
draw = true;
if !map.revealed_tiles[idx] {
bg = RGB::named(BLACK);
}
}
}
}
@ -118,16 +197,51 @@ pub fn render_camera(ecs: &World, ctx: &mut BTerm) {
}
}
if draw {
ctx.set(
entity_offset_x + x_offset,
entity_offset_y + y_offset,
fg,
bg,
render.glyph
);
/* if let Some(sprite) = render.sprite {
ctx.set_active_console(0);
ctx.add_sprite(
Rect::with_size(
entity_offset_x * 16 + bounds.x_offset * 16,
entity_offset_y * 16 + bounds.y_offset * 16,
16,
16
),
render.render_order,
RGBA::named(WHITE),
sprite
);
ctx.set_active_console(ENTITY_LAYER);
} else */ {
ctx.set(
entity_offset_x + bounds.x_offset,
entity_offset_y + bounds.y_offset,
fg,
bg,
render.glyph
);
}
if let Some(pool) = pools.get(*ent) {
if pool.hit_points.current < pool.hit_points.max {
ctx.set_active_console(HP_BAR_LAYER);
crate::gui::draw_lerping_bar(
ctx,
(entity_offset_x + bounds.x_offset) * 16 + 2,
(entity_offset_y + bounds.y_offset) * 16 - 1,
14,
pool.hit_points.current,
pool.hit_points.max,
RGB::named(GREEN),
RGB::named(RED),
false,
false
);
ctx.set_active_console(ENTITY_LAYER);
}
}
}
}
}
ctx.set_active_console(TILE_LAYER);
}
}

View file

@ -20,7 +20,7 @@ pub struct SerializationHelper {
#[derive(Component, Serialize, Deserialize, Clone)]
pub struct DMSerializationHelper {
pub map: super::map::MasterDungeonMap,
pub log: Vec<Vec<crate::gamelog::LogFragment>>,
pub log: std::collections::BTreeMap<i32, Vec<crate::gamelog::LogFragment>>,
pub event_counts: HashMap<String, i32>,
pub events: HashMap<u32, Vec<String>>,
}
@ -38,12 +38,91 @@ pub struct OtherLevelPosition {
pub id: i32,
}
#[derive(Component, ConvertSaveload, Clone)]
#[derive(Debug, Component, ConvertSaveload, Clone)]
pub struct Renderable {
pub glyph: FontCharType,
pub glyph: FontCharType, // Legacy, and for drawing the morgue map.
pub sprite: String,
pub sprite_alt: Option<String>,
pub fg: RGB,
pub bg: RGB,
pub fg_alt: Option<RGB>,
pub render_order: i32,
pub render_order_alt: Option<i32>,
pub offset: (f32, f32),
pub offset_alt: Option<(f32, f32)>,
// 0 = always on top: particle effects
// 1 = things that should appear infront of the player: railings, etc.
// 2 = the player
// 3 = other mobs
// 4 = interactable items
// 5 = other props: table, etc.
}
impl Renderable {
pub fn new(glyph: FontCharType, sprite: String, fg: RGB, render_order: i32) -> Self {
Self {
glyph,
sprite,
sprite_alt: None,
fg,
fg_alt: None,
render_order,
render_order_alt: None,
offset: (0.0, 0.0),
offset_alt: None,
}
}
pub fn swap(&mut self) {
let sprite = self.swap_sprite();
let fg = self.swap_fg();
let render_order = self.swap_render_order();
let offset = self.swap_offset();
let did_something = sprite || fg || render_order || offset;
if !did_something {
unreachable!(
".swap() was called on a Renderable component, but nothing happened. {:?}",
self
);
}
}
pub fn swap_sprite(&mut self) -> bool {
if let Some(sprite_alt) = &mut self.sprite_alt {
std::mem::swap(&mut self.sprite, sprite_alt);
return true;
}
false
}
pub fn swap_fg(&mut self) -> bool {
if let Some(fg_alt) = &mut self.fg_alt {
std::mem::swap(&mut self.fg, fg_alt);
return true;
}
false
}
pub fn swap_render_order(&mut self) -> bool {
if let Some(render_order_alt) = &mut self.render_order_alt {
std::mem::swap(&mut self.render_order, render_order_alt);
return true;
}
false
}
pub fn swap_offset(&mut self) -> bool {
if let Some(offset_alt) = &mut self.offset_alt {
std::mem::swap(&mut self.offset, offset_alt);
return true;
}
false
}
}
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Avatar {
pub sprite: String,
}
impl Avatar {
pub fn new(sprite: String) -> Self {
Self { sprite }
}
}
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
@ -131,6 +210,9 @@ pub struct BlocksVisibility {}
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Door {
pub open: bool,
pub locked: bool,
pub blocks_vis: bool,
pub blocks_move: bool,
}
#[derive(Serialize, Deserialize, Copy, Clone, PartialEq)]
@ -182,8 +264,36 @@ pub struct Pools {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Attribute {
pub base: i32,
pub modifiers: i32,
pub bonus: i32,
pub bonuses: i32,
pub exercise: i32,
}
impl Attribute {
pub fn new(base: i32) -> Self {
Self {
base,
bonuses: 0,
exercise: 0,
}
}
// Raw attribute score. e.g. 10 base, + 4 from armour: 14 strength.
pub fn current(&self) -> i32 {
self.base + self.bonuses
}
// Attribute bonus. e.g. 14 strength = +2, 8 strength = -1
pub fn modifier(&self) -> i32 {
crate::gamesystem::attr_bonus(self.current())
}
pub fn improve(&mut self) {
if self.exercise < 50 {
self.exercise += 1;
}
}
pub fn abuse(&mut self) {
if self.exercise > -50 {
self.exercise -= 1;
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
@ -215,7 +325,6 @@ pub struct GrantsSpell {
}
// TODO: GrantsIntrinsic, Intrinsics, etc. ? Done the same way as spells?
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Attributes {
pub strength: Attribute,
@ -226,6 +335,93 @@ pub struct Attributes {
pub charisma: Attribute,
}
impl Attributes {
pub const STR: i32 = 0;
pub const DEX: i32 = 1;
pub const CON: i32 = 2;
pub const INT: i32 = 3;
pub const WIS: i32 = 4;
pub const CHA: i32 = 5;
pub fn default() -> Self {
Self {
strength: Attribute::new(10),
dexterity: Attribute::new(10),
constitution: Attribute::new(10),
intelligence: Attribute::new(10),
wisdom: Attribute::new(10),
charisma: Attribute::new(10),
}
}
pub fn with_stats(str: i32, dex: i32, con: i32, int: i32, wis: i32, cha: i32) -> Self {
Self {
strength: Attribute::new(str),
dexterity: Attribute::new(dex),
constitution: Attribute::new(con),
intelligence: Attribute::new(int),
wisdom: Attribute::new(wis),
charisma: Attribute::new(cha),
}
}
pub fn attr_from_index(&self, attr: i32) -> &Attribute {
match attr {
Self::STR => &self.strength,
Self::DEX => &self.dexterity,
Self::CON => &self.constitution,
Self::INT => &self.intelligence,
Self::WIS => &self.wisdom,
Self::CHA => &self.charisma,
_ => unreachable!("Tried to get an attribute that doesn't exist."),
}
}
pub fn exercise(&mut self, attr: i32, improve: bool) {
match attr {
Self::STR => {
if improve {
self.strength.improve();
} else {
self.strength.abuse();
}
}
Self::DEX => {
if improve {
self.dexterity.improve();
} else {
self.dexterity.abuse();
}
}
Self::CON => {
if improve {
self.constitution.improve();
} else {
self.constitution.abuse();
}
}
Self::INT => {
if improve {
self.intelligence.improve();
} else {
self.intelligence.abuse();
}
}
Self::WIS => {
if improve {
self.wisdom.improve();
} else {
self.wisdom.abuse();
}
}
Self::CHA => {
if improve {
self.charisma.improve();
} else {
self.charisma.abuse();
}
}
_ => unreachable!("Tried to exercise an attribute that doesn't exist."),
}
}
}
#[derive(Component, Debug, ConvertSaveload, Clone)]
pub struct WantsToMelee {
pub target: Entity,
@ -319,9 +515,23 @@ pub struct IdentifiedItem {
pub name: String,
}
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Stackable {}
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct EquipmentChanged {}
#[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(PartialEq, Copy, Clone, Serialize, Deserialize)]
pub enum BurdenLevel {
Burdened,
@ -539,6 +749,9 @@ pub struct InBackpack {
pub owner: Entity,
}
#[derive(Component, Serialize, Deserialize, Clone)]
pub struct WantsToAssignKey {}
#[derive(Component, Debug, ConvertSaveload)]
pub struct WantsToPickupItem {
pub collected_by: Entity,
@ -588,7 +801,9 @@ pub struct Charges {
#[derive(Component, Serialize, Deserialize, Clone)]
pub struct SpawnParticleLine {
pub glyph: FontCharType,
pub sprite: String,
pub tail_glyph: FontCharType,
pub tail_sprite: String,
pub colour: RGB,
pub lifetime_ms: f32,
pub trail_colour: RGB,
@ -598,6 +813,7 @@ pub struct SpawnParticleLine {
#[derive(Component, Serialize, Deserialize, Clone)]
pub struct SpawnParticleSimple {
pub glyph: FontCharType,
pub sprite: String,
pub colour: RGB,
pub lifetime_ms: f32,
}
@ -605,8 +821,11 @@ pub struct SpawnParticleSimple {
#[derive(Component, Serialize, Deserialize, Clone)]
pub struct SpawnParticleBurst {
pub glyph: FontCharType,
pub sprite: String,
pub head_glyph: FontCharType,
pub head_sprite: String,
pub tail_glyph: FontCharType,
pub tail_sprite: String,
pub colour: RGB,
pub lerp: RGB,
pub lifetime_ms: f32,
@ -648,20 +867,3 @@ pub struct EntityMoved {}
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
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

@ -34,7 +34,7 @@ impl Default for Config {
fn default() -> Self {
Config {
logging: LogConfig {
show_mapgen: false,
show_mapgen: true,
log_combat: false,
log_spawning: false,
log_ticks: false,
@ -61,11 +61,11 @@ impl Config {
requires_write |= config.logging.apply_values(&parsed_config);
requires_write |= config.visuals.apply_values(&parsed_config);
if requires_write {
console::log("Parsed config, but some values were changed. Saving new ver.");
if let Err(write_err) = config.save_to_file(filename) {
console::log(format!("Error writing config: {:?}", write_err));
}
}
return config;
}
}
@ -101,6 +101,8 @@ 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, show_mapgen);
apply_bool_value!(self, section, missing, log_combat);
apply_bool_value!(self, section, missing, log_spawning);
apply_bool_value!(self, section, missing, log_ticks);
missing

View file

@ -1,4 +1,4 @@
pub const DEFAULT_VIEWSHED_STANDARD: i32 = 16; // Standard viewshed radius for almost all entities.
pub const DEFAULT_VIEWSHED_STANDARD: i32 = 7; // Standard viewshed radius for almost all entities.
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 SPEED_MOD_BURDENED: f32 = 0.75;

32
src/consts/mod.rs Normal file
View file

@ -0,0 +1,32 @@
pub mod entity;
pub mod visuals;
pub mod messages;
pub mod char_create;
pub mod events;
pub mod ids;
pub mod names;
pub mod sprites;
pub mod prelude {
pub use super::visuals::{ TILE_LAYER, ENTITY_LAYER, TEXT_LAYER, HP_BAR_LAYER };
pub use super::visuals::{ VIEWPORT_H, VIEWPORT_W };
}
pub struct Spritesize {
pub x: f32,
pub y: f32,
pub sprite_x: f32,
pub sprite_y: f32,
}
pub const TILESIZE: Spritesize = Spritesize {
x: 16.0,
y: 16.0,
sprite_x: 16.0 * ZOOM_FACTOR,
sprite_y: 24.0 * ZOOM_FACTOR,
};
pub const ZOOM_FACTOR: f32 = 2.0;
pub const FONTSIZE: f32 = 16.0;
pub const DISPLAYWIDTH: u32 = 120;
pub const DISPLAYHEIGHT: u32 = 67;

119
src/consts/sprites.rs Normal file
View file

@ -0,0 +1,119 @@
// Row 1
pub const UNKN: usize = 0;
pub const UNKN2: usize = 1;
pub const UNKN3: usize = 2;
pub const CANDLE: usize = 3;
pub const CANDLE2: usize = 4;
pub const CANDLE3: usize = 5;
pub const CANDLE4: usize = 6;
pub const CANDLE5: usize = 7;
pub const CANDLE6: usize = 8;
pub const CAULDRON: usize = 9;
pub const CAULDRON2: usize = 10;
pub const POTS: usize = 11;
pub const POTS2: usize = 12;
pub const POT: usize = 13;
pub const SPIKES: usize = 14;
pub const SPIKES2: usize = 15;
// Row 2
pub const WINDOW: usize = 16;
pub const DOOR: usize = 17;
pub const DOOR_OPEN: usize = 18;
pub const ROOF_BASE: usize = 19;
pub const ROOF_BASE2: usize = 20;
pub const ROOF: usize = 21;
pub const ROOF2: usize = 22;
pub const ROOF_CHIMNEY: usize = 23;
pub const SIGN: usize = 24;
pub const SIGN_BLACKSMITH: usize = 25;
pub const SIGN_POTION: usize = 26;
pub const SIGN_FURNITURE: usize = 27;
pub const WINDOW_LIT: usize = 28;
pub const STATUE_ANGEL: usize = 29;
pub const STATUE: usize = 30;
pub const STATUE_SPIDER: usize = 31;
// Row 3
pub const UNKN4: usize = 32;
pub const UNKN5: usize = 33;
pub const UNKN6: usize = 34;
pub const UNKN7: usize = 35;
pub const UNKN8: usize = 36;
pub const TREE: usize = 37;
pub const TREE2: usize = 38;
pub const PATH_GRASS: usize = 39;
pub const PATH_GRASS_QUAD: usize = 40;
pub const PATH_GRASS_QUAD2: usize = 41;
pub const PATH_GRASS_QUAD3: usize = 42;
pub const CAMPFIRE: usize = 43;
pub const CAMPFIRE_LIT: usize = 44;
pub const CAMPFIRE_LIT2: usize = 45; // ANIMATE WITH % 2 AND SOMETHING TO DO WITH FRAME TIME
pub const THRONE: usize = 46;
pub const THRONE2: usize = 47;
// Row 4
pub const BOOKSHELF: usize = 48;
pub const BOOKSHELF_EMPTY: usize = 49;
pub const BED: usize = 50;
pub const CHAIR: usize = 51;
pub const TABLE: usize = 52;
pub const TABLE_L: usize = 53;
pub const TABLE_M: usize = 54;
pub const TABLE_R: usize = 55;
pub const CHAIR_AT_TABLE_L: usize = 56;
pub const TABLE_M_PARCHMENT: usize = 57;
pub const CHAIR_AT_TABLE_R: usize = 58;
pub const TABLE_DARK: usize = 59;
pub const TABLE_DARK_SKULL: usize = 60;
pub const TABLE_DARK_L: usize = 61;
pub const TABLE_DARK_M: usize = 62;
pub const TABLE_DARK_R: usize = 63;
// Row 5
pub const GRASS: usize = 64;
pub const GRASS2: usize = 65;
pub const GRASS3: usize = 66;
pub const GRASS4: usize = 67;
pub const GRASS5: usize = 68;
pub const MUSHROOM: usize = 69;
pub const MUSHROOM_PURPLE: usize = 70;
pub const MUSHROOM_ORANGE: usize = 71;
pub const LILYPAD: usize = 72;
pub const LILYPAD2: usize = 73;
pub const LILYPAD3: usize = 74;
pub const LILYPAD4: usize = 75;
pub const LILYPAD5: usize = 76;
pub const LILYPAD6: usize = 77;
pub const LILYPAD7: usize = 78;
pub const LILYPAD8: usize = 79;
// Row 6 (80-95)
// Row 7 (96-111)
// Row 8 (112-127)
pub const FLOOR_WOOD: usize = 124;
// Row 9 (128-143)
// Row 10 (144-159)
// Row 11 (160-175)
pub const WATER_DEEP: usize = 164;
// Row 12 (176-191)
// Row 13 (192-207)
// Row 14 (208-223)
pub const FLOOR_GRASS: usize = 216;
// Row 15 (224-239)
pub const FLOOR: usize = 224;
// Row 16 (240-255)
// Row 17 (256-271)
// Row 18 (272-287)
// Row 19 (288-303)
pub const WALL_BASE: usize = 288;
pub const WALL_BASE2: usize = 289;
pub const WALL_BASE3: usize = 290;
pub const WALL_BASE4: usize = 291;
pub const WALL_CLOTH_BASE: usize = 292;
pub const WALL_CRACKED_BASE: usize = 293;
pub const WALL: usize = 294;
pub const WALL2: usize = 295;
pub const WALL3: usize = 296;
pub const WALL4: usize = 297;
pub const WALL_CRACKED: usize = 298;
pub const WALL_CLOTH_H: usize = 299;
pub const STAIR_D: usize = 300;
pub const STAIR_A: usize = 301;
pub const BASIN: usize = 302;
pub const BASIN_EMPTY: usize = 303;

View file

@ -1,17 +1,34 @@
use bracket_lib::prelude::*;
use super::ZOOM_FACTOR;
// 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.
// Counted in 16x16 tiles, because that's how most of the screen is drawn. However,
// the viewport itself uses 16x24 sprites - so this translates to 70x28 tiles drawn.
// It also works nicely for zooming in, displaying 35x14 tiles cleanly onscreen.
pub const VIEWPORT_W: i32 = 70;
pub const VIEWPORT_H: i32 = 54;
pub const TILES_IN_VIEWPORT_W: i32 = 70 / (ZOOM_FACTOR as i32);
pub const TILES_IN_VIEWPORT_H: i32 = 36 / (ZOOM_FACTOR as i32);
pub const TILE_LAYER: usize = 1;
pub const ENTITY_LAYER: usize = 2;
pub const TEXT_LAYER: usize = 3;
pub const HP_BAR_LAYER: usize = 4;
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 GLOBAL_OFFSET_MAX_CLAMP: f32 = 0.5;
pub const SPRITE_OFFSET_MIN_CLAMP: f32 = 0.85;
pub const SPRITE_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: f32 = 0.1; // 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: f32 = 0.2; // 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 START_DARKEN_AT_N_TILES: f32 = 1.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;
@ -57,6 +74,7 @@ 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 BARS_OFFSETS: (i32, i32, i32) = (10, 10, 10);
pub const IMPASSABLE_MOUNTAIN_COLOUR: (u8, u8, u8) = (20, 23, 20);
pub const IMPASSABLE_MOUNTAIN_OFFSETS: (i32, i32, i32) = (4, 4, 4);
// FOREST THEME

View file

@ -16,7 +16,7 @@ use super::{
};
use bracket_lib::prelude::*;
use specs::prelude::*;
use crate::data::events;
use crate::consts::events;
pub fn delete_the_dead(ecs: &mut World) {
let mut dead: Vec<Entity> = Vec::new();
@ -72,8 +72,8 @@ pub fn delete_the_dead(ecs: &mut World) {
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.
// Add items marked for deletion to the list, but only if they've already had their key
// assignments handled, to ensure we don't leave any dangling references behind.
for (e, _d, _r) in (&entities, &delete, !&removekeys).join() {
items_to_delete.push(e);
}

View file

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

32
src/effects/attr.rs Normal file
View file

@ -0,0 +1,32 @@
use specs::prelude::*;
use bracket_lib::prelude::*;
use super::{ EffectSpawner, EffectType };
use crate::components::Attributes;
const ATTRIBUTE_SOFTCAP: i32 = 20;
const ABUSE_CHANCE: i32 = 2; // 1 in this chance of abuse. 2 = 50%, 3 = 33%, etc.
pub(crate) fn exercise(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
// Unwrap vars from the effect
let (attr, inc) = if let EffectType::Exercise { attribute, increment } = effect.effect_type {
(attribute, increment)
} else {
return;
};
// Get target attributes
let mut attributes = ecs.write_storage::<Attributes>();
if let Some(has_attr) = attributes.get_mut(target) {
// Roll a d20. If we're trying to exercise a stat, we need to roll higher
// than the stat's current value. If we're abusing a stat, flip a coin.
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
let success = if inc {
rng.roll_dice(1, ATTRIBUTE_SOFTCAP) > has_attr.attr_from_index(attr).current()
} else {
rng.roll_dice(1, ABUSE_CHANCE) == 1
};
if success {
has_attr.exercise(attr, inc);
}
}
}

View file

@ -15,12 +15,13 @@ use crate::{
HungerState,
Bleeds,
HasDamageModifiers,
DamageType,
};
use crate::gui::with_article;
use crate::data::visuals::{ DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME };
use crate::data::messages::LEVELUP_PLAYER;
use crate::data::events::*;
use crate::data::messages::*;
use crate::consts::visuals::{ DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME };
use crate::consts::messages::LEVELUP_PLAYER;
use crate::consts::events::*;
use crate::consts::messages::*;
use bracket_lib::prelude::*;
use specs::prelude::*;
@ -38,33 +39,88 @@ pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) {
};
target_pool.hit_points.current -= ((amount as f32) * mult) as i32;
let bleeders = ecs.read_storage::<Bleeds>();
// If the target bleeds, handle bloodstains and use the bleed colour for sfx.
if let Some(bleeds) = bleeders.get(target) {
add_effect(
None,
EffectType::Bloodstain { colour: bleeds.colour },
Targets::Entity { target }
);
}
add_effect(
None,
EffectType::Particle {
glyph: to_cp437('‼'),
fg: RGB::named(ORANGE),
bg: RGB::named(BLACK),
lifespan: DEFAULT_PARTICLE_LIFETIME,
delay: 0.0,
},
Targets::Entity { target }
);
if target_pool.hit_points.current < 1 {
super::DEAD_ENTITIES.lock().unwrap().push_back(target);
add_effect(damage.source, EffectType::EntityDeath, Targets::Entity { target });
if target_pool.hit_points.current < 1 {
super::DEAD_ENTITIES.lock().unwrap().push_back(target);
add_effect(damage.source, EffectType::EntityDeath, Targets::Entity {
target,
});
for i in 0..3 {
let sprite = (
match i {
0 => "explode1",
1 => "explode2",
_ => "explode3",
}
).to_string();
add_effect(
None,
EffectType::Particle {
glyph: to_cp437('‼'),
sprite,
fg: bleeds.colour,
lifespan: 75.0,
delay: 75.0 * (i as f32),
},
Targets::Entity { target }
);
add_effect(
None,
EffectType::Bloodstain { colour: bleeds.colour },
Targets::Entity { target }
);
}
} else {
// Regular damage taken effect - use damagetype to determine which one to play.
add_effect(
None,
EffectType::Particle {
glyph: to_cp437('‼'),
sprite: "gnome".to_string(), // FIXME: REMOVE THE GNOMES
fg: RGB::named(ORANGE),
lifespan: DEFAULT_PARTICLE_LIFETIME,
delay: 0.0,
},
Targets::Entity { target }
);
add_effect(
None,
EffectType::Bloodstain { colour: bleeds.colour },
Targets::Entity { target }
);
}
} else {
// TODO: Damage taken particle effects when the target does not bleed.
// Also damage types, etc.
return;
}
}
}
} else if let Some(_destructible) = ecs.read_storage::<Destructible>().get(target) {
add_effect(damage.source, EffectType::EntityDeath, Targets::Entity { target });
}
if let EffectType::Damage { amount: _, damage_type } = &damage.effect_type {
get_random_damage_sound(ecs, damage_type, target);
}
}
fn get_random_damage_sound(ecs: &World, damage_type: &DamageType, target: Entity) {
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
match damage_type {
DamageType::Physical => {
let sound = (
match rng.roll_dice(1, 4) {
1 => "whoosh1",
2 => "whoosh2",
3 => "whoosh3",
_ => "whoosh4",
}
).to_string();
add_effect(None, EffectType::Sound { sound }, Targets::Entity { target });
}
_ => {}
}
}
pub fn heal_damage(ecs: &mut World, heal: &EffectSpawner, target: Entity) {
@ -83,10 +139,10 @@ pub fn heal_damage(ecs: &mut World, heal: &EffectSpawner, target: Entity) {
}
add_effect(
None,
EffectType::Particle {
EffectType::Particle { // GNOMES
glyph: to_cp437('♥'),
sprite: "gnome".to_string(),
fg: RGB::named(BLUE),
bg: RGB::named(BLACK),
lifespan: DEFAULT_PARTICLE_LIFETIME,
delay: 0.0,
},
@ -184,7 +240,7 @@ fn get_death_message(ecs: &World, source: Entity) -> String {
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()
format!("{} {}", PLAYER_DIED_NAMED_ATTACKER, with_article(&name.name)).as_str()
);
} else {
result.push_str(format!("{}", PLAYER_DIED_UNKNOWN).as_str());
@ -266,11 +322,11 @@ pub fn entity_death(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
for i in 0..5 {
if player_pos.y - i > 1 {
add_effect(
None,
None, // FIXME: REMOVE THE GNOMES
EffectType::Particle {
glyph: to_cp437('░'),
sprite: "gnome".to_string(),
fg: RGB::named(GOLD),
bg: RGB::named(BLACK),
lifespan: LONG_PARTICLE_LIFETIME,
delay: (i as f32) * 100.0,
},
@ -281,8 +337,8 @@ pub fn entity_death(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
None,
EffectType::Particle {
glyph: to_cp437('░'),
sprite: "gnome".to_string(),
fg: RGB::named(GOLD),
bg: RGB::named(BLACK),
lifespan: LONG_PARTICLE_LIFETIME,
delay: (i as f32) * 100.0,
},
@ -297,8 +353,8 @@ pub fn entity_death(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
None,
EffectType::Particle {
glyph: to_cp437('░'),
sprite: "gnome".to_string(),
fg: RGB::named(GOLD),
bg: RGB::named(BLACK),
lifespan: LONG_PARTICLE_LIFETIME,
delay: (i as f32) * 100.0,
},
@ -319,11 +375,11 @@ pub fn entity_death(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
let hp_gained = hp_per_level(
&mut rng,
source_attributes.constitution.base + source_attributes.constitution.modifiers
source_attributes.constitution.base + source_attributes.constitution.bonuses
);
let mana_gained = mana_per_level(
&mut rng,
source_attributes.intelligence.base + source_attributes.intelligence.modifiers
source_attributes.intelligence.base + source_attributes.intelligence.bonuses
);
source_pools.hit_points.max += hp_gained;
source_pools.hit_points.current += hp_gained;

View file

@ -2,6 +2,7 @@ use super::BUC;
use crate::spatial;
use bracket_lib::prelude::*;
use specs::prelude::*;
use notan::prelude::*;
use std::collections::VecDeque;
use std::sync::Mutex;
use crate::components::*;
@ -11,7 +12,9 @@ mod hunger;
mod particles;
mod targeting;
mod triggers;
mod attr;
mod intrinsics;
pub mod sound;
pub use targeting::aoe_tiles;
@ -32,6 +35,10 @@ pub enum EffectType {
amount: i32,
increment_max: bool,
},
Exercise {
attribute: i32,
increment: bool,
},
Confusion {
turns: i32,
},
@ -40,8 +47,8 @@ pub enum EffectType {
},
Particle {
glyph: FontCharType,
sprite: String,
fg: RGB,
bg: RGB,
lifespan: f32,
delay: f32,
},
@ -58,6 +65,9 @@ pub enum EffectType {
TriggerFire {
trigger: Entity,
},
Sound {
sound: String,
},
}
#[derive(Clone)]
@ -90,7 +100,7 @@ pub fn add_effect(source: Option<Entity>, effect_type: EffectType, target: Targe
}
/// Iterates through the effects queue, applying each effect to their target.
pub fn run_effects_queue(ecs: &mut World) {
pub fn run_effects_queue(app: &mut App, ecs: &mut World) {
// First removes any effect in the EFFECT_QUEUE with a dead entity as its source.
loop {
let dead_entity: Option<Entity> = DEAD_ENTITIES.lock().unwrap().pop_front();
@ -106,7 +116,7 @@ pub fn run_effects_queue(ecs: &mut World) {
loop {
let effect: Option<EffectSpawner> = EFFECT_QUEUE.lock().unwrap().pop_front();
if let Some(effect) = effect {
target_applicator(ecs, &effect);
target_applicator(app, ecs, &effect);
} else {
break;
}
@ -114,7 +124,7 @@ pub fn run_effects_queue(ecs: &mut World) {
}
/// Applies an effect to the correct target(s).
fn target_applicator(ecs: &mut World, effect: &EffectSpawner) {
fn target_applicator(app: &mut App, ecs: &mut World, effect: &EffectSpawner) {
// Item use is handled differently - it creates other effects with itself
// as the source, passing all effects attached to the item into the queue.
if let EffectType::ItemUse { item } = effect.effect_type {
@ -126,25 +136,26 @@ fn target_applicator(ecs: &mut World, effect: &EffectSpawner) {
}
// Otherwise, just match the effect and enact it directly.
match &effect.target {
Targets::Tile { target } => affect_tile(ecs, effect, *target),
Targets::Tile { target } => affect_tile(app, 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.iter().for_each(|target| affect_tile(app, ecs, effect, *target)),
Targets::Entity { target } => affect_entity(app, ecs, effect, *target),
Targets::EntityList { targets } =>
targets.iter().for_each(|target| affect_entity(ecs, effect, *target)),
targets.iter().for_each(|target| affect_entity(app, ecs, effect, *target)),
}
}
/// Runs an effect on a given tile index
fn affect_tile(ecs: &mut World, effect: &EffectSpawner, target: usize) {
fn affect_tile(app: &mut App, ecs: &mut World, effect: &EffectSpawner, target: usize) {
if tile_effect_hits_entities(&effect.effect_type) {
spatial::for_each_tile_content(target, |entity| {
affect_entity(ecs, effect, entity);
affect_entity(app, ecs, effect, entity);
});
}
match &effect.effect_type {
EffectType::Particle { .. } => particles::particle_to_tile(ecs, target as i32, &effect),
EffectType::Sound { .. } => sound::play_sound(app, ecs, &effect, target),
_ => {}
}
// Run the effect
@ -163,10 +174,11 @@ fn tile_effect_hits_entities(effect: &EffectType) -> bool {
}
/// Runs an effect on a given entity
fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
fn affect_entity(app: &mut App, ecs: &mut World, effect: &EffectSpawner, target: Entity) {
match &effect.effect_type {
EffectType::Damage { .. } => damage::inflict_damage(ecs, effect, target),
EffectType::Healing { .. } => damage::heal_damage(ecs, effect, target),
EffectType::Exercise { .. } => attr::exercise(ecs, effect, target),
EffectType::Confusion { .. } => damage::add_confusion(ecs, effect, target),
EffectType::Bloodstain { colour } => {
if let Some(pos) = targeting::entity_position(ecs, target) {
@ -181,6 +193,11 @@ fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) {
EffectType::EntityDeath => damage::entity_death(ecs, effect, target),
EffectType::ModifyNutrition { .. } => hunger::modify_nutrition(ecs, effect, target),
EffectType::AddIntrinsic { .. } => intrinsics::add_intrinsic(ecs, effect, target),
EffectType::Sound { .. } => {
if let Some(pos) = targeting::entity_position(ecs, target) {
sound::play_sound(app, ecs, &effect, pos);
}
}
_ => {}
}
}

View file

@ -4,27 +4,27 @@ use bracket_lib::prelude::*;
use specs::prelude::*;
pub fn particle_to_tile(ecs: &mut World, target: i32, effect: &EffectSpawner) {
if let EffectType::Particle { glyph, fg, bg, lifespan, delay } = effect.effect_type {
if let EffectType::Particle { glyph, sprite, fg, lifespan, delay } = &effect.effect_type {
let map = ecs.fetch::<Map>();
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
*fg,
*glyph,
sprite.clone(),
*lifespan
);
} else {
particle_builder.delay(
target % map.width,
target / map.width,
fg,
bg,
glyph,
lifespan,
delay
*fg,
*glyph,
sprite.clone(),
*lifespan,
*delay
);
}
}
@ -36,8 +36,8 @@ pub fn handle_simple_particles(ecs: &World, entity: Entity, target: &Targets) {
None,
EffectType::Particle {
glyph: part.glyph,
sprite: part.sprite.clone(),
fg: part.colour,
bg: RGB::named(BLACK),
lifespan: part.lifetime_ms,
delay: 0.0,
},
@ -56,7 +56,9 @@ pub fn handle_burst_particles(ecs: &World, entity: Entity, target: &Targets) {
end_pos,
&(SpawnParticleLine {
glyph: part.head_glyph,
sprite: part.head_sprite.clone(),
tail_glyph: part.tail_glyph,
tail_sprite: part.tail_sprite.clone(),
colour: part.colour,
trail_colour: part.trail_colour,
lifetime_ms: part.trail_lifetime_ms, // 75.0 is good here.
@ -75,8 +77,8 @@ pub fn handle_burst_particles(ecs: &World, entity: Entity, target: &Targets) {
None,
EffectType::Particle {
glyph: part.glyph,
sprite: part.sprite.clone(),
fg: part.colour.lerp(part.lerp, (i as f32) * 0.1),
bg: RGB::named(BLACK),
lifespan: part.lifetime_ms / 10.0, // ~50-80 is good here.
delay: burst_delay + ((i as f32) * part.lifetime_ms) / 10.0, // above + burst_delay
},
@ -163,8 +165,8 @@ fn spawn_line_particles(ecs: &World, start: i32, end: i32, part: &SpawnParticleL
None,
EffectType::Particle {
glyph: part.glyph,
sprite: part.sprite.clone(),
fg: part.colour,
bg: RGB::named(BLACK),
lifespan: part.lifetime_ms,
delay: (i as f32) * part.lifetime_ms,
},
@ -175,8 +177,8 @@ fn spawn_line_particles(ecs: &World, start: i32, end: i32, part: &SpawnParticleL
None,
EffectType::Particle {
glyph: part.tail_glyph,
sprite: part.tail_sprite.clone(),
fg: part.trail_colour,
bg: RGB::named(BLACK),
lifespan: part.trail_lifetime_ms,
delay: (i as f32) * part.lifetime_ms,
},

145
src/effects/sound.rs Normal file
View file

@ -0,0 +1,145 @@
use bracket_lib::prelude::*;
use notan::prelude::*;
use specs::prelude::*;
use std::sync::Mutex;
use std::collections::HashMap;
use super::{ EffectSpawner, EffectType, Targets, add_effect };
use crate::Map;
lazy_static::lazy_static! {
pub static ref SOUNDS: Mutex<HashMap<String, (AudioSource, AudioType)>> = Mutex::new(HashMap::new());
pub static ref VOLUME: Mutex<f32> = Mutex::new(1.0);
pub static ref AMBIENCE: Mutex<Option<Sound>> = Mutex::new(None);
}
#[derive(PartialEq, Copy, Clone)]
pub enum AudioType {
Ambient,
SFX,
}
const AMBIENCE_VOL_MUL: f32 = 0.8;
const SFX_VOL_MUL: f32 = 1.0;
pub fn play_sound(app: &mut App, ecs: &mut World, effect: &EffectSpawner, target: usize) {
// Extract sound from the EffectType, or panic if we somehow called this with the wrong effect.
let sound = if let EffectType::Sound { sound } = &effect.effect_type {
sound
} else {
unreachable!("add_intrinsic() called with the wrong EffectType")
};
// Fetch all the relevant precursors.
let sounds = SOUNDS.lock().unwrap();
let volume = VOLUME.lock().unwrap();
let source = sounds.get(sound).unwrap();
let (vol, repeat) = match source.1 {
AudioType::Ambient => (*volume * AMBIENCE_VOL_MUL, true),
AudioType::SFX => {
let map = ecs.fetch::<Map>();
let ppos = ecs.fetch::<Point>();
// Get a slight variation on sound volume, just so things don't sound too uniform.
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
let vol_random = rng.range(0.9, 1.1);
// Calc distance from player to target.
let dist = DistanceAlg::PythagorasSquared.distance2d(
*ppos,
Point::new((target as i32) % map.width, (target as i32) / map.width)
);
// Play sound at volume proportional to distance.
(*volume * SFX_VOL_MUL * vol_random * (1.0 - (dist as f32) / 14.0), false)
}
};
// Play the sound.
let sound: Sound = app.audio.play_sound(&source.0, vol, repeat);
if repeat {
replace_ambience(app, &sound);
}
}
pub fn stop(app: &mut App) {
let mut ambience = AMBIENCE.lock().unwrap();
if let Some(old) = ambience.take() {
app.audio.stop(&old);
}
}
pub fn ambience(sound: &str) {
add_effect(None, EffectType::Sound { sound: sound.to_string() }, Targets::Tile { target: 0 })
}
pub fn replace_ambience(app: &mut App, sound: &Sound) {
let mut ambience = AMBIENCE.lock().unwrap();
if let Some(old) = ambience.take() {
app.audio.stop(&old);
}
*ambience = Some(sound.clone());
}
pub fn init_sounds(app: &mut App) {
let sound_data: &[(&str, &[u8], AudioType)] = &[
// (key, file_path, audiotype)
("a_relax", include_bytes!("../../resources/sounds/amb/relaxed.ogg"), AudioType::Ambient),
("whoosh1", include_bytes!("../../resources/sounds/hit/whoosh1.ogg"), AudioType::SFX),
("whoosh2", include_bytes!("../../resources/sounds/hit/whoosh2.ogg"), AudioType::SFX),
("whoosh3", include_bytes!("../../resources/sounds/hit/whoosh3.ogg"), AudioType::SFX),
("whoosh4", include_bytes!("../../resources/sounds/hit/whoosh4.ogg"), AudioType::SFX),
("d_blocked1", include_bytes!("../../resources/sounds/door/blocked1.wav"), AudioType::SFX),
("d_blocked2", include_bytes!("../../resources/sounds/door/blocked2.wav"), AudioType::SFX),
("d_blocked3", include_bytes!("../../resources/sounds/door/blocked3.wav"), AudioType::SFX),
("d_open1", include_bytes!("../../resources/sounds/door/open1.wav"), AudioType::SFX),
("d_open2", include_bytes!("../../resources/sounds/door/open2.wav"), AudioType::SFX),
("d_open3", include_bytes!("../../resources/sounds/door/open3.wav"), AudioType::SFX),
("d_close1", include_bytes!("../../resources/sounds/door/close1.wav"), AudioType::SFX),
("d_close2", include_bytes!("../../resources/sounds/door/close2.wav"), AudioType::SFX),
("d_close3", include_bytes!("../../resources/sounds/door/close3.wav"), AudioType::SFX),
];
let mut sounds = SOUNDS.lock().unwrap();
for (k, bytes, audiotype) in sound_data {
sounds.insert(k.to_string(), (app.audio.create_source(bytes).unwrap(), *audiotype));
}
}
pub fn set_volume(vol: f32) {
let mut volume = VOLUME.lock().unwrap();
*volume = vol;
}
pub fn clean(app: &mut App) {
app.audio.clean();
}
// Shorthand functions for adding generic, frequent SFX to the effect queue.
pub fn door_open(idx: usize) {
let mut rng = RandomNumberGenerator::new();
let sound = (
match rng.range(0, 3) {
0 => "d_open1",
1 => "d_open2",
_ => "d_open3",
}
).to_string();
super::add_effect(None, EffectType::Sound { sound }, Targets::Tile { target: idx });
}
pub fn door_resist(idx: usize) {
let mut rng = RandomNumberGenerator::new();
let sound = (
match rng.range(0, 3) {
0 => "d_blocked1",
1 => "d_blocked2",
_ => "d_blocked3",
}
).to_string();
add_effect(None, EffectType::Sound { sound }, Targets::Tile { target: idx });
}
pub fn door_close(idx: usize) {
let mut rng = RandomNumberGenerator::new();
let sound = (
match rng.range(0, 3) {
0 => "d_close1",
1 => "d_close2",
_ => "d_close3",
}
).to_string();
add_effect(None, EffectType::Sound { sound }, Targets::Tile { target: idx });
}

View file

@ -36,7 +36,7 @@ use crate::{
WantsToRemoveKey,
WantsToDelete,
};
use crate::data::messages::*;
use crate::consts::messages::*;
use bracket_lib::prelude::*;
use specs::prelude::*;
pub fn item_trigger(source: Option<Entity>, item: Entity, target: &Targets, ecs: &mut World) {
@ -223,7 +223,9 @@ fn handle_healing(
let renderables = ecs.read_storage::<Renderable>();
if ecs.read_storage::<Player>().get(target).is_some() {
logger = logger
.colour(renderable_colour(&renderables, target))
.append("You")
.colour(WHITE)
.append(HEAL_PLAYER_HIT)
.buc(event.buc.clone(), None, Some(HEAL_PLAYER_HIT_BLESSED));
} else {
@ -265,7 +267,9 @@ fn handle_damage(
let player_viewshed = viewsheds.get(*ecs.fetch::<Entity>()).unwrap();
if ecs.read_storage::<Player>().get(target).is_some() {
logger = logger
.colour(renderable_colour(&renderables, target))
.append("You")
.colour(WHITE)
.append(DAMAGE_PLAYER_HIT);
event.log = true;
} else if

View file

@ -65,6 +65,7 @@ impl Logger {
/// Pushes the finished log entry.
pub fn log(self) {
return append_entry(self.fragments);
let key = crate::gamelog::get_event_count(crate::consts::events::EVENT::COUNT_TURN);
return append_entry(key, self.fragments);
}
}

View file

@ -1,7 +1,7 @@
use std::collections::{ HashSet, HashMap };
use std::sync::Mutex;
use crate::data::events::EVENT;
use crate::data::names::*;
use crate::consts::events::EVENT;
use crate::consts::names::*;
lazy_static! {
/// A count of each event that has happened over the run. i.e. "turns", "descended", "ascended"
@ -126,7 +126,7 @@ pub fn record_event(event: EVENT) {
new_event = format!("Discovered {}", name);
}
EVENT::Identified(name) => {
new_event = format!("Identified {}", crate::gui::with_article(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.

View file

@ -1,91 +1,70 @@
use super::{ events, LogFragment, Logger };
use bracket_lib::prelude::*;
use std::sync::Mutex;
use std::collections::BTreeMap;
use notan::prelude::*;
use notan::text::CreateText;
use crate::consts::{ TILESIZE, FONTSIZE };
use crate::consts::visuals::VIEWPORT_W;
use crate::Fonts;
lazy_static! {
static ref LOG: Mutex<Vec<Vec<LogFragment>>> = Mutex::new(Vec::new());
pub static ref LOG: Mutex<BTreeMap<i32, Vec<LogFragment>>> = Mutex::new(BTreeMap::new());
}
#[allow(dead_code)]
pub fn append_fragment(fragment: LogFragment) {
LOG.lock().unwrap().push(vec![fragment]);
/// Render with specificied params.
pub fn render_log(
draw: &RenderTexture,
gfx: &mut Graphics,
font: &Fonts,
pos: &(f32, f32),
width: f32,
entries: usize
) {
let mut text = gfx.create_text();
let log = LOG.lock().unwrap();
let latest: Vec<_> = log.iter().rev().take(entries).collect();
let mut initialised = false;
let mut y = pos.1;
for (_, entries) in latest {
let mut written_on_line = false;
for frag in entries.iter() {
if !written_on_line {
text.add(&frag.text)
.font(font.n())
.position(pos.0, y)
.size(FONTSIZE)
.max_width(width)
.color(Color::from_rgb(frag.colour.r, frag.colour.g, frag.colour.b))
.v_align_bottom();
written_on_line = true;
initialised = true;
} else {
text.chain(&frag.text)
.color(Color::from_rgb(frag.colour.r, frag.colour.g, frag.colour.b))
.size(FONTSIZE);
}
}
if initialised {
y = text.last_bounds().min_y();
}
}
gfx.render_to(draw, &text);
}
pub fn append_entry(fragments: Vec<LogFragment>) {
LOG.lock().unwrap().push(fragments);
pub fn append_entry(turn: i32, fragments: Vec<LogFragment>) {
let mut log = LOG.lock().unwrap();
if let Some(existing) = log.get_mut(&turn) {
existing.extend(fragments);
} else {
log.insert(turn, fragments);
}
}
pub fn clear_log() {
LOG.lock().unwrap().clear();
}
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 x = pos.x;
// Reverse the log, take the number we want to show, and iterate through them
LOG.lock()
.unwrap()
.iter()
.rev()
.take(len)
.for_each(|log| {
let mut entry_len = -2;
// Iterate through each message fragment, and get the total length
// in lines, by adding the length of every fragment and dividing it
// by the maximum length we desire. Then shuffle our start-y by that much.
log.iter().for_each(|frag| {
entry_len += frag.text.len() as i32;
});
let lines = entry_len / maximum_len;
y -= lines;
let mut i = 0;
log.iter().for_each(|frag| {
// Split every fragment up into single characters.
let parts = frag.text.split("");
for part in parts {
// This is an extremely hacky solution to a problem I don't understand yet.
// -- without this, the lines *here* and the line count *above* wont match.
if part == "" || part == "\\" {
continue;
}
if i > entry_len {
break;
}
i += 1;
if x + (part.len() as i32) > pos.x + maximum_len {
if y > pos.y - (len as i32) {
console.print(x, y, "-");
}
y += 1;
x = pos.x;
}
// Stay within bounds
if y > pos.y - (len as i32) {
console.print_color(
x,
y,
frag.colour.into(),
RGB::named(BLACK).into(),
part
);
}
x += part.len() as i32;
}
});
// Take away one from the y-axis, because we want to start each entry
// on a new line, and go up an additional amount depending on how many
// lines our *previous* entry took.
y -= 1 + lines;
x = pos.x;
});
}
pub fn setup_log() {
clear_log();
events::clear_events();
@ -98,11 +77,11 @@ pub fn setup_log() {
.log();
}
pub fn clone_log() -> Vec<Vec<crate::gamelog::LogFragment>> {
pub fn clone_log() -> BTreeMap<i32, Vec<LogFragment>> {
return LOG.lock().unwrap().clone();
}
pub fn restore_log(log: &mut Vec<Vec<crate::gamelog::LogFragment>>) {
pub fn restore_log(log: &mut BTreeMap<i32, Vec<LogFragment>>) {
LOG.lock().unwrap().clear();
LOG.lock().unwrap().append(log);
}

View file

@ -2,7 +2,7 @@ mod builder;
pub use builder::*;
mod logstore;
use logstore::*;
pub use logstore::{ clear_log, clone_log, print_log, restore_log, setup_log };
pub use logstore::{ LOG, clear_log, clone_log, render_log, restore_log, setup_log };
mod events;
pub use events::*;

View file

@ -1,7 +1,7 @@
use super::{ Skill, Skills };
use crate::gui::{ Ancestry, Class };
use crate::data::entity;
use crate::data::char_create::*;
use crate::consts::entity;
use crate::consts::char_create::*;
use bracket_lib::prelude::*;
use std::cmp::max;
@ -85,6 +85,7 @@ pub fn get_attribute_rolls(
ancestry: Ancestry
) -> (i32, i32, i32, i32, i32, i32) {
let (mut str, mut dex, mut con, mut int, mut wis, mut cha) = match class {
Class::Unset => VILLAGER_MIN_ATTR,
Class::Fighter => FIGHTER_MIN_ATTR,
Class::Rogue => ROGUE_MIN_ATTR,
Class::Wizard => WIZARD_MIN_ATTR,
@ -92,6 +93,7 @@ pub fn get_attribute_rolls(
};
let mut remaining_points = TOTAL_ATTRIBUTE_POINTS_MAXIMUM - (str + dex + con + int + wis + cha);
let improve_chance: [i32; 6] = match class {
Class::Unset => VILLAGER_IMPR_CHANCE,
Class::Fighter => FIGHTER_IMPR_CHANCE,
Class::Rogue => ROGUE_IMPR_CHANCE,
Class::Wizard => WIZARD_IMPR_CHANCE,

View file

@ -8,8 +8,8 @@ use super::{
RunState,
State,
};
use crate::data::entity;
use crate::data::char_create::*;
use crate::consts::entity;
use crate::consts::char_create::*;
use crate::{
raws,
Attribute,
@ -28,10 +28,11 @@ use bracket_lib::prelude::*;
use serde::{ Deserialize, Serialize };
use specs::prelude::*;
use std::collections::HashMap;
use crate::consts::prelude::*;
#[derive(Serialize, Deserialize, Copy, Clone, PartialEq)]
#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Ancestry {
NULL,
Unset,
Human,
Dwarf,
Gnome,
@ -39,8 +40,9 @@ pub enum Ancestry {
Catfolk,
}
#[derive(Serialize, Deserialize, Copy, Clone, PartialEq)]
#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Class {
Unset,
Fighter,
Rogue,
Wizard,
@ -48,48 +50,53 @@ pub enum Class {
}
lazy_static! {
static ref ANCESTRY_CLASS_DATA: HashMap<String, Vec<String>> = {
static ref ANCESTRYDATA: HashMap<Ancestry, Vec<String>> = {
let mut m = HashMap::new();
// Ancestry
m.insert(
"human".to_string(),
Ancestry::Human,
vec![
"nothing".to_string()]);
m.insert(
"dwarf".to_string(),
Ancestry::Dwarf,
vec![
"a natural bonus to defence".to_string()]);
m.insert(
"elf".to_string(),
Ancestry::Elf,
vec![
"minor telepathy".to_string(),
"a slightly increased speed".to_string()]);
m.insert(
"catfolk".to_string(),
Ancestry::Catfolk,
vec![
"increased speed".to_string(),
"increased unarmed damage".to_string()]);
// Class
return m;
};
}
lazy_static! {
static ref CLASSDATA: HashMap<Class, Vec<String>> = {
let mut m = HashMap::new();
m.insert(
"fighter".to_string(),
Class::Fighter,
vec![
format!("a longsword, ring mail, and {} food", FIGHTER_STARTING_FOOD),
"10 str, 8 dex, 10 con, 6 int, 6 wis, 8 cha".to_string(),
"and 27 random stat points".to_string()]);
m.insert(
"rogue".to_string(),
Class::Rogue,
vec![
format!("a rapier, leather armour, and {} food", ROGUE_STARTING_FOOD),
"8 str, 10 dex, 8 con, 6 int, 8 wis, 10 cha".to_string(),
"and 35 random stat points".to_string()]);
m.insert(
"wizard".to_string(),
Class::Wizard,
vec![
format!("a dagger, random scrolls/potions, and {} food", WIZARD_STARTING_FOOD),
"6 str, 8 dex, 6 con, 10 int, 10 wis, 8 cha".to_string(),
"and 17 random stat points".to_string()]);
m.insert(
"villager".to_string(),
Class::Villager,
vec![
format!("the first weapon you could find, and {} food", VILLAGER_STARTING_FOOD),
"6 str, 6 dex, 6 con, 6 int, 6 wis, 6 cha".to_string(),
@ -110,142 +117,123 @@ pub enum CharCreateResult {
},
}
/// Handles the player character creation screen.
pub fn character_creation(gs: &mut State, ctx: &mut BTerm) -> CharCreateResult {
let runstate = gs.ecs.fetch::<RunState>();
use notan::prelude::*;
use notan::draw::{ Draw, DrawTextSection };
use super::{ FONTSIZE, TILESIZE };
use crate::consts::DISPLAYHEIGHT;
use crate::Fonts;
let mut x = 2;
let mut y = 11;
let column_width = 20;
ctx.print_color(x, y, RGB::named(WHITE), RGB::named(BLACK), CHAR_CREATE_HEADER);
y += 2;
if let RunState::CharacterCreation { ancestry, class } = *runstate {
let selected_fg = RGB::named(GREEN);
let unselected_fg = RGB::named(WHITE);
let mut fg;
let bg = RGB::named(BLACK);
// Ancestry
ctx.print_color(x, y, bg, unselected_fg, "Ancestry");
ctx.print_color(x + column_width, y, bg, unselected_fg, "Class");
y += 1;
let mut race_str = "human";
if ancestry == Ancestry::Human {
fg = selected_fg;
} else {
fg = unselected_fg;
}
ctx.print_color(x, y, fg, bg, "h. Human");
if ancestry == Ancestry::Elf {
fg = selected_fg;
race_str = "elf";
} else {
fg = unselected_fg;
}
ctx.print_color(x, y + 1, fg, bg, "e. Elf");
if ancestry == Ancestry::Dwarf {
fg = selected_fg;
race_str = "dwarf";
} else {
fg = unselected_fg;
}
ctx.print_color(x, y + 2, fg, bg, "d. Dwarf");
if ancestry == Ancestry::Catfolk {
fg = selected_fg;
race_str = "catfolk";
} else {
fg = unselected_fg;
}
ctx.print_color(x, y + 3, fg, bg, "c. Catfolk");
// Class
let mut class_str = "fighter";
x += column_width;
if class == Class::Fighter {
fg = selected_fg;
} else {
fg = unselected_fg;
}
ctx.print_color(x, y, fg, bg, "f. Fighter");
if class == Class::Rogue {
fg = selected_fg;
class_str = "rogue";
} else {
fg = unselected_fg;
}
ctx.print_color(x, y + 1, fg, bg, "r. Rogue");
if class == Class::Wizard {
fg = selected_fg;
class_str = "wizard";
} else {
fg = unselected_fg;
}
ctx.print_color(x, y + 2, fg, bg, "w. Wizard");
if class == Class::Villager {
fg = selected_fg;
class_str = "villager";
} else {
fg = unselected_fg;
}
ctx.print_color(x, y + 3, fg, bg, "v. Villager");
// Selected ancestry/class benefits
x += column_width;
ctx.print_color(x, y, selected_fg, bg, ANCESTRY_INFO_HEADER);
for line in ANCESTRY_CLASS_DATA.get(race_str).unwrap().iter() {
y += 1;
ctx.print_color(x + 1, y, unselected_fg, bg, line);
}
y += 2;
ctx.print_color(x, y, selected_fg, bg, CLASS_INFO_HEADER);
for line in ANCESTRY_CLASS_DATA.get(class_str).unwrap().iter() {
y += 1;
ctx.print_color(x + 1, y, unselected_fg, bg, line);
}
match ctx.key {
None => {
return CharCreateResult::NoSelection { ancestry, class };
}
Some(key) =>
match key {
VirtualKeyCode::Escape => {
return CharCreateResult::Selected { ancestry: Ancestry::NULL, class };
}
VirtualKeyCode::Return => {
return CharCreateResult::Selected { ancestry, class };
}
VirtualKeyCode::H => {
return CharCreateResult::NoSelection { ancestry: Ancestry::Human, class };
}
VirtualKeyCode::E => {
return CharCreateResult::NoSelection { ancestry: Ancestry::Elf, class };
}
VirtualKeyCode::D => {
return CharCreateResult::NoSelection { ancestry: Ancestry::Dwarf, class };
}
VirtualKeyCode::C => {
return CharCreateResult::NoSelection { ancestry: Ancestry::Catfolk, class };
}
VirtualKeyCode::F => {
return CharCreateResult::NoSelection { ancestry, class: Class::Fighter };
}
VirtualKeyCode::R => {
return CharCreateResult::NoSelection { ancestry, class: Class::Rogue };
}
VirtualKeyCode::W => {
return CharCreateResult::NoSelection { ancestry, class: Class::Wizard };
}
VirtualKeyCode::V => {
return CharCreateResult::NoSelection { ancestry, class: Class::Villager };
}
_ => {
return CharCreateResult::NoSelection { ancestry, class };
}
}
}
pub fn draw_charcreation(
ecs: &World,
draw: &mut Draw,
atlas: &HashMap<String, Texture>,
font: &Fonts
) {
let runstate = ecs.read_resource::<RunState>();
let (class, ancestry) = match *runstate {
RunState::CharacterCreation { class, ancestry } => (class, ancestry),
_ => unreachable!("draw_charcreation() called outside of CharacterCreation runstate."),
};
let (mut x, mut y) = (2.0 * TILESIZE.x, ((DISPLAYHEIGHT as f32) * TILESIZE.x) / 4.0);
const COLUMN_WIDTH: f32 = 20.0 * TILESIZE.x;
draw.text(&font.ib(), "Who are you?")
.size(FONTSIZE * 2.0)
.position(x, y)
.h_align_left();
y = draw.last_text_bounds().max_y();
let initial_y = y;
let ancestries = [
("h. Human", Ancestry::Human),
("e. Elf", Ancestry::Elf),
("d. Dwarf", Ancestry::Dwarf),
("c. Catfolk", Ancestry::Catfolk),
];
for (k, v) in &ancestries {
draw.text(font.n(), k)
.size(FONTSIZE)
.position(x, y)
.h_align_left()
.color(get_colour(ancestry, *v));
y = draw.last_text_bounds().max_y();
}
return CharCreateResult::NoSelection { ancestry: Ancestry::Human, class: Class::Fighter };
y = initial_y;
x += COLUMN_WIDTH;
let classes = [
("f. Fighter", Class::Fighter),
("r. Rogue", Class::Rogue),
("w. Wizard", Class::Wizard),
("v. Villager", Class::Villager),
];
for (k, v) in &classes {
draw.text(font.n(), k)
.size(FONTSIZE)
.position(x, y)
.h_align_left()
.color(get_colour(class, *v));
y = draw.last_text_bounds().max_y();
}
y = initial_y;
x += COLUMN_WIDTH;
for line in ANCESTRYDATA.get(&ancestry).unwrap().iter() {
draw.text(font.n(), line).size(FONTSIZE).position(x, y).h_align_left();
y = draw.last_text_bounds().max_y();
}
y += TILESIZE.x;
for line in CLASSDATA.get(&class).unwrap().iter() {
draw.text(font.n(), line).size(FONTSIZE).position(x, y).h_align_left();
y = draw.last_text_bounds().max_y();
}
}
fn get_colour<T>(selected: T, desired: T) -> Color where T: PartialEq {
if selected == desired { Color::from_rgb(0.0, 1.0, 0.0) } else { Color::WHITE }
}
/// Handles the player character creation screen.
pub fn character_creation(gs: &mut State, ctx: &mut App) -> CharCreateResult {
let runstate = gs.ecs.fetch::<RunState>();
let (ancestry, class) = match *runstate {
RunState::CharacterCreation { ancestry, class } => (ancestry, class),
_ => unreachable!("character_creation() called outside of CharacterCreation runstate."),
};
let key = &ctx.keyboard;
for keycode in key.pressed.iter() {
match *keycode {
KeyCode::Escape => {
return CharCreateResult::Selected { ancestry: Ancestry::Unset, class };
}
KeyCode::Return => {
return CharCreateResult::Selected { ancestry, class };
}
KeyCode::H => {
return CharCreateResult::NoSelection { ancestry: Ancestry::Human, class };
}
KeyCode::E => {
return CharCreateResult::NoSelection { ancestry: Ancestry::Elf, class };
}
KeyCode::D => {
return CharCreateResult::NoSelection { ancestry: Ancestry::Dwarf, class };
}
KeyCode::C => {
return CharCreateResult::NoSelection { ancestry: Ancestry::Catfolk, class };
}
KeyCode::F => {
return CharCreateResult::NoSelection { ancestry, class: Class::Fighter };
}
KeyCode::R => {
return CharCreateResult::NoSelection { ancestry, class: Class::Rogue };
}
KeyCode::W => {
return CharCreateResult::NoSelection { ancestry, class: Class::Wizard };
}
KeyCode::V => {
return CharCreateResult::NoSelection { ancestry, class: Class::Villager };
}
_ => {}
};
}
return CharCreateResult::NoSelection { ancestry, class };
}
/// Handles player ancestry setup.
@ -267,25 +255,9 @@ pub fn setup_player_ancestry(ecs: &mut World, ancestry: Ancestry) {
match ancestry {
Ancestry::Human => {}
Ancestry::Dwarf => {
renderables
.insert(*player, Renderable {
glyph: to_cp437(DWARF_GLYPH),
fg: RGB::named(DWARF_COLOUR),
bg: RGB::named(BLACK),
render_order: 0,
})
.expect("Unable to insert renderable component");
*player_skills.skills.entry(Skill::Defence).or_insert(0) += DWARF_DEFENCE_MOD;
}
Ancestry::Elf => {
renderables
.insert(*player, Renderable {
glyph: to_cp437(ELF_GLYPH),
fg: RGB::named(ELF_COLOUR),
bg: RGB::named(BLACK),
render_order: 0,
})
.expect("Unable to insert renderable component");
let mut telepaths = ecs.write_storage::<Telepath>();
telepaths
.insert(*player, Telepath {
@ -303,14 +275,6 @@ pub fn setup_player_ancestry(ecs: &mut World, ancestry: Ancestry) {
.expect("Unable to insert energy component");
}
Ancestry::Catfolk => {
renderables
.insert(*player, Renderable {
glyph: to_cp437(CATFOLK_GLYPH),
fg: RGB::named(CATFOLK_COLOUR),
bg: RGB::named(BLACK),
render_order: 0,
})
.expect("Unable to insert renderable component");
let mut speeds = ecs.write_storage::<Energy>();
speeds
.insert(*player, Energy {
@ -342,14 +306,7 @@ pub fn setup_player_class(ecs: &mut World, class: Class, ancestry: Ancestry) {
let mut attributes = ecs.write_storage::<Attributes>();
let (str, dex, con, int, wis, cha) = get_attribute_rolls(&mut rng, class, ancestry);
attributes
.insert(player, Attributes {
strength: Attribute { base: str, modifiers: 0, bonus: attr_bonus(str) },
dexterity: Attribute { base: dex, modifiers: 0, bonus: attr_bonus(dex) },
constitution: Attribute { base: con, modifiers: 0, bonus: attr_bonus(con) },
intelligence: Attribute { base: int, modifiers: 0, bonus: attr_bonus(int) },
wisdom: Attribute { base: wis, modifiers: 0, bonus: attr_bonus(wis) },
charisma: Attribute { base: cha, modifiers: 0, bonus: attr_bonus(cha) },
})
.insert(player, Attributes::with_stats(str, dex, con, int, wis, cha))
.expect("Unable to insert attributes component");
let mut pools = ecs.write_storage::<Pools>();
@ -407,6 +364,9 @@ fn get_starting_inventory(
let mut carried: Vec<String> = Vec::new();
let starting_food: &str;
match class {
Class::Unset => {
starting_food = VILLAGER_STARTING_FOOD;
}
Class::Fighter => {
starting_food = FIGHTER_STARTING_FOOD;
equipped = vec![
@ -440,7 +400,7 @@ fn get_starting_inventory(
}
Class::Villager => {
starting_food = VILLAGER_STARTING_FOOD;
pick_random_table_item(rng, &mut equipped, "villager_equipment", "1", None);
pick_random_table_item(rng, &mut equipped, "villager_equipment", "1d1", None);
}
}
pick_random_table_item(rng, &mut carried, "food", starting_food, None);
@ -454,7 +414,9 @@ fn pick_random_table_item(
dice_str: &'static str,
difficulty: Option<i32>
) {
let dice = parse_dice_string(dice_str).expect("Error parsing dice");
let dice = parse_dice_string(dice_str).expect(
format!("Error parsing dice: {}", dice_str).as_str()
);
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));
}

View file

@ -1,5 +1,9 @@
use super::State;
use super::{ State };
use bracket_lib::prelude::*;
use notan::prelude::*;
use notan::draw::DrawTextSection;
use std::collections::HashMap;
use crate::consts::{ TILESIZE, FONTSIZE };
#[derive(PartialEq, Copy, Clone)]
pub enum CheatMenuResult {
@ -12,53 +16,50 @@ pub enum CheatMenuResult {
GodMode,
}
pub fn show_cheat_menu(_gs: &mut State, ctx: &mut BTerm) -> CheatMenuResult {
let (x_offset, y_offset) = (1, 10);
ctx.print_color(
1 + x_offset,
1 + y_offset,
RGB::named(RED),
RGB::named(BLACK),
"DEBUG MENU! [aA-zZ][Esc.]"
);
let x = 1 + x_offset;
let mut y = 3 + y_offset;
let count = 5;
let width = 19;
ctx.draw_box(x, y, width, (count + 1) as i32, RGB::named(RED), RGB::named(BLACK));
y += 1;
// Asc
ctx.set(x_offset + 2, y, RGB::named(YELLOW), RGB::named(BLACK), to_cp437('a'));
ctx.print(x_offset + 4, y, "ASCEND A FLOOR");
y += 1;
// Desc
ctx.set(x_offset + 2, y, RGB::named(YELLOW), RGB::named(BLACK), to_cp437('d'));
ctx.print(x_offset + 4, y, "DESCEND A FLOOR");
y += 1;
// Heal
ctx.set(x_offset + 2, y, RGB::named(YELLOW), RGB::named(BLACK), to_cp437('h'));
ctx.print(x_offset + 4, y, "HEAL TO FULL");
y += 1;
// Reveal map
ctx.set(x_offset + 2, y, RGB::named(YELLOW), RGB::named(BLACK), to_cp437('m'));
ctx.print(x_offset + 4, y, "MAGIC MAP REVEAL");
y += 1;
// Godmode
ctx.set(x_offset + 2, y, RGB::named(YELLOW), RGB::named(BLACK), to_cp437('g'));
ctx.print(x_offset + 4, y, "GOD MODE");
// Match keys
match ctx.key {
None => CheatMenuResult::NoResponse,
Some(key) =>
match key {
VirtualKeyCode::A => CheatMenuResult::Ascend,
VirtualKeyCode::D => CheatMenuResult::Descend,
VirtualKeyCode::H => CheatMenuResult::Heal,
VirtualKeyCode::M => CheatMenuResult::MagicMap,
VirtualKeyCode::G => CheatMenuResult::GodMode,
VirtualKeyCode::Escape => CheatMenuResult::Cancel,
_ => CheatMenuResult::NoResponse,
pub fn show_cheat_menu(_gs: &mut State, ctx: &mut App) -> CheatMenuResult {
let key = &ctx.keyboard;
for keycode in key.pressed.iter() {
match *keycode {
KeyCode::A => {
return CheatMenuResult::Ascend;
}
KeyCode::D => {
return CheatMenuResult::Descend;
}
KeyCode::H => {
return CheatMenuResult::Heal;
}
KeyCode::M => {
return CheatMenuResult::MagicMap;
}
KeyCode::G => {
return CheatMenuResult::GodMode;
}
KeyCode::Escape => {
return CheatMenuResult::Cancel;
}
_ => {}
};
}
return CheatMenuResult::NoResponse;
}
pub fn draw_cheat_menu(
draw: &mut notan::draw::Draw,
_atlas: &HashMap<String, Texture>,
font: &crate::Fonts
) {
let offsets = crate::camera::get_offset();
const DEBUG_MENU: &str =
r#"DEBUG MENU! [aA-zZ][Esc.]
a - ASCEND A FLOOR
d - DESCEND A FLOOR
h - HEAL TO FULL
m - MAGIC MAP REVEAL
g - GOD MODE"#;
draw.text(&font.n(), DEBUG_MENU)
.position((1.0 + (offsets.x as f32)) * TILESIZE.x, (1.0 + (offsets.y as f32)) * TILESIZE.x)
.color(Color::RED)
.size(FONTSIZE);
}

View file

@ -1,5 +1,10 @@
use super::{ State, RunState, tooltip::draw_tooltips, camera::get_screen_bounds };
use super::{ State, RunState, World, tooltip::draw_tooltips, camera::get_offset };
use bracket_lib::prelude::*;
use notan::prelude::*;
use notan::draw::{ Draw, DrawImages };
use std::collections::HashMap;
use crate::consts::visuals::{ TILES_IN_VIEWPORT_H, TILES_IN_VIEWPORT_W };
use crate::consts::TILESIZE;
#[derive(PartialEq, Copy, Clone)]
pub enum FarlookResult {
@ -10,45 +15,62 @@ pub enum FarlookResult {
Cancel,
}
pub fn show_farlook(gs: &mut State, ctx: &mut BTerm) -> FarlookResult {
pub fn show_farlook(gs: &mut State, ctx: &mut App) -> 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 },
let x = x.clamp(0, TILES_IN_VIEWPORT_W - 1);
let y = y.clamp(0, TILES_IN_VIEWPORT_H - 1);
let key = &ctx.keyboard;
// Movement
for keycode in key.pressed.iter() {
match *keycode {
KeyCode::Escape | KeyCode::X => {
return FarlookResult::Cancel;
}
};
KeyCode::Numpad1 => {
return FarlookResult::NoResponse { x: x - 1, y: y + 1 };
}
KeyCode::Numpad2 => {
return FarlookResult::NoResponse { x, y: y + 1 };
}
KeyCode::Numpad3 => {
return FarlookResult::NoResponse { x: x + 1, y: y + 1 };
}
KeyCode::Numpad4 => {
return FarlookResult::NoResponse { x: x - 1, y };
}
KeyCode::Numpad6 => {
return FarlookResult::NoResponse { x: x + 1, y };
}
KeyCode::Numpad7 => {
return FarlookResult::NoResponse { x: x - 1, y: y - 1 };
}
KeyCode::Numpad8 => {
return FarlookResult::NoResponse { x, y: y - 1 };
}
KeyCode::Numpad9 => {
return FarlookResult::NoResponse { x: x + 1, y: y - 1 };
}
_ => {}
}
}
return 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 };
return FarlookResult::NoResponse { x: TILES_IN_VIEWPORT_W / 2, y: TILES_IN_VIEWPORT_H / 2 };
}
}
pub fn draw_farlook(
ecs: &World,
x: i32,
y: i32,
draw: &mut Draw,
atlas: &HashMap<String, Texture>
) {
let placement = super::viewport_to_px(x, y);
draw.image(atlas.get("select1").unwrap())
.position(placement.x, placement.y)
.size(TILESIZE.sprite_x, TILESIZE.sprite_y);
let _idx = super::viewport_to_idx(ecs, x, y);
// Get tooltip for idx, etc.
}

View file

@ -4,9 +4,11 @@ use super::{
obfuscate_name_ecs,
print_options,
unique_ecs,
check_key,
letter_to_option,
renderable_colour,
ItemMenuResult,
UniqueInventoryItem,
BUC,
Key,
};
use crate::{
gamelog,
@ -19,7 +21,6 @@ use crate::{
Name,
ObfuscatedName,
Renderable,
Key,
states::state::*,
};
use bracket_lib::prelude::*;
@ -96,7 +97,7 @@ pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<Enti
return (ItemMenuResult::Selected, Some(build_identify_iterator().nth(0).unwrap().0));
}
let mut player_inventory: super::PlayerInventory = HashMap::new();
for (entity, _i, _r, _n, key) in build_identify_iterator() {
for (entity, _i, renderable, name, key) in build_identify_iterator() {
let unique_item = unique_ecs(&gs.ecs, entity);
player_inventory
.entry(unique_item)
@ -107,39 +108,45 @@ pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<Enti
}
// Get display args
let width = get_max_inventory_width(&player_inventory);
let (_, _, _, _, x_offset, y_offset) = crate::camera::get_screen_bounds(&gs.ecs, ctx);
let (x, y) = (x_offset + 1, y_offset + 3);
let offsets = crate::camera::get_offset();
let (x, y) = (offsets.x + 1, offsets.y + 3);
// Draw menu
ctx.print_color(
1 + x_offset,
1 + y_offset,
1 + offsets.x,
1 + offsets.y,
RGB::named(WHITE),
RGB::named(BLACK),
"Identify which item? [aA-zZ][Esc.]"
);
ctx.draw_box(x, y, width + 2, count + 1, RGB::named(WHITE), RGB::named(BLACK));
print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx);
// Input
match ctx.key {
/*match ctx.key {
None => (ItemMenuResult::NoResponse, None),
Some(key) =>
match key {
VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None),
_ => {
let selection = letter_to_option::letter_to_option(key, ctx.shift);
if selection != -1 && check_key(selection as usize) {
// Get the first entity with a Key {} component that has an idx matching "selection".
let entities = gs.ecs.entities();
let keyed_items = gs.ecs.read_storage::<Key>();
let backpack = gs.ecs.read_storage::<InBackpack>();
for (e, key, _b) in (&entities, &keyed_items, &backpack).join() {
if key.idx == (selection as usize) {
return (ItemMenuResult::Selected, Some(e));
}
}
let selection = letter_to_option(key);
if selection > -1 && selection < (count as i32) {
let item = player_inventory
.iter()
.nth(selection as usize)
.unwrap().1.0;
gamelog::Logger
::new()
.append("You identify the")
.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)
}

76
src/gui/inventory.rs Normal file
View file

@ -0,0 +1,76 @@
use notan::prelude::*;
use notan::draw::{ Draw, Font, DrawTextSection };
use specs::prelude::*;
use super::TILESIZE;
use crate::{ Fonts, camera::get_offset };
use super::{ items, Filter, print_options, ItemType, FONTSIZE };
pub enum Location {
All,
Backpack,
Equipped,
}
pub fn all_itemtypes() -> Vec<ItemType> {
vec![
ItemType::Weapon,
ItemType::Armour,
ItemType::Comestible,
ItemType::Potion,
ItemType::Scroll,
ItemType::Spellbook,
ItemType::Wand,
ItemType::Amulet,
ItemType::Ring
]
}
pub fn draw_items(
ecs: &World,
draw: &mut Draw,
font: &Fonts,
x: f32,
y: f32,
loc: Location,
itemtypes: Option<Vec<ItemType>>
) {
let mut y = y;
if let Some(itemtypes) = itemtypes {
for itemtype in itemtypes {
let filter = match loc {
Location::All => Filter::All(Some(itemtype)),
Location::Backpack => Filter::Backpack(Some(itemtype)),
Location::Equipped => Filter::Equipped,
};
let inv = items(ecs, filter);
if inv.is_empty() {
continue;
}
draw.text(&font.b(), itemtype.string())
.position(x, y)
.color(Color::WHITE)
.size(FONTSIZE);
y += TILESIZE.x;
y = print_options(ecs, draw, font, &inv, x, y) + TILESIZE.x;
}
} else {
let filter = match loc {
Location::All => Filter::All(None),
Location::Backpack => Filter::Backpack(None),
Location::Equipped => Filter::Equipped,
};
let inv = items(ecs, filter);
if inv.is_empty() {
return;
}
y = print_options(ecs, draw, font, &inv, x, y) + TILESIZE.x;
}
}
pub fn draw_all_items(ecs: &World, draw: &mut Draw, font: &Fonts, x: f32, y: f32) {
draw_items(ecs, draw, font, x, y, Location::All, Some(all_itemtypes()));
}
pub fn draw_backpack_items(ecs: &World, draw: &mut Draw, font: &Fonts, x: f32, y: f32) {
draw_items(ecs, draw, font, x, y, Location::Backpack, Some(all_itemtypes()));
}

View file

@ -1,65 +1,65 @@
use bracket_lib::prelude::*;
use notan::prelude::*;
pub fn letter_to_option(key: VirtualKeyCode, shift: bool) -> i32 {
pub fn letter_to_option(key: KeyCode, shift: bool) -> Option<usize> {
if shift {
match key {
VirtualKeyCode::A => 26,
VirtualKeyCode::B => 27,
VirtualKeyCode::C => 28,
VirtualKeyCode::D => 29,
VirtualKeyCode::E => 30,
VirtualKeyCode::F => 31,
VirtualKeyCode::G => 32,
VirtualKeyCode::H => 33,
VirtualKeyCode::I => 34,
VirtualKeyCode::J => 35,
VirtualKeyCode::K => 36,
VirtualKeyCode::L => 37,
VirtualKeyCode::M => 38,
VirtualKeyCode::N => 39,
VirtualKeyCode::O => 40,
VirtualKeyCode::P => 41,
VirtualKeyCode::Q => 42,
VirtualKeyCode::R => 43,
VirtualKeyCode::S => 44,
VirtualKeyCode::T => 45,
VirtualKeyCode::U => 46,
VirtualKeyCode::V => 47,
VirtualKeyCode::W => 48,
VirtualKeyCode::X => 49,
VirtualKeyCode::Y => 50,
VirtualKeyCode::Z => 51,
_ => -1,
KeyCode::A => Some(26),
KeyCode::B => Some(27),
KeyCode::C => Some(28),
KeyCode::D => Some(29),
KeyCode::E => Some(30),
KeyCode::F => Some(31),
KeyCode::G => Some(32),
KeyCode::H => Some(33),
KeyCode::I => Some(34),
KeyCode::J => Some(35),
KeyCode::K => Some(36),
KeyCode::L => Some(37),
KeyCode::M => Some(38),
KeyCode::N => Some(39),
KeyCode::O => Some(40),
KeyCode::P => Some(41),
KeyCode::Q => Some(42),
KeyCode::R => Some(43),
KeyCode::S => Some(44),
KeyCode::T => Some(45),
KeyCode::U => Some(46),
KeyCode::V => Some(47),
KeyCode::W => Some(48),
KeyCode::X => Some(49),
KeyCode::Y => Some(50),
KeyCode::Z => Some(51),
_ => None,
}
} else {
match key {
VirtualKeyCode::A => 0,
VirtualKeyCode::B => 1,
VirtualKeyCode::C => 2,
VirtualKeyCode::D => 3,
VirtualKeyCode::E => 4,
VirtualKeyCode::F => 5,
VirtualKeyCode::G => 6,
VirtualKeyCode::H => 7,
VirtualKeyCode::I => 8,
VirtualKeyCode::J => 9,
VirtualKeyCode::K => 10,
VirtualKeyCode::L => 11,
VirtualKeyCode::M => 12,
VirtualKeyCode::N => 13,
VirtualKeyCode::O => 14,
VirtualKeyCode::P => 15,
VirtualKeyCode::Q => 16,
VirtualKeyCode::R => 17,
VirtualKeyCode::S => 18,
VirtualKeyCode::T => 19,
VirtualKeyCode::U => 20,
VirtualKeyCode::V => 21,
VirtualKeyCode::W => 22,
VirtualKeyCode::X => 23,
VirtualKeyCode::Y => 24,
VirtualKeyCode::Z => 25,
_ => -1,
KeyCode::A => Some(0),
KeyCode::B => Some(1),
KeyCode::C => Some(2),
KeyCode::D => Some(3),
KeyCode::E => Some(4),
KeyCode::F => Some(5),
KeyCode::G => Some(6),
KeyCode::H => Some(7),
KeyCode::I => Some(8),
KeyCode::J => Some(9),
KeyCode::K => Some(10),
KeyCode::L => Some(11),
KeyCode::M => Some(12),
KeyCode::N => Some(13),
KeyCode::O => Some(14),
KeyCode::P => Some(15),
KeyCode::Q => Some(16),
KeyCode::R => Some(17),
KeyCode::S => Some(18),
KeyCode::T => Some(19),
KeyCode::U => Some(20),
KeyCode::V => Some(21),
KeyCode::W => Some(22),
KeyCode::X => Some(23),
KeyCode::Y => Some(24),
KeyCode::Z => Some(25),
_ => None,
}
}
}

47
src/gui/main_menu.rs Normal file
View file

@ -0,0 +1,47 @@
use notan::prelude::*;
use notan::draw::{ Draw, CreateDraw, DrawTextSection, Font };
use specs::prelude::*;
use std::collections::HashMap;
use super::{ FONTSIZE, RunState, DISPLAYWIDTH, TILESIZE, MainMenuSelection };
use crate::consts::DISPLAYHEIGHT;
use crate::Fonts;
pub fn draw_mainmenu(ecs: &World, draw: &mut Draw, atlas: &HashMap<String, Texture>, font: &Fonts) {
let runstate = ecs.read_resource::<RunState>();
let selected = match *runstate {
RunState::MainMenu { menu_selection } => menu_selection,
_ => unreachable!("draw_mainmenu() called outside of MainMenu runstate."),
};
let save_exists = crate::saveload_system::does_save_exist();
const MID_X: f32 = ((DISPLAYWIDTH as f32) * TILESIZE.x) / 2.0;
let (x, mut y) = (MID_X, ((DISPLAYHEIGHT as f32) * TILESIZE.x) / 4.0);
draw.text(&font.ib(), "RUST-RL")
.size(FONTSIZE * 2.0)
.position(x, y)
.h_align_center();
y = draw.last_text_bounds().max_y();
draw.text(&font.n(), "New Game")
.size(FONTSIZE)
.position(x, y)
.h_align_center()
.color(get_colour(selected, MainMenuSelection::NewGame));
if save_exists {
y = draw.last_text_bounds().max_y();
draw.text(font.n(), "Load Game")
.size(FONTSIZE)
.position(x, y)
.h_align_center()
.color(get_colour(selected, MainMenuSelection::LoadGame));
}
y = draw.last_text_bounds().max_y();
draw.text(&font.n(), "Quit")
.size(FONTSIZE)
.position(x, y)
.h_align_center()
.color(get_colour(selected, MainMenuSelection::Quit));
}
fn get_colour(selected: MainMenuSelection, desired: MainMenuSelection) -> Color {
if selected == desired { Color::from_rgb(0.0, 1.0, 0.0) } else { Color::WHITE }
}

File diff suppressed because it is too large Load diff

View file

@ -4,9 +4,9 @@ use super::{
obfuscate_name_ecs,
print_options,
unique_ecs,
check_key,
letter_to_option,
renderable_colour,
ItemMenuResult,
UniqueInventoryItem,
InventorySlot,
};
use crate::{
@ -91,7 +91,19 @@ pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<
return (ItemMenuResult::Selected, Some(item));
}
let mut player_inventory: super::PlayerInventory = HashMap::new();
for (entity, _i, _b, _r, _n, key) in build_cursed_iterator() {
for (entity, _i, _b, renderable, name, key) in build_cursed_iterator() {
let (singular, plural) = obfuscate_name_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 = unique_ecs(&gs.ecs, entity);
player_inventory
.entry(unique_item)
@ -106,39 +118,44 @@ pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<
}
// Get display args
let width = get_max_inventory_width(&player_inventory);
let (_, _, _, _, x_offset, y_offset) = crate::camera::get_screen_bounds(&gs.ecs, ctx);
let (x, y) = (x_offset + 1, y_offset + 3);
let offsets = crate::camera::get_offset();
let (x, y) = (offsets.x + 1, offsets.y + 3);
// Draw menu
ctx.print_color(
1 + x_offset,
1 + y_offset,
1 + offsets.x,
1 + offsets.y,
RGB::named(WHITE),
RGB::named(BLACK),
"Decurse which item? [aA-zZ][Esc.]"
);
ctx.draw_box(x, y, width + 2, count + 1, RGB::named(WHITE), RGB::named(BLACK));
print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx);
// Input
match ctx.key {
/*match ctx.key {
None => (ItemMenuResult::NoResponse, None),
Some(key) =>
match key {
VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None),
_ => {
let selection = letter_to_option::letter_to_option(key, ctx.shift);
if selection != -1 && check_key(selection as usize) {
// Get the first entity with a Key {} component that has an idx matching "selection".
let entities = gs.ecs.entities();
let keyed_items = gs.ecs.read_storage::<Key>();
let backpack = gs.ecs.read_storage::<InBackpack>();
for (e, key, _b) in (&entities, &keyed_items, &backpack).join() {
if key.idx == (selection as usize) {
return (ItemMenuResult::Selected, Some(e));
}
}
let selection = letter_to_option(key);
if selection > -1 && selection < (count as i32) {
let item = player_inventory
.iter()
.nth(selection as usize)
.unwrap().1.0;
gamelog::Logger
::new()
.append("You decurse the")
.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

@ -11,7 +11,8 @@ use super::{
RGB,
};
use crate::TileType;
use crate::data::ids::*;
use crate::consts::ids::*;
use crate::consts::prelude::*;
use bracket_lib::prelude::*;
use specs::prelude::*;
@ -45,6 +46,7 @@ impl Tooltip {
return (self.lines.len() as i32) + 2i32;
}
fn render(&self, ctx: &mut BTerm, x: i32, y: i32) {
ctx.set_active_console(TEXT_LAYER);
ctx.draw_box(
x,
y,
@ -56,12 +58,13 @@ impl Tooltip {
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.set_active_console(TILE_LAYER);
}
}
#[rustfmt::skip]
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 bounds = get_screen_bounds(ecs, false);
let map = ecs.fetch::<Map>();
let names = ecs.read_storage::<Name>();
let positions = ecs.read_storage::<Position>();
@ -74,8 +77,8 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut BTerm, xy: Option<(i32, i32)>) {
let mouse_pos = if xy.is_none() { ctx.mouse_pos() } else { xy.unwrap() };
let mut mouse_pos_adjusted = mouse_pos;
mouse_pos_adjusted.0 += min_x - x_offset;
mouse_pos_adjusted.1 += min_y - y_offset;
mouse_pos_adjusted.0 += bounds.min_x - bounds.x_offset;
mouse_pos_adjusted.1 += bounds.min_y - bounds.y_offset;
if mouse_pos_adjusted.0 >= map.width
|| mouse_pos_adjusted.1 >= map.height
|| mouse_pos_adjusted.1 < 0 // Might need to be 1, and -1 from map height/width.
@ -121,18 +124,18 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut BTerm, xy: Option<(i32, i32)>) {
let attr = attributes.get(entity);
if let Some(a) = attr {
let mut s = "".to_string();
if a.strength.bonus < -2 { s += "weak "};
if a.strength.bonus > 2 { s += "strong "};
if a.dexterity.bonus < -2 { s += "clumsy "};
if a.dexterity.bonus > 2 { s += "agile "};
if a.constitution.bonus < -2 { s += "frail "};
if a.constitution.bonus > 2 { s += "hardy "};
if a.intelligence.bonus < -2 { s += "dim "};
if a.intelligence.bonus > 2 { s += "smart "};
if a.wisdom.bonus < -2 { s += "unwise "};
if a.wisdom.bonus > 2 { s += "wisened "};
if a.charisma.bonus < -2 { s += "ugly"};
if a.charisma.bonus > 2 { s += "attractive"};
if a.strength.modifier() < -2 { s += "weak "};
if a.strength.modifier() > 2 { s += "strong "};
if a.dexterity.modifier() < -2 { s += "clumsy "};
if a.dexterity.modifier() > 2 { s += "agile "};
if a.constitution.modifier() < -2 { s += "frail "};
if a.constitution.modifier() > 2 { s += "hardy "};
if a.intelligence.modifier() < -2 { s += "dim "};
if a.intelligence.modifier() > 2 { s += "smart "};
if a.wisdom.modifier() < -2 { s += "unwise "};
if a.wisdom.modifier() > 2 { s += "wisened "};
if a.charisma.modifier() < -2 { s += "ugly"};
if a.charisma.modifier() > 2 { s += "attractive"};
if !s.is_empty() {
if s.ends_with(" ") {
s.pop();
@ -175,13 +178,15 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut BTerm, xy: Option<(i32, i32)>) {
if mouse_pos.0 > 35 {
// Render to the left
arrow = to_cp437('→');
arrow_x = mouse_pos.0 - 1;
arrow_x = mouse_pos.0 * 2 - 1;
} else {
// Render to the right
arrow = to_cp437('←');
arrow_x = mouse_pos.0 + 1;
arrow_x = (mouse_pos.0 + 1) * 2;
}
ctx.set_active_console(TEXT_LAYER);
ctx.set(arrow_x, arrow_y, white, RGB::named(BLACK), arrow);
ctx.set_active_console(TILE_LAYER);
let mut total_height = 0;
for t in tooltips.iter() {
@ -195,9 +200,9 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut BTerm, xy: Option<(i32, i32)>) {
for t in tooltips.iter() {
let x = if mouse_pos.0 > 35 {
mouse_pos.0 - (1 + t.width())
(mouse_pos.0 * 2) - (1 + t.width())
} else {
mouse_pos.0 + (1 + 1)
(mouse_pos.0 * 2) + 2 + 1
};
t.render(ctx, x, y);
y += t.height();

View file

@ -13,9 +13,11 @@ use crate::{
Position,
WantsToPickupItem,
WantsToAssignKey,
Renderable,
Stackable,
};
use specs::prelude::*;
use crate::data::messages;
use crate::consts::messages;
use bracket_lib::prelude::*;
pub struct ItemCollectionSystem {}
@ -31,10 +33,12 @@ impl<'a> System<'a> for ItemCollectionSystem {
WriteStorage<'a, EquipmentChanged>,
ReadStorage<'a, MagicItem>,
ReadStorage<'a, ObfuscatedName>,
ReadStorage<'a, Renderable>,
ReadStorage<'a, Beatitude>,
ReadExpect<'a, MasterDungeonMap>,
ReadStorage<'a, Charges>,
ReadStorage<'a, WantsToAssignKey>,
ReadStorage<'a, Stackable>,
);
fn run(&mut self, data: Self::SystemData) {
@ -47,13 +51,16 @@ impl<'a> System<'a> for ItemCollectionSystem {
mut equipment_changed,
magic_items,
obfuscated_names,
renderables,
beatitudes,
dm,
wands,
wants_key,
stackable,
) = data;
let mut to_remove: Vec<Entity> = Vec::new();
// For every item that wants to be picked up that *isn't* waiting on a key assignment.
// For every item that wants to be picked up, that *isn't* still waiting on a key assignment.
for (pickup, _key) in (&wants_pickup, !&wants_key).join() {
if pickup.collected_by == *player_entity {
gamelog::Logger
@ -81,10 +88,10 @@ impl<'a> System<'a> for ItemCollectionSystem {
positions.remove(pickup.item);
backpack
.insert(pickup.item, InBackpack { owner: pickup.collected_by })
.expect("Unable to pickup item");
.expect("Unable to pickup item.");
equipment_changed
.insert(pickup.collected_by, EquipmentChanged {})
.expect("Unable to insert EquipmentChanged");
.expect("Unable to insert EquipmentChanged.");
to_remove.push(pickup.collected_by);
}
for item in to_remove.iter() {

View file

@ -15,7 +15,7 @@ use crate::{
WantsToRemoveKey,
};
use specs::prelude::*;
use crate::data::messages;
use crate::consts::messages;
use bracket_lib::prelude::*;
pub struct ItemDropSystem {}

View file

@ -16,7 +16,7 @@ use crate::{
BUC,
};
use specs::prelude::*;
use crate::data::messages;
use crate::consts::messages;
use bracket_lib::prelude::*;
pub struct ItemEquipSystem {}

View file

@ -9,7 +9,7 @@ use crate::{
Player,
};
use specs::prelude::*;
use crate::data::events::*;
use crate::consts::events::*;
use crate::gamelog;
pub struct ItemIdentificationSystem {}

View file

@ -14,13 +14,13 @@ use crate::{
Key,
};
use specs::prelude::*;
use crate::data::messages;
use crate::consts::messages;
use bracket_lib::prelude::*;
use crate::invkeys::*;
pub struct KeyHandling {}
const DEBUG_KEYHANDLING: bool = true;
const DEBUG_KEYHANDLING: bool = false;
impl<'a> System<'a> for KeyHandling {
#[allow(clippy::type_complexity)]
@ -75,28 +75,40 @@ impl<'a> System<'a> for KeyHandling {
),
);
if stacks {
console::log(&format!("KEYHANDLING: Item is stackable."));
if DEBUG_KEYHANDLING {
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."));
if DEBUG_KEYHANDLING {
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));
if DEBUG_KEYHANDLING {
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() {
if DEBUG_KEYHANDLING {
console::log(
&format!("KEYHANDLING: Assigned next available index {} to item.", idx)
&format!("KEYHANDLING: Item is not stackable, or no existing stack found.")
);
}
if let Some(idx) = assign_next_available() {
if DEBUG_KEYHANDLING {
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."));
if DEBUG_KEYHANDLING {
console::log(&format!("KEYHANDLING: No more keys available."));
}
gamelog::Logger
::new()
.append(messages::NO_MORE_KEYS)
@ -113,37 +125,47 @@ impl<'a> System<'a> for KeyHandling {
}
// 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)
);
if DEBUG_KEYHANDLING {
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."
)
);
if DEBUG_KEYHANDLING {
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));
if DEBUG_KEYHANDLING {
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)
);
if DEBUG_KEYHANDLING {
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."));
if DEBUG_KEYHANDLING {
console::log(&format!("KEYHANDLING: Removing key component from item."));
}
keys.remove(e);
}

View file

@ -13,7 +13,7 @@ use crate::{
};
use bracket_lib::prelude::*;
use specs::prelude::*;
use crate::data::messages;
use crate::consts::messages;
pub struct ItemRemoveSystem {}

View file

@ -1,5 +1,6 @@
use std::sync::Mutex;
use std::collections::HashMap;
use std::collections::{ HashMap };
use specs::prelude::*;
use crate::gui::UniqueInventoryItem;
lazy_static! {
@ -25,7 +26,6 @@ pub fn check_key(idx: usize) -> bool {
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 {

View file

@ -29,7 +29,7 @@ pub mod trigger_system;
pub mod inventory;
pub mod particle_system;
pub mod ai;
pub mod data;
pub mod consts;
pub mod config;
pub mod effects;
pub mod gamesystem;
@ -45,3 +45,4 @@ use particle_system::ParticleBuilder;
pub use map::*;
pub use states::runstate::RunState;
pub use states::state::State;
pub use states::state::Fonts;

View file

@ -1,31 +1,70 @@
use rust_rl::*;
use notan::prelude::*;
use notan::math::{ vec2, vec3, Mat4, Vec2 };
use notan::draw::create_textures_from_atlas;
use notan::draw::{ CreateFont, CreateDraw, DrawImages, Draw, DrawTextSection, DrawShapes };
use specs::prelude::*;
use specs::saveload::{ SimpleMarker, SimpleMarkerAllocator };
use bracket_lib::prelude::*;
use std::collections::HashMap;
use crate::consts::{ DISPLAYHEIGHT, DISPLAYWIDTH, TILESIZE, FONTSIZE };
use crate::states::state::Fonts;
const DISPLAYWIDTH: i32 = 105;
const DISPLAYHEIGHT: i32 = 56;
const WORK_SIZE: Vec2 = vec2(
(DISPLAYWIDTH as f32) * TILESIZE.x,
(DISPLAYHEIGHT as f32) * TILESIZE.x
);
fn main() -> BError {
// Embedded resources for use in wasm build
const CURSES_14_16_BYTES: &[u8] = include_bytes!("../resources/curses14x16.png");
EMBED.lock().add_resource("resources/curses14x16.png".to_string(), CURSES_14_16_BYTES);
//link_resource!(CURSES14X16, "../resources/curses_14x16.png");
let mut context = BTermBuilder::new()
.with_title("rust-rl")
.with_dimensions(DISPLAYWIDTH, DISPLAYHEIGHT)
.with_font("curses14x16.png", 14, 16)
.with_tile_dimensions(14, 16)
.with_simple_console(DISPLAYWIDTH, DISPLAYHEIGHT, "curses14x16.png")
.build()?;
if config::CONFIG.visuals.with_scanlines {
context.with_post_scanlines(config::CONFIG.visuals.with_screen_burn);
}
#[notan_main]
fn main() -> Result<(), String> {
let win_config = WindowConfig::new()
.set_size(DISPLAYWIDTH * (TILESIZE.x as u32), DISPLAYHEIGHT * (TILESIZE.x as u32))
.set_title("RUST-RL")
.set_resizable(true)
.set_fullscreen(true)
.set_taskbar_icon_data(Some(include_bytes!("../resources/icon.png")))
.set_vsync(true);
notan
::init_with(setup)
.add_config(win_config)
.add_config(notan::draw::DrawConfig)
.draw(draw)
.update(update)
.build()
}
fn setup(app: &mut App, gfx: &mut Graphics) -> State {
effects::sound::init_sounds(app);
effects::sound::ambience("a_relax");
let texture = gfx
.create_texture()
.from_image(include_bytes!("../resources/atlas.png"))
.build()
.unwrap();
let data = include_bytes!("../resources/atlas.json");
let atlas = create_textures_from_atlas(data, &texture).unwrap();
let texture = gfx
.create_texture()
.from_image(include_bytes!("../resources/td.png"))
.build()
.unwrap();
let data = include_bytes!("../resources/td.json");
let interface = create_textures_from_atlas(data, &texture).unwrap();
let font = Fonts::new(
gfx.create_font(include_bytes!("../resources/fonts/Greybeard-16px.ttf")).unwrap(),
Some(
gfx.create_font(include_bytes!("../resources/fonts/Greybeard-16px-Bold.ttf")).unwrap()
),
Some(
gfx.create_font(include_bytes!("../resources/fonts/Greybeard-16px-Italic.ttf")).unwrap()
)
);
let mut gs = State {
ecs: World::new(),
//audio: sounds,
atlas,
interface,
font,
mapgen_next_state: Some(RunState::MainMenu {
menu_selection: gui::MainMenuSelection::NewGame,
}),
@ -37,6 +76,7 @@ fn main() -> BError {
gs.ecs.register::<Position>();
gs.ecs.register::<OtherLevelPosition>();
gs.ecs.register::<Renderable>();
gs.ecs.register::<Avatar>();
gs.ecs.register::<Burden>();
gs.ecs.register::<Prop>();
gs.ecs.register::<Player>();
@ -111,12 +151,12 @@ fn main() -> BError {
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::<IntrinsicChanged>();
gs.ecs.register::<SimpleMarker<SerializeMe>>();
gs.ecs.register::<SerializationHelper>();
gs.ecs.register::<DMSerializationHelper>();
@ -132,13 +172,526 @@ fn main() -> BError {
gs.ecs.insert(gui::Ancestry::Human); // ancestry
let player_entity = spawner::player(&mut gs.ecs, 0, 0);
gs.ecs.insert(player_entity); // Player entity
gs.ecs.insert(RunState::MapGeneration {}); // RunState
gs.ecs.insert(RunState::MapGeneration {}); // TODO: Set this back to RunState::MapGen
gs.ecs.insert(particle_system::ParticleBuilder::new());
gs.ecs.insert(rex_assets::RexAssets::new());
gamelog::setup_log();
gamelog::record_event(data::events::EVENT::Level(1));
gamelog::record_event(consts::events::EVENT::Level(1));
gs.generate_world_map(1, TileType::Floor);
main_loop(context, gs)
gs
}
const ASCII_MODE: bool = false; // Change this to config setting
const SHOW_BOUNDARIES: bool = false; // Config setting
#[derive(PartialEq)]
enum DrawType {
None,
Player,
Visible,
Telepathy,
}
#[derive(PartialEq, Eq, Hash)]
struct DrawKey {
x: i32,
y: i32,
render_order: i32,
}
struct DrawInfo {
e: Entity,
draw_type: DrawType,
}
fn draw_entities(
map: &Map,
ecs: &World,
draw: &mut Draw,
atlas: &HashMap<String, Texture>,
_font: &Fonts
) {
{
let bounds = crate::camera::get_screen_bounds(ecs, false);
let bounds_to_px = bounds.to_px();
let offset_x = bounds_to_px.x_offset - bounds_to_px.min_x;
let offset_y = bounds_to_px.y_offset - bounds_to_px.min_y;
let positions = ecs.read_storage::<Position>();
let renderables = ecs.read_storage::<Renderable>();
let hidden = ecs.read_storage::<Hidden>();
let minds = ecs.read_storage::<Mind>();
let pools = ecs.read_storage::<Pools>();
let entities = ecs.entities();
let player = ecs.read_storage::<Player>();
let data = (&positions, &renderables, &entities, !&hidden).join().collect::<Vec<_>>();
let mut to_draw: HashMap<DrawKey, DrawInfo> = HashMap::new();
for (pos, render, e, _h) in data.iter() {
let idx = map.xy_idx(pos.x, pos.y);
if
crate::camera::in_bounds(
pos.x,
pos.y,
bounds.min_x,
bounds.min_y,
bounds.max_x,
bounds.max_y
)
{
let draw_type = if map.visible_tiles[idx] {
// If it's anything else, just draw it.
if player.get(*e).is_some() {
DrawType::Player
} else {
DrawType::Visible
}
} else if map.telepath_tiles[idx] {
let has_mind = minds.get(*e);
if has_mind.is_some() {
// Mobs we see through telepathy - generally we just
// draw these, but it uses a unique enum variant so
// it can be treated differently if needed in future.
DrawType::Telepathy
} else {
DrawType::None
}
} else {
// If we don't see it, and we don't sense it with
// telepathy, don't draw it at all.
DrawType::None
};
match draw_type {
DrawType::None => {}
_ => {
to_draw.insert(
DrawKey { x: pos.x, y: pos.y, render_order: render.render_order },
DrawInfo { e: *e, draw_type }
);
}
}
}
}
let mut entries: Vec<(&DrawKey, &DrawInfo)> = to_draw.iter().collect();
entries.sort_by_key(|&(k, _v)| std::cmp::Reverse(k.render_order));
for entry in entries.iter() {
// TODO: ABSTRACT THESE INTO FUNCTIONS ONCE FUNCTIONALITY IS SETTLED ON.
match entry.1.draw_type {
DrawType::Visible | DrawType::Telepathy => {
let renderable = renderables.get(entry.1.e).unwrap();
let id = if let Some(sprite) = atlas.get(&renderable.sprite) {
sprite
} else {
panic!("No entity sprite found for ID: {}", &renderable.sprite);
};
let x_pos = (entry.0.x as f32) * TILESIZE.sprite_x + offset_x;
let y_pos = (entry.0.y as f32) * TILESIZE.sprite_y + offset_y;
let mul = themes::darken_by_distance(
Point::new(entry.0.x, entry.0.y),
*ecs.fetch::<Point>()
);
let col = Color::from_rgb(
renderable.fg.r * mul,
renderable.fg.g * mul,
renderable.fg.b * mul
);
draw.image(id)
.position(
x_pos + renderable.offset.0 * TILESIZE.sprite_x,
y_pos + renderable.offset.1 * TILESIZE.sprite_y
)
.color(col)
.size(TILESIZE.sprite_x, TILESIZE.sprite_y);
if let Some(pool) = pools.get(entry.1.e) {
if pool.hit_points.current < pool.hit_points.max {
draw_entity_hp(x_pos, y_pos, pool, draw);
}
}
}
DrawType::Player => {
let (x_pos, y_pos) = (
(entry.0.x as f32) * TILESIZE.sprite_x + offset_x,
(entry.0.y as f32) * TILESIZE.sprite_y + offset_y,
);
let textures = get_avatar_textures(ecs, atlas);
for (tex, col) in textures.iter() {
draw.image(tex)
.position(x_pos, y_pos)
.color(*col)
.size(TILESIZE.sprite_x, TILESIZE.sprite_y);
}
}
_ => {}
}
}
}
}
fn get_avatar_textures(ecs: &World, atlas: &HashMap<String, Texture>) -> Vec<(Texture, Color)> {
let player = ecs.fetch::<Entity>();
let renderables = ecs.read_storage::<Renderable>();
let equipped = ecs.read_storage::<Equipped>();
let has_avatar = ecs.read_storage::<Avatar>();
let mut avis = Vec::new();
if let Some(renderables) = renderables.get(*player) {
if let Some(sprite) = atlas.get(&renderables.sprite) {
avis.push((
sprite.clone(),
Color::from_rgb(renderables.fg.r, renderables.fg.g, renderables.fg.b),
));
} else {
panic!("No player sprite found for ID: {}", &renderables.sprite);
}
} else {
panic!("No player renderable found!");
}
for (_e, a, r) in (&equipped, &has_avatar, &renderables)
.join()
.filter(|item| item.0.owner == *player) {
if let Some(sprite) = atlas.get(&a.sprite) {
avis.push((sprite.clone(), Color::from_rgb(r.fg.r, r.fg.g, r.fg.b)));
} else {
panic!("No avatar sprite found for ID: {}", &a.sprite);
}
}
avis
}
// Draws a HP bar LINE_WIDTH pixels thick centered above the entity.
fn draw_entity_hp(x: f32, y: f32, hp: &Pools, draw: &mut Draw) {
const LINE_WIDTH: f32 = 3.0;
let y = y + LINE_WIDTH + 1.0;
let x = x;
let fill_pct = (hp.hit_points.current as f32) / (hp.hit_points.max as f32);
draw.line((x + 1.0, y), (x + (TILESIZE.sprite_x - 1.0) * fill_pct, y))
.width(LINE_WIDTH)
.color(Color::GREEN);
}
fn render_map_in_view(
map: &Map,
ecs: &World,
draw: &mut Draw,
atlas: &HashMap<String, Texture>,
mapgen: bool
) {
let bounds = crate::camera::get_screen_bounds(ecs, mapgen);
let mut y = 0;
for tile_y in bounds.min_y..bounds.max_y {
let mut x = 0;
for tile_x in bounds.min_x..bounds.max_x {
if crate::camera::in_bounds(tile_x, tile_y, 0, 0, map.width, map.height) {
let idx = map.xy_idx(tile_x, tile_y);
if map.revealed_tiles[idx] || mapgen {
let draw_x =
(x as f32) * TILESIZE.sprite_x + (bounds.x_offset as f32) * TILESIZE.x;
let draw_y =
(y as f32) * TILESIZE.sprite_y + (bounds.y_offset as f32) * TILESIZE.x;
if ASCII_MODE {
let (glyph, fg, bg) = crate::map::themes::get_tile_renderables_for_id(
idx,
&*map,
Some(*ecs.fetch::<Point>()),
None
);
// TODO: Draw ASCII
} else {
let (id, tint) = crate::map::themes::get_sprite_for_id(
idx,
&*map,
Some(*ecs.fetch::<Point>())
);
let sprite = if let Some(sprite) = atlas.get(id) {
sprite
} else {
panic!("No sprite found for ID: {}", id);
};
draw.image(sprite)
.position(draw_x, draw_y)
.color(tint)
.size(TILESIZE.sprite_x, TILESIZE.sprite_y);
}
if !map.visible_tiles[idx] {
// Recall map memory. TODO: Improve this? Optimize? Do we need to remember more fields?
if let Some(memories) = map.memory.get(&idx) {
let mut sorted: Vec<_> = memories.iter().collect();
sorted.sort_by(|a, b| a.render_order.cmp(&b.render_order));
for memory in sorted.iter() {
let mult = consts::visuals::NON_VISIBLE_MULTIPLIER;
let col = Color::from_rgb(
memory.fg.r * mult,
memory.fg.g * mult,
memory.fg.b * mult
);
let sprite = if let Some(sprite) = atlas.get(&memory.sprite) {
sprite
} else {
panic!("No sprite found for ID: {}", memory.sprite);
};
draw.image(sprite)
.position(
draw_x + memory.offset.0 * TILESIZE.sprite_x,
draw_y + memory.offset.1 * TILESIZE.sprite_y
)
.color(col)
.size(TILESIZE.sprite_x, TILESIZE.sprite_y);
}
}
}
}
} else if SHOW_BOUNDARIES {
// TODO: Draw boundaries
}
x += 1;
}
y += 1;
}
}
struct BoxDraw {
frame: String,
fill: bool,
top_left: (i32, i32),
top_right: (i32, i32),
bottom_left: (i32, i32),
bottom_right: (i32, i32),
}
fn draw_spritebox(panel: BoxDraw, draw: &mut Draw, atlas: &HashMap<String, Texture>) {
draw.image(atlas.get(&format!("{}_1", panel.frame)).unwrap()).position(
(panel.top_left.0 as f32) * TILESIZE.x,
(panel.top_left.1 as f32) * TILESIZE.x
);
for i in panel.top_left.0 + 1..panel.top_right.0 {
draw.image(atlas.get(&format!("{}_2", panel.frame)).unwrap()).position(
(i as f32) * TILESIZE.x,
(panel.top_left.1 as f32) * TILESIZE.x
);
}
draw.image(atlas.get(&format!("{}_3", panel.frame)).unwrap()).position(
(panel.top_right.0 as f32) * TILESIZE.x,
(panel.top_right.1 as f32) * TILESIZE.x
);
for i in panel.top_left.1 + 1..panel.bottom_left.1 {
draw.image(atlas.get(&format!("{}_4", panel.frame)).unwrap()).position(
(panel.top_left.0 as f32) * TILESIZE.x,
(i as f32) * TILESIZE.x
);
}
if panel.fill {
for i in panel.top_left.0 + 1..panel.top_right.0 {
for j in panel.top_left.1 + 1..panel.bottom_left.1 {
draw.image(atlas.get(&format!("{}_5", panel.frame)).unwrap()).position(
(i as f32) * TILESIZE.x,
(j as f32) * TILESIZE.x
);
}
}
}
for i in panel.top_right.1 + 1..panel.bottom_right.1 {
draw.image(atlas.get(&format!("{}_6", panel.frame)).unwrap()).position(
(panel.top_right.0 as f32) * TILESIZE.x,
(i as f32) * TILESIZE.x
);
}
draw.image(atlas.get(&format!("{}_7", panel.frame)).unwrap()).position(
(panel.bottom_left.0 as f32) * TILESIZE.x,
(panel.bottom_left.1 as f32) * TILESIZE.x
);
for i in panel.bottom_left.0 + 1..panel.bottom_right.0 {
draw.image(atlas.get(&format!("{}_8", panel.frame)).unwrap()).position(
(i as f32) * TILESIZE.x,
(panel.bottom_left.1 as f32) * TILESIZE.x
);
}
draw.image(atlas.get(&format!("{}_9", panel.frame)).unwrap()).position(
(panel.bottom_right.0 as f32) * TILESIZE.x,
(panel.bottom_right.1 as f32) * TILESIZE.x
);
}
use crate::consts::visuals::{ VIEWPORT_H, VIEWPORT_W };
fn draw_bg(_ecs: &World, draw: &mut Draw, atlas: &HashMap<String, Texture>) {
let offset = crate::camera::get_offset();
let log = BoxDraw {
frame: "line".to_string(),
fill: false,
top_left: (0, 0),
top_right: (offset.x + VIEWPORT_W, 0),
bottom_left: (0, offset.y - 2),
bottom_right: (offset.x + VIEWPORT_W, offset.y - 2),
};
let game = BoxDraw {
frame: "line".to_string(),
fill: false,
top_left: (offset.x - 1, offset.y - 1),
top_right: (offset.x + VIEWPORT_W, offset.y - 1),
bottom_left: (offset.x - 1, offset.y + VIEWPORT_H),
bottom_right: (offset.x + VIEWPORT_W, offset.y + VIEWPORT_H),
};
let attr = BoxDraw {
frame: "line".to_string(),
fill: false,
top_left: (offset.x - 1, offset.y + VIEWPORT_H + 1),
top_right: (offset.x + VIEWPORT_W, offset.y + VIEWPORT_H + 1),
bottom_left: (offset.x - 1, (DISPLAYHEIGHT as i32) - 1),
bottom_right: (offset.x + VIEWPORT_W, (DISPLAYHEIGHT as i32) - 1),
};
let sidebox = BoxDraw {
frame: "line".to_string(),
fill: false,
top_left: (offset.x + VIEWPORT_W + 1, 0),
top_right: ((DISPLAYWIDTH as i32) - 1, 0),
bottom_left: (offset.x + VIEWPORT_W + 1, (DISPLAYHEIGHT as i32) - 1),
bottom_right: ((DISPLAYWIDTH as i32) - 1, (DISPLAYHEIGHT as i32) - 1),
};
draw_spritebox(log, draw, atlas);
draw_spritebox(game, draw, atlas);
draw_spritebox(attr, draw, atlas);
draw_spritebox(sidebox, draw, atlas);
}
fn draw(_app: &mut App, gfx: &mut Graphics, gs: &mut State) {
let mut draw = gfx.create_draw();
draw.clear(Color::BLACK);
let mut log = false;
let runstate = *gs.ecs.fetch::<RunState>();
match runstate {
RunState::MainMenu { .. } => {
gui::draw_mainmenu(&gs.ecs, &mut draw, &gs.atlas, &gs.font);
}
RunState::CharacterCreation { .. } => {
gui::draw_charcreation(&gs.ecs, &mut draw, &gs.atlas, &gs.font);
}
RunState::PreRun { .. } => {}
RunState::MapGeneration => {
draw_bg(&gs.ecs, &mut draw, &gs.interface);
if config::CONFIG.logging.show_mapgen && gs.mapgen_history.len() > 0 {
render_map_in_view(
&gs.mapgen_history[gs.mapgen_index],
&gs.ecs,
&mut draw,
&gs.atlas,
true
);
}
gui::draw_ui2(&gs.ecs, &mut draw, &gs.atlas, &gs.font);
}
_ => {
let map = gs.ecs.fetch::<Map>();
draw_bg(&gs.ecs, &mut draw, &gs.interface);
render_map_in_view(&*map, &gs.ecs, &mut draw, &gs.atlas, false);
// Special case: targeting needs to be drawn *below* entities, but above tiles.
if let RunState::ShowTargeting { range, item: _, x, y, aoe } = runstate {
gui::draw_targeting(&gs.ecs, &mut draw, &gs.atlas, x, y, range, aoe);
}
draw_entities(&*map, &gs.ecs, &mut draw, &gs.atlas, &gs.font);
gui::draw_ui2(&gs.ecs, &mut draw, &gs.atlas, &gs.font);
log = true;
}
}
match runstate {
RunState::Farlook { x, y } => {
gui::draw_farlook(&gs.ecs, x, y, &mut draw, &gs.atlas);
//draw_tooltips(&gs.ecs, ctx, Some((x, y))); TODO: Put this in draw loop
}
RunState::ShowCheatMenu => {
gui::draw_cheat_menu(&mut draw, &gs.atlas, &gs.font);
}
RunState::ActionWithDirection { .. } => {
corner_text("In what direction? [0-9]/[YUHJKLBN]", &mut draw, &gs.font);
}
RunState::GameOver => {
corner_text("Create morgue file? [Y/N]", &mut draw, &gs.font);
}
RunState::ShowInventory => {
corner_text("Use what? [aA-zZ]/[Esc.]", &mut draw, &gs.font);
let offset = crate::camera::get_offset();
let (x, y) = (
((1 + offset.x) as f32) * TILESIZE.x,
((3 + offset.y) as f32) * TILESIZE.x,
);
gui::draw_backpack_items(&gs.ecs, &mut draw, &gs.font, x, y);
}
RunState::ShowDropItem => {
corner_text("Drop what? [aA-zZ]/[Esc.]", &mut draw, &gs.font);
let offset = crate::camera::get_offset();
let (x, y) = (
((1 + offset.x) as f32) * TILESIZE.x,
((3 + offset.y) as f32) * TILESIZE.x,
);
gui::draw_backpack_items(&gs.ecs, &mut draw, &gs.font, x, y);
}
RunState::ShowRemoveItem => {
corner_text("Unequip which item? [aA-zZ]/[Esc.]", &mut draw, &gs.font);
let offset = crate::camera::get_offset();
let (x, y) = (
((1 + offset.x) as f32) * TILESIZE.x,
((3 + offset.y) as f32) * TILESIZE.x,
);
gui::draw_items(&gs.ecs, &mut draw, &gs.font, x, y, gui::Location::Equipped, None);
}
RunState::ShowTargeting { .. } => {
corner_text("Targeting which tile? [0-9]/[YUHJKLBN]", &mut draw, &gs.font);
}
RunState::HelpScreen => {
corner_text("The help screen is a placeholder! [?]", &mut draw, &gs.font);
}
_ => {}
}
// TODO: Once the rest of drawing is finalised, this should be abstracted
// into some functions that make it easier to tell what is going on. But
// for the short-term:
// 1. notan::Text is required for rich text drawing, rather than just the
// basics that are accessible with notan::Draw's .text() method.
// 2. notan::Text cannot be projected, and rendering both Draw and Text
// requires two GPU calls instead of just one.
// 3. To fix this, our log is drawn to notan::Text, then rendered to a
// render texture, and applied as any other image to notan::Draw.
// 4. notan::Draw is projected, and then rendered, and everything works.
// Further stuff: Make the render texture only as large as is required,
// so text cannot escape the bounds of the logbox.
let (width, height) = gfx.size();
let win_size = vec2(width as f32, height as f32);
let (projection, _) = calc_projection(win_size, WORK_SIZE);
if log {
let buffer = gfx
.create_render_texture(width, height)
.build()
.expect("Failed to create render texture");
gamelog::render_log(
&buffer,
gfx,
&gs.font,
&(TILESIZE.x, TILESIZE.x * 6.0 + 4.0),
(VIEWPORT_W as f32) * TILESIZE.x,
5
);
draw.image(&buffer)
.position(0.0, 0.0)
.size(width as f32, height as f32);
}
draw.set_projection(Some(projection));
gfx.render(&draw);
}
fn update(ctx: &mut App, state: &mut State) {
state.update(ctx);
}
fn corner_text(text: &str, draw: &mut Draw, font: &Fonts) {
let offset = crate::camera::get_offset();
draw.text(&font.b(), &text)
.position(((offset.x + 1) as f32) * TILESIZE.x, ((offset.y + 1) as f32) * TILESIZE.x)
.size(FONTSIZE);
}
fn calc_projection(win_size: Vec2, work_size: Vec2) -> (Mat4, f32) {
let ratio = (win_size.x / work_size.x).min(win_size.y / work_size.y);
let proj = Mat4::orthographic_rh_gl(0.0, win_size.x, win_size.y, 0.0, -1.0, 1.0);
let scale = Mat4::from_scale(vec3(ratio, ratio, 1.0));
let position = vec3(
(win_size.x - work_size.x * ratio) * 0.5,
(win_size.y - work_size.y * ratio) * 0.5,
1.0
);
let trans = Mat4::from_translation(position);
(proj * trans * scale, ratio)
}

View file

@ -4,7 +4,7 @@ use bracket_lib::prelude::*;
use serde::{ Deserialize, Serialize };
use specs::prelude::*;
use std::collections::{ HashMap, HashSet };
use crate::data::events::*;
use crate::consts::events::*;
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct MasterDungeonMap {
@ -112,26 +112,7 @@ fn make_scroll_name(rng: &mut RandomNumberGenerator) -> String {
return name;
}
const POTION_COLOURS: &[&str] = &[
"red",
"orange",
"yellow",
"green",
"blue",
"indigo",
"violet",
"black",
"white",
"silver",
"gold",
"rainbow",
"blood",
"purple",
"cyan",
"brown",
"grey",
"octarine",
];
const POTION_COLOURS: &[&str] = &["blue", "red", "green", "yellow", "black"];
const POTION_ADJECTIVES: &[&str] = &[
"swirling",
"viscous",

View file

@ -10,7 +10,7 @@ use crate::{
};
use specs::prelude::*;
use bracket_lib::prelude::*;
use crate::data::events::*;
use crate::consts::events::*;
const TRY_SPAWN_CHANCE: i32 = 70;
const FEATURE_MESSAGE_CHANCE: i32 = 110;

View file

@ -1,23 +1,42 @@
use bracket_lib::prelude::*;
use specs::prelude::*;
use serde::{ Deserialize, Serialize };
use std::collections::{ HashSet, HashMap };
mod tiletype;
pub use tiletype::{ tile_cost, tile_opaque, tile_walkable, TileType, get_dest, Destination };
pub use tiletype::{
tile_cost,
tile_opaque,
tile_walkable,
tile_blocks_telepathy,
TileType,
get_dest,
Destination,
};
mod interval_spawning_system;
pub use interval_spawning_system::{ maybe_map_message, try_spawn_interval };
pub mod dungeon;
pub use dungeon::{ level_transition, MasterDungeonMap };
pub mod themes;
use super::data::visuals::{
use super::consts::visuals::{
BRIGHTEN_FG_COLOUR_BY,
GLOBAL_OFFSET_MIN_CLAMP,
GLOBAL_OFFSET_MAX_CLAMP,
SPRITE_OFFSET_MIN_CLAMP,
SPRITE_OFFSET_MAX_CLAMP,
};
// 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.
// on a map size of 42*42, the player can see entities up to 2 tiles to their right.
#[derive(Clone, Serialize, Deserialize)]
pub struct MapMemory {
pub sprite: String,
pub fg: RGB,
pub offset: (f32, f32),
pub render_order: i32,
}
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct Map {
pub overmap: bool,
@ -25,6 +44,7 @@ pub struct Map {
pub width: i32,
pub height: i32,
pub revealed_tiles: Vec<bool>,
pub memory: HashMap<usize, Vec<MapMemory>>,
pub visible_tiles: Vec<bool>,
pub lit_tiles: Vec<bool>,
pub telepath_tiles: Vec<bool>,
@ -63,6 +83,7 @@ impl Map {
width: width,
height: height,
revealed_tiles: vec![false; map_tile_count],
memory: HashMap::new(),
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.
telepath_tiles: vec![false; map_tile_count],
@ -91,9 +112,9 @@ impl Map {
rng.range(GLOBAL_OFFSET_MIN_CLAMP, GLOBAL_OFFSET_MAX_CLAMP),
);
map.colour_offset[idx].1 = (
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),
rng.range(SPRITE_OFFSET_MIN_CLAMP, SPRITE_OFFSET_MAX_CLAMP),
rng.range(SPRITE_OFFSET_MIN_CLAMP, SPRITE_OFFSET_MAX_CLAMP),
rng.range(SPRITE_OFFSET_MIN_CLAMP, SPRITE_OFFSET_MAX_CLAMP),
);
}

View file

@ -1,9 +1,53 @@
use super::{ Map, Point, TileType };
use crate::data::visuals::*;
use crate::consts::visuals::*;
use crate::config::CONFIG;
use crate::data::ids::*;
use crate::consts::ids::*;
use bracket_lib::prelude::*;
use std::ops::{ Add, Mul };
use notan::prelude::*;
pub fn get_sprite_for_id(idx: usize, map: &Map, other_pos: Option<Point>) -> (&str, Color) {
let bloody = if map.bloodstains.contains_key(&idx) {
Some(map.bloodstains[&idx])
} else {
None
};
let f = map.colour_offset[idx].0.0; // Using offset as a source of random.
let (sprite, offset, mut colour) = match map.tiles[idx] {
TileType::Wall =>
(
map.tiles[idx].sprite(check_if_base(TileType::Wall, idx, map), f, bloody),
map.tiles[idx].offset(),
map.tiles[idx].col(bloody),
),
_ =>
(
map.tiles[idx].sprite(false, f, bloody),
map.tiles[idx].offset(),
map.tiles[idx].col(bloody),
),
};
// Get the right modifier for visibility - darkened by distance from POV, or full dark for out-of-view.
let visibility = if !map.visible_tiles[idx] {
NON_VISIBLE_MULTIPLIER
} else {
if other_pos.is_some() {
darken_by_distance(
Point::new((idx as i32) % map.width, (idx as i32) / map.width),
other_pos.unwrap()
)
} else {
1.0
}
};
// Apply our offsets to our base colour.
colour = apply_colour_offset(colour, map, idx, offset, true);
// Apply our visibility modifier
colour = colour.mul(visibility);
// Convert to a notan colour.
let tint = Color::from_rgb(colour.r, colour.g, colour.b);
return (sprite, tint);
}
/// Gets the renderables for a tile, with darkening/offset/post-processing/etc. Passing a val for "debug" will ignore viewshed.
pub fn get_tile_renderables_for_id(
@ -136,10 +180,24 @@ fn get_forest_theme_renderables(idx:usize, map: &Map, debug: Option<bool>) -> (F
return (glyph, fg, bg, offsets, bg_offsets);
}
fn is_revealed_and_wall(map: &Map, x: i32, y: i32, debug: Option<bool>) -> bool {
fn is_revealed_and(tt: TileType, map: &Map, x: i32, y: i32, debug: Option<bool>) -> bool {
let idx = map.xy_idx(x, y);
map.tiles[idx] == TileType::Wall &&
(if debug.is_none() { map.revealed_tiles[idx] } else { true })
map.tiles[idx] == tt && (if debug.is_none() { map.revealed_tiles[idx] } else { true })
}
fn check_if_base(tt: TileType, idx: usize, map: &Map) -> bool {
let x = (idx as i32) % map.width;
let y = (idx as i32) / map.width;
// If we're on the edge, it can only be a base sprite.
if y > map.height - 2 {
return true;
}
// If the tile below is a revealed wall, we're not the base.
if is_revealed_and(tt, map, x, y + 1, None) {
return false;
}
// If the tile below isn't a revealed wall, we're the base.
return true;
}
fn wall_glyph(map: &Map, x: i32, y: i32, debug: Option<bool>) -> FontCharType {
@ -156,37 +214,37 @@ fn wall_glyph(map: &Map, x: i32, y: i32, debug: Option<bool>) -> FontCharType {
let mut mask: u8 = 0;
let diagonals_matter: Vec<u8> = vec![7, 11, 13, 14, 15];
if is_revealed_and_wall(map, x, y - 1, debug) {
if is_revealed_and(TileType::Wall, map, x, y - 1, debug) {
// N
mask += 1;
}
if is_revealed_and_wall(map, x, y + 1, debug) {
if is_revealed_and(TileType::Wall, map, x, y + 1, debug) {
// S
mask += 2;
}
if is_revealed_and_wall(map, x - 1, y, debug) {
if is_revealed_and(TileType::Wall, map, x - 1, y, debug) {
// W
mask += 4;
}
if is_revealed_and_wall(map, x + 1, y, debug) {
if is_revealed_and(TileType::Wall, map, x + 1, y, debug) {
// E
mask += 8;
}
if diagonals_matter.contains(&mask) {
if is_revealed_and_wall(map, x + 1, y - 1, debug) {
if is_revealed_and(TileType::Wall, map, x + 1, y - 1, debug) {
// Top right
mask += 16;
}
if is_revealed_and_wall(map, x - 1, y - 1, debug) {
if is_revealed_and(TileType::Wall, map, x - 1, y - 1, debug) {
// Top left
mask += 32;
}
if is_revealed_and_wall(map, x + 1, y + 1, debug) {
if is_revealed_and(TileType::Wall, map, x + 1, y + 1, debug) {
// Bottom right
mask += 64;
}
if is_revealed_and_wall(map, x - 1, y + 1, debug) {
if is_revealed_and(TileType::Wall, map, x - 1, y + 1, debug) {
// Bottom left
mask += 128;
}
@ -326,11 +384,11 @@ pub fn multiply_by_float(rgb: RGB, offsets: (f32, f32, f32)) -> RGB {
return RGB::from_f32(r, g, b);
}
fn darken_by_distance(pos: Point, other_pos: Point) -> f32 {
pub 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 interp_factor =
(distance - START_DARKEN_AT_N_TILES) /
((crate::data::entity::DEFAULT_VIEWSHED_STANDARD as f32) - START_DARKEN_AT_N_TILES);
((crate::consts::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 result =
1.0 -

View file

@ -1,4 +1,6 @@
use serde::{ Deserialize, Serialize };
use bracket_lib::prelude::*;
use crate::consts::visuals::*;
#[derive(PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize, Debug)]
pub enum TileType {
@ -27,9 +29,117 @@ pub enum TileType {
ToOvermap(i32),
ToLocal(i32),
}
impl TileType {
pub fn sprite(&self, base: bool, float: f32, bloody: Option<RGB>) -> &str {
if base {
return self.h(float, bloody);
}
return self.v(float, bloody);
}
fn h(&self, float: f32, _bloody: Option<RGB>) -> &str {
let options = match self {
TileType::Wall => vec!["wall_b", "wall_b_cracked"],
_ => unreachable!("Tried to get a h (base) sprite for a non-wall tile."),
};
return options[(float * (options.len() as f32)) as usize];
}
fn v(&self, float: f32, bloody: Option<RGB>) -> &str {
let mut options = match self {
TileType::ImpassableMountain => vec!["wall_b"],
TileType::Wall => vec!["wall"],
TileType::DeepWater => vec!["water", "water2"],
TileType::Fence => vec!["tiles4"],
TileType::Bars => vec!["wall_b"],
TileType::Floor => vec!["dot"],
TileType::WoodFloor => vec!["planks", "planks_missing", "planks_missing2"],
TileType::Gravel => vec!["fluff", "fluff2"],
TileType::Road =>
vec![
"tiles",
"tiles_missing",
"tiles_missing2",
"tiles_missing3",
"tiles_missing4",
"tiles_missing5",
"tiles_missing6"
],
TileType::Grass => vec!["fluff", "fluff2"],
TileType::Foliage => vec!["grass_small", "grass"],
TileType::HeavyFoliage => vec!["grass_flower"],
TileType::Sand => vec!["fluff", "fluff2"],
TileType::ShallowWater => vec!["water", "water2"],
TileType::Bridge => vec!["planks"],
TileType::DownStair => vec!["stair_down"],
TileType::UpStair => vec!["stair_up"],
TileType::ToLocal(_) => vec!["stair_down"],
TileType::ToOvermap(_) => vec!["stair_up"],
};
if bloody.is_some() && tile_walkable(*self) {
options.extend(
vec!["blood1", "blood2", "blood3", "blood4", "blood5", "blood6", "blood7"]
);
}
return options[(float * (options.len() as f32)) as usize];
}
pub fn offset(&self) -> (i32, i32, i32) {
match self {
TileType::ImpassableMountain => IMPASSABLE_MOUNTAIN_OFFSETS,
TileType::Wall => WALL_OFFSETS,
TileType::DeepWater => DEEP_WATER_OFFSETS,
TileType::Fence => FENCE_OFFSETS,
TileType::Bars => BARS_OFFSETS,
TileType::Floor => FLOOR_OFFSETS,
TileType::WoodFloor => WOOD_FLOOR_OFFSETS,
TileType::Gravel => GRAVEL_OFFSETS,
TileType::Road => ROAD_OFFSETS,
TileType::Grass => GRASS_OFFSETS,
TileType::Foliage => FOLIAGE_OFFSETS,
TileType::HeavyFoliage => HEAVY_FOLIAGE_OFFSETS,
TileType::Sand => SAND_OFFSETS,
TileType::ShallowWater => SHALLOW_WATER_OFFSETS,
TileType::Bridge => BRIDGE_OFFSETS,
TileType::DownStair => STAIR_OFFSETS,
TileType::UpStair => STAIR_OFFSETS,
TileType::ToLocal(_) => WALL_OFFSETS,
TileType::ToOvermap(_) => WALL_OFFSETS,
}
}
pub fn col(&self, bloody: Option<RGB>) -> RGB {
if let Some(bloody) = bloody {
return bloody;
}
RGB::named(match self {
TileType::ImpassableMountain => IMPASSABLE_MOUNTAIN_COLOUR,
TileType::Wall => WALL_COLOUR,
TileType::DeepWater => DEEP_WATER_COLOUR,
TileType::Fence => FENCE_COLOUR,
TileType::Bars => BARS_COLOUR,
TileType::Floor => FLOOR_COLOUR,
TileType::WoodFloor => WOOD_FLOOR_COLOUR,
TileType::Gravel => GRAVEL_COLOUR,
TileType::Road => ROAD_COLOUR,
TileType::Grass => GRASS_COLOUR,
TileType::Foliage => FOLIAGE_COLOUR,
TileType::HeavyFoliage => HEAVY_FOLIAGE_COLOUR,
TileType::Sand => SAND_COLOUR,
TileType::ShallowWater => SHALLOW_WATER_COLOUR,
TileType::Bridge => BRIDGE_COLOUR,
TileType::DownStair => STAIR_COLOUR,
TileType::UpStair => STAIR_COLOUR,
TileType::ToLocal(_) => WALL_COLOUR,
TileType::ToOvermap(_) => WALL_COLOUR,
})
}
}
pub fn tile_walkable(tt: TileType) -> bool {
match tt {
TileType::ImpassableMountain | TileType::Wall | TileType::DeepWater | TileType::Fence | TileType::Bars => false,
| TileType::ImpassableMountain
| TileType::Wall
| TileType::DeepWater
| TileType::Fence
| TileType::Bars => false,
_ => true,
}
}
@ -40,6 +150,11 @@ pub fn tile_opaque(tt: TileType) -> bool {
_ => false,
}
}
pub fn tile_blocks_telepathy(tt: TileType) -> bool {
match tt {
_ => false,
}
}
pub fn tile_cost(tt: TileType) -> f32 {
match tt {
TileType::Road => 0.75,

View file

@ -12,7 +12,7 @@ use super::{
Foliage,
};
use bracket_lib::prelude::*;
use crate::data::names::*;
use crate::consts::names::*;
pub fn forest_builder(
new_id: i32,

View file

@ -1,6 +1,8 @@
use super::{ spawner, Map, Position, Rect, TileType };
use bracket_lib::prelude::*;
mod room_accretion;
use room_accretion::RoomAccretionBuilder;
mod bsp_dungeon;
use bsp_dungeon::BspDungeonBuilder;
mod bsp_interior;
@ -38,8 +40,8 @@ use common::*;
use specs::prelude::*;
use voronoi_spawning::VoronoiSpawning;
use super::config::CONFIG;
use super::data::ids::*;
use super::data::names::*;
use super::consts::ids::*;
use super::consts::names::*;
//use wfc::WaveFunctionCollapseBuilder;
mod room_exploder;
use room_exploder::RoomExploder;
@ -213,12 +215,12 @@ fn random_start_position(rng: &mut RandomNumberGenerator) -> (XStart, YStart) {
}
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, 2); // TODO: BSP Interiors, change this to 1d3 and uncomment.
// Start with a room builder.
match build_roll {
1 => builder.start_with(SimpleMapBuilder::new(None)),
2 => builder.start_with(BspDungeonBuilder::new()),
_ => builder.start_with(BspInteriorBuilder::new()),
_ => builder.start_with(BspDungeonBuilder::new()),
//_ => builder.start_with(BspInteriorBuilder::new()),
}
// BspInterior makes its own doorways. If we're not using that one,
@ -299,7 +301,7 @@ fn random_shape_builder(
end: bool
) -> bool {
// Pick an initial builder
let builder_roll = rng.roll_dice(1, 16);
let builder_roll = rng.roll_dice(1, 13);
let mut want_doors = true;
match builder_roll {
1 => builder.start_with(CellularAutomataBuilder::new()),
@ -317,11 +319,7 @@ fn random_shape_builder(
10 => builder.start_with(DLABuilder::central_attractor()),
11 => builder.start_with(DLABuilder::insectoid()),
12 => builder.start_with(VoronoiBuilder::pythagoras()),
13 => builder.start_with(VoronoiBuilder::manhattan()),
_ =>
builder.start_with(
PrefabBuilder::constant(prefab_builder::prefab_levels::WFC_POPULATED)
),
_ => builder.start_with(VoronoiBuilder::manhattan()),
}
// 'Select' the centre by placing a starting position, and cull everywhere unreachable.
@ -437,6 +435,8 @@ pub fn random_builder(
builder
}
use crate::consts::prelude::*;
pub fn level_builder(
id: i32,
rng: &mut RandomNumberGenerator,
@ -452,8 +452,8 @@ pub fn level_builder(
random_builder(
id,
rng,
width,
height,
VIEWPORT_W,
VIEWPORT_H,
2,
1,
initial_player_level,
@ -464,8 +464,8 @@ pub fn level_builder(
random_builder(
id,
rng,
width,
height,
VIEWPORT_W,
VIEWPORT_H,
4 + diff(ID_INFINITE, id),
1 + diff(ID_INFINITE, id),
initial_player_level,
@ -476,8 +476,8 @@ pub fn level_builder(
random_builder(
id,
rng,
width,
height,
VIEWPORT_W,
VIEWPORT_H,
1,
404,
initial_player_level,
@ -490,3 +490,10 @@ pub fn level_builder(
fn diff(branch_id: i32, lvl_id: i32) -> i32 {
return lvl_id - branch_id;
}
fn room_accretion() -> BuilderChain {
let mut builder = BuilderChain::new(false, 110, 64, 64, 0, "room_accretion", "accretion", 0, 1);
builder.start_with(RoomAccretionBuilder::new());
builder.with(AreaStartingPosition::new(XStart::CENTRE, YStart::CENTRE));
builder
}

View file

@ -4,7 +4,7 @@ pub mod prefab_levels;
pub mod prefab_sections;
pub mod prefab_vaults;
use std::collections::HashSet;
use crate::data::ids::*;
use crate::consts::ids::*;
#[derive(PartialEq, Copy, Clone)]
#[allow(dead_code)]

View file

@ -5,61 +5,12 @@ pub struct PrefabLevel {
pub height: usize,
}
#[allow(dead_code)]
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)]
const LEVEL_MAP: &str =
"
################################################################################
#          ########################################################    #########
#    @     ######    #########       ####     ###################        #######
#          ####   g  #                          ###############            #####
#          #### #    # #######       ####       #############                ###
##### ######### #    # #######       #########  ####    #####                ###
##### ######### ###### #######   o   #########  #### ## #####                ###
##                        ####       #########   ### ##         o            ###
##### ######### ###       ####       #######         ## #####                ###
##### ######### ###       ####       ####### #   ### ## #####                ###
##### ######### ###       ####       ####### #######    #####     o          ###
###          ## ###       ####       ####### ################                ###
###          ## ###   o   ###### ########### #   ############                ###
###          ## ###       ###### ###########     ###                         ###
###    %                  ###### ########### #   ###   !   ##                ###
###          ## ###              ######   ## #######       ##                ###
###          ## ###       ## ### #####     # ########################      #####
###          ## ###       ## ### #####     # #   ######################    #####
#### ## ####### ###### ##### ### ####          o ###########     ######    #####
#### ## ####### ###### ####   ## ####        #   #########         ###### ######
#    ## ####### ###### ####   ## ####        ############           ##### ######
# g  ## ####### ###### ####   ##        %    ###########   o      o  #### #    #
#    ## ###            ####   ## ####        #   #######   ##    ##  ####   g  #
#######                  ####### ####            ######     !    !    ### #    #
######                     ##### ####        #   ######               ### ######
#####                            #####     # ##########               ### ######
#####           !           ### ######     # ##########      o##o     ### #   ##
#####                       ### #######   ## #   ######               ###   g ##
#   ##                     #### ######## ###   o #######  ^########^ #### #   ##
# g    #                 ###### ######## #####   #######  ^        ^ #### ######
#   ##g####           ######    ######## ################           ##### ######
#   ## ########## ##########    ######## #################         ######      #
#####   ######### ########## %  ######## ###################     ######## ##   #
#### ### ######## ##########    ######## #################### ##########   #   #
### ##### ######   #########    ########          ########### #######   # g#   #
### #####           ###############      ###      ########### #######   ####   #
### ##### ####       ############## ######## g  g ########### ####         # ^ #
#### ###^####         ############# ########      #####       ####      # g#   #
#####   ######       ###            ########      ##### g     ####   !  ####^^ #
#!%^## ###  ##           ########## ########  gg                 g         # > #
#!%^   ###  ###     ############### ########      ##### g     ####      # g#   #
# %^##  ^   ###     ############### ########      #####       ##################
################################################################################";
const OVERMAP_TEMPLATE: &str =
"
^^^^^^^^^^^^^^^^^^^^^^^^^^^........
^^^^^^^^^^^^^^^^^^^^^^^^^^...........
^^^^^^^^^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^^^^^^^........
^^^^^^^^^^^^^^^....^^^^^^.............
^^^^^^^^^^^^^^.................................
^^^^^^^^^^^^^.....................................

View file

@ -0,0 +1,127 @@
use lazy_static::lazy_static;
use bracket_lib::prelude::*;
pub const HEIGHT: usize = 64;
pub const WIDTH: usize = 64;
pub const HALLWAY_CHANCE: f32 = 0.5;
pub const VERTICAL_CORRIDOR_MIN_LENGTH: i32 = 2;
pub const VERTICAL_CORRIDOR_MAX_LENGTH: i32 = 9;
pub const HORIZONTAL_CORRIDOR_MIN_LENGTH: i32 = 5;
pub const HORIZONTAL_CORRIDOR_MAX_LENGTH: i32 = 15;
pub enum Operator {
LessThan,
GreaterThan,
LessThanEqualTo,
GreaterThanEqualTo,
EqualTo,
}
impl Operator {
pub fn eval(&self, a: i32, b: i32) -> bool {
match self {
Operator::LessThan => a < b,
Operator::GreaterThan => a > b,
Operator::LessThanEqualTo => a <= b,
Operator::GreaterThanEqualTo => a >= b,
Operator::EqualTo => a == b,
}
}
pub fn string(&self) -> &str {
match self {
Operator::LessThan => "<",
Operator::GreaterThan => ">",
Operator::LessThanEqualTo => "<=",
Operator::GreaterThanEqualTo => ">=",
Operator::EqualTo => "==",
}
}
}
pub struct CellRules {
pub adjacent_type: i32,
pub into: i32,
pub operator: Operator,
pub n: i32,
}
impl CellRules {
const fn new(adjacent_type: i32, into: i32, operator: Operator, n: i32) -> CellRules {
CellRules {
adjacent_type,
into,
operator,
n,
}
}
}
lazy_static! {
pub static ref CA: Vec<Vec<CellRules>> = vec![
vec![CellRules::new(1, 1, Operator::GreaterThanEqualTo, 4)],
vec![
CellRules::new(0, 0, Operator::GreaterThanEqualTo, 5),
CellRules::new(1, 0, Operator::LessThan, 2)
]
];
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Direction {
NoDir = -1,
North = 0,
East = 1,
South = 2,
West = 3,
}
impl Direction {
pub fn transform(&self) -> Point {
match self {
Direction::NoDir => unreachable!("Direction::NoDir should never be transformed"),
Direction::North => Point::new(0, -1),
Direction::East => Point::new(1, 0),
Direction::South => Point::new(0, 1),
Direction::West => Point::new(-1, 0),
}
}
pub fn opposite_dir(&self) -> Direction {
match self {
Direction::NoDir => unreachable!("Direction::NoDir has no opposite."),
Direction::North => Direction::South,
Direction::East => Direction::West,
Direction::South => Direction::North,
Direction::West => Direction::East,
}
}
}
pub struct DirectionIterator {
current: Direction,
}
impl DirectionIterator {
pub fn new() -> DirectionIterator {
DirectionIterator {
current: Direction::North,
}
}
}
impl Iterator for DirectionIterator {
type Item = Direction;
fn next(&mut self) -> Option<Self::Item> {
use Direction::*;
let next_direction = match self.current {
North => East,
East => South,
South => West,
West => {
return None;
}
NoDir => unreachable!("Direction::NoDir should never be iterated over."),
};
self.current = next_direction;
Some(next_direction)
}
}

View file

@ -0,0 +1,397 @@
use super::{ BuilderMap, Map, InitialMapBuilder, TileType, Point };
use bracket_lib::prelude::*;
mod consts;
use consts::*;
/// Room Accretion map builder.
pub struct RoomAccretionBuilder {}
impl InitialMapBuilder for RoomAccretionBuilder {
#[allow(dead_code)]
fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
self.build(rng, build_data);
}
}
impl RoomAccretionBuilder {
/// Constructor for Room Accretion.
pub fn new() -> Box<RoomAccretionBuilder> {
Box::new(RoomAccretionBuilder {})
}
fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
accrete_rooms(rng, build_data);
}
}
fn grid_with_dimensions(h: usize, w: usize, value: i32) -> Vec<Vec<i32>> {
vec![vec![value; w]; h]
}
fn in_bounds(row: i32, col: i32, build_data: &BuilderMap) -> bool {
row > 0 && row < build_data.height && col > 0 && col < build_data.width
}
fn draw_continuous_shape_on_grid(
room: &Vec<Vec<i32>>,
top_offset: usize,
left_offset: usize,
grid: &mut Vec<Vec<i32>>
) {
for row in 0..room.len() {
for col in 0..room[0].len() {
if room[row][col] != 0 {
let target_row = row + top_offset;
let target_col = col + left_offset;
if target_row < grid.len() && target_col < grid[0].len() {
grid[target_row][target_col] = room[row][col];
}
}
}
}
}
struct Coordinate {
pub location: Point,
pub value: i32,
}
fn draw_individual_coordinates_on_grid(coordinates: &Vec<Coordinate>, grid: &mut Vec<Vec<i32>>) {
for c in coordinates {
let x = c.location.x as usize;
let y = c.location.y as usize;
if y < grid.len() && x < grid[0].len() {
grid[y][x] = c.value;
}
}
}
fn get_cell_neighbours(
cells: &Vec<Vec<i32>>,
row: usize,
col: usize,
h: usize,
w: usize
) -> Vec<i32> {
let mut neighbours = Vec::new();
for x in row.saturating_sub(1)..=std::cmp::min(row + 1, h - 1) {
for y in col.saturating_sub(1)..=std::cmp::min(col + 1, w - 1) {
if x != row || y != col {
neighbours.push(cells[x][y]);
}
}
}
console::log(&format!("neighbours: {:?}", neighbours));
neighbours
}
fn make_ca_room(rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) -> Vec<Vec<i32>> {
let width = rng.range(5, 12);
let height = rng.range(5, 12);
let mut cells = grid_with_dimensions(height, width, 0);
cells = cells
.into_iter()
.map(|row| {
row.into_iter()
.map(|_| if rng.roll_dice(1, 2) == 1 { 1 } else { 0 })
.collect()
})
.collect();
let transform_cell = |state: i32, neighbours: &Vec<i32>| -> i32 {
let rules: &[CellRules] = &CA[state as usize];
let mut new_state = state;
for rule in rules {
let n_neighbours = neighbours
.iter()
.filter(|&&neighbour| neighbour == rule.adjacent_type)
.count();
if rule.operator.eval(n_neighbours as i32, rule.n) {
new_state = rule.into;
}
}
new_state
};
for _ in 0..5 {
let mut new_cells = vec![vec![0; width]; height];
for row in 0..height {
for col in 0..width {
let neighbours = get_cell_neighbours(&cells, row, col, height, width);
let new_state = transform_cell(cells[row][col], &neighbours);
new_cells[row][col] = new_state;
}
}
cells = new_cells;
}
// TODO: Floodfill to keep largest contiguous blob
cells
}
fn room_fits_at(
hyperspace: Vec<Vec<i32>>,
top_offset: usize,
left_offset: usize,
build_data: &BuilderMap
) -> bool {
let mut x_dungeon: usize;
let mut y_dungeon: usize;
for y in 0..HEIGHT {
for x in 0..WIDTH {
if hyperspace[y][x] != 2 {
y_dungeon = y + top_offset;
x_dungeon = x + left_offset;
for i in y_dungeon.saturating_sub(1)..=std::cmp::min(y_dungeon + 1, WIDTH - 1) {
for j in x_dungeon.saturating_sub(1)..=std::cmp::min(
x_dungeon + 1,
HEIGHT - 1
) {
let pt = build_data.map.xy_idx(i as i32, j as i32);
if
!in_bounds(i as i32, j as i32, &build_data) ||
!(build_data.map.tiles[pt] == TileType::Wall) ||
build_data.spawn_list.contains(&(pt, "door".to_string()))
{
return false;
}
}
}
}
}
}
return true;
}
fn direction_of_door(
grid: &Vec<Vec<i32>>,
row: usize,
col: usize,
build_data: &BuilderMap
) -> Direction {
if grid[row][col] != 0 {
return Direction::NoDir;
}
let mut solution = Direction::NoDir;
let mut dir_iter = DirectionIterator::new();
for dir in &mut dir_iter {
let new_col = (col as i32) + dir.transform().x;
let new_row = (row as i32) + dir.transform().y;
let opp_col = (col as i32) - dir.transform().x;
let opp_row = (row as i32) - dir.transform().y;
if
in_bounds(new_row, new_col, &build_data) &&
in_bounds(opp_row, opp_col, &build_data) &&
grid[opp_row as usize][opp_col as usize] != 0
{
solution = dir;
}
}
return solution;
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct DoorSite {
pub x: i32,
pub y: i32,
pub dir: Direction,
}
fn choose_random_door_site(
room: Vec<Vec<i32>>,
rng: &mut RandomNumberGenerator,
build_data: &BuilderMap
) -> Vec<Option<DoorSite>> {
let mut grid = grid_with_dimensions(HEIGHT, WIDTH, 0);
let mut door_sites: Vec<DoorSite> = Vec::new();
const LEFT_OFFSET: usize = ((WIDTH as f32) / 2.0) as usize;
const TOP_OFFSET: usize = ((HEIGHT as f32) / 2.0) as usize;
draw_continuous_shape_on_grid(&room, TOP_OFFSET, LEFT_OFFSET, &mut grid);
for row in 0..HEIGHT {
for col in 0..WIDTH {
if grid[row][col] == 0 {
let door_dir = direction_of_door(&grid, row, col, &build_data);
if door_dir == Direction::NoDir {
continue;
}
let mut door_failed = false;
let (mut trace_row, mut trace_col) = (
(row as i32) + door_dir.transform().y,
(col as i32) + door_dir.transform().x,
);
let mut i = 0;
while i < 10 && in_bounds(trace_row, trace_col, &build_data) && !door_failed {
if grid[trace_row as usize][trace_col as usize] != 0 {
door_failed = true;
}
trace_col += door_dir.transform().x;
trace_row += door_dir.transform().y;
i += 1;
}
if !door_failed {
// May need more information here.
door_sites.push(DoorSite {
x: col as i32,
y: row as i32,
dir: door_dir,
});
}
}
}
}
let mut chosen_doors: Vec<Option<DoorSite>> = vec![None; 4];
let mut dir_iter = DirectionIterator::new();
for dir in &mut dir_iter {
let doors_facing_this_dir: Vec<&DoorSite> = door_sites
.iter()
.filter(|&door| door.dir == dir)
.collect();
if !doors_facing_this_dir.is_empty() {
let index = rng.range(0, doors_facing_this_dir.len());
chosen_doors[dir as usize] = Some(*doors_facing_this_dir[index]);
}
}
chosen_doors
}
fn shuffle<T>(list: &mut Vec<T>, rng: &mut RandomNumberGenerator) {
let len = list.len();
for i in (1..len).rev() {
let j = rng.range(0, i + 1);
list.swap(i, j);
}
}
fn clamp<T: Ord>(x: T, min: T, max: T) -> T {
if x < min { min } else if x > max { max } else { x }
}
fn attach_hallway_to(
door_sites: &mut Vec<Option<DoorSite>>,
hyperspace: &mut Vec<Vec<i32>>,
rng: &mut RandomNumberGenerator,
build_data: &BuilderMap
) {
let mut directions = vec![Direction::North, Direction::East, Direction::South, Direction::West];
shuffle(&mut directions, rng);
let mut hallway_dir: Direction = Direction::NoDir;
for i in 0..4 {
hallway_dir = directions[i];
if
door_sites[hallway_dir as usize].is_some() &&
in_bounds(
door_sites[hallway_dir as usize].unwrap().y +
hallway_dir.transform().y * VERTICAL_CORRIDOR_MAX_LENGTH,
door_sites[hallway_dir as usize].unwrap().x +
hallway_dir.transform().x * HORIZONTAL_CORRIDOR_MAX_LENGTH,
&build_data
)
{
break;
}
}
let transform = hallway_dir.transform();
let hallway_len: i32 = match hallway_dir {
Direction::NoDir => {
return;
}
Direction::North | Direction::South =>
rng.range(VERTICAL_CORRIDOR_MIN_LENGTH, VERTICAL_CORRIDOR_MAX_LENGTH + 1),
Direction::East | Direction::West =>
rng.range(HORIZONTAL_CORRIDOR_MIN_LENGTH, HORIZONTAL_CORRIDOR_MAX_LENGTH + 1),
};
let mut x = door_sites[hallway_dir as usize].unwrap().x;
let mut y = door_sites[hallway_dir as usize].unwrap().y;
for _i in 0..hallway_len {
if in_bounds(y, x, &build_data) {
hyperspace[y as usize][x as usize] = 1; // Dig out corridor.
}
x += transform.x;
y += transform.y;
}
y = clamp(y - transform.y, 0, (HEIGHT as i32) - 1);
x = clamp(x - transform.x, 0, (WIDTH as i32) - 1);
let mut dir_iter = DirectionIterator::new();
for dir in &mut dir_iter {
if dir != hallway_dir.opposite_dir() {
let door_y = y + dir.transform().y;
let door_x = x + dir.transform().x;
door_sites[dir as usize] = Some(DoorSite {
x: door_x,
y: door_y,
dir,
});
} else {
door_sites[dir as usize] = None;
}
}
console::log(&format!("door_sites: {:?}", door_sites));
}
fn design_room_in_hyperspace(
rng: &mut RandomNumberGenerator,
build_data: &mut BuilderMap
) -> Vec<Vec<i32>> {
// Project onto hyperspace
let mut hyperspace = grid_with_dimensions(HEIGHT, WIDTH, 0);
let room_type = rng.range(0, 1);
let room = match room_type {
0 => make_ca_room(rng, build_data),
_ => unreachable!("Invalid room type."),
};
draw_continuous_shape_on_grid(&room, HEIGHT / 2, WIDTH / 2, &mut hyperspace);
let mut door_sites = choose_random_door_site(room, rng, &build_data);
let roll: f32 = rng.rand();
if roll < HALLWAY_CHANCE {
attach_hallway_to(&mut door_sites, &mut hyperspace, rng, &build_data);
}
let coords: Vec<Coordinate> = door_sites
.iter()
.filter(|&door| door.is_some())
.map(|&door| Coordinate {
location: Point::new(door.unwrap().x, door.unwrap().y),
value: 2,
})
.collect();
draw_individual_coordinates_on_grid(&coords, &mut hyperspace);
hyperspace
}
fn map_i32_to_tiletype(val: i32, build_data: &mut BuilderMap) -> TileType {
match val {
0 => TileType::Wall,
1 => TileType::Floor,
2 => TileType::Floor, // With door.
_ => unreachable!("Unknown TileType"),
}
}
fn flatten_hyperspace_into_dungeon(
hyperspace: Vec<Vec<i32>>,
build_data: &mut BuilderMap
) -> Vec<TileType> {
let flattened_hyperspace: Vec<i32> = hyperspace.into_iter().flatten().collect();
flattened_hyperspace
.into_iter()
.enumerate()
.map(|(idx, cell)| {
if cell != 0 {
match cell {
2 => build_data.spawn_list.push((idx, "door".to_string())),
_ => {}
}
map_i32_to_tiletype(cell, build_data)
} else {
build_data.map.tiles[idx % (build_data.map.width as usize)]
}
})
.collect()
}
fn accrete_rooms(rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) {
let hyperspace = design_room_in_hyperspace(rng, build_data);
build_data.map.tiles = flatten_hyperspace_into_dungeon(hyperspace, build_data);
build_data.take_snapshot();
}

View file

@ -1,6 +1,6 @@
use super::{ BuilderMap, MetaMapBuilder, Rect, TileType };
use crate::tile_walkable;
use crate::data::messages::{
use crate::consts::messages::{
FEATURE_TREANTS,
FEATURE_BARRACKS_GOBLIN,
FEATURE_BARRACKS_KOBOLD,

View file

@ -1,6 +1,6 @@
use super::{ BuilderChain, BuilderMap, InitialMapBuilder, Position, TileType, FillEdges };
use std::collections::HashSet;
use crate::data::names::*;
use crate::consts::names::*;
use bracket_lib::prelude::*;
pub fn town_builder(
@ -271,6 +271,7 @@ impl TownBuilder {
building.1 + building.3 / 2
);
build_data.map.tiles[exit_idx] = TileType::DownStair;
build_data.spawn_list.push((exit_idx, "trapdoor".to_string()));
let mut to_place: Vec<&str> = vec!["npc_miner", "npc_miner", "npc_guard", "prop_chair"];
self.random_building_spawn(building, build_data, rng, &mut to_place, exit_idx)
}

View file

@ -149,7 +149,7 @@ impl<'a> System<'a> for MeleeCombatSystem {
let attack_verb = attack.1;
// Get all offensive bonuses
let d20 = rng.roll_dice(1, 20);
let attribute_hit_bonus = attacker_attributes.dexterity.bonus;
let attribute_hit_bonus = attacker_attributes.dexterity.modifier();
let skill_hit_bonus = gamesystem::skill_bonus(Skill::Melee, &*attacker_skills);
let mut equipment_hit_bonus = weapon_info.hit_bonus;
for (wielded, to_hit) in (&equipped, &to_hit).join() {
@ -187,7 +187,7 @@ impl<'a> System<'a> for MeleeCombatSystem {
// Get armour class
let bac = target_pools.bac;
let attribute_ac_bonus = target_attributes.dexterity.bonus / 2;
let attribute_ac_bonus = target_attributes.dexterity.modifier() / 2;
let skill_ac_bonus = gamesystem::skill_bonus(Skill::Defence, &*target_skills);
let mut armour_ac_bonus = 0;
for (wielded, ac) in (&equipped, &ac).join() {
@ -245,19 +245,19 @@ impl<'a> System<'a> for MeleeCombatSystem {
let mut attribute_damage_bonus = weapon_info.damage_bonus;
match weapon_info.attribute {
WeaponAttribute::Dexterity => {
attribute_damage_bonus += attacker_attributes.dexterity.bonus;
attribute_damage_bonus += attacker_attributes.dexterity.modifier();
}
WeaponAttribute::Strength => {
attribute_damage_bonus += attacker_attributes.strength.bonus;
attribute_damage_bonus += attacker_attributes.strength.modifier();
}
WeaponAttribute::Finesse => {
if
attacker_attributes.dexterity.bonus >
attacker_attributes.strength.bonus
attacker_attributes.dexterity.modifier() >
attacker_attributes.strength.modifier()
{
attribute_damage_bonus += attacker_attributes.dexterity.bonus;
attribute_damage_bonus += attacker_attributes.dexterity.modifier();
} else {
attribute_damage_bonus += attacker_attributes.strength.bonus;
attribute_damage_bonus += attacker_attributes.strength.modifier();
}
}
}
@ -295,11 +295,6 @@ impl<'a> System<'a> for MeleeCombatSystem {
);
}
}
let pos = positions.get(wants_melee.target);
if let Some(pos) = pos {
particle_builder.damage_taken(pos.x, pos.y);
}
add_effect(
Some(entity),
EffectType::Damage { amount: damage, damage_type: weapon_info.damage_type },

View file

@ -9,7 +9,7 @@ use specs::prelude::*;
use bracket_lib::prelude::*;
use to_char;
use std::collections::HashMap;
use crate::data::events::*;
use crate::consts::events::*;
#[cfg(target_arch = "wasm32")]
pub fn create_morgue_file(ecs: &World) {
@ -36,6 +36,7 @@ fn create_file_name(ecs: &World, morgue_dir: &str) -> String {
let pools = ecs.read_storage::<Pools>();
let pool = pools.get(*e).unwrap();
let class = match ecs.read_storage::<HasClass>().get(*e).unwrap().name {
Class::Unset => "classless",
Class::Fighter => "fighter",
Class::Wizard => "wizard",
Class::Rogue => "rogue",
@ -47,7 +48,7 @@ fn create_file_name(ecs: &World, morgue_dir: &str) -> String {
Ancestry::Dwarf => "dwarf",
Ancestry::Gnome => "gnome",
Ancestry::Catfolk => "catfolk",
Ancestry::NULL => "NULL",
Ancestry::Unset => "NULL",
};
return format!(
"{}/lv{}-{}-{}-{}.txt",
@ -64,6 +65,7 @@ fn create_morgue_string(ecs: &World) -> String {
let mut morgue_info: String = Default::default();
let e = ecs.fetch::<Entity>();
let class = match ecs.read_storage::<HasClass>().get(*e).unwrap().name {
Class::Unset => "classless",
Class::Fighter => "fighter",
Class::Wizard => "wizard",
Class::Rogue => "rogue",
@ -75,7 +77,7 @@ fn create_morgue_string(ecs: &World) -> String {
Ancestry::Dwarf => "dwarf",
Ancestry::Gnome => "gnome",
Ancestry::Catfolk => "catfolk",
Ancestry::NULL => "NULL",
Ancestry::Unset => "NULL",
};
let pools = ecs.read_storage::<Pools>();
let pool = pools.get(*e).unwrap();
@ -127,19 +129,19 @@ fn draw_tombstone(ecs: &World, len: usize) -> String {
pool.mana.max,
"",
"",
attr.strength.base + attr.strength.modifiers,
attr.strength.bonus,
attr.constitution.base + attr.constitution.modifiers,
attr.constitution.bonus,
attr.wisdom.base + attr.wisdom.modifiers,
attr.wisdom.bonus,
attr.strength.base + attr.strength.bonuses,
attr.strength.modifier(),
attr.constitution.base + attr.constitution.bonuses,
attr.constitution.modifier(),
attr.wisdom.base + attr.wisdom.bonuses,
attr.wisdom.modifier(),
"",
attr.dexterity.base + attr.dexterity.modifiers,
attr.dexterity.bonus,
attr.intelligence.base + attr.intelligence.modifiers,
attr.intelligence.bonus,
attr.charisma.base + attr.charisma.modifiers,
attr.charisma.bonus,
attr.dexterity.base + attr.dexterity.bonuses,
attr.dexterity.modifier(),
attr.intelligence.base + attr.intelligence.bonuses,
attr.intelligence.modifier(),
attr.charisma.base + attr.charisma.bonuses,
attr.charisma.modifier(),
"",
"",
map.name,
@ -162,10 +164,10 @@ fn draw_map(ecs: &World) -> String {
if idx == map.xy_idx(point.x, point.y) {
glyph_u16 = to_cp437('@');
} else if crate::spatial::has_tile_content(idx) {
let mut render_order = 0;
let mut render_order = 4;
crate::spatial::for_each_tile_content(idx, |e| {
if let Some(renderable) = ecs.read_storage::<Renderable>().get(e) {
if renderable.render_order >= render_order {
if renderable.render_order <= render_order {
render_order = renderable.render_order;
glyph_u16 = renderable.glyph;
}

View file

@ -1,26 +1,27 @@
use super::{ ParticleLifetime, Position, Renderable, BTerm };
use bracket_lib::prelude::*;
use notan::prelude::*;
use specs::prelude::*;
use crate::data::visuals::{ DEFAULT_PARTICLE_LIFETIME, SHORT_PARTICLE_LIFETIME };
use crate::consts::visuals::{ DEFAULT_PARTICLE_LIFETIME, SHORT_PARTICLE_LIFETIME };
/// Runs each tick, deleting particles who are past their expiry.
// Should make an addition to this to also spawn delayed particles,
// running through a list and removing the frame_time_ms from the
// delay. When delay is <= 0, make a particle_builder.request for
// the particle.
pub fn particle_ticker(ecs: &mut World, ctx: &BTerm) {
pub fn particle_ticker(ecs: &mut World, ctx: &App) {
cull_dead_particles(ecs, ctx);
create_delayed_particles(ecs, ctx);
}
fn cull_dead_particles(ecs: &mut World, ctx: &BTerm) {
fn cull_dead_particles(ecs: &mut World, ctx: &App) {
let mut dead_particles: Vec<Entity> = Vec::new();
{
// Age out particles
let mut particles = ecs.write_storage::<ParticleLifetime>();
let entities = ecs.entities();
for (entity, mut particle) in (&entities, &mut particles).join() {
particle.lifetime_ms -= ctx.frame_time_ms;
particle.lifetime_ms -= ctx.timer.delta_f32() * 1000.0;
if particle.lifetime_ms < 0.0 {
dead_particles.push(entity);
}
@ -39,18 +40,18 @@ pub fn check_queue(ecs: &World) -> bool {
return false;
}
fn create_delayed_particles(ecs: &mut World, ctx: &BTerm) {
fn create_delayed_particles(ecs: &mut World, ctx: &App) {
let mut particle_builder = ecs.write_resource::<ParticleBuilder>();
let mut handled_particles: Vec<ParticleRequest> = Vec::new();
for delayed_particle in particle_builder.delayed_requests.iter_mut() {
delayed_particle.delay -= ctx.frame_time_ms;
delayed_particle.delay -= ctx.timer.delta_f32() * 1000.0;
if delayed_particle.delay < 0.0 {
handled_particles.push(ParticleRequest {
x: delayed_particle.particle.x,
y: delayed_particle.particle.y,
fg: delayed_particle.particle.fg,
bg: delayed_particle.particle.bg,
glyph: delayed_particle.particle.glyph,
sprite: delayed_particle.particle.sprite.clone(),
lifetime: delayed_particle.particle.lifetime,
});
}
@ -80,12 +81,7 @@ fn create_delayed_particles(ecs: &mut World, ctx: &BTerm) {
.insert(p, Position { x: handled.x, y: handled.y })
.expect("Could not insert position");
renderables
.insert(p, Renderable {
fg: handled.fg,
bg: handled.bg,
glyph: handled.glyph,
render_order: 0,
})
.insert(p, Renderable::new(handled.glyph, handled.sprite, handled.fg, 0))
.expect("Could not insert renderables");
particles
.insert(p, ParticleLifetime { lifetime_ms: handled.lifetime })
@ -98,8 +94,8 @@ pub struct ParticleRequest {
x: i32,
y: i32,
fg: RGB,
bg: RGB,
glyph: FontCharType,
sprite: String,
lifetime: f32,
}
@ -126,11 +122,11 @@ impl ParticleBuilder {
x: i32,
y: i32,
fg: RGB,
bg: RGB,
glyph: FontCharType,
sprite: String,
lifetime: f32
) {
self.requests.push(ParticleRequest { x, y, fg, bg, glyph, lifetime });
self.requests.push(ParticleRequest { x, y, fg, glyph, sprite, lifetime });
}
pub fn delay(
@ -138,150 +134,43 @@ impl ParticleBuilder {
x: i32,
y: i32,
fg: RGB,
bg: RGB,
glyph: FontCharType,
sprite: String,
lifetime: f32,
delay: f32
) {
self.delayed_requests.push(DelayedParticleRequest {
delay: delay,
particle: ParticleRequest { x, y, fg, bg, glyph, lifetime },
particle: ParticleRequest { x, y, fg, glyph, sprite, lifetime },
});
}
// MASSIVE TODO: Animate these, or make them random. PLACEHOLDER.
pub fn damage_taken(&mut self, x: i32, y: i32) {
self.request(
x,
y,
RGB::named(ORANGE),
RGB::named(BLACK),
to_cp437('‼'),
DEFAULT_PARTICLE_LIFETIME
);
self.request(x, y, RGB::named(RED), to_cp437('‼'), "slash1".to_string(), 75.0);
self.delay(x, y, RGB::named(RED), to_cp437('‼'), "slash2".to_string(), 75.0, 75.0);
self.delay(x, y, RGB::named(RED), to_cp437('‼'), "slash3".to_string(), 75.0, 150.0);
}
pub fn attack_miss(&mut self, x: i32, y: i32) {
self.request(
x,
y,
RGB::named(CYAN),
RGB::named(BLACK),
to_cp437('‼'),
"slash1".to_string(),
DEFAULT_PARTICLE_LIFETIME
);
}
pub fn kick(&mut self, x: i32, y: i32) {
self.request(
x,
y,
RGB::named(CHOCOLATE),
RGB::named(BLACK),
to_cp437('‼'),
"kick".to_string(),
SHORT_PARTICLE_LIFETIME
);
}
// Makes a particle request in the shape of an 'x'. Sort of.
#[allow(dead_code)]
pub fn request_star(
&mut self,
x: i32,
y: i32,
fg: RGB,
bg: RGB,
glyph: FontCharType,
lifetime: f32,
secondary_fg: RGB
) {
let eighth_l = lifetime / 8.0;
let quarter_l = eighth_l * 2.0;
self.request(x, y, fg, bg, glyph, lifetime);
self.delay(
x + 1,
y + 1,
secondary_fg.lerp(bg, 0.8),
bg,
to_cp437('/'),
quarter_l,
eighth_l
);
self.delay(
x + 1,
y - 1,
secondary_fg.lerp(bg, 0.6),
bg,
to_cp437('\\'),
quarter_l,
quarter_l
);
self.delay(
x - 1,
y - 1,
secondary_fg.lerp(bg, 0.2),
bg,
to_cp437('/'),
quarter_l,
eighth_l * 3.0
);
self.delay(
x - 1,
y + 1,
secondary_fg.lerp(bg, 0.4),
bg,
to_cp437('\\'),
quarter_l,
lifetime
);
}
// Makes a rainbow particle request in the shape of an 'x'. Sort of.
#[allow(dead_code)]
pub fn request_rainbow_star(&mut self, x: i32, y: i32, glyph: FontCharType, lifetime: f32) {
let bg = RGB::named(BLACK);
let eighth_l = lifetime / 8.0;
let quarter_l = eighth_l * 2.0;
let half_l = quarter_l * 2.0;
self.request(x, y, RGB::named(CYAN), bg, glyph, lifetime);
self.delay(x + 1, y + 1, RGB::named(RED), bg, to_cp437('\\'), half_l, eighth_l);
self.delay(x + 1, y - 1, RGB::named(ORANGE), bg, to_cp437('/'), half_l, quarter_l);
self.delay(x - 1, y - 1, RGB::named(GREEN), bg, to_cp437('\\'), half_l, eighth_l * 3.0);
self.delay(x - 1, y + 1, RGB::named(YELLOW), bg, to_cp437('/'), half_l, half_l);
}
// Makes a rainbow particle request. Sort of.
#[allow(dead_code)]
pub fn request_rainbow(&mut self, x: i32, y: i32, glyph: FontCharType, lifetime: f32) {
let bg = RGB::named(BLACK);
let eighth_l = lifetime / 8.0;
self.request(x, y, RGB::named(RED), bg, glyph, eighth_l);
self.delay(x, y, RGB::named(ORANGE), bg, glyph, eighth_l, eighth_l);
self.delay(x, y, RGB::named(YELLOW), bg, glyph, eighth_l, eighth_l * 2.0);
self.delay(x, y, RGB::named(GREEN), bg, glyph, eighth_l, eighth_l * 3.0);
self.delay(x, y, RGB::named(BLUE), bg, glyph, eighth_l, eighth_l * 4.0);
self.delay(x, y, RGB::named(INDIGO), bg, glyph, eighth_l, eighth_l * 5.0);
self.delay(x, y, RGB::named(VIOLET), bg, glyph, eighth_l, eighth_l * 6.0);
}
/// Makes a particle request in the shape of a +.
#[allow(dead_code)]
pub fn request_plus(
&mut self,
x: i32,
y: i32,
fg: RGB,
bg: RGB,
glyph: FontCharType,
lifetime: f32
) {
self.request(x, y, fg, bg, glyph, lifetime * 2.0);
self.request(x + 1, y, fg, bg, to_cp437('─'), lifetime);
self.request(x - 1, y, fg, bg, to_cp437('─'), lifetime);
self.request(x, y + 1, fg, bg, to_cp437('│'), lifetime);
self.request(x, y - 1, fg, bg, to_cp437('│'), lifetime);
}
}
pub struct ParticleSpawnSystem {}
@ -305,12 +194,15 @@ impl<'a> System<'a> for ParticleSpawnSystem {
.insert(p, Position { x: new_particle.x, y: new_particle.y })
.expect("Could not insert position");
renderables
.insert(p, Renderable {
fg: new_particle.fg,
bg: new_particle.bg,
glyph: new_particle.glyph,
render_order: 0,
})
.insert(
p,
Renderable::new(
new_particle.glyph,
new_particle.sprite.clone(),
new_particle.fg,
0
)
)
.expect("Could not insert renderables");
particles
.insert(p, ParticleLifetime { lifetime_ms: new_particle.lifetime })

View file

@ -34,12 +34,16 @@ use super::{
get_dest,
Destination,
DamageType,
effects::sound,
};
use bracket_lib::prelude::*;
use specs::prelude::*;
use std::cmp::{ max, min };
use crate::data::events::*;
use crate::data::ids::*;
use crate::consts::events::*;
use crate::consts::ids::*;
use crate::gui::with_article;
use notan::prelude::*;
use std::collections::HashMap;
pub fn try_door(i: i32, j: i32, ecs: &mut World) -> RunState {
let mut positions = ecs.write_storage::<Position>();
@ -90,6 +94,7 @@ pub fn try_door(i: i32, j: i32, ecs: &mut World) -> RunState {
if door.open == true {
let renderables = ecs.read_storage::<Renderable>();
if multiple_tile_content {
sound::door_resist(destination_idx);
if let Some(name) = names.get(potential_target) {
gamelog::Logger
::new()
@ -100,7 +105,8 @@ pub fn try_door(i: i32, j: i32, ecs: &mut World) -> RunState {
.append("is blocked.")
.log();
}
} else if rng.roll_dice(1, 6) + attributes.strength.bonus < 2 {
} else if rng.roll_dice(1, 6) + attributes.strength.modifier() < 2 {
sound::door_resist(destination_idx);
if let Some(name) = names.get(potential_target) {
gamelog::Logger
::new()
@ -112,13 +118,18 @@ pub fn try_door(i: i32, j: i32, ecs: &mut World) -> RunState {
.log();
}
} else {
sound::door_close(destination_idx);
door.open = false;
blocks_visibility
.insert(potential_target, BlocksVisibility {})
.expect("Unable to insert BlocksVisibility.");
blocks_movement
.insert(potential_target, BlocksTile {})
.expect("Unable to insert BlocksTile.");
if door.blocks_vis {
blocks_visibility
.insert(potential_target, BlocksVisibility {})
.expect("Unable to insert BlocksVisibility.");
}
if door.blocks_move {
blocks_movement
.insert(potential_target, BlocksTile {})
.expect("Unable to insert BlocksTile.");
}
if let Some(name) = names.get(potential_target) {
gamelog::Logger
::new()
@ -133,7 +144,7 @@ pub fn try_door(i: i32, j: i32, ecs: &mut World) -> RunState {
std::mem::drop(renderables);
let mut renderables = ecs.write_storage::<Renderable>();
let render_data = renderables.get_mut(potential_target).unwrap();
render_data.glyph = to_cp437('+'); // Nethack open door, maybe just use '/' instead.
render_data.swap();
door_pos = Some(Point::new(pos.x + delta_x, pos.y + delta_y));
}
result = RunState::Ticking;
@ -202,7 +213,8 @@ pub fn open(i: i32, j: i32, ecs: &mut World) -> RunState {
if let Some(door) = door {
if door.open == false {
let renderables = ecs.read_storage::<Renderable>();
if rng.roll_dice(1, 6) + attributes.strength.bonus < 2 {
if rng.roll_dice(1, 6) + attributes.strength.modifier() < 2 {
sound::door_resist(destination_idx);
if let Some(name) = names.get(potential_target) {
gamelog::Logger
::new()
@ -214,6 +226,7 @@ pub fn open(i: i32, j: i32, ecs: &mut World) -> RunState {
.log();
}
} else {
sound::door_open(destination_idx);
door.open = true;
blocks_visibility.remove(potential_target);
blocks_movement.remove(potential_target);
@ -230,7 +243,7 @@ pub fn open(i: i32, j: i32, ecs: &mut World) -> RunState {
std::mem::drop(renderables);
let mut renderables = ecs.write_storage::<Renderable>();
let render_data = renderables.get_mut(potential_target).unwrap();
render_data.glyph = to_cp437('▓'); // Nethack open door, maybe just use '/' instead.
render_data.swap();
door_pos = Some(Point::new(pos.x + delta_x, pos.y + delta_y));
}
result = RunState::Ticking;
@ -331,15 +344,15 @@ pub fn kick(i: i32, j: i32, ecs: &mut World) -> RunState {
None,
EffectType::Particle {
glyph: to_cp437('‼'),
sprite: "gnome".to_string(), // FIXME: REMOVE THE GNOMES
fg: RGB::named(CHOCOLATE),
bg: RGB::named(BLACK),
lifespan: 150.0,
delay: 0.0,
},
Targets::Entity { target: potential_target }
);
// ~33% chance of breaking it down + str
if rng.roll_dice(1, 10) + attributes.strength.bonus > 6 {
if rng.roll_dice(1, 10) + attributes.strength.modifier() > 6 {
gamelog::Logger
::new()
.append("As you kick the")
@ -391,8 +404,8 @@ pub fn kick(i: i32, j: i32, ecs: &mut World) -> RunState {
None,
EffectType::Particle {
glyph: to_cp437('‼'),
sprite: "gnome".to_string(), // FIXME: REMOVE THE GNOMES
fg: RGB::named(CHOCOLATE),
bg: RGB::named(BLACK),
lifespan: 150.0,
delay: 0.0,
},
@ -492,7 +505,7 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> RunState
}
let door = doors.get_mut(potential_target);
if let Some(door) = door {
if door.open == false {
if door.open == false && door.blocks_move {
if let Some(name) = names.get(potential_target) {
let colour = if
let Some(_) = ecs.read_storage::<Item>().get(potential_target)
@ -561,11 +574,11 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> RunState
let mut logger = gamelog::Logger::new().append("You see");
for i in 0..seen_items.len() {
if i > 0 && i < seen_items.len() {
logger = logger.append(", a");
logger = logger.append(", ");
}
logger = logger
.colour(seen_items[i].1)
.append_n(&seen_items[i].0)
.append_n(with_article(&seen_items[i].0))
.colour(WHITE);
}
logger.period().log();
@ -645,136 +658,118 @@ fn get_item(ecs: &mut World) -> RunState {
}
}
pub fn player_input(gs: &mut State, ctx: &mut BTerm, on_overmap: bool) -> RunState {
match ctx.key {
None => {
return RunState::AwaitingInput;
}
Some(key) =>
match key {
// Cardinals
VirtualKeyCode::Left | VirtualKeyCode::Numpad4 | VirtualKeyCode::H => {
return try_move_player(-1, 0, &mut gs.ecs);
}
VirtualKeyCode::Right | VirtualKeyCode::Numpad6 | VirtualKeyCode::L => {
return try_move_player(1, 0, &mut gs.ecs);
}
VirtualKeyCode::Up | VirtualKeyCode::Numpad8 | VirtualKeyCode::K => {
return try_move_player(0, -1, &mut gs.ecs);
}
VirtualKeyCode::Down | VirtualKeyCode::Numpad2 | VirtualKeyCode::J => {
return try_move_player(0, 1, &mut gs.ecs);
}
// Diagonals
VirtualKeyCode::Numpad9 | VirtualKeyCode::U => {
return try_move_player(1, -1, &mut gs.ecs);
}
VirtualKeyCode::Numpad7 | VirtualKeyCode::Y => {
return try_move_player(-1, -1, &mut gs.ecs);
}
VirtualKeyCode::Numpad3 | VirtualKeyCode::N => {
return try_move_player(1, 1, &mut gs.ecs);
}
VirtualKeyCode::Numpad1 | VirtualKeyCode::B => {
return try_move_player(-1, 1, &mut gs.ecs);
}
// id
VirtualKeyCode::Period => {
if ctx.shift {
let dest = try_change_level(&mut gs.ecs, false);
let curr_map_id = gs.ecs.fetch::<Map>().id;
return match dest {
// If we have no destination, do nothing.
Destination::None => RunState::AwaitingInput,
// If we want to go to the next level, go to the up-stair tile of id + 1.
Destination::NextLevel =>
RunState::GoToLevel(curr_map_id + 1, TileType::UpStair),
// If we want to go to the previous level, go to the down-stair tile of id - 1.
Destination::PreviousLevel =>
RunState::GoToLevel(curr_map_id - 1, TileType::DownStair),
Destination::ToLocal(id) =>
RunState::GoToLevel(ID_OVERMAP, TileType::ToLocal(id)),
Destination::ToOvermap(id) =>
RunState::GoToLevel(id, TileType::ToOvermap(id)),
};
} else {
return skip_turn(&mut gs.ecs); // (Wait a turn)
}
}
VirtualKeyCode::Comma => {
if ctx.shift {
let dest = try_change_level(&mut gs.ecs, true);
let curr_map_id = gs.ecs.fetch::<Map>().id;
return match dest {
Destination::None => RunState::AwaitingInput,
Destination::NextLevel =>
RunState::GoToLevel(curr_map_id + 1, TileType::UpStair),
Destination::PreviousLevel =>
RunState::GoToLevel(curr_map_id - 1, TileType::DownStair),
Destination::ToLocal(id) =>
RunState::GoToLevel(ID_OVERMAP, TileType::ToLocal(id)),
Destination::ToOvermap(id) =>
RunState::GoToLevel(id, TileType::ToOvermap(id)),
};
}
}
VirtualKeyCode::Slash => {
if ctx.shift {
return RunState::HelpScreen;
}
}
VirtualKeyCode::NumpadDecimal => {
return skip_turn(&mut gs.ecs);
}
fn try_descend(ecs: &mut World) -> RunState {
let dest = try_change_level(ecs, false);
let curr_map_id = ecs.fetch::<Map>().id;
return match dest {
Destination::None => RunState::AwaitingInput,
Destination::NextLevel => RunState::GoToLevel(curr_map_id + 1, TileType::UpStair),
Destination::PreviousLevel => RunState::GoToLevel(curr_map_id - 1, TileType::DownStair),
Destination::ToLocal(id) => RunState::GoToLevel(ID_OVERMAP, TileType::ToLocal(id)),
Destination::ToOvermap(id) => RunState::GoToLevel(id, TileType::ToOvermap(id)),
};
}
fn try_ascend(ecs: &mut World) -> RunState {
let dest = try_change_level(ecs, true);
let curr_map_id = ecs.fetch::<Map>().id;
return match dest {
Destination::None => RunState::AwaitingInput,
Destination::NextLevel => RunState::GoToLevel(curr_map_id + 1, TileType::UpStair),
Destination::PreviousLevel => RunState::GoToLevel(curr_map_id - 1, TileType::DownStair),
Destination::ToLocal(id) => RunState::GoToLevel(ID_OVERMAP, TileType::ToLocal(id)),
Destination::ToOvermap(id) => RunState::GoToLevel(id, TileType::ToOvermap(id)),
};
}
// Items
VirtualKeyCode::C => {
if !on_overmap {
return RunState::ActionWithDirection { function: try_door };
}
pub fn player_input(gs: &mut State, ctx: &mut App, on_overmap: bool) -> RunState {
let key = &ctx.keyboard;
// Movement
for keycode in key.pressed.iter() {
match *keycode {
KeyCode::Numpad1 | KeyCode::B => {
return try_move_player(-1, 1, &mut gs.ecs);
}
KeyCode::Numpad2 | KeyCode::Down | KeyCode::J => {
return try_move_player(0, 1, &mut gs.ecs);
}
KeyCode::Numpad3 | KeyCode::N => {
return try_move_player(1, 1, &mut gs.ecs);
}
KeyCode::Numpad4 | KeyCode::Left | KeyCode::H => {
return try_move_player(-1, 0, &mut gs.ecs);
}
KeyCode::Numpad6 | KeyCode::Right | KeyCode::L => {
return try_move_player(1, 0, &mut gs.ecs);
}
KeyCode::Numpad7 | KeyCode::Y => {
return try_move_player(-1, -1, &mut gs.ecs);
}
KeyCode::Numpad8 | KeyCode::Up | KeyCode::K => {
return try_move_player(0, -1, &mut gs.ecs);
}
KeyCode::Numpad9 | KeyCode::U => {
return try_move_player(1, -1, &mut gs.ecs);
}
KeyCode::Period => {
if key.shift() {
return try_descend(&mut gs.ecs);
}
VirtualKeyCode::O => {
if !on_overmap {
return RunState::ActionWithDirection { function: open };
}
}
VirtualKeyCode::F => {
if !on_overmap {
return RunState::ActionWithDirection { function: kick };
}
}
VirtualKeyCode::G => {
return get_item(&mut gs.ecs);
}
VirtualKeyCode::I => {
return RunState::ShowInventory;
}
VirtualKeyCode::D => {
return RunState::ShowDropItem;
}
VirtualKeyCode::R => {
return RunState::ShowRemoveItem;
}
// Other
VirtualKeyCode::Minus => {
return RunState::ShowCheatMenu;
}
VirtualKeyCode::Escape => {
return RunState::SaveGame;
}
VirtualKeyCode::X => {
let (min_x, _max_x, min_y, _max_y, x_offset, y_offset) = get_screen_bounds(
&gs.ecs,
ctx
);
let ppos = gs.ecs.fetch::<Point>();
let (x, y) = (ppos.x + x_offset - min_x, ppos.y + y_offset - min_y);
return RunState::Farlook { x, y };
}
_ => {
return RunState::AwaitingInput;
return skip_turn(&mut gs.ecs);
}
KeyCode::Comma => {
if key.shift() {
return try_ascend(&mut gs.ecs);
}
}
KeyCode::Slash => {
if key.shift() {
return RunState::HelpScreen;
}
}
KeyCode::C => {
if !on_overmap {
return RunState::ActionWithDirection { function: try_door };
}
}
KeyCode::O => {
if !on_overmap {
return RunState::ActionWithDirection { function: open };
}
}
KeyCode::F => {
if !on_overmap {
return RunState::ActionWithDirection { function: kick };
}
}
KeyCode::G => {
return get_item(&mut gs.ecs);
}
KeyCode::I => {
return RunState::ShowInventory;
}
KeyCode::D => {
return RunState::ShowDropItem;
}
KeyCode::R => {
return RunState::ShowRemoveItem;
}
KeyCode::Minus => {
return RunState::ShowCheatMenu;
}
KeyCode::Escape => {
return RunState::SaveGame;
}
KeyCode::X => {
let bounds = get_screen_bounds(&gs.ecs, false);
let ppos = gs.ecs.fetch::<Point>();
let (x, y) = (
ppos.x + bounds.x_offset - bounds.min_x,
ppos.y + bounds.y_offset - bounds.min_y,
);
return RunState::Farlook { x, y };
}
_ => {}
}
}
return RunState::AwaitingInput;
}
@ -784,6 +779,18 @@ fn try_change_level(ecs: &mut World, backtracking: bool) -> Destination {
let map = ecs.fetch::<Map>();
let player_idx = map.xy_idx(player_pos.x, player_pos.y);
let this_tile = map.tiles[player_idx];
let mut blocked = false;
crate::spatial::for_each_tile_content(player_idx, |potential| {
if let Some(is_door) = ecs.read_storage::<Door>().get(potential) {
if is_door.open == false {
blocked = true;
gamelog::Logger::new().append("The way is blocked.").log();
}
}
});
if blocked {
return Destination::None;
}
return get_dest(this_tile, backtracking);
}

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