Compare commits

...
Sign in to create a new pull request.

28 commits

Author SHA1 Message Date
Llywelwyn
c29b93337c
Update README.md 2025-03-09 11:12:11 +00:00
Llywelwyn
0584d07a1f
Update cargo-build-test.yml to use ubuntu-22.04 2025-03-09 11:04:32 +00:00
Llywelwyn
45b9b33039
Update cargo-build-test.yml 2025-03-09 11:02:16 +00:00
Lewis Wynne
bdcd55c8a5 Fixes miscoloured logs (fixes #26) 2025-03-09 10:50:35 +00:00
Llywelwyn
99c17f8521 draw hunger uses Point 2024-06-17 23:22:30 +01:00
Llywelwyn
6324449c16 draw_hunger() 2024-06-17 23:19:20 +01:00
Llywelwyn
d465592c0f abstracts ui drawing 2024-06-16 10:31:06 +01:00
Llywelwyn
f494efbf3f bump ver 2024-06-15 20:41:21 +01:00
Llywelwyn
ba5d120fef fix warns, bump ver 2024-06-15 20:40:51 +01:00
Llywelwyn
c5106a63b5 static inventory keys - items remember their slots
this is the biggest refactor of my entire life
2024-06-15 20:14:38 +01:00
Llywelwyn
9719ebbe88 docs tldr 2024-06-15 17:35:40 +01:00
Llywelwyn
2eaf431942 disable show_mapgen for default config 2024-06-15 17:24:37 +01:00
Llywelwyn
a7c5d2167c back to serde_json 2024-06-15 16:46:15 +01:00
Llywelwyn
678636c57d Revert "Infallible -> NoError"
This reverts commit 9c8f301491.
2024-06-15 16:44:13 +01:00
Llywelwyn
30697a98bb rm room_accretion for now 2024-06-15 16:43:17 +01:00
Llywelwyn
c73f9a5458 Revert "cherry pick -> serde_json saves to bincode"
This reverts commit 180532ee3e.
2024-06-15 16:42:59 +01:00
Llywelwyn
9c8f301491 Infallible -> NoError 2023-10-24 23:32:42 +01:00
Llywelwyn
180532ee3e cherry pick -> serde_json saves to bincode 2023-10-24 11:13:43 +01:00
Llywelwyn
46bbe14bea added effects for adding intrinsics 2023-10-02 23:02:46 +01:00
Llywelwyn
fa4612cf1f changed get_noncursed() to helper on BUC struct 2023-10-02 23:02:34 +01:00
Llywelwyn
4d21bd46d4 add_intr!() macro for adding intrinsics to the Player
If needed, player can just be replaced by another arg to the macro so this works on every other entity - but right now the player is the only thing to gain/lose intrinsics.
2023-10-02 22:14:00 +01:00
Llywelwyn
b5743819ec .describe() for Intrinsics, for use in tooltips later 2023-10-02 21:11:12 +01:00
Llywelwyn
190543a361 move all doors to the ends of corridors 2023-10-02 07:39:45 +01:00
Llywelwyn
7a27321bec initial tweaks - starting room w/ corridors + doors 2023-10-02 07:00:28 +01:00
Llywelwyn
97ca3a25e3 doors and door directions - RA 2023-10-02 04:43:01 +01:00
Llywelwyn
1fa7432dfe room accretion - initial 2023-10-01 20:56:46 +01:00
Llywelwyn
fa3b906dce adds article to ID morgue msg 2023-09-23 11:21:34 +01:00
Llywelwyn
2d33c90af8 fixes infini-dungeon difficulty 2023-09-23 11:12:28 +01:00
31 changed files with 1049 additions and 461 deletions

View file

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

View file

@ -1,6 +1,6 @@
[package]
name = "rust-rl"
version = "0.1.1"
version = "0.1.4"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View file

@ -2,18 +2,18 @@
#### using _rltk/bracket-lib_, and _specs_
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:
[![Rust](https://github.com/Llywelwyn/rust-rl/actions/workflows/cargo-build-test.yml/badge.svg)](https://github.com/Llywelwyn/rust-rl/actions/workflows/cargo-build-test.yml)
check out the page in the header for the wasm version, pick [a release](https://github.com/Llywelwyn/rust-rl/releases), or build manually with:
`git clone https://github.com/Llywelwyn/rust-rl/ && cd rust-rl && cargo build --release`,
![image](https://github.com/Llywelwyn/rust-rl/assets/82828093/b05e4f0b-2062-4abe-9fee-c679f9ef420d)
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,3 +157,4 @@ i'm also working on translating over my progress into blog entries on my site @
![squares](https://github.com/Llywelwyn/rust-rl/assets/82828093/b752e1cb-340d-475d-84ae-68fdb4977a80)
</details>
</details>

View file

@ -49,3 +49,7 @@ 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

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

View file

@ -65,9 +65,7 @@ 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 {
@ -94,9 +92,7 @@ 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));

View file

@ -243,16 +243,55 @@ pub enum BUC {
Blessed,
}
impl BUC {
pub fn noncursed(&self) -> bool {
match self {
BUC::Cursed => false,
_ => true,
}
}
}
#[derive(Component, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Clone)]
pub struct Beatitude {
pub buc: BUC,
pub known: bool,
}
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]
pub enum ItemType {
Amulet,
Weapon,
Armour,
Comestible,
Scroll,
Spellbook,
Potion,
Ring,
Wand,
}
impl ItemType {
pub fn string(&self) -> &str {
match self {
ItemType::Amulet => "Amulets",
ItemType::Weapon => "Weapons",
ItemType::Armour => "Armour",
ItemType::Comestible => "Comestibles",
ItemType::Scroll => "Scrolls",
ItemType::Spellbook => "Spellbooks",
ItemType::Potion => "Potions",
ItemType::Ring => "Rings",
ItemType::Wand => "Wands",
}
}
}
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Item {
pub weight: f32, // in lbs
pub value: f32, // base
pub category: ItemType,
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
@ -427,11 +466,45 @@ pub enum Intrinsic {
Speed, // 4/3x speed multiplier
}
impl Intrinsic {
pub fn describe(&self) -> &str {
match self {
Intrinsic::Regeneration => "regenerates health",
Intrinsic::Speed => "is hasted",
}
}
}
#[derive(Component, Serialize, Deserialize, Debug, Clone)]
pub struct Intrinsics {
pub list: HashSet<Intrinsic>,
}
impl Intrinsics {
pub fn describe(&self) -> String {
let mut descriptions = Vec::new();
for intrinsic in &self.list {
descriptions.push(intrinsic.describe());
}
match descriptions.len() {
0 =>
unreachable!("describe() should never be called on an empty Intrinsics component."),
1 => format!("It {}.", descriptions[0]),
_ => {
let last = descriptions.pop().unwrap();
let joined = descriptions.join(", ");
format!("It {}, and {}.", joined, last)
}
}
}
}
#[derive(Component, Serialize, Deserialize, Debug, Clone)]
pub struct IntrinsicChanged {
pub gained: HashSet<Intrinsic>,
pub lost: HashSet<Intrinsic>,
}
#[derive(Component, Debug, ConvertSaveload, Clone)]
pub struct InflictsDamage {
pub damage_type: DamageType,
@ -575,3 +648,20 @@ 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

@ -11,6 +11,8 @@ use super::{
Position,
Renderable,
RunState,
WantsToRemoveKey,
WantsToDelete,
};
use bracket_lib::prelude::*;
use specs::prelude::*;
@ -65,7 +67,17 @@ pub fn delete_the_dead(ecs: &mut World) {
}
}
}
let (items_to_delete, loot_to_spawn) = handle_dead_entity_items(ecs, &dead);
let (mut items_to_delete, loot_to_spawn) = handle_dead_entity_items(ecs, &dead);
{
let entities = ecs.entities();
let removekeys = ecs.read_storage::<WantsToRemoveKey>();
let delete = ecs.read_storage::<WantsToDelete>();
// Add items marked for deletion to the list, but only if they've already had their
// key assignments handled, to ensurew we don't leave any dangling references behind.
for (e, _d, _r) in (&entities, &delete, !&removekeys).join() {
items_to_delete.push(e);
}
}
for loot in loot_to_spawn {
crate::raws::spawn_named_entity(
&crate::raws::RAWS.lock().unwrap(),
@ -82,6 +94,7 @@ pub fn delete_the_dead(ecs: &mut World) {
// For everything that died, increment the event log, and delete.
for victim in dead {
gamelog::record_event(events::EVENT::Turn(1));
// TODO: Delete stuff from inventory? This should be handled elsewhere.
ecs.delete_entity(victim).expect("Unable to delete.");
}
}

View file

@ -25,6 +25,7 @@ pub const NUTRITION_BLESSED: &str = "Delicious";
pub const LEVELUP_PLAYER: &str = "Welcome to experience level";
pub const YOU_PICKUP_ITEM: &str = "You pick up the";
pub const NO_MORE_KEYS: &str = "Your backpack cannot accomodate any more items";
pub const YOU_DROP_ITEM: &str = "You drop the";
pub const YOU_EQUIP_ITEM: &str = "You equip the";
pub const YOU_REMOVE_ITEM: &str = "You unequip your";

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

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

View file

@ -4,13 +4,14 @@ use bracket_lib::prelude::*;
use specs::prelude::*;
use std::collections::VecDeque;
use std::sync::Mutex;
use crate::components::DamageType;
use crate::components::*;
mod damage;
mod hunger;
mod particles;
mod targeting;
mod triggers;
mod intrinsics;
pub use targeting::aoe_tiles;
@ -51,6 +52,9 @@ pub enum EffectType {
ModifyNutrition {
amount: i32,
},
AddIntrinsic {
intrinsic: Intrinsic,
},
TriggerFire {
trigger: Entity,
},
@ -153,6 +157,7 @@ fn tile_effect_hits_entities(effect: &EffectType) -> bool {
EffectType::Healing { .. } => true,
EffectType::ModifyNutrition { .. } => true,
EffectType::Confusion { .. } => true,
EffectType::AddIntrinsic { .. } => true,
_ => false,
}
}
@ -175,6 +180,7 @@ 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),
_ => {}
}
}

View file

@ -1,4 +1,4 @@
use super::{ add_effect, get_noncursed, particles, spatial, EffectType, Entity, Targets, World };
use super::{ add_effect, particles, spatial, EffectType, Entity, Targets, World };
use crate::{
gamelog,
gui::item_colour_ecs,
@ -33,6 +33,8 @@ use crate::{
KnownSpells,
Position,
Viewshed,
WantsToRemoveKey,
WantsToDelete,
};
use crate::data::messages::*;
use bracket_lib::prelude::*;
@ -57,7 +59,10 @@ pub fn item_trigger(source: Option<Entity>, item: Entity, target: &Targets, ecs:
let did_something = event_trigger(source, item, target, ecs);
// If it's a consumable, delete it
if did_something && ecs.read_storage::<Consumable>().get(item).is_some() {
ecs.entities().delete(item).expect("Failed to delete item");
let mut removekey = ecs.write_storage::<WantsToRemoveKey>();
removekey.insert(item, WantsToRemoveKey {}).expect("Unable to insert WantsToRemoveKey");
let mut delete = ecs.write_storage::<WantsToDelete>();
delete.insert(item, WantsToDelete {}).expect("Unable to insert WantsToDelete");
}
}
@ -205,7 +210,7 @@ fn handle_healing(
healing_item.modifier;
add_effect(
event.source,
EffectType::Healing { amount: roll, increment_max: get_noncursed(&event.buc) },
EffectType::Healing { amount: roll, increment_max: event.buc.noncursed() },
event.target.clone()
);
for target in get_entity_targets(&event.target) {
@ -218,9 +223,7 @@ 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 {
@ -262,9 +265,7 @@ 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

@ -126,7 +126,7 @@ pub fn record_event(event: EVENT) {
new_event = format!("Discovered {}", name);
}
EVENT::Identified(name) => {
new_event = format!("Identified {}", name);
new_event = format!("Identified {}", crate::gui::with_article(name));
}
EVENT::PlayerDied(str) => {
// Generating the String is handled in the death effect, to avoid passing the ecs here.

View file

@ -3,10 +3,10 @@ use super::{
item_colour_ecs,
obfuscate_name_ecs,
print_options,
renderable_colour,
unique_ecs,
check_key,
letter_to_option,
ItemMenuResult,
UniqueInventoryItem,
BUC,
};
use crate::{
gamelog,
@ -19,11 +19,12 @@ use crate::{
Name,
ObfuscatedName,
Renderable,
Key,
states::state::*,
};
use bracket_lib::prelude::*;
use specs::prelude::*;
use std::collections::BTreeMap;
use std::collections::HashMap;
/// Handles the Identify menu.
pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<Entity>) {
@ -37,9 +38,12 @@ pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<Enti
let names = gs.ecs.read_storage::<Name>();
let renderables = gs.ecs.read_storage::<Renderable>();
let beatitudes = gs.ecs.read_storage::<Beatitude>();
let keys = gs.ecs.read_storage::<Key>();
let build_identify_iterator = || {
(&entities, &items, &renderables, &names).join().filter(|(item_entity, _i, _r, n)| {
(&entities, &items, &renderables, &names, &keys)
.join()
.filter(|(item_entity, _i, _r, n, _k)| {
// If not owned by the player, return false.
let mut keep = false;
if let Some(bp) = backpack.get(*item_entity) {
@ -91,34 +95,15 @@ pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<Enti
.log();
return (ItemMenuResult::Selected, Some(build_identify_iterator().nth(0).unwrap().0));
}
let mut player_inventory: super::PlayerInventory = BTreeMap::new();
for (entity, _i, renderable, name) in build_identify_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 = UniqueInventoryItem {
display_name: super::DisplayName { singular: singular.clone(), plural: plural.clone() },
rgb: item_colour_ecs(&gs.ecs, entity),
renderables: renderable_colour(&renderables, entity),
glyph: renderable.glyph,
beatitude_status: beatitude_status,
name: name.name.clone(),
};
let mut player_inventory: super::PlayerInventory = HashMap::new();
for (entity, _i, _r, _n, key) in build_identify_iterator() {
let unique_item = unique_ecs(&gs.ecs, entity);
player_inventory
.entry(unique_item)
.and_modify(|(_e, count)| {
*count += 1;
.and_modify(|slot| {
slot.count += 1;
})
.or_insert((entity, 1));
.or_insert(super::InventorySlot { item: entity, count: 1, idx: key.idx });
}
// Get display args
let width = get_max_inventory_width(&player_inventory);
@ -133,7 +118,7 @@ pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<Enti
"Identify which item? [aA-zZ][Esc.]"
);
ctx.draw_box(x, y, width + 2, count + 1, RGB::named(WHITE), RGB::named(BLACK));
print_options(&player_inventory, x + 1, y + 1, ctx);
print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx);
// Input
match ctx.key {
None => (ItemMenuResult::NoResponse, None),
@ -141,21 +126,17 @@ pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<Enti
match key {
VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None),
_ => {
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));
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));
}
}
}
(ItemMenuResult::NoResponse, None)
}

View file

@ -32,6 +32,9 @@ use super::{
Skills,
Viewshed,
BUC,
Key,
Item,
ItemType,
data::ids::get_local_col,
};
use crate::data::entity::CARRY_CAPACITY_PER_STRENGTH;
@ -43,7 +46,9 @@ use crate::data::visuals::{
};
use bracket_lib::prelude::*;
use specs::prelude::*;
use std::collections::BTreeMap;
use std::collections::HashMap;
use crate::invkeys::check_key;
mod character_creation;
mod cheat_menu;
mod letter_to_option;
@ -101,6 +106,101 @@ pub fn draw_lerping_bar(
ctx.print(sx + width, sy, "]");
}
fn draw_xp(ctx: &mut BTerm, pt: Point, pool: &Pools) {
ctx.print_color(
pt.x,
pt.y,
RGB::named(WHITE),
RGB::named(BLACK),
format!("XP{}/{}", pool.level, pool.xp)
);
}
fn calc_ac(ecs: &World, skills: &Skills, stats: &Pools, attr: &Attributes) -> i32 {
let skill_ac_bonus = gamesystem::skill_bonus(Skill::Defence, skills);
let mut armour_ac_bonus = 0;
let equipped = ecs.read_storage::<Equipped>();
let ac = ecs.read_storage::<ArmourClassBonus>();
let player_entity = ecs.fetch::<Entity>();
for (wielded, ac) in (&equipped, &ac).join() {
if wielded.owner == *player_entity {
armour_ac_bonus += ac.amount;
}
}
stats.bac - attr.dexterity.bonus / 2 - skill_ac_bonus - armour_ac_bonus
}
fn draw_ac(ctx: &mut BTerm, pt: Point, ac: i32) {
ctx.print_color(pt.x, pt.y, RGB::named(PINK), RGB::named(BLACK), "AC");
ctx.print_color(pt.x + 2, pt.y, RGB::named(WHITE), RGB::named(BLACK), ac);
}
fn draw_attributes(ctx: &mut BTerm, pt: Point, a: &Attributes) {
ctx.print_color(pt.x, pt.y, RGB::named(RED), RGB::named(BLACK), "STR");
ctx.print_color(pt.x + 3, pt.y, RGB::named(WHITE), RGB::named(BLACK), a.strength.base);
ctx.print_color(pt.x + 7, pt.y, RGB::named(GREEN), RGB::named(BLACK), "DEX");
ctx.print_color(pt.x + 10, pt.y, RGB::named(WHITE), RGB::named(BLACK), a.dexterity.base);
ctx.print_color(pt.x + 14, pt.y, RGB::named(ORANGE), RGB::named(BLACK), "CON");
ctx.print_color(pt.x + 17, pt.y, RGB::named(WHITE), RGB::named(BLACK), a.constitution.base);
ctx.print_color(pt.x, 54, RGB::named(CYAN), RGB::named(BLACK), "INT");
ctx.print_color(pt.x + 3, pt.y + 1, RGB::named(WHITE), RGB::named(BLACK), a.intelligence.base);
ctx.print_color(pt.x + 7, pt.y + 1, RGB::named(YELLOW), RGB::named(BLACK), "WIS");
ctx.print_color(pt.x + 10, pt.y + 1, RGB::named(WHITE), RGB::named(BLACK), a.wisdom.base);
ctx.print_color(pt.x + 14, pt.y + 1, RGB::named(PURPLE), RGB::named(BLACK), "CHA");
ctx.print_color(pt.x + 17, pt.y + 1, RGB::named(WHITE), RGB::named(BLACK), a.charisma.base);
}
fn draw_hunger(ctx: &mut BTerm, pt: Point, hunger: &HungerClock) {
match hunger.state {
HungerState::Satiated => {
ctx.print_color_right(
pt.x,
pt.y,
get_hunger_colour(hunger.state),
RGB::named(BLACK),
"Satiated"
);
}
HungerState::Normal => {}
HungerState::Hungry => {
ctx.print_color_right(
pt.x,
pt.y,
get_hunger_colour(hunger.state),
RGB::named(BLACK),
"Hungry"
);
}
HungerState::Weak => {
ctx.print_color_right(
pt.x,
pt.y,
get_hunger_colour(hunger.state),
RGB::named(BLACK),
"Weak"
);
}
HungerState::Fainting => {
ctx.print_color_right(
pt.x,
pt.y,
get_hunger_colour(hunger.state),
RGB::named(BLACK),
"Fainting"
);
}
HungerState::Starving => {
ctx.print_color_right(
pt.x,
pt.y,
get_hunger_colour(hunger.state),
RGB::named(BLACK),
"Starving"
);
}
}
}
pub fn draw_ui(ecs: &World, ctx: &mut BTerm) {
// Render stats
let pools = ecs.read_storage::<Pools>();
@ -137,111 +237,12 @@ pub fn draw_ui(ecs: &World, ctx: &mut BTerm) {
RGB::named(BLUE),
RGB::named(BLACK)
);
// Draw AC
let skill_ac_bonus = gamesystem::skill_bonus(Skill::Defence, &*skills);
let mut armour_ac_bonus = 0;
let equipped = ecs.read_storage::<Equipped>();
let ac = ecs.read_storage::<ArmourClassBonus>();
let player_entity = ecs.fetch::<Entity>();
for (wielded, ac) in (&equipped, &ac).join() {
if wielded.owner == *player_entity {
armour_ac_bonus += ac.amount;
}
}
let armour_class =
stats.bac - attributes.dexterity.bonus / 2 - skill_ac_bonus - armour_ac_bonus;
ctx.print_color(26, 53, RGB::named(PINK), RGB::named(BLACK), "AC");
ctx.print_color(28, 53, RGB::named(WHITE), RGB::named(BLACK), armour_class);
// Draw level
ctx.print_color(
26,
54,
RGB::named(WHITE),
RGB::named(BLACK),
format!("XP{}/{}", stats.level, stats.xp)
);
// Draw attributes
let x = 38;
ctx.print_color(x, 53, RGB::named(RED), RGB::named(BLACK), "STR");
ctx.print_color(x + 3, 53, RGB::named(WHITE), RGB::named(BLACK), attributes.strength.base);
ctx.print_color(x + 7, 53, RGB::named(GREEN), RGB::named(BLACK), "DEX");
ctx.print_color(
x + 10,
53,
RGB::named(WHITE),
RGB::named(BLACK),
attributes.dexterity.base
);
ctx.print_color(x + 14, 53, RGB::named(ORANGE), RGB::named(BLACK), "CON");
ctx.print_color(
x + 17,
53,
RGB::named(WHITE),
RGB::named(BLACK),
attributes.constitution.base
);
ctx.print_color(x, 54, RGB::named(CYAN), RGB::named(BLACK), "INT");
ctx.print_color(
x + 3,
54,
RGB::named(WHITE),
RGB::named(BLACK),
attributes.intelligence.base
);
ctx.print_color(x + 7, 54, RGB::named(YELLOW), RGB::named(BLACK), "WIS");
ctx.print_color(x + 10, 54, RGB::named(WHITE), RGB::named(BLACK), attributes.wisdom.base);
ctx.print_color(x + 14, 54, RGB::named(PURPLE), RGB::named(BLACK), "CHA");
ctx.print_color(x + 17, 54, RGB::named(WHITE), RGB::named(BLACK), attributes.charisma.base);
// Draw hunger
match hunger.state {
HungerState::Satiated => {
ctx.print_color_right(
70,
53,
get_hunger_colour(hunger.state),
RGB::named(BLACK),
"Satiated"
);
}
HungerState::Normal => {}
HungerState::Hungry => {
ctx.print_color_right(
70,
53,
get_hunger_colour(hunger.state),
RGB::named(BLACK),
"Hungry"
);
}
HungerState::Weak => {
ctx.print_color_right(
70,
53,
get_hunger_colour(hunger.state),
RGB::named(BLACK),
"Weak"
);
}
HungerState::Fainting => {
ctx.print_color_right(
70,
53,
get_hunger_colour(hunger.state),
RGB::named(BLACK),
"Fainting"
);
}
HungerState::Starving => {
ctx.print_color_right(
70,
53,
get_hunger_colour(hunger.state),
RGB::named(BLACK),
"Starving"
);
}
}
draw_ac(ctx, Point::new(26, 53), calc_ac(ecs, skills, stats, attributes));
draw_xp(ctx, Point::new(26, 54), stats);
draw_attributes(ctx, Point::new(38, 53), attributes);
draw_hunger(ctx, Point::new(70, 53), hunger);
// Burden
let player_entity = ecs.fetch::<Entity>();
if let Some(burden) = burden.get(*player_entity) {
match burden.level {
crate::BurdenLevel::Burdened => {
@ -271,41 +272,16 @@ pub fn draw_ui(ecs: &World, ctx: &mut BTerm) {
ctx.print_color(20, 20, RGB::named(YELLOW), RGB::named(BLACK), "--- GODMODE: ON ---");
}
// Draw equipment
let renderables = ecs.read_storage::<Renderable>();
let mut equipment: Vec<(String, RGB, RGB, FontCharType)> = Vec::new();
let entities = ecs.entities();
for (entity, _equipped, renderable) in (&entities, &equipped, &renderables)
.join()
.filter(|item| item.1.owner == *player_entity) {
equipment.push((
obfuscate_name_ecs(ecs, entity).0,
RGB::named(item_colour_ecs(ecs, entity)),
renderable.fg,
renderable.glyph,
));
}
let mut y = 1;
let equipment = items(&ecs, Filter::Equipped);
if !equipment.is_empty() {
ctx.print_color(72, y, RGB::named(BLACK), RGB::named(WHITE), "Equipment");
let mut j = 0;
for item in equipment {
y += 1;
ctx.set(72, y, RGB::named(YELLOW), RGB::named(BLACK), 97 + (j as FontCharType));
j += 1;
ctx.set(74, y, item.2, RGB::named(BLACK), item.3);
ctx.print_color(76, y, item.1, RGB::named(BLACK), &item.0);
ctx.print_color(
76 + &item.0.len() + 1,
y,
RGB::named(WHITE),
RGB::named(BLACK),
"(worn)"
);
}
y += 2;
y = print_options(&ecs, &equipment, 72, y, ctx);
y += 1;
}
// Draw consumables
// Draw backpack
ctx.print_color(72, y, RGB::named(BLACK), RGB::named(WHITE), "Backpack");
ctx.print_color(
81,
@ -320,8 +296,8 @@ pub fn draw_ui(ecs: &World, ctx: &mut BTerm) {
)
);
y += 1;
let player_inventory = get_player_inventory(&ecs);
y = print_options(&player_inventory, 72, y, ctx).0;
let backpack = items(&ecs, Filter::Backpack);
y = print_options(&ecs, &backpack, 72, y, ctx);
// Draw spells - if we have any -- NYI!
if let Some(known_spells) = ecs.read_storage::<KnownSpells>().get(*player_entity) {
@ -505,46 +481,46 @@ pub enum ItemMenuResult {
}
pub fn print_options(
_ecs: &World,
inventory: &PlayerInventory,
mut x: i32,
mut y: i32,
ctx: &mut BTerm
) -> (i32, i32) {
let mut j = 0;
) -> i32 {
let initial_x: i32 = x;
let mut width: i32 = -1;
for (item, (_e, item_count)) in inventory {
let mut sorted: Vec<_> = inventory.iter().collect();
sorted.sort_by(|a, b| a.1.idx.cmp(&b.1.idx));
for (info, slot) in sorted {
x = initial_x;
// Print the character required to access this item. i.e. (a)
if j < 26 {
ctx.set(x, y, RGB::named(YELLOW), RGB::named(BLACK), 97 + (j as FontCharType));
if slot.idx < 26 {
ctx.set(x, y, RGB::named(YELLOW), RGB::named(BLACK), 97 + slot.idx);
} else {
// If we somehow have more than 26, start using capitals
ctx.set(x, y, RGB::named(YELLOW), RGB::named(BLACK), 65 - 26 + (j as FontCharType));
ctx.set(x, y, RGB::named(YELLOW), RGB::named(BLACK), 65 - 26 + slot.idx);
}
x += 2;
let fg = RGB::from_u8(item.renderables.0, item.renderables.1, item.renderables.2);
ctx.set(x, y, fg, RGB::named(BLACK), item.glyph);
let fg = RGB::from_u8(info.renderables.0, info.renderables.1, info.renderables.2);
ctx.set(x, y, fg, RGB::named(BLACK), info.glyph);
x += 2;
let fg = RGB::from_u8(item.rgb.0, item.rgb.1, item.rgb.2);
if item_count > &1 {
let fg = RGB::from_u8(info.rgb.0, info.rgb.1, info.rgb.2);
if slot.count > 1 {
// If more than one, print the number and pluralise
// i.e. (a) 3 daggers
ctx.print_color(x, y, fg, RGB::named(BLACK), item_count);
ctx.print_color(x, y, fg, RGB::named(BLACK), slot.count);
x += 2;
ctx.print_color(x, y, fg, RGB::named(BLACK), item.display_name.plural.to_string());
let this_width = x - initial_x + (item.display_name.plural.len() as i32);
width = if width > this_width { width } else { this_width };
ctx.print_color(x, y, fg, RGB::named(BLACK), info.display_name.plural.to_string());
} else {
if item.display_name.singular.to_lowercase().ends_with("s") {
if info.display_name.singular.to_lowercase().ends_with("s") {
ctx.print_color(x, y, fg, RGB::named(BLACK), "some");
x += 5;
} else if
['a', 'e', 'i', 'o', 'u']
.iter()
.any(|&v| item.display_name.singular.to_lowercase().starts_with(v))
.any(|&v| info.display_name.singular.to_lowercase().starts_with(v))
{
// If one and starts with a vowel, print 'an'
// i.e. (a) an apple
@ -556,40 +532,54 @@ pub fn print_options(
ctx.print_color(x, y, fg, RGB::named(BLACK), "a");
x += 2;
}
ctx.print_color(x, y, fg, RGB::named(BLACK), item.display_name.singular.to_string());
let this_width = x - initial_x + (item.display_name.singular.len() as i32);
width = if width > this_width { width } else { this_width };
/*
let text = if let Some(worn) = ecs.read_storage::<Equipped>().get(slot.item) {
use crate::EquipmentSlot;
let text = match worn.slot {
EquipmentSlot::Melee | EquipmentSlot::Shield => "being held",
_ => "being worn",
};
format!("{} ({})", info.display_name.singular.to_string(), text)
} else {
info.display_name.singular.to_string()
};
*/
let text = info.display_name.singular.to_string();
ctx.print_color(x, y, fg, RGB::named(BLACK), text);
}
y += 1;
j += 1;
}
return (y, width);
return y;
}
const PADDING: i32 = 4;
const SOME: i32 = 4;
const AN: i32 = 2;
const A: i32 = 1;
pub fn get_max_inventory_width(inventory: &PlayerInventory) -> i32 {
let mut width: i32 = 0;
for (item, (_e, count)) in inventory {
for (item, slot) in inventory {
let mut this_width = item.display_name.singular.len() as i32;
// Clean this up. It should use consts.
this_width += 4; // The spaces before and after the character to select this item, etc.
if count <= &1 {
if slot.count <= 1 {
if item.display_name.singular == item.display_name.plural {
this_width += 4; // "some".len
this_width += SOME;
} else if
['a', 'e', 'i', 'o', 'u'].iter().any(|&v| item.display_name.singular.starts_with(v))
{
this_width += 2; // "an".len
this_width += AN;
} else {
this_width += 1; // "a".len
this_width += A;
}
} else {
this_width += count.to_string().len() as i32; // i.e. "12".len
this_width =
(item.display_name.plural.len() as i32) + (slot.count.to_string().len() as i32); // i.e. "12".len
}
width = if width > this_width { width } else { this_width };
}
return width;
return width + PADDING;
}
// Inside the ECS
@ -636,7 +626,7 @@ pub fn obfuscate_name(
if has_beatitude.known {
let prefix = match has_beatitude.buc {
BUC::Cursed => Some("cursed "),
BUC::Uncursed => None,
BUC::Uncursed => Some("uncursed "),
BUC::Blessed => Some("blessed "),
};
if prefix.is_some() {
@ -831,13 +821,13 @@ pub fn show_help(ctx: &mut BTerm) -> YesNoResult {
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct DisplayName {
singular: String,
plural: String,
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct UniqueInventoryItem {
display_name: DisplayName,
rgb: (u8, u8, u8),
@ -847,57 +837,71 @@ pub struct UniqueInventoryItem {
name: String,
}
pub type PlayerInventory = BTreeMap<UniqueInventoryItem, (Entity, i32)>;
pub fn get_player_inventory(ecs: &World) -> PlayerInventory {
let player_entity = ecs.fetch::<Entity>();
let names = ecs.read_storage::<Name>();
let backpack = ecs.read_storage::<InBackpack>();
let entities = ecs.entities();
let renderables = ecs.read_storage::<Renderable>();
let mut player_inventory: BTreeMap<UniqueInventoryItem, (Entity, i32)> = BTreeMap::new();
for (entity, _pack, name, renderable) in (&entities, &backpack, &names, &renderables)
.join()
.filter(|item| item.1.owner == *player_entity) {
// RGB can't be used as a key. This is converting the RGB (tuple of f32) into a tuple of u8s.
let item_colour = item_colour_ecs(ecs, entity);
let renderables = (
(renderable.fg.r * 255.0) as u8,
(renderable.fg.g * 255.0) as u8,
(renderable.fg.b * 255.0) as u8,
);
let (singular, plural) = obfuscate_name_ecs(ecs, entity);
let beatitude_status = if let Some(beatitude) = ecs.read_storage::<Beatitude>().get(entity) {
match beatitude.buc {
BUC::Blessed => 1,
BUC::Uncursed => 2,
BUC::Cursed => 3,
pub struct InventorySlot {
pub item: Entity,
pub count: i32,
pub idx: usize,
}
} else {
0
};
let unique_item = UniqueInventoryItem {
display_name: DisplayName { singular: singular.clone(), plural: plural },
rgb: item_colour,
renderables: renderables,
glyph: renderable.glyph,
beatitude_status: beatitude_status,
name: name.name.clone(),
};
player_inventory
.entry(unique_item)
.and_modify(|(_e, count)| {
*count += 1;
pub type PlayerInventory = HashMap<UniqueInventoryItem, InventorySlot>;
pub enum Filter {
All,
Backpack,
Equipped,
Category(ItemType),
}
macro_rules! includeitem {
($inv:expr, $ecs:expr, $e:expr, $k:expr) => {
$inv.entry(unique_ecs($ecs, $e))
.and_modify(|slot| {
slot.count += 1;
})
.or_insert((entity, 1));
.or_insert(InventorySlot {
item: $e,
count: 1,
idx: $k.idx,
});
};
}
return player_inventory;
pub fn items(ecs: &World, filter: Filter) -> PlayerInventory {
let entities = ecs.entities();
let keys = ecs.read_storage::<Key>();
let mut inv: PlayerInventory = HashMap::new();
match filter {
Filter::All => {
for (e, k) in (&entities, &keys).join() {
includeitem!(inv, ecs, e, k);
}
}
Filter::Backpack => {
let backpack = ecs.read_storage::<InBackpack>();
for (e, k, _b) in (&entities, &keys, &backpack).join() {
includeitem!(inv, ecs, e, k);
}
}
Filter::Equipped => {
let equipped = ecs.read_storage::<Equipped>();
for (e, k, _e) in (&entities, &keys, &equipped).join() {
includeitem!(inv, ecs, e, k);
}
}
Filter::Category(itemtype) => {
let items = ecs.read_storage::<Item>();
for (e, k, _i) in (&entities, &keys, &items)
.join()
.filter(|e| e.2.category == itemtype) {
includeitem!(inv, ecs, e, k);
}
}
}
inv
}
pub fn show_inventory(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<Entity>) {
let player_inventory = get_player_inventory(&gs.ecs);
let player_inventory = items(&gs.ecs, Filter::Backpack);
let count = player_inventory.len();
let (x_offset, y_offset) = (1, 10);
@ -915,7 +919,7 @@ pub fn show_inventory(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio
let y = 3 + y_offset;
let width = get_max_inventory_width(&player_inventory);
ctx.draw_box(x, y, width + 2, (count + 1) as i32, RGB::named(WHITE), RGB::named(BLACK));
print_options(&player_inventory, x + 1, y + 1, ctx);
print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx);
match ctx.key {
None => (ItemMenuResult::NoResponse, None),
@ -924,22 +928,23 @@ pub fn show_inventory(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio
VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None),
_ => {
let selection = letter_to_option::letter_to_option(key, ctx.shift);
if selection > -1 && selection < (count as i32) {
if selection != -1 && check_key(selection as usize) {
if on_overmap {
gamelog::Logger
::new()
.append("You can't use items on the overmap.")
.log();
} else {
return (
ItemMenuResult::Selected,
Some(
player_inventory
.iter()
.nth(selection as usize)
.unwrap().1.0
),
);
// Get the first entity with a Key {} component that has 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));
}
}
// TODO: Gamelog about not having selected item?
}
}
(ItemMenuResult::NoResponse, None)
@ -949,7 +954,7 @@ pub fn show_inventory(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio
}
pub fn drop_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<Entity>) {
let player_inventory = get_player_inventory(&gs.ecs);
let player_inventory = items(&gs.ecs, Filter::Backpack);
let count = player_inventory.len();
let (x_offset, y_offset) = (1, 10);
@ -967,7 +972,7 @@ pub fn drop_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio
let y = 3 + y_offset;
let width = get_max_inventory_width(&player_inventory);
ctx.draw_box(x, y, width + 2, (count + 1) as i32, RGB::named(WHITE), RGB::named(BLACK));
print_options(&player_inventory, x + 1, y + 1, ctx);
print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx);
match ctx.key {
None => (ItemMenuResult::NoResponse, None),
@ -975,23 +980,23 @@ pub fn drop_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio
match key {
VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None),
_ => {
let selection = letter_to_option(key);
if selection > -1 && selection < (count as i32) {
let selection = letter_to_option::letter_to_option(key, ctx.shift);
if selection != -1 && check_key(selection as usize) {
if on_overmap {
gamelog::Logger
::new()
.append("You can't drop items on the overmap.")
.log();
} else {
return (
ItemMenuResult::Selected,
Some(
player_inventory
.iter()
.nth(selection as usize)
.unwrap().1.0
),
);
// 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));
}
}
}
}
(ItemMenuResult::NoResponse, None)
@ -1001,11 +1006,8 @@ pub fn drop_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio
}
pub fn remove_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<Entity>) {
let player_entity = gs.ecs.fetch::<Entity>();
let backpack = gs.ecs.read_storage::<Equipped>();
let entities = gs.ecs.entities();
let inventory = (&backpack).join().filter(|item| item.owner == *player_entity);
let count = inventory.count();
let player_inventory = items(&gs.ecs, Filter::Equipped);
let count = player_inventory.len();
let (x_offset, y_offset) = (1, 10);
@ -1017,38 +1019,11 @@ pub fn remove_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Opt
"Unequip what? [aA-zZ][Esc.]"
);
let mut equippable: Vec<(Entity, String)> = Vec::new();
let mut width = 2;
for (entity, _pack) in (&entities, &backpack)
.join()
.filter(|item| item.1.owner == *player_entity) {
let this_name = &obfuscate_name_ecs(&gs.ecs, entity).0;
let this_width = 5 + this_name.len();
width = if width > this_width { width } else { this_width };
equippable.push((entity, this_name.to_string()));
}
let x = 1 + x_offset;
let mut y = 3 + y_offset;
ctx.draw_box(x, y, width, (count + 1) as i32, RGB::named(WHITE), RGB::named(BLACK));
y += 1;
let mut j = 0;
let renderables = gs.ecs.read_storage::<Renderable>();
for (e, name) in &equippable {
let (mut fg, glyph) = if let Some(renderable) = renderables.get(*e) {
(renderable.fg, renderable.glyph)
} else {
(RGB::named(WHITE), to_cp437('-'))
};
ctx.set(x + 1, y, RGB::named(YELLOW), RGB::named(BLACK), 97 + (j as FontCharType));
ctx.set(x + 3, y, fg, RGB::named(BLACK), glyph);
fg = RGB::named(item_colour_ecs(&gs.ecs, *e));
ctx.print_color(x + 5, y, fg, RGB::named(BLACK), name);
y += 1;
j += 1;
}
let y = 3 + y_offset;
let width = get_max_inventory_width(&player_inventory);
ctx.draw_box(x, y, width + 2, (count + 1) as i32, RGB::named(WHITE), RGB::named(BLACK));
print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx);
match ctx.key {
None => (ItemMenuResult::NoResponse, None),
@ -1056,9 +1031,17 @@ pub fn remove_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Opt
match key {
VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None),
_ => {
let selection = letter_to_option(key);
if selection > -1 && selection < (count as i32) {
return (ItemMenuResult::Selected, Some(equippable[selection as usize].0));
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 equipped = gs.ecs.read_storage::<Equipped>();
for (e, key, _e) in (&entities, &keyed_items, &equipped).join() {
if key.idx == (selection as usize) {
return (ItemMenuResult::Selected, Some(e));
}
}
}
(ItemMenuResult::NoResponse, None)
}
@ -1458,3 +1441,72 @@ pub fn with_article(name: String) -> String {
}
format!("a {}", name)
}
pub fn unique(
entity: Entity,
names: &ReadStorage<Name>,
obfuscated_names: &ReadStorage<ObfuscatedName>,
renderables: &ReadStorage<Renderable>,
beatitudes: &ReadStorage<Beatitude>,
magic_items: &ReadStorage<MagicItem>,
charges: Option<&ReadStorage<Charges>>,
dm: &MasterDungeonMap
) -> UniqueInventoryItem {
let item_colour = item_colour(entity, beatitudes);
let (singular, plural) = obfuscate_name(
entity,
names,
magic_items,
obfuscated_names,
beatitudes,
dm,
charges
);
let (renderables, glyph) = if let Some(renderable) = renderables.get(entity) {
(
(
(renderable.fg.r * 255.0) as u8,
(renderable.fg.g * 255.0) as u8,
(renderable.fg.b * 255.0) as u8,
),
renderable.glyph,
)
} else {
unreachable!("Item has no renderable component.")
};
let name = if let Some(name) = names.get(entity) {
name
} else {
unreachable!("Item has no name component.")
};
let beatitude_status = if let Some(beatitude) = beatitudes.get(entity) {
match beatitude.buc {
BUC::Blessed => 1,
BUC::Uncursed => 2,
BUC::Cursed => 3,
}
} else {
0
};
UniqueInventoryItem {
display_name: DisplayName { singular: singular.clone(), plural },
rgb: item_colour,
renderables,
glyph,
beatitude_status,
name: name.name.clone(),
}
}
pub fn unique_ecs(ecs: &World, entity: Entity) -> UniqueInventoryItem {
return unique(
entity,
&ecs.read_storage::<Name>(),
&ecs.read_storage::<ObfuscatedName>(),
&ecs.read_storage::<Renderable>(),
&ecs.read_storage::<Beatitude>(),
&ecs.read_storage::<MagicItem>(),
Some(&ecs.read_storage::<Charges>()),
&ecs.fetch::<MasterDungeonMap>()
);
}

View file

@ -3,9 +3,11 @@ use super::{
item_colour_ecs,
obfuscate_name_ecs,
print_options,
renderable_colour,
unique_ecs,
check_key,
letter_to_option,
ItemMenuResult,
UniqueInventoryItem,
InventorySlot,
};
use crate::{
gamelog,
@ -18,10 +20,11 @@ use crate::{
Renderable,
states::state::*,
BUC,
Key,
};
use bracket_lib::prelude::*;
use specs::prelude::*;
use std::collections::BTreeMap;
use std::collections::HashMap;
/// Handles the Remove Curse menu.
pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<Entity>) {
@ -33,11 +36,12 @@ pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<
let beatitudes = gs.ecs.read_storage::<Beatitude>();
let names = gs.ecs.read_storage::<Name>();
let renderables = gs.ecs.read_storage::<Renderable>();
let keys = gs.ecs.read_storage::<Key>();
let build_cursed_iterator = || {
(&entities, &items, &beatitudes, &renderables, &names)
(&entities, &items, &beatitudes, &renderables, &names, &keys)
.join()
.filter(|(item_entity, _i, b, _r, _n)| {
.filter(|(item_entity, _i, b, _r, _n, _k)| {
// Set all items to FALSE initially.
let mut keep = false;
// If found in the player's backpack, set to TRUE
@ -86,34 +90,19 @@ pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<
.log();
return (ItemMenuResult::Selected, Some(item));
}
let mut player_inventory: super::PlayerInventory = BTreeMap::new();
for (entity, _i, _b, renderable, name) 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 = UniqueInventoryItem {
display_name: super::DisplayName { singular: singular.clone(), plural: plural.clone() },
rgb: item_colour_ecs(&gs.ecs, entity),
renderables: renderable_colour(&renderables, entity),
glyph: renderable.glyph,
beatitude_status: beatitude_status,
name: name.name.clone(),
};
let mut player_inventory: super::PlayerInventory = HashMap::new();
for (entity, _i, _b, _r, _n, key) in build_cursed_iterator() {
let unique_item = unique_ecs(&gs.ecs, entity);
player_inventory
.entry(unique_item)
.and_modify(|(_e, count)| {
*count += 1;
.and_modify(|slot| {
slot.count += 1;
})
.or_insert((entity, 1));
.or_insert(InventorySlot {
item: entity,
count: 1,
idx: key.idx,
});
}
// Get display args
let width = get_max_inventory_width(&player_inventory);
@ -128,7 +117,7 @@ pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<
"Decurse which item? [aA-zZ][Esc.]"
);
ctx.draw_box(x, y, width + 2, count + 1, RGB::named(WHITE), RGB::named(BLACK));
print_options(&player_inventory, x + 1, y + 1, ctx);
print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx);
// Input
match ctx.key {
None => (ItemMenuResult::NoResponse, None),
@ -136,21 +125,17 @@ pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option<
match key {
VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None),
_ => {
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));
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));
}
}
}
(ItemMenuResult::NoResponse, None)
}

View file

@ -111,6 +111,12 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut BTerm, xy: Option<(i32, i32)>) {
if position.x == mouse_pos_adjusted.0 && position.y == mouse_pos_adjusted.1 {
let mut tip = Tooltip::new();
tip.add(crate::gui::obfuscate_name_ecs(ecs, entity).0, renderable.fg);
let intrinsics = ecs.read_storage::<crate::components::Intrinsics>();
if let Some(intrinsics) = intrinsics.get(entity) {
if !intrinsics.list.is_empty() {
tip.add(intrinsics.describe(), RGB::named(WHITE));
}
}
// Attributes
let attr = attributes.get(entity);
if let Some(a) = attr {

View file

@ -12,6 +12,7 @@ use crate::{
ObfuscatedName,
Position,
WantsToPickupItem,
WantsToAssignKey,
};
use specs::prelude::*;
use crate::data::messages;
@ -33,6 +34,7 @@ impl<'a> System<'a> for ItemCollectionSystem {
ReadStorage<'a, Beatitude>,
ReadExpect<'a, MasterDungeonMap>,
ReadStorage<'a, Charges>,
ReadStorage<'a, WantsToAssignKey>,
);
fn run(&mut self, data: Self::SystemData) {
@ -48,17 +50,11 @@ impl<'a> System<'a> for ItemCollectionSystem {
beatitudes,
dm,
wands,
wants_key,
) = data;
for pickup in wants_pickup.join() {
positions.remove(pickup.item);
backpack
.insert(pickup.item, InBackpack { owner: pickup.collected_by })
.expect("Unable to pickup item.");
equipment_changed
.insert(pickup.collected_by, EquipmentChanged {})
.expect("Unable to insert EquipmentChanged.");
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 (pickup, _key) in (&wants_pickup, !&wants_key).join() {
if pickup.collected_by == *player_entity {
gamelog::Logger
::new()
@ -82,8 +78,17 @@ impl<'a> System<'a> for ItemCollectionSystem {
.period()
.log();
}
positions.remove(pickup.item);
backpack
.insert(pickup.item, InBackpack { owner: pickup.collected_by })
.expect("Unable to pickup item");
equipment_changed
.insert(pickup.collected_by, EquipmentChanged {})
.expect("Unable to insert EquipmentChanged");
to_remove.push(pickup.collected_by);
}
for item in to_remove.iter() {
wants_pickup.remove(*item);
}
wants_pickup.clear();
}
}

View file

@ -12,6 +12,7 @@ use crate::{
ObfuscatedName,
Position,
WantsToDropItem,
WantsToRemoveKey,
};
use specs::prelude::*;
use crate::data::messages;
@ -34,6 +35,7 @@ impl<'a> System<'a> for ItemDropSystem {
ReadStorage<'a, ObfuscatedName>,
ReadExpect<'a, MasterDungeonMap>,
ReadStorage<'a, Charges>,
WriteStorage<'a, WantsToRemoveKey>,
);
fn run(&mut self, data: Self::SystemData) {
@ -50,6 +52,7 @@ impl<'a> System<'a> for ItemDropSystem {
obfuscated_names,
dm,
wands,
mut keys,
) = data;
for (entity, to_drop) in (&entities, &wants_drop).join() {
@ -68,6 +71,9 @@ impl<'a> System<'a> for ItemDropSystem {
backpack.remove(to_drop.item);
if entity == *player_entity {
keys.insert(to_drop.item, WantsToRemoveKey {}).expect(
"Unable to insert WantsToRemoveKey"
);
gamelog::Logger
::new()
.append(messages::YOU_DROP_ITEM)

View file

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

View file

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

59
src/invkeys.rs Normal file
View file

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

View file

@ -8,6 +8,9 @@ extern crate serde;
#[macro_use]
extern crate lazy_static;
#[macro_use]
pub mod macros;
pub mod camera;
pub mod components;
pub mod raws;
@ -35,6 +38,7 @@ pub mod rex_assets;
pub mod spatial;
pub mod morgue;
pub mod states;
pub mod invkeys;
pub use components::*;
use particle_system::ParticleBuilder;

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

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

View file

@ -111,6 +111,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::<SimpleMarker<SerializeMe>>();
gs.ecs.register::<SerializationHelper>();
gs.ecs.register::<DMSerializationHelper>();

View file

@ -438,21 +438,19 @@ pub fn random_builder(
}
pub fn level_builder(
new_id: i32,
id: i32,
rng: &mut RandomNumberGenerator,
width: i32,
height: i32,
initial_player_level: i32
) -> BuilderChain {
// TODO: With difficulty and ID/depth decoupled, this can be used for branches later.
let difficulty = new_id;
match new_id {
match id {
ID_OVERMAP => overmap_builder(),
ID_TOWN => town_builder(new_id, rng, width, height, 0, initial_player_level),
ID_TOWN2 => forest_builder(new_id, rng, width, height, 1, initial_player_level),
ID_TOWN => town_builder(id, rng, width, height, 0, initial_player_level),
ID_TOWN2 => forest_builder(id, rng, width, height, 1, initial_player_level),
ID_TOWN3 =>
random_builder(
new_id,
id,
rng,
width,
height,
@ -462,25 +460,25 @@ pub fn level_builder(
true,
BuildType::Room
),
_ if new_id >= ID_INFINITE =>
_ if id >= ID_INFINITE =>
random_builder(
new_id,
id,
rng,
width,
height,
difficulty,
new_id - ID_INFINITE + 1,
4 + diff(ID_INFINITE, id),
1 + diff(ID_INFINITE, id),
initial_player_level,
false,
BuildType::Room
),
_ =>
_ => // This should be unreachable!() eventually. Right now it's reachable with the debug/cheat menu. It should not be in normal gameplay.
random_builder(
new_id,
id,
rng,
width,
height,
difficulty,
1,
404,
initial_player_level,
false,
@ -488,3 +486,7 @@ pub fn level_builder(
),
}
}
fn diff(branch_id: i32, lvl_id: i32) -> i32 {
return lvl_id - branch_id;
}

View file

@ -30,6 +30,7 @@ use super::{
Viewshed,
WantsToMelee,
WantsToPickupItem,
WantsToAssignKey,
get_dest,
Destination,
DamageType,
@ -633,7 +634,9 @@ fn get_item(ecs: &mut World) -> RunState {
return RunState::AwaitingInput;
}
Some(item) => {
let mut assignkey = ecs.write_storage::<WantsToAssignKey>();
let mut pickup = ecs.write_storage::<WantsToPickupItem>();
assignkey.insert(item, WantsToAssignKey {}).expect("Unable to insert WantsToAssignKey");
pickup
.insert(*player_entity, WantsToPickupItem { collected_by: *player_entity, item })
.expect("Unable to insert want to pickup item.");

View file

@ -6,6 +6,7 @@ pub struct Item {
pub id: String,
pub name: Name,
pub renderable: Option<Renderable>,
pub class: String,
pub weight: Option<f32>,
pub value: Option<f32>,
pub equip: Option<Equippable>,

View file

@ -66,6 +66,7 @@ macro_rules! apply_flags {
"IDENTIFY" => $eb = $eb.with(ProvidesIdentify {}),
"DIGGER" => $eb = $eb.with(Digger {}),
"MAGICMAP" => $eb = $eb.with(MagicMapper {}),
"STACKABLE" => $eb = $eb.with(Stackable {}),
// CAN BE DESTROYED BY DAMAGE
"DESTRUCTIBLE" => $eb = $eb.with(Destructible {}),
// --- EQUIP SLOTS ---
@ -281,6 +282,7 @@ pub fn spawn_named_item(
if known_beatitude && !identified_items.contains(&item_template.name.name) {
dm.identified_items.insert(item_template.name.name.clone());
}
let needs_key = is_player_owned(&player_entity, &pos);
std::mem::drop(player_entity);
std::mem::drop(dm);
// -- DROP EVERYTHING THAT INVOLVES THE ECS BEFORE THIS POINT ---
@ -293,9 +295,23 @@ pub fn spawn_named_item(
eb = eb.with(Item {
weight: item_template.weight.unwrap_or(0.0),
value: item_template.value.unwrap_or(0.0),
category: match item_template.class.as_str() {
"amulet" => ItemType::Amulet,
"weapon" => ItemType::Weapon,
"armour" => ItemType::Armour,
"comestible" => ItemType::Comestible,
"scroll" => ItemType::Scroll,
"spellbook" => ItemType::Spellbook,
"potion" => ItemType::Potion,
"ring" => ItemType::Ring,
"wand" => ItemType::Wand,
_ => unreachable!("Unknown item type."),
},
});
eb = spawn_position(pos, eb, key, raws);
if needs_key {
eb = eb.with(WantsToAssignKey {});
}
if let Some(renderable) = &item_template.renderable {
eb = eb.with(get_renderable_component(renderable));
}
@ -392,6 +408,7 @@ pub fn spawn_named_mob(
if raws.mob_index.contains_key(key) {
let mob_template = &raws.raws.mobs[raws.mob_index[key]];
let mut player_level = 1;
let needs_key;
{
let pools = ecs.read_storage::<Pools>();
let player_entity = ecs.fetch::<Entity>();
@ -399,12 +416,15 @@ pub fn spawn_named_mob(
if let Some(pool) = player_pool {
player_level = pool.level;
}
needs_key = is_player_owned(&player_entity, &pos);
}
let mut eb;
// New entity with a position, name, combatstats, and viewshed
eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
eb = spawn_position(pos, eb, key, raws);
if needs_key {
eb = eb.with(WantsToAssignKey {});
}
eb = eb.with(Name { name: mob_template.name.clone(), plural: mob_template.name.clone() });
eb = eb.with(Viewshed {
visible_tiles: Vec::new(),
@ -632,10 +652,18 @@ pub fn spawn_named_prop(
pos: SpawnType
) -> Option<Entity> {
if raws.prop_index.contains_key(key) {
let needs_key;
{
let player_entity = ecs.fetch::<Entity>();
needs_key = is_player_owned(&player_entity, &pos);
}
// ENTITY BUILDER PREP
let prop_template = &raws.raws.props[raws.prop_index[key]];
let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
eb = spawn_position(pos, eb, key, raws);
if needs_key {
eb = eb.with(WantsToAssignKey {});
}
// APPLY MANDATORY COMPONENTS FOR A PROP:
// - Name
// - Prop {}
@ -686,6 +714,23 @@ fn spawn_position<'a>(
eb
}
fn is_player_owned(player: &Entity, pos: &SpawnType) -> bool {
match pos {
SpawnType::Carried { by } => {
if by == player {
return true;
}
}
SpawnType::Equipped { by } => {
if by == player {
return true;
}
}
_ => {}
}
false
}
fn get_renderable_component(
renderable: &super::item_structs::Renderable
) -> crate::components::Renderable {

View file

@ -9,6 +9,7 @@ use specs::saveload::{
SimpleMarker,
SimpleMarkerAllocator,
};
use std::fs;
use std::fs::File;
use std::path::Path;
@ -95,8 +96,10 @@ pub fn save_game(ecs: &mut World) {
IdentifiedItem,
InBackpack,
InflictsDamage,
IntrinsicChanged,
Intrinsics,
Item,
Key,
KnownSpells,
LootTable,
MagicItem,
@ -126,17 +129,21 @@ pub fn save_game(ecs: &mut World) {
SpawnParticleBurst,
SpawnParticleLine,
SpawnParticleSimple,
Stackable,
TakingTurn,
Telepath,
ToHitBonus,
Viewshed,
Charges,
WantsToApproach,
WantsToAssignKey,
WantsToDelete,
WantsToDropItem,
WantsToFlee,
WantsToMelee,
WantsToPickupItem,
WantsToRemoveItem,
WantsToRemoveKey,
WantsToUseItem,
SerializationHelper,
DMSerializationHelper
@ -227,8 +234,10 @@ pub fn load_game(ecs: &mut World) {
IdentifiedItem,
InBackpack,
InflictsDamage,
IntrinsicChanged,
Intrinsics,
Item,
Key,
KnownSpells,
LootTable,
MagicItem,
@ -258,17 +267,21 @@ pub fn load_game(ecs: &mut World) {
SpawnParticleBurst,
SpawnParticleLine,
SpawnParticleSimple,
Stackable,
TakingTurn,
Telepath,
ToHitBonus,
Viewshed,
Charges,
WantsToApproach,
WantsToAssignKey,
WantsToDelete,
WantsToDropItem,
WantsToFlee,
WantsToMelee,
WantsToPickupItem,
WantsToRemoveItem,
WantsToRemoveKey,
WantsToUseItem,
SerializationHelper,
DMSerializationHelper

View file

@ -64,12 +64,13 @@ impl State {
fn resolve_entity_decisions(&mut self) {
let mut trigger_system = trigger_system::TriggerSystem {};
let mut inventory_system = inventory::ItemCollectionSystem {};
let mut item_equip_system = inventory::ItemEquipSystem {};
let mut item_use_system = inventory::ItemUseSystem {};
let mut item_drop_system = inventory::ItemDropSystem {};
let mut item_remove_system = inventory::ItemRemoveSystem {};
let mut inventory_system = inventory::ItemCollectionSystem {};
let mut item_id_system = inventory::ItemIdentificationSystem {};
let mut key_system = inventory::KeyHandling {};
let mut melee_system = MeleeCombatSystem {};
trigger_system.run_now(&self.ecs);
inventory_system.run_now(&self.ecs);
@ -78,6 +79,7 @@ impl State {
item_drop_system.run_now(&self.ecs);
item_remove_system.run_now(&self.ecs);
item_id_system.run_now(&self.ecs);
key_system.run_now(&self.ecs);
melee_system.run_now(&self.ecs);
effects::run_effects_queue(&mut self.ecs);
@ -342,7 +344,11 @@ impl GameState for State {
gui::ItemMenuResult::NoResponse => {}
gui::ItemMenuResult::Selected => {
let item_entity = result.1.unwrap();
let mut removekey = self.ecs.write_storage::<WantsToRemoveKey>();
let mut intent = self.ecs.write_storage::<WantsToDropItem>();
removekey
.insert(item_entity, WantsToRemoveKey {})
.expect("Unable to insert WantsToRemoveKey");
intent
.insert(*self.ecs.fetch::<Entity>(), WantsToDropItem {
item: item_entity,