Control Flow
I want to endorse and flesh out a bit Edward’s advice on removing the goto statements. A better approach here is some kind of state machine. You’ve got some kind of game state that tracks what mode you’re in, such as whether you’re selecting an ability, moving around, starting an encounter, and so on.
You draw whatever screen is appropriate to the current state. (Say, a map in overland mode, ASCII art of a monster in battle mode, maybe a box-drawn first-person dungeon if you’re paying homage to Wizardry or Ultima?) The options displayed depend on the game mode, what abilities you have, and whatever else is appropriate (such as what items are in your inventory, maybe, or how much energy you have, or a cooldown period). Selecting any option then updates the game state. For example, winning a battle might send you to the loot screen. The program logic is a loop something like this:
while ( !state.hasQuit() ) {
state.displayScreen();
state.getActionAndUpdate();
}
Then displayScreen() could, say, clear the screen, print a status bar, print the ASCII art, look up what abilities your character has, print those, and give you a menu prompt.
If the state becomes big and bulky enough, you would no longer want to keep the entire game state in a single “God object” that everything needs to muck around with. Player stats might go into one object, world maps in another, enemy stats in others. Separate subsystems however it makes sense. But that kind of loop is a nice, simple pattern for tracking which game mode you’re currently in and acting appropriately.
A variant is to make the last line something like state = state->getNewState(), where state is a std::unique_ptr to the parent class of the different states, and getNewState() returns a smart pointer to a new state. (Using a smart pointer means the compiler will do all the memory management for you automatically.) That lets you split up the abstract concept of a game mode into concrete instances, like overland mode or combat mode. These can be daughter classes whose member functions implement the correct behavior for that one mode. Each of those member functions would then be shorter and clearer than one big function that contains all the code paths for every mode. You select between them by returning a new state object, no if, switch or goto involved.
If you do have any remaining sections of code that fit the pattern of a different code branch for each case, the classic solution is to define an enum with a constant for every possible case and then write a switch statement. Your compiler might even have a prayer of warning you if you forget or mistype one of the cases.
Avoid Global Variables
These make your program very hard to debug, because any part of the code could have changed them. A better solution is to have the player state store internally things like how many health potions you have. Then, only the code to use one decrements the count, and only the code to check inventory looks it up and displays it.
You also would want to store numeric data as a number, rather than as a string that you repeatedly convert back into a number.
Use Data Structures
In general, I would advise moving your special cases into data structures, not code branches. Jerry Coffin had good suggestions about how to do this for player abilities.
But, for example, your phenomenal ASCII art and other monster data could be stored as values in a hash table (std::unordered_map in the STL) with the names you’re using to select them as the keys. This would have several advantages. One of the most important is that, if you add another monster, you just need to add its entry to the table in one place.
Treating every new ability and every new monster as a special case that needs special handling in every piece of code that deals with them becomes a real nightmare: you’ve always got to check that you remembered them all everywhere and dealt with them consistently. If you put all the data in one place, and you write code in another place that can handle any piece of the data, adding stuff becomes so much easier.
Some Technicalities
The portable ways to write a multiline string literal include
constexpr char literal[] = "Line 1\n"
"Line 2\n";
and (uglier, but there for backward compatibility with C):
constexpr char literal[] =
"Line 1\n\
Line 2\n";
However, in this case, you might consider storing your ASCII art as vectors of rows. (Since two-dimensional array can mean a few different things, a good unambiguous name for this is a rectangular array.) This would make it possible to, for example, display a picture of the monster in a box and wrap an infodump around it, or show its stats on the left or right of the picture, or overlay a caption like "-999 HP" on the frame.
I want to repeat and emphasize the advice to separate the data from the display code. You want code orthogonal to your game data. You shouldn’t be rewriting the display code for every kind of monster, because that makes it a nightmare to ever change. Don’t repeat yourself! That also makes it possible for you to do a lot more things with your data or run your code on arbitrary new date.
If you make the art arrays wide characters, then use std::wcout, you can use Unicode art, and not just ASCII art. You might not necessarily want to. Your art is spectacular. The classic games you’re hearkening back to used the box-drawing and geometric shape characters now in Unicode. But the option is there. Note that Windows needs a bit of non-standard initialization code inside an #ifdef block for this to work, but I can share it if you want.
On many systems even today, you can also insert ANSI color codes (16 colors, foreground and background, and some special features like bold and underline, like in the late 8-bit or early 16-bit era, and exactly like classic Unix terminals over telnet) or xterm color (more than 200). Can’t get more authentically retro than that. The Linux console supports those codes natively, and I think the OSX terminal does too.