The official distribution of olcConsoleGameEngine, a tool used in javidx9's YouTube videos and projects
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
videos/olcRolePlayingGame/RPG_Engine.cpp

693 lines
19 KiB

/*
+-------------------------------------------------------------+
| OneLoneCoder RPG Game Engine |
| "The Legend of WittyBit, Fantasy Quest VI" - javidx9 |
+-------------------------------------------------------------+
What is this?
~~~~~~~~~~~~~
This is the code I created as part of my
"Code-It-Yourself! Role Playing Game" series on YouTube. This is
NOT a game. The project will compile and demonstrate several
systems developed as part of that series. My original intention
was to develop a small yet complete RPG, alas real life got in
the way. After several months, I've decided to just open the source
"as is", so it will contain bugs, be confusing and all round not
up to the usual "quality" I strive for.
Part 1: https://youtu.be/xXXt3htgDok
Part 2: https://youtu.be/AWY_ITpldRk
Part 3: https://youtu.be/UcNSb-m4YQU
Part 4: https://youtu.be/AnyoUfeNZ1Y
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2018, 2019 OneLoneCoder.com
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions or derivations of source code must retain the above
copyright notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce
the above copyright notice. This list of conditions and the following
disclaimer must be reproduced in the documentation and/or other
materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Links
~~~~~
YouTube: https://www.youtube.com/javidx9
Discord: https://discord.gg/WhwHUMV
Twitter: https://www.twitter.com/javidx9
Twitch: https://www.twitch.tv/javidx9
GitHub: https://www.github.com/onelonecoder
Homepage: https://www.onelonecoder.com
Patreon: https://www.patreon.com/javidx9
Author
~~~~~~
David Barr, aka javidx9, <EFBFBD>OneLoneCoder 2018, 2019
*/
#include "RPG_Engine.h"
#define X(n) m_script.AddCommand(new cCommand_ ## n)
RPG_Engine::RPG_Engine()
{
m_sAppName = L"Top Down Role Playing Game";
}
bool RPG_Engine::OnUserCreate()
{
cCommand::g_engine = this;
cMap::g_script = &m_script;
cQuest::g_script = &m_script;
cQuest::g_engine = this;
cDynamic::g_engine = this;
cItem::g_engine = this;
RPG_Assets::get().LoadSprites();
RPG_Assets::get().LoadMaps();
RPG_Assets::get().LoadItems();
m_sprFont = RPG_Assets::get().GetSprite("font");
m_listQuests.push_front(new cQuest_MainQuest());
m_pPlayer = new cDynamic_Creature_Witty();
m_listItems.push_back(RPG_Assets::get().GetItem("Basic Sword"));
ChangeMap("coder town", 5, 5);
return true;
}
bool RPG_Engine::OnUserUpdate(float fElapsedTime)
{
switch (m_nGameMode)
{
//case MODE_TITLE:
//return UpdateTitleScreen(fElapsedTime);
case MODE_LOCAL_MAP:
return UpdateLocalMap(fElapsedTime);
//case MODE_WORLD_MAP:
// return UpdateWorldMap(fElapsedTime);
case MODE_INVENTORY:
return UpdateInventory(fElapsedTime);
//case MODE_SHOP:
//return UpdateShop(fElapsedTime);
}
return true;
}
bool RPG_Engine::UpdateLocalMap(float fElapsedTime)
{
// Update script
m_script.ProcessCommands(fElapsedTime);
// Erase and delete redundant projectiles
m_vecProjectiles.erase(
remove_if(m_vecProjectiles.begin(), m_vecProjectiles.end(),
[](const cDynamic* d) {return ((cDynamic_Projectile*)d)->bRedundant; }), m_vecProjectiles.end());
if (m_script.bUserControlEnabled)
{
m_pPlayer->vx = 0.0f;
m_pPlayer->vy = 0.0f;
if (!m_bShowDialog && m_pPlayer->bControllable)
{
// Handle Input
if (IsFocused())
{
if (GetKey(VK_UP).bHeld)
{
m_pPlayer->vy = -4.0f;
}
if (GetKey(VK_DOWN).bHeld)
{
m_pPlayer->vy = 4.0f;
}
if (GetKey(VK_LEFT).bHeld)
{
m_pPlayer->vx = -4.0f;
}
if (GetKey(VK_RIGHT).bHeld)
{
m_pPlayer->vx = 4.0f;
}
if (GetKey(L'Z').bReleased)
m_nGameMode = MODE_INVENTORY;
if (GetKey(VK_SPACE).bReleased) // Interaction requested
{
// Grab a point from the direction the player is facing and check for interactions
float fTestX, fTestY;
if (m_pPlayer->GetFacingDirection() == 0) // South
{
fTestX = m_pPlayer->px + 0.5f;
fTestY = m_pPlayer->py + 1.5f;
}
if (m_pPlayer->GetFacingDirection() == 1) // West
{
fTestX = m_pPlayer->px - 0.5f;
fTestY = m_pPlayer->py + 0.5f;
}
if (m_pPlayer->GetFacingDirection() == 2) // North
{
fTestX = m_pPlayer->px + 0.5f;
fTestY = m_pPlayer->py - 0.5f;
}
if (m_pPlayer->GetFacingDirection() == 3) // East
{
fTestX = m_pPlayer->px + 1.5f;
fTestY = m_pPlayer->py + 0.5f;
}
// Check if test point has hit a dynamic object
bool bHitSomething = false;
for (auto dyns : m_vecDynamics)
{
if (fTestX > dyns->px && fTestX < (dyns->px + 1.0f) && fTestY > dyns->py && fTestY < (dyns->py + 1.0f))
{
if (dyns->bFriendly)
{
bHitSomething = true;
// Iterate through quest stack until something responds, the base quests should capture
// interactions that are not specfied in other quests
for (auto &quest : m_listQuests)
if (quest->OnInteraction(m_vecDynamics, dyns, cQuest::TALK))
{
break;
}
// Some objects just do stuff when you interact with them
dyns->OnInteract(m_pPlayer);
// Then check if it is map related
m_pCurrentMap->OnInteraction(m_vecDynamics, dyns, cMap::TALK);
}
else
{
// Interaction was with something not friendly - only enemies
// are not friendly, so perfrom attack
m_pPlayer->PerformAttack();
}
}
}
if (!bHitSomething) // Default action is attack
{
m_pPlayer->PerformAttack();
}
}
}
}
}
else
{
// Scripting system is in control
if (m_bShowDialog)
{
if (GetKey(VK_SPACE).bReleased)
{
m_bShowDialog = false;
m_script.CompleteCommand();
}
}
}
bool bWorkingWithProjectiles = false;
for (auto &source : { &m_vecDynamics, &m_vecProjectiles })
{
for (auto &object : *source)
{
float fNewObjectPosX = object->px + object->vx * fElapsedTime;
float fNewObjectPosY = object->py + object->vy * fElapsedTime;
// Collision
float fBorder = 0.1f;
bool bCollisionWithMap = false;
if (object->vx <= 0)
{
if (m_pCurrentMap->GetSolid(fNewObjectPosX + fBorder, object->py + fBorder + 0.0f) || m_pCurrentMap->GetSolid(fNewObjectPosX + fBorder, object->py + (1.0f - fBorder)))
{
fNewObjectPosX = (int)fNewObjectPosX + 1;
object->vx = 0;
bCollisionWithMap = true;
}
}
else
{
if (m_pCurrentMap->GetSolid(fNewObjectPosX + (1.0f - fBorder), object->py + fBorder + 0.0f) || m_pCurrentMap->GetSolid(fNewObjectPosX + (1.0f - fBorder), object->py + (1.0f - fBorder)))
{
fNewObjectPosX = (int)fNewObjectPosX;
object->vx = 0;
bCollisionWithMap = true;
}
}
if (object->vy <= 0)
{
if (m_pCurrentMap->GetSolid(fNewObjectPosX + fBorder + 0.0f, fNewObjectPosY + fBorder) || m_pCurrentMap->GetSolid(fNewObjectPosX + (1.0f - fBorder), fNewObjectPosY + fBorder))
{
fNewObjectPosY = (int)fNewObjectPosY + 1;
object->vy = 0;
bCollisionWithMap = true;
}
}
else
{
if (m_pCurrentMap->GetSolid(fNewObjectPosX + fBorder + 0.0f, fNewObjectPosY + (1.0f - fBorder)) || m_pCurrentMap->GetSolid(fNewObjectPosX + (1.0f - fBorder), fNewObjectPosY + (1.0f - fBorder)))
{
fNewObjectPosY = (int)fNewObjectPosY;
object->vy = 0;
bCollisionWithMap = true;
}
}
if (object->bIsProjectile && bCollisionWithMap)
{
object->bRedundant = true;
}
float fDynamicObjectPosX = fNewObjectPosX;
float fDynamicObjectPosY = fNewObjectPosY;
// Object V Object collisions
for (auto &dyn : m_vecDynamics)
{
if (dyn != object)
{
// If the object is solid then the player must not overlap it
if (dyn->bSolidVsDyn && object->bSolidVsDyn)
{
// Check if bounding rectangles overlap
if (fDynamicObjectPosX < (dyn->px + 1.0f) && (fDynamicObjectPosX + 1.0f) > dyn->px &&
object->py < (dyn->py + 1.0f) && (object->py + 1.0f) > dyn->py)
{
// First Check Horizontally - Check Left
if (object->vx <= 0)
fDynamicObjectPosX = dyn->px + 1.0f;
else
fDynamicObjectPosX = dyn->px - 1.0f;
}
if (fDynamicObjectPosX < (dyn->px + 1.0f) && (fDynamicObjectPosX + 1.0f) > dyn->px &&
fDynamicObjectPosY < (dyn->py + 1.0f) && (fDynamicObjectPosY + 1.0f) > dyn->py)
{
// First Check Vertically - Check Left
if (object->vy <= 0)
fDynamicObjectPosY = dyn->py + 1.0f;
else
fDynamicObjectPosY = dyn->py - 1.0f;
}
}
else
{
if (object == m_vecDynamics[0])
{
// Object is player and can interact with things
if (fDynamicObjectPosX < (dyn->px + 1.0f) && (fDynamicObjectPosX + 1.0f) > dyn->px &&
object->py < (dyn->py + 1.0f) && (object->py + 1.0f) > dyn->py)
{
// First check if object is part of a quest
for (auto &quest : m_listQuests)
if (quest->OnInteraction(m_vecDynamics, dyn, cQuest::WALK))
break;
// Then check if it is map related
m_pCurrentMap->OnInteraction(m_vecDynamics, dyn, cMap::WALK);
// Finally just check the object
dyn->OnInteract(object);
}
}
else
{
if (bWorkingWithProjectiles)
{
if (fDynamicObjectPosX < (dyn->px + 1.0f) && (fDynamicObjectPosX + 1.0f) > dyn->px &&
fDynamicObjectPosY < (dyn->py + 1.0f) && (fDynamicObjectPosY + 1.0f) > dyn->py)
{
if (dyn->bFriendly != object->bFriendly)
{
// We know object is a projectile, so dyn is something
// opposite that it has overlapped with
if (dyn->bIsAttackable)
{
// Dynamic object is a creature
Damage((cDynamic_Projectile*)object, (cDynamic_Creature*)dyn);
}
}
}
}
}
}
}
}
object->px = fDynamicObjectPosX;
object->py = fDynamicObjectPosY;
}
bWorkingWithProjectiles = true;
}
for (auto &source : { &m_vecDynamics, &m_vecProjectiles })
for (auto &dyns : *source)
dyns->Update(fElapsedTime, m_pPlayer);
// Remove quests that have been completed
auto i = remove_if(m_listQuests.begin(), m_listQuests.end(), [](const cQuest* d) {return d->bCompleted; });
if (i != m_listQuests.end())
m_listQuests.erase(i);
fCameraPosX = m_pPlayer->px;
fCameraPosY = m_pPlayer->py;
// Draw Level
int nTileWidth = 16;
int nTileHeight = 16;
int nVisibleTilesX = ScreenWidth() / nTileWidth;
int nVisibleTilesY = ScreenHeight() / nTileHeight;
// Calculate Top-Leftmost visible tile
float fOffsetX = fCameraPosX - (float)nVisibleTilesX / 2.0f;
float fOffsetY = fCameraPosY - (float)nVisibleTilesY / 2.0f;
// Clamp camera to game boundaries
if (fOffsetX < 0) fOffsetX = 0;
if (fOffsetY < 0) fOffsetY = 0;
if (fOffsetX > m_pCurrentMap->nWidth - nVisibleTilesX) fOffsetX = m_pCurrentMap->nWidth - nVisibleTilesX;
if (fOffsetY > m_pCurrentMap->nHeight - nVisibleTilesY) fOffsetY = m_pCurrentMap->nHeight - nVisibleTilesY;
// Get offsets for smooth movement
float fTileOffsetX = (fOffsetX - (int)fOffsetX) * nTileWidth;
float fTileOffsetY = (fOffsetY - (int)fOffsetY) * nTileHeight;
// Draw visible tile map
for (int x = -1; x < nVisibleTilesX + 1; x++)
{
for (int y = -1; y < nVisibleTilesY + 1; y++)
{
int idx = m_pCurrentMap->GetIndex(x + fOffsetX, y + fOffsetY);
int sx = idx % 10;
int sy = idx / 10;
DrawPartialSprite(x * nTileWidth - fTileOffsetX, y * nTileHeight - fTileOffsetY, m_pCurrentMap->pSprite, sx * nTileWidth, sy * nTileHeight, nTileWidth, nTileHeight);
}
}
// Draw Object
for (auto &source : { &m_vecDynamics, &m_vecProjectiles })
for (auto &dyns : *source)
dyns->DrawSelf(this, fOffsetX, fOffsetY);
m_pPlayer->DrawSelf(this, fOffsetX, fOffsetY);
string sHealth = "HP: " + to_string(m_pPlayer->nHealth) + "/" + to_string(m_pPlayer->nHealthMax);
DisplayDialog({ sHealth }, 160, 10);
// Draw any dialog being displayed
if (m_bShowDialog)
DisplayDialog(m_vecDialogToShow, 20, 20);
return true;
}
void RPG_Engine::ShowDialog(vector<string> vecLines)
{
m_vecDialogToShow = vecLines;
m_bShowDialog = true;
}
void RPG_Engine::DisplayDialog(vector<string> vecText, int x, int y)
{
int nMaxLineLength = 0;
int nLines = vecText.size();
for (auto l : vecText) if (l.size() > nMaxLineLength) nMaxLineLength = l.size();
// Draw Box
Fill(x - 1, y - 1, x + nMaxLineLength * 8 + 1, y + nLines * 8 + 1, PIXEL_SOLID, FG_DARK_BLUE);
DrawLine(x - 2, y - 2, x - 2, y + nLines * 8 + 1);
DrawLine(x + nMaxLineLength * 8 + 1, y - 2, x + nMaxLineLength * 8 + 1, y + nLines * 8 + 1);
DrawLine(x - 2, y - 2, x + nMaxLineLength * 8 + 1, y - 2);
DrawLine(x - 2, y + nLines * 8 + 1, x + nMaxLineLength * 8 + 1, y + nLines * 8 + 1);
for (int l = 0; l<vecText.size(); l++)
DrawBigText(vecText[l], x, y + l * 8);
}
void RPG_Engine::DrawBigText(string sText, int x, int y)
{
int i = 0;
for (auto c : sText)
{
int sx = ((c - 32) % 16) * 8;
int sy = ((c - 32) / 16) * 8;
DrawPartialSprite(x + i * 8, y, m_sprFont, sx, sy, 8, 8);
i++;
}
}
void RPG_Engine::ChangeMap(string sMapName, float x, float y)
{
// Destroy all dynamics
m_vecDynamics.clear();
m_vecDynamics.push_back(m_pPlayer);
// Set current map
m_pCurrentMap = RPG_Assets::get().GetMap(sMapName);
// Update player location
m_pPlayer->px = x;
m_pPlayer->py = y;
// Create new dynamics from map
m_pCurrentMap->PopulateDynamics(m_vecDynamics);
// Create new dynamics from quests
for (auto q : m_listQuests)
q->PopulateDynamics(m_vecDynamics, m_pCurrentMap->sName);
}
void RPG_Engine::AddQuest(cQuest* quest)
{
m_listQuests.push_front(quest);
}
bool RPG_Engine::GiveItem(cItem* item)
{
//m_script.AddCommand(new cCommand_ShowDialog({ "You have found a" , item->sName }));
m_listItems.push_back(item);
return true;
}
bool RPG_Engine::TakeItem(cItem* item)
{
if (item != nullptr)
{
m_listItems.erase(find(m_listItems.begin(), m_listItems.end(), item));
return true;
}
else
return false;
}
bool RPG_Engine::HasItem(cItem* item)
{
if (item != nullptr)
return find(m_listItems.begin(), m_listItems.end(), item) != m_listItems.end();
else
return false;
}
bool RPG_Engine::UpdateInventory(float fElapsedTime)
{
Fill(0, 0, ScreenWidth(), ScreenHeight(), L' ');
DrawBigText("INVENTORY", 4, 4);
int i = 0;
cItem* highlighted = nullptr;
// Draw Consumables
for (auto &item : m_listItems)
{
int x = i % 4;
int y = i / 4;
i++;
DrawPartialSprite(8 + x * 20, 20 + y * 20, item->pSprite, 0, 0, 16, 16);
if (m_nInvSelectX == x && m_nInvSelectY == y)
highlighted = item;
}
// Draw selection reticule
DrawLine(6 + (m_nInvSelectX) * 20, 18 + (m_nInvSelectY) * 20, 6 + (m_nInvSelectX + 1) * 20, 18 + (m_nInvSelectY) * 20);
DrawLine(6 + (m_nInvSelectX) * 20, 18 + (m_nInvSelectY + 1) * 20, 6 + (m_nInvSelectX + 1) * 20, 18 + (m_nInvSelectY + 1) * 20);
DrawLine(6 + (m_nInvSelectX) * 20, 18 + (m_nInvSelectY) * 20, 6 + (m_nInvSelectX) * 20, 18 + (m_nInvSelectY + 1) * 20);
DrawLine(6 + (m_nInvSelectX + 1) * 20, 18 + (m_nInvSelectY) * 20, 6 + (m_nInvSelectX + 1) * 20, 18 + (m_nInvSelectY + 1) * 20);
if (GetKey(VK_LEFT).bReleased) m_nInvSelectX--;
if (GetKey(VK_RIGHT).bReleased) m_nInvSelectX++;
if (GetKey(VK_UP).bReleased) m_nInvSelectY--;
if (GetKey(VK_DOWN).bReleased) m_nInvSelectY++;
if (m_nInvSelectX < 0) m_nInvSelectX = 3;
if (m_nInvSelectX >= 4) m_nInvSelectX = 0;
if (m_nInvSelectY < 0) m_nInvSelectY = 3;
if (m_nInvSelectY >= 4) m_nInvSelectY = 0;
if (GetKey(L'Z').bReleased)
m_nGameMode = MODE_LOCAL_MAP;
DrawBigText("SELECTED:", 8, 160);
if (highlighted != nullptr)
{
DrawBigText("SELECTED:", 8, 160);
DrawBigText(highlighted->sName, 8, 170);
DrawBigText("DESCRIPTION:", 8, 190);
DrawBigText(highlighted->sDescription, 8, 200);
if (!highlighted->bKeyItem)
{
DrawBigText("(Press SPACE to use)", 80, 160);
}
if (GetKey(VK_SPACE).bReleased)
{
// Use selected item
if (!highlighted->bKeyItem)
{
if (highlighted->OnUse(m_pPlayer))
{
// Item has signalled it must be consumed, so remove it
TakeItem(highlighted);
}
}
else
{
}
}
}
DrawBigText("LOCATION:", 128, 8);
DrawBigText(m_pCurrentMap->sName, 128, 16);
DrawBigText("HEALTH: " + to_string(m_pPlayer->nHealth), 128, 32);
DrawBigText("MAX HEALTH: " + to_string(m_pPlayer->nHealthMax), 128, 40);
return true;
}
void RPG_Engine::Attack(cDynamic_Creature* aggressor, cWeapon *weapon)
{
weapon->OnUse(aggressor);
}
void RPG_Engine::AddProjectile(cDynamic_Projectile *proj)
{
m_vecProjectiles.push_back(proj);
}
void RPG_Engine::Damage(cDynamic_Projectile* projectile, cDynamic_Creature* victim)
{
if (victim != nullptr)
{
// Attack victim with damage
victim->nHealth -= projectile->nDamage;
// Knock victim back
float tx = victim->px - projectile->px;
float ty = victim->py - projectile->py;
float d = sqrtf(tx*tx + ty * ty);
if (d < 1) d = 1.0f;
// After a hit, they object experiences knock back, where it is temporarily
// under system control. This delivers two functions, the first being
// a visual indicator to the player that something has happened, and the second
// it stops the ability to spam attacks on a single creature
victim->KnockBack(tx / d, ty / d, 0.2f);
if (victim != m_pPlayer)
{
victim->OnInteract(m_pPlayer);
}
else
{
// We must ensure the player is never pushed out of bounds by the physics engine. This
// is a bit of a hack, but it allows knockbacks to occur providing there is an exit
// point for the player to be knocked back into. If the player is "mobbed" then they
// become trapped, and must fight their way out
victim->bSolidVsDyn = true;
}
if (projectile->bOneHit)
projectile->bRedundant = true;
}
}