Texty: An Animated C++ Game
Texty is a windows console app written in C++ for an assignment. I wanted to explore how far I could take the console and also take a look into game engines. The overall gameplay is tenuous at best, but the look and feel go beyond what I had of thought a console application could produce.
The premise of the actual game play is to navigate a maze of rooms in which you have no orientation. Collect items, defeat monsters and solve riddles.
When I wrote this I was pretty interested in decoupling my code, I was also reading stuff about data oriented design and trying to fight the OO approach. There's a decent amount of code so I'll only be showing the bits I find most interesting. The codebase is made up of an editor, a string library and the game itself. Though I will only show the game here.
The main is very clean, easy to read and simple. if only the rest of the application was too! It looks like this:
int main()
{
Engine* e = Engine::Get(); //bit easier
e->AddSystem(StateManager::Get()); //push subsystems to engines system list
e->Init(); //init engine and all subsystems (only 1 subsystem left lol)
e->ShowCursor(false); //hide that ugly cursor
IntroState* s = new IntroState(); //create first state.. statemanager will delete this
StateManager::Get()->Push(*s); //push state onto our states stack
e->MainLoop(); //enter our main loop!
}
The game engine is based off this awesome book: www.gameprogrammingpatterns.com. It is sort of a frankenstein as it has an observer, singleton and statemanager in it, not to mention I may have bent some of the ideas to meet my goals, not great but learning is learning.
At engine init, we set up our window title, size, load our assets (which in itself is a pretty cool function!) The mainloop controls the framerate, as originally it had 1000s of frames per second. It also calls update on all the subsystems, which is where the main processing is done.
void Engine::Init()
{
HANDLE hndW = GetStdHandle(STD_OUTPUT_HANDLE);
SMALL_RECT windowsz = { 0, 0, _wWidth - 1, _wHeight - 1 }; //make window size smaller than actual to hide scrollbar on side
COORD buffsz = { _wWidth, _wHeight };
//set window properties
SetConsoleTitleA(_wTitle);
SetConsoleWindowInfo(hndW, TRUE, &windowsz);
SetConsoleScreenBufferSize(hndW, buffsz);
bool errexit = false;
if (!Assets::LoadMonstersFile("Monsters.txt")) errexit = true; //fill our main structs (monsters, items, riddles)
if (!Assets::LoadItemsFile("Items.txt")) errexit = true; //if failed to load, free memory and exit
if (!Assets::LoadRiddlesFile("Riddles.txt")) errexit = true;
if (errexit){
printf_s("Failed to load asset files.");
getchar();
ProcessExit();
exit(0);
}
//init all subsystems *down to only one* lol
for (int i = 0; i < _nSystems; ++i)
_systems[i]->Init();
}
void Engine::MainLoop()
{
int now = (int)time(0), lastframe = now, dt;
while (!_wantExit){ //loop while we dont want to quit
now = (int)time(0);
dt = now - lastframe;
lastframe = now;
if (dt < _frameRate) //if our framerate is too fast, wait for some time
Sleep(_frameRate - dt);
for (int i = 0; i < _nSystems; ++i) //loop through subsystems and call their updates
_systems[i]->Update();
if (_wantExit) //if something wants to exit then call destroy for all systems
ProcessExit();
}
}
void Engine::ShowCursor(bool show)
{
HANDLE hndW = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursorInfo;
GetConsoleCursorInfo(hndW, &cursorInfo);
cursorInfo.bVisible = show;
SetConsoleCursorInfo(hndW, &cursorInfo);
}
The file reader was designed to read a file in order to fill a struct without knowing the layout of the struct. This let me use the same filereader function for reading in all the different asset files which had different formats. It's also super enjoyable writing code like this.
Doing this introduced a pretty good understanding of structs and padding. I'm sure it's not the most efficient way of doing things but it sure was fun. The comments provide most of the insights.
//Example: format: "%s32%i%i" => struct unknown { char[32]; int; int } or "%i%c%p3" => struct unknown { int; char; }
#include "FileReader.h"
#include
#include
int FileReader::CountLines(const char* fname) //assumes there is at least one valid line^^
{
FILE* fp;
int c, lines = 1;
fopen_s(&fp, fname, "r"); //open file
if (!fp) return 0; //if couldnt open file return false
while ((c = fgetc(fp)) != EOF) //while getc isnt endoffile
if (c == '\n') //if char is a new line , count it
++lines;
fclose(fp); //close file
return lines; //return count
}
int FileReader::Read(void*& strct, int size, const char* fmt, const char* fname)
{
unsigned int numLines = CountLines(fname); //read how many records we need to store
if (!numLines) return 0; //either no lines in file, or file couldnt be opened
FILE* fp;
fopen_s(&fp, fname, "r"); //open file (again)
if (!fp) return 0;
strct = operator new(numLines*size);
char line[256] = { 0 }, buffer[256] = { 0 }; //some buffers to handle reading file
char* fmtptr = (char*)fmt, *strctptr = (char*)strct; //pointer to start of struct and start of formatting
char* bufptr = nullptr, *lineptr = nullptr;
while (fgets(line, sizeof(line), fp)) { //while we get line
lineptr = (char*)line; //set lineptr to start of line
while (*fmtptr){ //looop through our formatting, since we use this to copy in correct order
bufptr = (char*)buffer; //reset our buffer pointer
if (*fmtptr == '%') //move fmtptr to an interesting point
++fmtptr;
while (*lineptr && *lineptr != '\n' && *lineptr != '\t') //copy data from line to buffer
*bufptr++ = *lineptr++;
*bufptr = 0, lineptr++; //add null char
switch (*fmtptr) //switch formatting to handle different types of data
{
case 's': //if its a string
{
int strlen = _getfmtstrlen(fmtptr); //determine how much we need to offset
strcpy_s(strctptr, strlen, buffer); //copy buffer to struct
strctptr += strlen; //move structptr to pass the cstring variable within struct
break;
}
case 'c': //if its just a char
{
fmtptr++; //must fmtptr++ for next iteration
*(char*)strctptr = buffer[0]; //copy first char to struct
strctptr += sizeof(char); //move passed char
break;
}
case 'i': //if its an int
{
fmtptr++; //must fmtptr++ for next iteration
*(unsigned int*)strctptr = atoi(buffer); //copy buffer to int
strctptr += sizeof(unsigned int); //move structptr past int
break;
}
case 'p': //if padding
{
fmtptr++;
char c[2] = { *fmtptr++, 0 }; //padding shouldnt be more than 3 bytes
strctptr += atoi(c); //skip it some amount
break;
}
}
}
fmtptr = (char*)fmt; //next record, start over again
}
fclose(fp);
return numLines; //return how many records in struct
}
int FileReader::_countchar(char find, const char* str) //counts char in a string - not used anymore
{
char* cp = (char*)str; //set pointer to start of string
int counter = 0;
while (*cp) //while we have string
if (*cp++ == find) //if current pos == find
++counter; //increment counter
return counter;
}
int FileReader::_getfmtstrlen(char* &fmtptr)
{
char tmp[5] = { 0 }; //temp place to hold ints
for (int i = 0; i < sizeof(tmp) && *fmtptr++ && *fmtptr != '\n' && *fmtptr != '\t' && *fmtptr != '%'; ++i)
tmp[i] = *fmtptr; //while its an int, copy it to our temp spot
return atoi(tmp); //return it as an int
}
One of the more complex areas is the rendering system. I faced several issues with getting to what I thought was an acceptable look, the first major hurdle was the collision system. Originally the objects would overlap each other before collision was detected, the other issue was knowing where they collided whether it was on the horizontally, diagonally or vertically. I created bounding boxes, where the bounding box stores an array of indexes related to the canvas's index, so if two items are drawn in the same spot on the canvas then they will collide here.
Only the first 16bits are used to store the index, and bits 17/18 are used to indicate where the bound is if its on the side for example, it only needs to reverse the x velocity, and if its ontop it only needs the y velocity.
Below the FillCanvas() is the man workhorse of the Renderer class. It's main purpose is to fill the screen buffer (canvas) with all the objects that need to be rendererd. Afterwards drawing to the screen is as simple as calling WriteConsoleOutput() with our screen buffer.
It was super dooper fun playing with bits
//***********RENDERER.CPP
void Renderer::FillCanvas()
{
int ww = Engine::Get()->GetWindowWidth(); //width of window
int iCanvas, iCurrent;
int cW = _canvas->GetWidth();
int cH = _canvas->GetHeight();
RenderObject* cur = NULL;
_canvas->Clear(); //way easier to just clear canvas instead of updating only those that need updating (moving objects make it tedious)
for (auto current = _renderMap.begin(); current != _renderMap.end(); current++){ //for each object to be rendered
cur = current->second; //set pointer to current for easier typing
if (cur->GetVX() || cur->GetVY()){ //if its moving then move it and check for collision against all other objects
cur->Move();
for (auto nxt = _renderMap.begin(); nxt != _renderMap.end(); nxt++){
if (cur != nxt->second && cur->CheckCollision(nxt->second))
break; //collision occured, break out of loop
}
}
if (cur->isDraw()){ //if its drawn then
//loop through buffer and calc index -> copies buffer to _canvas so can draw all objects at once
for (int y = 0; y < current->second->GetHeight() && y < cH; ++y){ //loop through buffer
for (int x = 0; x < current->second->GetWidth() && x < cW; ++x){
iCanvas = (x + cur->GetPosX()) + ww *(y + cur->GetPosY()); //index to array pos for canvas buffer
iCurrent = x + cur->GetWidth()*y; //index to array pos for objs buffer
if (cur->GetCharAt(iCurrent)){ //only copy stuff if there is something there
_canvas->WriteToBuffer(iCanvas, cur->GetCharAt(iCurrent), cur->GetColourAt(iCurrent));
}
}
}
}
}
}
//***********RENDEROBJECT.CPP
unsigned int unpack(unsigned int element) //returns first two bytes as unsigned int
{
unsigned char c1 = element, c2 = element >> 8;
return ((c2 << 8) + c1);
}
void RenderObject::CreateBoundingBox()
{
int count = 0, ind, n = _w*_h; //todo maybe remove rundandcies
for (int i = 0; i < n; ++i){
if (_buffer[i].Char.AsciiChar != 0)
++count;
}
_boundingBoxSz = count * 5;
if (count){
if (_boundingBox)
delete[]_boundingBox;
_boundingBox = new unsigned int[_boundingBoxSz];
for (int i = 0; i < n; ++i){
if (_buffer[(i / n) + _w*(i % n)].Char.AsciiChar != 0){
ind = ((1 / n) + _window.Left) + 79 * ((i%n) + _window.Top);
_boundingBox[i] = ind - 1; //to left vx
_boundingBox[i] |= 1 << 17;
_boundingBox[++i] = ind; //to left vx
_boundingBox[i] |= 1 << 17;
_boundingBox[++i] = ind + 1; //to right vx
_boundingBox[i] |= 1 << 17;
_boundingBox[++i] = ind - 79; //above vy
_boundingBox[i] |= 1 << 16;
_boundingBox[++i] = ind + 79; //below vy
_boundingBox[i] |= 1 << 16;
}
}
}
}
/*
Loops through this objects boundery array and a given one, it then checks if they hit,
then it reads the bit flags to change velocities (read bitflags above function)
*/
bool RenderObject::CheckCollision(RenderObject* nxt)
{ //some checks
if (!_boundingBox || !nxt->_boundingBox || !_solid || !nxt->isSolid() || !nxt->isDraw()) return false;
bool colx = false, coly = false, col = false;
unsigned int x;
for (unsigned int i = 0; i < _boundingBoxSz; ++i){
for (unsigned int j = 0; j < nxt->_boundingBoxSz; ++j){ //loop through both bounding boxes
if (unpack(_boundingBox[i]) == unpack(nxt->_boundingBox[j])){ //if there is a match
x = nxt->_boundingBox[j];
if ((x >> 17) & 1 && !colx){ //vx flag and not changed vx yet
_vx *= -1; //reverse vx
colx = true;
}
else if ((x >> 16) & 1 && !coly){ //vy flag and not changed vy yet
_vy *= -1; //reverse vy
coly = true;
}
}
}
if (!col && (coly || colx)){ //only check col once col is true instead of coly and colx
if (nxt->GetVX() || nxt->GetVY())nxt->Move();
col = true;
}
coly = colx = false;
}
return col;
}
Each renderObject is made up of:
RenderObject::RenderObject(int x, int y, int w, int h)
:_vx(0),
_vy(0),
_solid(1),
_x(-1),
_y(-1),
_draw(true),
_boundingBoxSz(0)
{
_id = s_idGen++; //get our unique instance id (for render map)
_w = w; //store our widths heights
_h = h;
_window.Top = y; //setup our window ->window not really needed but i like to be able to draw without renderer
_window.Left = x;
_window.Right = x + w;
_window.Bottom = y + h;
_buffer = new CHAR_INFO[w*h]; //allocate enough size for our buffer
Clear(); //clear garbage from object
}
Due to the drawing being done with WriteConsoleOutput() which accepts an array of CHAR_INFO structs, for simplicity, my renderObjects all used the same buffer. This meant for each access, the actual position within the array had to be derived from the x position, y position and the width of buffer. The functions to note are: RenderObject::Write() which is the actual writing to the console, it is only a couple lines long.
void RenderObject::Clear() //clears buffer
{
for (int y = 0; y < _h; ++y){ //for all rows
for (int x = 0; x < _w; ++x){ //for columns
_buffer[x + _w*y].Char.AsciiChar = 0; //set that to 0
_buffer[x + _w*y].Attributes = 0; //set colour to 0
}
}
}
void RenderObject::Fill(char c, unsigned int colour) //fillls buffer with something (see above function)
{
for (int y = 0; y < _h; ++y){
for (int x = 0; x < (_w); ++x){
_buffer[x + _w*y].Char.AsciiChar = c;
_buffer[x + _w*y].Attributes = colour;
}
}
}
void RenderObject::WriteToBuffer(int index, unsigned char c, unsigned int colour)
{
_buffer[index].Char.AsciiChar = c;
_buffer[index].Attributes = colour;
}
void RenderObject::Write() //writes the buffer to screen, bypass renderer
{
if (_draw){
HANDLE hndW = GetStdHandle(STD_OUTPUT_HANDLE);
WriteConsoleOutput(hndW, _buffer, { _w, _h }, { 0, 0 }, &_window);
}
}
void RenderObject::Plot(const COORD& pt, unsigned char c, unsigned int colour) //copies point to buffer
{
int index = pt.X + _w*pt.Y; //calcs index of point to our buffer
_buffer[index].Char.AsciiChar = c;
_buffer[index].Attributes = colour;
}
void RenderObject::DrawBox(const COORD& start, int w, int h, unsigned char c, unsigned int colour) //draws box
{
h = start.Y + h;
w = start.X + w;
for (int y = start.Y; y < h && y <= _window.Bottom; ++y){
for (int x = start.X; x < w && x <= _window.Right; ++x){
if (y == start.Y || x == start.X || x == w - 1 || y == h - 1)
Plot({ x, y }, c, colour);
}
}
}
Now that all the drawing and engine stuff is accounted for, the statemanager is as simple as pushing on a state and popping it off. Only the most recent state will be updated. For this game the ESC will pop a state, so to exit back to the main intro screen, it is only ever one button away.
The code for the initial gamestate is super simple for everything it does. The DrawPlay() looks a bit crazy but it is just an unrolled loop to make things simpler and faster. Essentially it's just moving pixles into the middle of the screen to form the word PLAY
#include "IntroState.h"
#include "GameState.h"
#include
namespace Texty{
IntroState::IntroState()
:_play(false),
_nextState(nullptr)
{
int ww = Engine::Get()->GetWindowWidth();
int wh = Engine::Get()->GetWindowHeight();
for (int i = 0; i < MAX_PLAYTHING; ++i){
_plaything[i].CreateRenderObject(rand() % (ww - 10) + 5, rand() % (wh-10) + 5, 1, 1); //create bunch of objects at random position
_plaything[i].Plot({ 0, 0 }, CHAR_BLOCK, (i < 93 ? WHITE : rand() % COLOUR_MAX - 1 + 1)); //plot boxes, PLAY in white, extra in random colour
_plaything[i].SetV((i % 2 ? 1 : -1), (i % 2 ? 1 : -1)); //set all to velocity with x,y
_plaything[i].CreateBoundingBox(); //create bounding box so they collide
_renderer.Insert(_plaything[i].GetId(), &_plaything[i]); //insert them into renderer
}
}
void IntroState::Update()
{
_renderer.Update(); //draw our objects
if (_kbhit() != 0) { //if keyboard hit, do something
int c = _getch();
if (c == 27){ //escape key
StateManager::Get()->WantPop(); //pop state next chance we get - will exit game
}
else if (!_play){ //if its first keypress, do some pretty stuff
DrawPLAY(); //pretty play word
_play = true; //next key hit will mean we play
}
else{ //second key hit push our gamestate on
_play = false;
for (int i = 0; i < MAX_PLAYTHING; ++i){ //setting up this state so when we return it looks nicer
_plaything[i].SetV((i % 2 ? 1 : -1), (i % 2 ? 1 : -1));
}
_nextState = new GameState(); //create our new state
StateManager::Get()->Push(*_nextState); //push our new state onto stack
}
}
}
IntroState::~IntroState()
{
}
/*
prettiest function ever!
loooks pretty bad but won't somebody please think about the performance!
i think there are roughly 100(+-10) objects to loop through, it does it in 10 iterations!
*/
void IntroState::DrawPLAY()
{
static const int ALIGNX = Engine::Get()->GetWindowWidth() / 2 - 15; //just so its only ever called once
static const int ALIGNY = Engine::Get()->GetWindowHeight() / 2 - 10;
//VERTICAL LINES ONLY
for (int i = 0; i < 7; ++i){
_plaything[i].MoveTo(ALIGNX, ALIGNY + i); //P
_plaything[i + 7].MoveTo(ALIGNX + 1, ALIGNY + i); //P
_plaything[i + 14].MoveTo(ALIGNX + 9, ALIGNY + i); //L
_plaything[i + 21].MoveTo(ALIGNX + 10, ALIGNY + i); //L
_plaything[i + 28].MoveTo(ALIGNX + 16, ALIGNY + i); //A
_plaything[i + 35].MoveTo(ALIGNX + 17, ALIGNY + i); //A
_plaything[i + 42].MimgoveTo(ALIGNX + 21, ALIGNY + i); //A
_plaything[i + 49].MoveTo(ALIGNX + 22, ALIGNY + i); //A
if (i < 4){
_plaything[i + 56].MoveTo(ALIGNX + 5, ALIGNY + i); //P
_plaything[i + 60].MoveTo(ALIGNX + 6, ALIGNY + i); //P
}
else{
_plaything[i + 60].MoveTo(ALIGNX + 28, ALIGNY + i); //Y
_plaything[i + 63].MoveTo(ALIGNX + 29, ALIGNY + i); //Y
}
}
//horizontal lines
for (int i = 0, j = 70; i < 3; ++i, ++j){
_plaything[j].MoveTo(ALIGNX + i + 2, ALIGNY); //P
_plaything[j + 3].MoveTo(ALIGNX + i + 2, ALIGNY + 3); //P
_plaything[j + 6].MoveTo(ALIGNX + i + 11, ALIGNY + 6); //L
_plaything[j + 9].MoveTo(ALIGNX + i + 18, ALIGNY); //A
_plaything[j + 12].MoveTo(ALIGNX + i + 18, ALIGNY + 3); //A
_plaything[j + 15].MoveTo(ALIGNX + i + 25, ALIGNY + i); //Y
_plaything[j + 19].MoveTo(ALIGNX + 29 + i, ALIGNY + 3 - i); //Y
}
_plaything[88].MoveTo(ALIGNX + 28, ALIGNY + 3); //end of y
_plaything[92].MoveTo(ALIGNX + 32, ALIGNY);
}
}
Into the action! Intro scene combined with some gameplay
One of the main features of the game is the cool textbox. It draws the box and lines with functions from above. It features easy to use coloured text inspired by the Wolf:ET chat system although instead of using ^3 it uses _3_. An echo function is needed to show typed characters.
The animation of the doors is simply leveraging the draw system with MoveTo() as well as drawing after set frames.
///usage _currentGS->WriteToTextBox("_15_This room requires a _14_%s_15_ in order to be accessed.", Assets::s_items[_item].name);
void TextBox::WriteToTextBox(const char* txt, ...) //saves me using buffers && sprintf all over the place
{
//i would like to be able to change colour on the fly, _#_ where # represents colour code
//so _12_THIS IS RED_13_THIS IS WHITE ==> THIS IS RED THIS IS WHITE
char buffer[512] = { 0 };
va_list va;
va_start(va, txt);
vsprintf_s(buffer, sizeof(buffer), txt, va);
va_end(va);
_output = buffer;
if (_output.Length()){ //if there is text to write
if (_currentLine > _textBox->GetHeight() - 4){ //if there are no more lines, then clear box and restart from line 1
_currentLine = 1;
ClearTextBox();
}
for (int x = _textBox->GetPosX() + 1, i = 0; i<_output.Length() && x < _textBox->GetWidth()-1; ++x, ++i){
if (_output.CharAt(i) == '-' && _output.CharAt(i + 1) == 'n'){ //if newline then add a new line
i += 2;
++_currentLine;
x = _textBox->GetPosX()+1;
}
if (_output.CharAt(i) == '_'){ //see if colour code in string
_colour = _output.GetIntAt(i); //set our colour
if (_colour == -1) //if it wasnt a colour code, set it back to default
_colour = WHITE;
}
//if (_colour > WHITE && _colour <= 1)
// getchar();
_textBox->WriteToBuffer(x + _textBox->GetWidth()*_currentLine, _output.CharAt(i), _colour);
}
_currentLine++; //increment line
//printf_s("%i", _colour);
//getchar();
}
}
void TextBox::ClearInputBox() //only clear echo area - dont want what we type sticking in input area
{
const static int echoline = _textBox->GetHeight() - 2;
if (_input.Length()){
for (int x = _textBox->GetPosX() + 2, i = 0; x < _textBox->GetWidth()-1; ++x, ++i){
_textBox->WriteToBuffer(x + _textBox->GetWidth()*echoline, (unsigned int)0, (unsigned int)0);
}
}
}
void TextBox::Echo() //echo what keys we type so we can see what we read
{
const static int echoline = _textBox->GetHeight()-2;
if (_input.Length()){
for (int x = _textBox->GetPosX()+2, i = 0; x < _input.Length()+2; ++x, ++i){
_textBox->WriteToBuffer(x + _textBox->GetWidth()*echoline, _input.CharAt(i), WHITE);
}
}
}
}
An array is used to create the rooms, the array is randomly filled with locked rooms, monster rooms, riddle rooms and empty rooms. The riddle room consists of a random riddle where the correct answer grants a random reward. A monster room contains a random monster, some monsters are only defeatable with a good weapon equipped. A locked room requires a specific item, which may need to be farmed from riddles and Monsters , it is terribly flawed as you can go back and forth to gather lots of items quickly.
Here is part of the gamestate plus some of the room code. All items and riddles etc are loaded from .txt files on start up
// gamestate creation, how the rooms are created.
for (int y = 0; y < ROOM_H; ++y){
for (int x = 0; x < ROOM_W; ++x){
if ((x || y) && !(rand() % 4)) //yukky but ensures first door isnt locked
_rooms[x][y] = new LockedRoom(this, rand() % 3, rand() % 3, rand() % 3);
else if (!(rand() % 2))
_rooms[x][y] = new MonsterRoom(this, rand() % 3, rand() % 3, rand() % 3);
else if (!(rand() % 2))
_rooms[x][y] = new RiddleRoom(this, rand() % 3, rand() % 3, rand() % 3);
else
_rooms[x][y] = new EmptyRoom(this, rand() % 3, rand() % 3, rand() % 3);
}
}
//RIDDLE ROOM_Hbool RiddleRoom::OnRoomEnter(int dir)
{
if (!_drawable)
DrawDrawable(DARKGREEN, WHITE);
_drawable->SetDraw(true);
#ifndef SKIPANIMATION
HandleAnimation(dir);
#endif
_iRiddle = rand() % Assets::GetNumRiddles(); //generate random riddle on each entry
_currentGS->ClearTextBox();
_currentGS->WriteToTextBox("_15_A Riddler appears to be holding something that you want...-nHe offers a riddle.");
_currentGS->WriteToTextBox("_3_%s", Assets::s_riddles[_iRiddle].riddle);
return true;
}
void RiddleRoom::ProcessInput(const myString::String& input)
{
int item = rand() % Assets::GetNumItems();
if (input.Find(Assets::s_riddles[_iRiddle].answer) >= 0){
_currentGS->WriteToTextBox("_15_A look of horror comes across the Riddler's face, you've beaten him!");
_currentGS->WriteToTextBox("_15_The riddler reluctantly hands over his precious _14_%s_15_ to you!", Assets::s_items[item].name);
_currentGS->InsertPlayerItem(Assets::s_items[item]);
_passed = true;
}
else
_currentGS->WriteToTextBox("_15_The Riddler laughs are how foolish you appear to be!");
}
//MONSTER ROOM_H
void MonsterRoom::HandleAttack()
{
if (_monster.GetHP() > 0){
_currentGS->WriteToTextBox("_15_You attack the _12_%s_15_ with your whopping _12_%i_15_ attack power!",
Assets::s_monsters[_monster.GetId()].name, _currentGS->GetPlayerAttack());
if (!_currentGS->PlayerAttack(&_monster)){ //if monster isnt dead, it will attack back
_currentGS->WriteToTextBox("_15_It looks like you've made a poor decision...");
_currentGS->WriteToTextBox("_15_The _12_%s_15_ fights back, dealing a costly_12_ %i_15_ damage!",
Assets::s_monsters[_monster.GetId()].name, _monster.GetAttack());
_currentGS->HurtPlayer(_monster.GetAttack()); //attack player with monster
if (_currentGS->GetPlayerHP() <= 0){ //if player died
_currentGS->WriteToTextBox("You have died. Press any key to continue");
StateManager::Get()->WantPop(); //quit time
}
}
else{ //monster died
int item = rand() % Assets::GetNumItems(); //generate random drop item
_currentGS->InsertPlayerItem(Assets::s_items[item]); //insert item to our inventory
_currentGS->WriteToTextBox("_15_The _12_%s_15_ dies, and drops an awesome _14_%s",
Assets::s_monsters[_monster.GetId()].name,
Assets::s_items[item].name);
_drawable->SetDraw(false); //stop drawing the now dead monster
}
_currentGS->WriteToTextBox("_15_You have %iHP left.", _currentGS->GetPlayerHP());
}
else //trying to attack dead monster
_currentGS->WriteToTextBox("There is nothing else to attack");
}
}