opal

OPAL's Playable Almost Indefectibly. OPAL is a rogue-like dungeon crawler.

Log | Files | << Repositories


tree 6c22cc6abdc39bc72b1c4223200d241dbcc6a2a2
parent aca0d17d960a99be1060e21a79290bbe7c9065a5
author esote <esote.net@gmail.com> 1556324582 -0500
committer esote <esote.net@gmail.com> 1556324582 -0500
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iHUEABYIAB0WIQTXAxYDuIzimYoNSPuhTmRAjzzC8gUCXSAqbAAKCRChTmRAjzzC
 8usiAQCuk01Xwqfy21MprBkB56fEzjAbjh9t6Wz4ApDKq0yD7gEAx3XFutZSq+3D
 zH70PNIy8ZNzXFf4469K4z13GBWyJAg=
 =qm7J
 -----END PGP SIGNATURE-----

NPC desc file, use speed and defense

Teleport and defog are only available in debug mode.
Add sanity checks to dice and line length.
Fix player turn persisting floors.
Ensure player always goes first each floor.
Only allow one color, error otherwise.
Show initial border highlight if boss on floor.
Fix rarity probabilities.
Display player speed in border.

 Makefile  |   2 +-
 npc_desc  | 304 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 obj_desc  | 106 +++++++++++-----------
 opal.cpp  |   8 +-
 parse.cpp |   2 -
 parse.y   |  25 +++---
 turn.cpp  | 121 +++++++++++++++++++++----
 7 files changed, 481 insertions(+), 87 deletions(-)

diff --git a/Makefile b/Makefile
index e63c470..28408f9 100644
--- a/Makefile
+++ b/Makefile
@@ -33,7 +33,7 @@ opal: $(src) $(hdr)
 debug: $(src) $(hdr)
 	lex -v -d parse.l
 	yacc -v -d parse.y
-	$(CXX) $(CFLAGS) -o opal.out $(src) $(src_nodep) $(CFLAGS_END)
+	$(CXX) $(CFLAGS) -DDEBUG -o opal.out $(src) $(src_nodep) $(CFLAGS_END)
 
 bench: $(src) $(hdr)
 	lex -d parse.l
diff --git a/npc_desc b/npc_desc
new file mode 100644
index 0000000..c947ec7
--- /dev/null
+++ b/npc_desc
@@ -0,0 +1,304 @@
+OPAL NPC DESCRIPTION 1
+
+BEGIN NPC
+ABIL BOSS SMART TELE TUNNEL UNIQ
+COLOR GREEN
+DAM 102+35d6
+DESC
+Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn
+.
+HP 3000+5d30
+NAME Cthulhu
+RRTY 98
+SPEED 20+5d7
+SYMB C
+END
+
+BEGIN NPC
+ABIL BOSS SMART TELE TUNNEL UNIQ
+COLOR BLACK
+DAM 302+5d30
+DESC
+King of Dragons and harbinger of the apocalypse.
+.
+HP 6000+5d100
+NAME Alduin
+RRTY 99
+SPEED 60+5d15
+SYMB A
+END
+
+BEGIN NPC
+ABIL SMART TELE
+COLOR YELLOW
+DAM 10+0d1
+DESC
+If it looks, moves, and attacks like a player, it must be a player.
+.
+HP 50+2d5
+NAME Doppelganger
+RRTY 90
+SPEED 10+0d1
+SYMB @
+END
+
+BEGIN NPC
+ABIL TUNNEL
+COLOR GREEN
+DAM 3+3d2
+DESC
+Brutish and aggressive, yet still humanoid in appearance. Some wear the
+white badge of Isengard.
+.
+HP 6+7d3
+NAME Orc
+RRTY 20
+SPEED 3+5d2
+SYMB o
+END
+
+BEGIN NPC
+ABIL SMART TUNNEL
+COLOR GREEN
+DAM 7+3d3
+DESC
+Ancient shepherds of the forest, Ents roam the world in search of the
+Entwives. They suspect, with good reason, that you may be involved in the
+disappearance of the Entwives.
+.
+HP 16+7d4
+NAME Enraged Ent
+RRTY 60
+SPEED 1+3d3
+SYMB E
+END
+
+BEGIN NPC
+ABIL SMART TELE TUNNEL
+COLOR MAGENTA
+DAM 16+5d7
+DESC
+Wisened long ago to the power of magic and corrupted by the desire of
+supremacy over the mortal domain.
+.
+HP 5+3d5
+NAME Wizard
+RRTY 75
+SPEED 2+3d4
+SYMB W
+END
+
+BEGIN NPC
+ABIL ERRATIC TELE
+COLOR BLACK
+DAM 3+1d10
+DESC
+Bats have incredibly good hearing, and are able to locate your position
+through the sounds of your footsteps alone. When enraged they can use their
+sharp teeth to wound you and potentially cause serious blood loss.
+.
+HP 3+1d3
+NAME Bat
+RRTY 10
+SPEED 10+3d2
+SYMB b
+END
+
+BEGIN NPC
+ABIL SMART TELE
+COLOR WHITE
+DAM 15+3d6
+DESC
+Discarded by God and thrown from the kingdom of heaven, these pale, grotesque
+creatures barely resemble their holy brothers.
+.
+HP 15+6d3
+NAME Fallen Angel
+RRTY 70
+SPEED 9+3d2
+SYMB a
+END
+
+BEGIN NPC
+ABIL SMART
+COLOR BLUE
+DAM 3+4d2
+DESC
+When directly observed it appears as a pile of mismatched armor, often strewn
+across the floor. However, upon nearing the armor, or when your back is
+turned, it will slowly reassemble itself into the form of a warrior. While it
+is weaponless, the floating armor may completely surround and suffocate you.
+.
+HP 4+4d2
+NAME Animated Armor
+RRTY 45
+SPEED 3+1d4
+SYMB f
+END
+
+BEGIN NPC
+ABIL SMART UNIQ
+COLOR CYAN
+DAM 100+3d50
+DESC
+The glass golem is said to have been forged in the hearth of the Gods. It is
+incredibly fast, and can defeat most enemies with a single swing.
+.
+HP 10+0d1
+NAME Glass Golem
+RRTY 90
+SPEED 15+3d5
+SYMB G
+END
+
+BEGIN NPC
+ABIL ERRATIC
+COLOR YELLOW
+DAM 4+3d2
+DESC
+In the age of science, constructing mechanical automata was a popular
+activity for the aristocratic. Sadly, they were never built to last. With a
+limited lifespan they betrayed their creators in one last attempt to seize
+independence.
+.
+HP 5+1d5
+NAME Wandering Automata
+RRTY 20
+SPEED 3+1d4
+SYMB a
+END
+
+BEGIN NPC
+ABIL SMART
+COLOR YELLOW
+DAM 1+0d1
+DESC
+Floating specks of light usually indicating a higher level of nuclear
+radiation in the area.
+.
+HP 30+3d5
+NAME Wisp
+RRTY 30
+SPEED 15+3d9
+SYMB i
+END
+
+BEGIN NPC
+ABIL TUNNEL
+COLOR GREEN
+DAM 13+5d3
+DESC
+It leaves a trail of viscous flaming mucus wherever it glides.
+.
+HP 20+3d5
+NAME Fire Snail
+RRTY 20
+SPEED 2+0d1
+SYMB n
+END
+
+BEGIN NPC
+ABIL ERRATIC SMART
+COLOR BLUE
+DAM 4+2d3
+DESC
+The knowledge described on its pages will forever be kept secret.
+.
+HP 4+3d2
+NAME Flying Book
+RRTY 30
+SPEED 10+2d3
+SYMB b
+END
+
+BEGIN NPC
+ABIL ERRATIC TUNNEL UNIQ
+COLOR RED
+DAM 15+3d5
+DESC
+Wandering giants with a taste for wriggling flesh. Usually found deep in
+caves or roaming in large grasslands.
+.
+HP 20+2d3
+NAME Cyclops
+RRTY 40
+SPEED 3+3d2
+SYMB c
+END
+
+BEGIN NPC
+ABIL SMART TELE
+COLOR CYAN
+DAM 90+3d5
+DESC
+Masters of magic and manipulation of the ethereal plane.
+.
+HP 15+3d2
+NAME Arch Mage
+RRTY 80
+SPEED 4+5d3
+SYMB m
+END
+
+BEGIN NPC
+ABIL ERRATIC TUNNEL
+COLOR YELLOW
+DAM 9+2d5
+DESC
+It observed the formation of this world, and by absorbing the power of others
+it continues to grow stronger.
+.
+HP 30+20d3
+NAME Elder Gelatin
+RRTY 75
+SPEED 5+0d1
+SYMB j
+END
+
+BEGIN NPC
+ABIL SMART
+COLOR BLACK
+DAM 1+10d2
+DESC
+Members of the secretive Order of the Left Shoe. They seek to vanquish all
+those from this world who break eggs from the smaller end.
+.
+HP 2+3d3
+NAME Assassin
+RRTY 60
+SPEED 20+30d5
+SYMB z
+END
+
+BEGIN NPC
+ABIL SMART
+COLOR RED
+DAM 20+20d5
+DESC
+Sworn to project the royalty at all costs, including the end of their own
+lives.
+.
+HP 10+3d2
+NAME Death Knight
+RRTY 60
+SPEED 2+0d1
+SYMB d
+END
+
+BEGIN NPC
+ABIL ERRATIC SMART
+COLOR WHITE
+DAM 9999+0d1
+DESC
+A creature so foul, so cruel that no man has yet fought with it and lived!
+Bones of full fifty men lie strewn about its lair. So, brave adventurer, if
+you doubt your courage or your strength, come no further, for death awaits
+you with all nasty, big, pointy teeth.
+.
+HP 10+0d1
+NAME Rabbit of Caerbannog
+RRTY 99
+SPEED 20+3d5
+SYMB B
+END
+
diff --git a/obj_desc b/obj_desc
index 0238dde..dc4832b 100644
--- a/obj_desc
+++ b/obj_desc
@@ -29,16 +29,16 @@ COLOR BLUE
 DAM 0+0d1
 DEF 1+0d1
 DESC
-An enchanted amulet signifying one of the Nine Devines, Kynareth. Occasionally
-found near shrines. Increases stamina.
+An enchanted amulet signifying one of the Nine Devines, Kynareth.
+Occasionally found near shrines. Increases stamina.
 .
-DODGE 10+3d1
+DODGE 10+3d2
 HIT 0+0d1
 NAME Amulet of Kynareth
 RRTY 20
-SPEED 10+3d1
+SPEED 10+3d2
 TYPE AMULET
-VAL 20+2d1
+VAL 20+2d2
 WEIGHT 1+2d3
 END
 
@@ -47,7 +47,7 @@ ART FALSE
 ATTR 0+0d1
 COLOR WHITE
 DAM 2+3d5
-DEF 1+2d1
+DEF 1+2d2
 DESC
 The tooth is a little dull. Found during Blood Moons.
 .
@@ -55,10 +55,10 @@ DODGE 2+1d2
 HIT 0+0d1
 NAME Shark Tooth Necklace
 RRTY 25
-SPEED 1+2d1
+SPEED 1+2d2
 TYPE AMULET
 VAL 1+3d6
-WEIGHT 1+2d1
+WEIGHT 1+2d2
 END
 
 BEGIN OBJ
@@ -79,10 +79,10 @@ DODGE 6+1d7
 HIT 0+0d1
 NAME Shield of Evalach
 RRTY 95
-SPEED 1+2d1
+SPEED 1+2d2
 TYPE ARMOR
 VAL 999+0d1
-WEIGHT 1+3d1
+WEIGHT 1+3d2
 END
 
 BEGIN OBJ
@@ -90,7 +90,7 @@ ART FALSE
 ATTR 0+0d1
 COLOR WHITE
 DAM 0+0d1
-DEF 16+3d1
+DEF 16+3d2
 DESC
 Armor consisting of small metal rings linked together to form a mesh. While
 useful at blocking slashing blows from edged weapons, it is less useful
@@ -102,7 +102,7 @@ NAME Chain mail
 RRTY 30
 SPEED 1+1d3
 TYPE ARMOR
-VAL 15+3d1
+VAL 15+3d2
 WEIGHT 15+3d5
 END
 
@@ -113,8 +113,8 @@ COLOR YELLOW
 DAM 0+0d1
 DEF 15+3d5
 DESC
-Forged of the finest gold and enchanted with a weak spell of protection. Lined
-on the inside with shimmering samite.
+Forged of the finest gold and enchanted with a weak spell of protection.
+Lined on the inside with shimmering samite.
 .
 DODGE 5+0d1
 HIT 0+0d1
@@ -122,8 +122,8 @@ NAME Golden Armor
 RRTY 45
 SPEED 5+6d2
 TYPE ARMOR
-VAL 99+3d1
-WEIGHT 6+3d1
+VAL 99+3d2
+WEIGHT 6+3d2
 END
 
 BEGIN OBJ
@@ -133,11 +133,11 @@ COLOR YELLOW
 DAM 3+3d2
 DEF 15+0d1
 DESC
-The winged sandals of the messenger god Hermes. Made by the god Hepheastus of
+The winged sandals of the messenger god Hermes. Made by the god Hephaestus of
 imperishable gold and make their wearer as swift as a bird. The shoe strap
 was a bit cheap though.
 .
-DODGE 96+3d1
+DODGE 96+3d2
 HIT 0+0d1
 NAME Talaria
 RRTY 95
@@ -172,7 +172,7 @@ BEGIN OBJ
 ART FALSE
 ATTR 0+0d1
 COLOR RED
-DAM 2+3d1
+DAM 2+3d2
 DEF 5+5d2
 DESC
 Very useful when hiking or climbing mountains, yet you always seem to trip
@@ -213,7 +213,7 @@ ART FALSE
 ATTR 0+0d1
 COLOR BLUE
 DAM 0+0d1
-DEF 5+0d3
+DEF 5+0d1
 DESC
 A cloak that's useful on cold or windy occasions. Some say it's fashionable,
 but only when you use it to bow or hold it up a little bit and gallivant
@@ -234,12 +234,13 @@ ART FALSE
 ATTR 0+0d1
 COLOR GREEN
 DAM 0+0d1
-DEF 5+3d1
+DEF 5+3d2
 DESC
 Woven by Galadriel and her maidens. Each cloak fits to its wearer and is
-fashioned with a green broach. The color shifts to match natural surroundings.
+fashioned with a green broach. The color shifts to match natural
+surroundings.
 .
-DODGE 35+3d1
+DODGE 35+3d2
 HIT 0+0d1
 NAME Elven Cloak
 RRTY 45
@@ -253,7 +254,7 @@ BEGIN OBJ
 ART TRUE
 ATTR 0+0d1
 COLOR CYAN
-DAM 1+3d1
+DAM 1+3d2
 DEF 3+3d5
 DESC
 Iron gloves of the god Thor. Used to wield the hammer Mjolnir.
@@ -265,7 +266,7 @@ RRTY 95
 SPEED 0+0d1
 TYPE GLOVES
 VAL 999+0d1
-WEIGHT 41+3d1
+WEIGHT 41+3d2
 END
 
 BEGIN OBJ
@@ -273,17 +274,17 @@ ART FALSE
 ATTR 0+0d1
 COLOR BLUE
 DAM 0+0d1
-DEF 5+3d1
+DEF 5+3d2
 DESC
 An early virtual reality glove. While imprecise, it sure does look cool.
 .
-DODGE 5+6d1
+DODGE 5+6d2
 HIT 0+0d1
 NAME Power Glove
 RRTY 45
 SPEED 0+0d1
 TYPE GLOVES
-VAL 94+3d1
+VAL 94+3d2
 WEIGHT 0+0d1
 END
 
@@ -316,11 +317,11 @@ DEF 60+7d8
 DESC
 The helmet of King Arthur, passed down to him from Uther Pendragon.
 .
-DODGE 6+3d1
+DODGE 6+3d2
 HIT 0+0d1
 NAME Goswhit
 RRTY 95
-SPEED 6+3d1
+SPEED 6+3d2
 TYPE HELMET
 VAL 999+0d1
 WEIGHT 9+3d4
@@ -331,7 +332,7 @@ ART FALSE
 ATTR 0+0d1
 COLOR GREEN
 DAM 0+0d1
-DEF 6+7d1
+DEF 6+7d2
 DESC
 The helmet worn by Master Chief. Contains various heads-up display
 information, such as health and the count of sticky grenades.
@@ -340,7 +341,7 @@ DODGE 6+0d1
 HIT 0+0d1
 NAME Master Chief Helmet
 RRTY 56
-SPEED 9+3d1
+SPEED 9+3d2
 TYPE HELMET
 VAL 56+3d7
 WEIGHT 6+7d3
@@ -415,7 +416,7 @@ DEF 0+0d1
 DESC
 Large wooden stick with very flammable cloth at one end.
 .
-DODGE 1+0d3
+DODGE 1+0d1
 HIT 0+0d1
 NAME Torch
 RRTY 25
@@ -438,10 +439,10 @@ DODGE 0+0d1
 HIT 5+31d3
 NAME Ascalon
 RRTY 95
-SPEED 5+6d1
+SPEED 5+6d2
 TYPE OFFHAND
 VAL 999+0d1
-WEIGHT 5+3d1
+WEIGHT 5+3d2
 END
 
 BEGIN OBJ
@@ -453,7 +454,7 @@ DEF 0+0d1
 DESC
 Small, sharp knife. Can be easily concealed.
 .
-DODGE 5+6d1
+DODGE 5+6d2
 HIT 0+0d1
 NAME Dagger
 RRTY 30
@@ -467,7 +468,7 @@ BEGIN OBJ
 ART FALSE
 ATTR 0+0d1
 COLOR RED
-DAM 5+6d1
+DAM 5+6d2
 DEF 0+0d1
 DESC
 Long wooden pole with a sharp iron spike fastened to the end of the shaft.
@@ -487,7 +488,7 @@ ART TRUE
 ATTR 0+0d1
 COLOR RED
 DAM 35+6d7
-DEF 4+6d1
+DEF 4+6d2
 DESC
 Apollo's bow was crafted from sun rays.
 .
@@ -508,8 +509,8 @@ COLOR MAGENTA
 DAM 3+5d3
 DEF 0+0d1
 DESC
-As tall as the height of the player, this bow allows you to draw back an arrow
-far enough to reach an innocent bystander.
+As tall as the height of the player, this bow allows you to draw back an
+arrow far enough to reach an innocent bystander.
 .
 DODGE 3+0d1
 HIT 5+6d3
@@ -517,7 +518,7 @@ NAME Longbow
 RRTY 41
 SPEED 0+0d1
 TYPE RANGED
-VAL 6+3d1
+VAL 6+3d2
 WEIGHT 4+0d1
 END
 
@@ -530,8 +531,8 @@ DEF 3+0d1
 DESC
 An elastic ranged weapon allowing you to shoot arrows at a significant rate.
 .
-DODGE 6+3d1
-HIT 6+6d1
+DODGE 6+3d2
+HIT 6+6d2
 NAME Crossbow
 RRTY 50
 SPEED 5+6d3
@@ -586,7 +587,7 @@ BEGIN OBJ
 ART FALSE
 ATTR 0+0d1
 COLOR BLACK
-DAM 5+3d1
+DAM 5+3d2
 DEF 0+0d1
 DESC
 While it looks authentic, we things it's just made of old chocolate.
@@ -633,7 +634,7 @@ DODGE 3+9d2
 HIT 0+0d1
 NAME Ring of Defense
 RRTY 40
-SPEED 0+3d1
+SPEED 0+3d2
 TYPE RING
 VAL 15+3d5
 WEIGHT 3+0d1
@@ -666,20 +667,20 @@ BEGIN OBJ
 ART FALSE
 ATTR 0+0d1
 COLOR RED
-DAM 3+5d1
+DAM 3+5d2
 DEF 0+1d3
 DESC
 A solid wooden club with three nails poking out of it. The nails are for
 decoration, it's the splinters that get you.
 .
-DODGE 3+5d1
-HIT 5+3d1
+DODGE 3+5d2
+HIT 5+3d2
 NAME Club
 RRTY 20
 SPEED 2+1d3
 TYPE WEAPON
 VAL 0+0d1
-WEIGHT 5+3d1
+WEIGHT 5+3d2
 END
 
 BEGIN OBJ
@@ -687,7 +688,7 @@ ART FALSE
 ATTR 0+0d1
 COLOR BLACK
 DAM 6+3d5
-DEF 2+3d1
+DEF 2+3d2
 DESC
 The sword wielded by the samurai. Distinguished by its curved, single-edge
 blade.
@@ -709,7 +710,8 @@ COLOR GREEN
 DAM 3+4d5
 DEF 3+0d1
 DESC
-The trustworthy battle axe. Always gets the job done, if just a little slower.
+The trustworthy battle axe. Always gets the job done, if just a little
+slower.
 .
 DODGE 0+0d1
 HIT 5+6d7
@@ -718,5 +720,5 @@ RRTY 40
 SPEED 0+0d1
 TYPE WEAPON
 VAL 9+7d8
-WEIGHT 50+3d1
+WEIGHT 50+3d2
 END
diff --git a/opal.cpp b/opal.cpp
index 7418239..8fc17b1 100644
--- a/opal.cpp
+++ b/opal.cpp
@@ -109,7 +109,7 @@ main(int const argc, char *const argv[])
 	}
 
 	if (numnpcs == std::numeric_limits<unsigned int>::max()) {
-		numnpcs = rr.rrand<unsigned int>(3, 5);
+		numnpcs = rr.rrand<unsigned int>(3, 10);
 	}
 
 	if (numobjs == std::numeric_limits<unsigned int>::max()) {
@@ -166,7 +166,7 @@ main(int const argc, char *const argv[])
 
 	player.color = COLOR_PAIR(COLOR_YELLOW);
 	player.dam = {0, 1, 4};
-	player.hp = rr.rand_dice<uint64_t>(50, 2, 50);
+	player.hp = rr.rand_dice<uint64_t>(50, 30, 5);
 	player.speed = 10;
 	player.symb = PLAYER;
 	player.turn = 0;
@@ -184,8 +184,6 @@ main(int const argc, char *const argv[])
 			errx(1, "arrange_renew erase");
 		}
 
-		(void)box(win, 0, 0);
-
 		arrange_renew();
 
 		for (auto &n : npcs_parsed) {
@@ -194,6 +192,8 @@ main(int const argc, char *const argv[])
 			}
 		}
 
+		player.turn = 0;
+
 		goto retry;
 	case TURN_NONE:
 		errx(1, "turn_engine return value invalid");
diff --git a/parse.cpp b/parse.cpp
index cf887e9..45703d1 100644
--- a/parse.cpp
+++ b/parse.cpp
@@ -66,8 +66,6 @@ extern FILE	*yyin;
 std::vector<npc> npcs_parsed;
 std::vector<obj> objs_parsed;
 
-constexpr static int const line_max = 77;
-
 void
 parse_npc_file()
 {
diff --git a/parse.y b/parse.y
index 91bcc8e..a0c2cce 100644
--- a/parse.y
+++ b/parse.y
@@ -37,6 +37,8 @@ bool in_o;
 npc c_npc;
 obj c_obj;
 
+static int constexpr line_max = 77;
+
 static std::unordered_map<std::string, int> const color_map = {
 	{"BLACK", COLOR_PAIR(COLOR_BLACK)},
 	{"BLUE", COLOR_PAIR(COLOR_BLUE)},
@@ -147,7 +149,7 @@ npc_keywords
 
 npc_keyword
 	: ABIL abil
-	| COLOR color
+	| COLOR COLORS	{ c_npc.color = color_map.at($2); }
 	| DAM STR	{ c_npc.dam = parse_dice($2); }
 	| DESC desc DESC_END
 	| HP STR	{ c_npc.hp = parse_dice_value($2); }
@@ -179,7 +181,7 @@ obj_keywords
 obj_keyword
 	: ART BOOLEAN	{ c_obj.art = std::strcmp($2, "TRUE") == 0; }
 	| ATTR STR	{ c_obj.attr = parse_dice_value($2); }
-	| COLOR color
+	| COLOR COLORS	{ c_obj.color = color_map.at($2); }
 	| DAM STR	{ c_obj.dam = parse_dice($2); }
 	| DEF STR	{ c_obj.def = parse_dice_value($2); }
 	| DESC desc DESC_END
@@ -207,20 +209,18 @@ name
 			}
 	;
 
-color
-	: COLORS	{
-				if (in_n) c_npc.color = color_map.at($1);
-				if (in_o) c_obj.color = color_map.at($1);
-			}
-	| color COLORS
-	;
-
 desc
 	: DESC_INNER		{
+					if (strlen($1) > line_max + 1) {
+						yyerror("line too long");
+					}
 					if (in_n) c_npc.desc += $1;
 					if (in_o) c_obj.desc += $1;
 				}
 	| desc DESC_INNER	{
+					if (strlen($2) > line_max + 1) {
+						yyerror("line too long");
+					}
 					if (in_n) c_npc.desc += $2;
 					if (in_o) c_obj.desc += $2;
 				}
@@ -265,6 +265,11 @@ parse_dice(char *const s)
 
 	d.sides = (uint64_t)std::atoll(p);
 
+	if (d.sides == 1 && d.dice != 0) {
+		errx(1, "instead of using multiple 1-sided die, add them as "
+			"the base");
+	}
+
 	return d;
 }
 
diff --git a/turn.cpp b/turn.cpp
index d4cab1b..88479ac 100644
--- a/turn.cpp
+++ b/turn.cpp
@@ -58,10 +58,14 @@ static std::optional<std::pair<uint8_t, uint8_t>>	gen_obj();
 
 static void	npc_list(WINDOW *const, std::vector<npc *> const &);
 
+#ifdef DEBUG
 static void	defog(WINDOW *const);
+static bool	inspect(WINDOW *const, bool const);
+#else
+static bool	inspect(WINDOW *const);
+#endif
 
 static void	crosshair(WINDOW *const, uint8_t const, uint8_t const);
-static bool	inspect(WINDOW *const, bool const);
 
 static bool	viewable(int const, int const);
 static void	pc_viewbox(WINDOW *const, int const);
@@ -73,16 +77,20 @@ static void	equip_list(WINDOW *const, bool const);
 static void	carry_to_equip(int const);
 static void	equip_to_carry(int const, std::optional<std::string> &);
 
+static void	swap(std::optional<obj> &, std::optional<obj> &);
+
 static void	thing_details(WINDOW *const, dungeon_thing const &);
 
 enum pc_action {
+#ifdef DEBUG
 	PC_DEFOG,
+	PC_TELE,
+#endif
 	PC_NEXT,
 	PC_NONE,
 	PC_NPC_LIST,
 	PC_QUIT,
-	PC_RETRY,
-	PC_TELE
+	PC_RETRY
 };
 
 static enum pc_action	turn_npc(WINDOW *const, WINDOW *const, npc &);
@@ -170,8 +178,9 @@ static double constexpr CUTOFF = 4.0;
 static int constexpr PERSISTANCE = 5;
 static int constexpr KEY_ESC = 27;
 static int constexpr DEFAULT_LUMINANCE = 5;
-static unsigned int constexpr RETRIES = 150;
+static unsigned int constexpr RETRIES = 300;
 static int constexpr PC_CARRY_MAX = 10;
+static uint64_t constexpr HEAL_CAP = 500;
 
 static std::optional<obj> pc_carry[PC_CARRY_MAX];
 static equip pc_equip;
@@ -184,6 +193,7 @@ turn_engine(WINDOW *const win, unsigned int const numnpcs,
 	std::vector<npc *> npcs;
 	std::vector<obj *> objs;
 	unsigned int real_num = 0;
+	size_t bosses = 0;
 
 	WINDOW *sep;
 
@@ -212,7 +222,7 @@ turn_engine(WINDOW *const win, unsigned int const numnpcs,
 			i = rr.rrand<size_t>(0, npcs_parsed.size() - 1);
 			retries++;
 		} while (retries < RETRIES && (npcs_parsed[i].done
-			|| npcs_parsed[i].rrty <= rr.rrand<uint8_t>(0, 99)));
+			|| npcs_parsed[i].rrty >= rr.rrand<uint8_t>(0, 99)));
 
 		if (retries == RETRIES) {
 			break;
@@ -233,8 +243,13 @@ turn_engine(WINDOW *const win, unsigned int const numnpcs,
 			npcs_parsed[i].done = true;
 		}
 
+		if (n->type & BOSS) {
+			bosses++;
+		}
+
 		n->x = coords->first;
 		n->y = coords->second;
+		n->turn = 1;
 
 		tiles[n->y][n->x].n = n;
 
@@ -254,7 +269,7 @@ turn_engine(WINDOW *const win, unsigned int const numnpcs,
 			i = rr.rrand<size_t>(0, objs_parsed.size() - 1);
 			retries++;
 		} while (retries < RETRIES && (objs_parsed[i].done
-			|| objs_parsed[i].rrty <= rr.rrand<uint8_t>(0, 99)));
+			|| objs_parsed[i].rrty >= rr.rrand<uint8_t>(0, 99)));
 
 		if (retries == RETRIES) {
 			break;
@@ -297,10 +312,23 @@ turn_engine(WINDOW *const win, unsigned int const numnpcs,
 
 	(void)box(sep, 0, 0);
 
+	if (bosses == 1) {
+		wattron(win, COLOR_PAIR(COLOR_CYAN));
+	} else if (bosses > 1) {
+		wattron(win, COLOR_PAIR(COLOR_RED));
+	}
+	box(win, 0, 0);
+	if (bosses == 1) {
+		wattroff(win, COLOR_PAIR(COLOR_CYAN));
+	} else if (bosses > 1) {
+		wattroff(win, COLOR_PAIR(COLOR_RED));
+	}
+
 	pc_viewbox(win, DEFAULT_LUMINANCE);
 
 	(void)mvwprintw(win, HEIGHT - 1, 2,
-		"[ hp: %" PRIu64 " ]", player.hp);
+		"[ hp: %" PRIu64 "; speed: %" PRIu64 " ]", player.hp,
+			player.speed);
 
 	while (!heap.empty()) {
 		npc &n = heap.top();
@@ -331,9 +359,17 @@ turn_engine(WINDOW *const win, unsigned int const numnpcs,
 		}
 
 		switch(turn_npc(win, sep, n)) {
+#ifdef DEBUG
 		case PC_DEFOG:
 			defog(sep);
 			goto retry;
+		case PC_TELE:
+			if (inspect(win, true)) {
+				break;
+			} else {
+				goto retry;
+			}
+#endif
 		case PC_NEXT:
 			ret = TURN_NEXT;
 			goto exit;
@@ -347,12 +383,6 @@ turn_engine(WINDOW *const win, unsigned int const numnpcs,
 			goto exit;
 		case PC_RETRY:
 			goto retry;
-		case PC_TELE:
-			if (inspect(win, true)) {
-				break;
-			} else {
-				goto retry;
-			}
 		}
 
 		heap.push(n);
@@ -557,17 +587,24 @@ move_logic(WINDOW *const win, npc &n, uint8_t const y, uint8_t const x)
 
 		(void)box(win, 0, 0);
 		(void)mvwprintw(win, HEIGHT - 1, 2,
-			"[ hp: %" PRIu64 " ]", player.hp);
+			"[ hp: %" PRIu64 "; speed: %" PRIu64 " ]", player.hp,
+				player.speed);
 
 		if (n.type & PLAYER_TYPE) {
-			(void)mvwprintw(win, HEIGHT - 1, WIDTH / 4,
+			(void)mvwprintw(win, HEIGHT - 1, WIDTH / 2,
 				"[ delt %" PRIu64 " damage ]", dam);
 		} else {
-			(void)mvwprintw(win, HEIGHT - 1, WIDTH / 4,
+			(void)mvwprintw(win, HEIGHT - 1, WIDTH / 2,
 				"[ received %" PRIu64 " damage ]", dam);
 		}
 
 		if (tiles[y][x].n->hp == 0) {
+			if (n.hp > HEAL_CAP) {
+				n.hp += 5;
+			} else {
+				dam++;
+				n.hp += rr.rrand<uint64_t>(dam/2, dam);
+			}
 			tiles[y][x].n->dead = true;
 			tiles[y][x].n = NULL;
 			npc_obj_or_tile(win, y, x);
@@ -833,6 +870,10 @@ turn_pc(WINDOW *const win, WINDOW *const sep, npc &n)
 	uint8_t x = n.x;
 	bool exit = false;
 
+	(void)mvwprintw(win, HEIGHT - 1, 2,
+		"[ hp: %" PRIu64 "; speed: %" PRIu64 " ]", player.hp,
+			player.speed);
+
 	while (!exit) {
 		exit = true;
 		switch(wgetch(win)) {
@@ -923,10 +964,12 @@ turn_pc(WINDOW *const win, WINDOW *const sep, npc &n)
 		case 'Q':
 		case 'q':
 			return PC_QUIT;
+#ifdef DEBUG
 		case 'f':
 			return PC_DEFOG;
 		case 'g':
 			return PC_TELE;
+#endif
 		case 'i':
 			carry_list(sep, CARRY_LIST);
 			return PC_RETRY;
@@ -946,7 +989,11 @@ turn_pc(WINDOW *const win, WINDOW *const sep, npc &n)
 			carry_list(sep, CARRY_REMOVE);
 			return PC_RETRY;
 		case 'L':
+#ifdef DEBUG
 			inspect(win, false);
+#else
+			inspect(win);
+#endif
 			return PC_RETRY;
 		case 'I':
 			carry_list(sep, CARRY_INSPECT);
@@ -1031,6 +1078,7 @@ npc_list(WINDOW *const nwin, std::vector<npc *> const &npcs)
 	}
 }
 
+#ifdef DEBUG
 static void
 defog(WINDOW *const win)
 {
@@ -1052,6 +1100,7 @@ defog(WINDOW *const win)
 
 	(void)wgetch(win);
 }
+#endif
 
 static void
 crosshair(WINDOW *const win, uint8_t const y, uint8_t const x)
@@ -1085,7 +1134,11 @@ crosshair(WINDOW *const win, uint8_t const y, uint8_t const x)
 }
 
 static bool
+#ifdef DEBUG
 inspect(WINDOW *const win, bool const teleport)
+#else
+inspect(WINDOW *const win)
+#endif
 {
 	WINDOW *twin;
 	uint8_t y = player.y;
@@ -1103,15 +1156,19 @@ inspect(WINDOW *const win, bool const teleport)
 
 		crosshair(twin, y, x);
 
+#ifdef DEBUG
 		if (teleport) {
 			(void)mvwprintw(twin, HEIGHT - 1, 2,
 				"[ PC control keys; 'r' for random location; "
 				"'g' or 't' to teleport; ESC to exit ]");
 		} else {
+#endif
 			(void)mvwprintw(twin, HEIGHT - 1, 2,
 				"[ PC control keys; 'g' or 't' to inspect; "
 				"ESC to exit ]");
+#ifdef DEBUG
 		}
+#endif
 
 
 		if (wrefresh(twin) == ERR) {
@@ -1178,6 +1235,7 @@ inspect(WINDOW *const win, bool const teleport)
 			/* left */
 			x--;
 			break;
+#ifdef DEBUG
 		case 'r':
 			if (teleport) {
 				/* random teleport location */
@@ -1186,8 +1244,10 @@ inspect(WINDOW *const win, bool const teleport)
 			}
 
 			break;
+#endif
 		case 't':
 		case 'g':
+#ifdef DEBUG
 			if (teleport && tiles[y][x].n == NULL) {
 				/* complete teleport */
 				tiles[y][x].v = true;
@@ -1196,6 +1256,9 @@ inspect(WINDOW *const win, bool const teleport)
 			}
 
 			if (!teleport && tiles[y][x].n != NULL) {
+#else
+			if (tiles[y][x].n != NULL) {
+#endif
 				thing_details(twin, *tiles[y][x].n);
 			}
 
@@ -1522,7 +1585,7 @@ carry_to_equip(int const i)
 		errx(1, "carry_to_equip bad swap");
 	}
 
-	std::swap(pc_carry[i], *equip_slot);
+	swap(pc_carry[i], *equip_slot);
 }
 
 static void
@@ -1577,7 +1640,7 @@ equip_to_carry(int const i, std::optional<std::string> &error)
 
 	for (int j = 0; j < PC_CARRY_MAX; ++j) {
 		if (!pc_carry[j].has_value()) {
-			std::swap(pc_carry[j], *equip_slot);
+			swap(pc_carry[j], *equip_slot);
 			return;
 		}
 	}
@@ -1585,6 +1648,28 @@ equip_to_carry(int const i, std::optional<std::string> &error)
 	error = "no open slots in carry bag";
 }
 
+static void
+swap(std::optional<obj> &carry, std::optional<obj> &equip) {
+	if (!carry.has_value()) {
+		player.hp = subu64(player.hp, equip->def);
+		player.speed = subu64(player.speed, equip->speed);
+
+		if (player.hp == 0) {
+			player.hp = 1;
+		}
+
+		if (player.speed == 0) {
+			player.speed = 1;
+		}
+	} else {
+		player.hp = subu64(player.hp, equip->def);
+		player.hp += carry->def;
+
+		player.speed = subu64(player.speed, equip->speed);
+		player.speed += carry->speed;
+	}
+	std::swap(carry, equip);
+}
 static void
 thing_details(WINDOW *const win, dungeon_thing const &d)
 {