You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

723 lines
17 KiB
C++

#include <fstream>
#include <algorithm>
#include <string>
#include <iostream>
#include <vector>
#include "RLGame.hpp"
#include "utils.hpp"
#include "Map.hpp"
#include "Entity.hpp"
#ifdef _DEBUG
#include <chrono>
#endif // _DEBUG
void CreateMapRooms(std::vector<MapRoom>& rooms, int amount);
struct MapDoor
{
Position pos;
bool isConnected;
short direction;
};
struct MapRoom
{
MapRoom(int dx, int dy, int dwidth, int dheight);
MapRoom(MapRoom&& other) noexcept;
MapRoom(const MapRoom& other) = delete;
MapRoom& operator=(const MapRoom& other) = delete;
MapRoom& operator=(MapRoom&& other) noexcept = delete;
~MapRoom();
int x;
int y;
int width;
int height;
int amountDoors;
MapDoor* doors;
};
// fuck VC++
//const std::array<wchar_t, 6> Map::WALLS_ARRAY = { L'\x2514', L'\x250c', L'\x2510', L'\x2518', L'\x2500', L'\x2502' };
Map::Map()
: Map("Default", false)
{
}
Map::Map(const std::string& name, bool rooms, const Position& position)
: m_DownStairsPos{}
, m_MapName{ "RandomLevel-" + name }
, m_pTiles{ new std::wstring[G_MAP_HEIGHT] }
//, m_Items{}
, m_IsRooms{ rooms }
{
G_CURRENT_MAP = this; // will always only have 1 map loaded
std::wifstream mapFile{ ("resources/maps/" + m_MapName + ".level") };
if(mapFile) // load if exists
LoadMap(mapFile);
else // generate if not
GenerateMap(position);
}
Map::Map(Map&& other) noexcept
: m_DownStairsPos{ other.m_DownStairsPos }
, m_MapName{ other.m_MapName }
, m_pTiles{ other.m_pTiles }
//, m_Items{ other.m_Items }
, m_IsRooms{ other.m_IsRooms }
{
other.m_pTiles = nullptr;
}
#ifdef _DEBUG
#define _DEBUG_NO_MAP_
#endif // _DEBUG
Map::~Map()
{
if(m_pTiles != nullptr)
{
#ifndef _DEBUG_NO_MAP_
SaveMap();
#endif // _DEBUG_NO_MAP_
delete[] m_pTiles;
m_pTiles = nullptr;
}
//m_Items.clear();
}
void Map::LoadMap(std::wifstream& file)
{
std::cout << "Loading map " << m_MapName << "\n";
std::wstring line;
if(file.is_open())
{
int id{};
while(std::getline(file, line))
{
m_pTiles[id] = line;
++id;
}
file.close();
bool foundStairs{};
bool foundRooms{};
for(int y{}; y < G_MAP_HEIGHT; ++y)
{
for(int x{}; x < G_MAP_WIDTH; ++x)
{
if(m_pTiles[y][x] == L'>')
{
m_DownStairsPos.x = x;
m_DownStairsPos.y = y;
foundStairs = true;
}
else if(m_pTiles[y][x] == WALLS_ARRAY[0])
{
m_IsRooms = true;
foundRooms = true;
}
if(foundStairs && foundRooms)
goto finished;
}
}
finished:
return;
}
else
{
std::cerr << "\nMap " << m_MapName << " couldn't be read\n";
GenerateMap(Position{ G_SCREEN_WIDTH / 2, G_SCREEN_HEIGHT / 2 });
}
}
void Map::GenerateMap(const Position& position)
{
std::cout << "Generating new map...\n";
std::wstring line{};
Position start{ !(position == Position{-1, -1}) ? position : Position{G_SCREEN_WIDTH / 2, G_SCREEN_HEIGHT / 2} };
const Position reset{ start };
#ifdef _DEBUG
std::chrono::steady_clock::time_point t1{ std::chrono::steady_clock::now() };
std::chrono::steady_clock::time_point t2{};
#endif // _DEBUG
// fill the level with blocks
for(int x{}; x < G_MAP_WIDTH; ++x)
{
line += G_MAPTILE_WALL;
}
for(int y{}; y < G_MAP_HEIGHT; ++y)
{
m_pTiles[y] = line;
}
if(m_IsRooms)
{
// put new stuff here
// copy CreateRooms from Game.cpp, make it generate all rooms and store in vector, insert in level string, delete vector, save map, should work :)
std::vector<MapRoom> rooms{};
rooms.push_back(MapRoom{start.x - 1, start.y - 1, 5, 5}); // starting room
//m_Tiles[start.y - 2][start.x - 2] = L'<';
//m_Tiles[start.y - 2][start.x - 1] = L'>';
int amountToGenerate{ utils::RandomBetween(9, 25) };
CreateMapRooms(rooms, amountToGenerate);
// put in level string, copy from Room.cpp Draw()
for(auto& room : rooms)
{
for(int dx{ room.x }; dx < room.x + room.width; ++dx)
{
if(dx == room.x)
{
/*terminal_put(dx, room.y, L'┌');
terminal_put(dx, room.y + room.height - 1, L'└');*/
m_pTiles[room.y][dx] = L'\x250c';
m_pTiles[room.y + room.height - 1][dx] = L'\x2514';
}
else if(dx == room.x + room.width - 1)
{
/*terminal_put(dx, room.y, L'┐');
terminal_put(dx, room.y + room.height - 1, L'┘');*/
m_pTiles[room.y][dx] = L'\x2510';
m_pTiles[room.y + room.height - 1][dx] = L'\x2518';
}
else
{
//terminal_put(dx, room.y, L'─');
//terminal_put(dx, room.y + room.height - 1, L'─');
m_pTiles[room.y][dx] = L'\x2500';
m_pTiles[room.y + room.height - 1][dx] = L'\x2500';
}
}
for(int dy{ room.y + 1 }; dy < room.y + room.height - 1; ++dy)
{
/*terminal_put(room.x, dy, L'│');
terminal_put(room.x + room.width - 1, dy, L'│');*/
m_pTiles[dy][room.x] = L'\x2502';
m_pTiles[dy][room.x + room.width - 1] = L'\x2502';
for(int dx{ room.x + 1 }; dx < room.x + room.width - 1; ++dx)
{
//terminal_put(dx, dy, '.');
m_pTiles[dy][dx] = L'.';
}
}
for(int i{}; i < room.amountDoors; ++i)
{
//terminal_put(room.doors[i].x, room.doors[i].y, MAPTILE_DOOR);
m_pTiles[room.doors[i].pos.y][room.doors[i].pos.x] = G_MAPTILE_DOOR;
// NetHack style doors
/*switch(room.doors[i].direction)
{
case 0:
case 1:
m_Tiles[room.doors[i].pos.y][room.doors[i].pos.x] = L'|';
break;
case 2:
case 3:
m_Tiles[room.doors[i].pos.y][room.doors[i].pos.x] = L'─';
break;
}*/
}
}
/// TODO: fix all, read comments
// modified utils::Bresenham to insert in m_Tiles, should use an actual pathfinding algorithm instead of a line drawing algorithm
// needs more work but just testing
Position doorPos1{ rooms[0].doors[0].pos };
Position doorPos2{ rooms[1].doors[0].pos };
switch(rooms[0].doors[0].direction) // get tile in front of door
{
case 0: // up
doorPos1.y -= 1;
break;
case 1: // down
doorPos1.y += 1;
break;
case 2: // left
doorPos1.x -= 1;
break;
case 3: // right
doorPos1.x += 1;
break;
}
switch(rooms[1].doors[0].direction)
{
case 0: // up
doorPos2.y -= 1;
break;
case 1: // down
doorPos2.y += 1;
break;
case 2: // left
doorPos2.x -= 1;
break;
case 3: // right
doorPos2.x += 1;
break;
}
BresenhamMap(doorPos1.x, doorPos1.y, doorPos2.x, doorPos2.y);
// stairs
m_pTiles[start.y][start.x] = L'<';
m_pTiles[start.y + 2][start.x + 1] = L'>';
m_DownStairsPos.x = start.x + 1;
m_DownStairsPos.y = start.y + 2;
// vector isn't needed anymore
rooms.clear();
#ifdef _DEBUG
t2 = std::chrono::steady_clock::now();
float elapsedSec{ std::chrono::duration<float>(t2 - t1).count() };
std::cout << "Time to generate: " << elapsedSec << '\n';
#endif // _DEBUG
}
else
{
int amountGenerated{};
int amountToGenerate{ utils::RandomBetween(G_MAP_HEIGHT * G_MAP_WIDTH / 6, G_MAP_HEIGHT * G_MAP_WIDTH / 4) - 10000 };
int x{};
int y{};
const short horizontalDirection{ static_cast<short>(utils::RandomBetween(0, 10)) }; // the map will be more likely to generate in this direction
const short verticalDirection{ static_cast<short>(utils::RandomBetween(0, 10)) };
/*
0 - RIGHT
1 - LEFT
0 - UP
1 - DOWN
2 - CENTER
(any number above 2 is also CENTER for vertical, more chance for center)
*/
#ifdef _DEBUG
const char* hDirString[] = { "RIGHT", "LEFT", "CENTER" };
const char* vDirString[] = { "UP", "DOWN", "CENTER" };
if(horizontalDirection < 3)
std::cout << "HORIZONTAL DIRECTION: " << hDirString[horizontalDirection] << '\n';
else
std::cout << "HORIZONTAL DIRECTION: " << hDirString[2] << '\n';
if(verticalDirection < 3)
std::cout << "VERTICAL DIRECTION: " << vDirString[verticalDirection] << '\n';
else
std::cout << "VERTICAL DIRECTION: " << vDirString[2] << '\n';
#endif // _DEBUG
m_pTiles[start.y][start.x] = L'<';
do
{
x = 0;
y = 0;
if(utils::RandomBetween(0, 1) == 1)
{
x = utils::RandomBetween(-1, 1); // will sometimes be 0
if(x == 0)
{
switch(horizontalDirection)
{
case 0:
++x;
break;
case 1:
--x;
break;
case 2:
default:
if(utils::RandomBetween(0, 1) == 0)
++x;
else
--x;
break;
}
}
if(start.x + x < 1 || start.x + x >= G_MAP_WIDTH - 1)
{
/*start.x = SCREEN_WIDTH / 2;
start.y = SCREEN_HEIGHT / 2;*/
start = reset;
}
start.x += x;
}
else
{
y = utils::RandomBetween(-1, 1);
if(y == 0)
{
switch(verticalDirection)
{
case 0:
--y;
break;
case 1:
++y;
break;
case 2:
default:
if(utils::RandomBetween(0, 1) == 0)
++y;
else
--y;
break;
}
}
if(start.y + y < 1 || start.y + y >= G_MAP_HEIGHT - 1)
{
/*start.x = SCREEN_WIDTH / 2;
start.y = SCREEN_HEIGHT / 2;*/
start = reset;
}
start.y += y;
}
int tileStyle{};
if(m_pTiles[start.y][start.x] == G_MAPTILE_WALL)
{
tileStyle = utils::RandomBetween(0, 3);
wchar_t tile{};
switch(tileStyle)
{
case 0:
tile = L',';
break;
case 1:
tile = L'\'';
break;
case 2:
tile = L'`';
break;
case 3:
tile = L'.';
break;
}
m_pTiles[start.y][start.x] = tile;
--amountToGenerate;
/*if(amountToGenerate == 50 && position != Position{ -1, -1 })
{
m_Tiles[start.y][start.x] = L'<';
}
else*/ if(amountToGenerate == 250)
{
m_pTiles[start.y][start.x] = L'>';
m_DownStairsPos = start;
}
}
++amountGenerated;
}
while(amountToGenerate != 0 && amountGenerated < 20000);
#ifdef _DEBUG
t2 = std::chrono::steady_clock::now();
float elapsedSec{ std::chrono::duration<float>(t2 - t1).count() };
std::cout << "Time to generate: " << elapsedSec << '\n' << "Times called: " << amountGenerated << '\n';
#endif // _DEBUG
}
}
void Map::SaveMap()
{
std::wfstream file{ "resources/maps/" + m_MapName + ".level", std::ios::out | std::ios::trunc };
if(file.is_open())
{
std::cout << "Saving map as " << m_MapName << '\n';
for(int id{}; id < G_MAP_HEIGHT; ++id)
{
file << m_pTiles[id] << '\n';
}
}
else
std::wcerr << "\nCouldn't write to file\n";
}
void Map::Draw(const Position& playerPos) const
{
wchar_t tile;
// could change this so it doesn't draw parts of the map that can't even be seen
// ok did it
//Position onScreenPos{ playerPos.x + G_OFFSET_X, playerPos.y + G_OFFSET_Y }; // where the player actually is the screen
const int halfWidth{ G_SCREEN_WIDTH / 2 };
const int halfHeight{ G_SCREEN_HEIGHT / 2 }; // player is always at the center of the screen, so only need to remove half of it from playerPos
for(int y{}; y < G_SCREEN_HEIGHT - G_SCREEN_BOTTOM; ++y)
{
int yPos{ playerPos.y - halfHeight + y };
if(yPos >= G_MAP_HEIGHT || yPos < 0)
continue;
for(int x{}; x < G_SCREEN_WIDTH; ++x)
{
int xPos{ playerPos.x - halfWidth + x };
if(xPos >= G_MAP_WIDTH || xPos < 0)
continue;
tile = wchar_t(CheckTile(xPos, yPos));
terminal_color(G_COL_GRAY);
terminal_bkcolor(color_from_name("floor"));
if(G_CH_BITS[G_CH_CODES::FUNNYMODE])
{
terminal_color(utils::RandomColor());
}
else
{
switch(tile)
{
case G_MAPTILE_WALL:
terminal_color(color_from_name("walls"));
break;
case L'>':
case L'<':
terminal_color(G_COL_AMBER);
break;
}
}
terminal_put(xPos + G_OFFSET_X, yPos + G_OFFSET_Y, tile);
}
}
terminal_color(G_COL_WHITE);
terminal_bkcolor(G_COL_BLACK);
}
/*
int Map::GetId(int x, int y) const
{
return y * (MAP_WIDTH) + x;
}
*/
int Map::CheckTile(int x, int y) const
{
/*if(x >= G_MAP_WIDTH || x < 0 || y >= G_MAP_HEIGHT || y < 0)
return 0;
else*/
return m_pTiles[y][x];
}
int Map::CheckTile(const Position& pos) const
{
return m_pTiles[pos.y][pos.x];
}
std::string Map::GetName() const
{
return m_MapName;
}
bool Map::IsRoomLevel() const
{
return m_IsRooms;
}
// Creates randomly generated rooms with random amount
// Also checks if they intersect or appear out of the map, in which case it will be discarded and another one will be generated
void CreateMapRooms(std::vector<MapRoom>& rooms, int amount)
{
//int amountToGenerate{ utils::RandomBetween(10, 25) };
bool inWindow;
bool noOverlap;
bool isOk;
int width;
int height;
int x;
int y;
for(int index{}; index < amount; ++index)
{
// generate a room
do
{
inWindow = true;
noOverlap = true;
isOk = true;
width = utils::RandomBetween(4, 20);
height = utils::RandomBetween(4, 10);
x = utils::RandomBetween(3, G_MAP_WIDTH / 2 - width - 1);
y = utils::RandomBetween(3, G_MAP_HEIGHT / 2 - height - 1);
// check if overlaps
for(size_t id{}; id < rooms.size(); ++id)
{
if(rooms.size() < 1)
break;
noOverlap = !utils::IsOverlapping(x, y, width, height, rooms[id].x - 1, rooms[id].y - 1, rooms[id].width + 2, rooms[id].height + 2);
if(!noOverlap)
break;
}
// check if out of the window
if(x + width >= G_MAP_WIDTH || x < 0 || y + height >= G_MAP_HEIGHT || y < 0)
inWindow = false;
else
inWindow = true;
isOk = inWindow && noOverlap;
}
while(!isOk);
rooms.push_back(MapRoom{ x, y, width, height });
}
}
// From http://www.roguebasin.com/index.php?title=Bresenham%27s_Line_Algorithm
// and https://www.cs.helsinki.fi/group/goa/mallinnus/lines/bresenh.html
void Map::BresenhamMap(int x1, int y1, const int x2, const int y2)
{
int delta_x(x2 - x1);
// if x1 == x2, then it does not matter what we set here
const signed char ix((delta_x > 0) - (delta_x < 0));
delta_x = std::abs(delta_x) << 1;
int delta_y(y2 - y1);
// if y1 == y2, then it does not matter what we set here
const signed char iy((delta_y > 0) - (delta_y < 0));
delta_y = std::abs(delta_y) << 1;
//plot(x1, y1);
//std::cout << "\nx1: " << x1 << " y1: " << y1 << '\n';
//terminal_put(x1, y1, L'B');
if(m_pTiles[y1][x1] == G_MAPTILE_WALL)
m_pTiles[y1][x1] = G_MAPTILE_CORRIDOR;
//terminal_refresh();
if(delta_x >= delta_y)
{
// error may go below zero
int error(delta_y - (delta_x >> 1));
while(x1 != x2)
{
// reduce error, while taking into account the corner case of error == 0
if((error > 0) || (!error && (ix > 0)))
{
error -= delta_x;
y1 += iy;
}
// else do nothing
error += delta_y;
x1 += ix;
//plot(x1, y1);
//std::cout << "\nx1: " << x1 << " y1: " << y1 << '\n';
//terminal_put(x1 + G_OFFSET_X, y1 + G_OFFSET_Y, L'x');
if(m_pTiles[y1][x1] == G_MAPTILE_WALL)
m_pTiles[y1][x1] = G_MAPTILE_CORRIDOR;
//terminal_refresh();
}
}
else
{
// error may go below zero
int error(delta_x - (delta_y >> 1));
while(y1 != y2)
{
// reduce error, while taking into account the corner case of error == 0
if((error > 0) || (!error && (iy > 0)))
{
error -= delta_y;
x1 += ix;
}
// else do nothing
error += delta_x;
y1 += iy;
//plot(x1, y1);
//std::cout << "\nx1: " << x1 << " y1: " << y1 << '\n';
//terminal_put(x1 + G_OFFSET_X, y1 + G_OFFSET_Y, L'x');
//terminal_refresh();
if(m_pTiles[y1][x1] == G_MAPTILE_WALL)
m_pTiles[y1][x1] = G_MAPTILE_CORRIDOR;
}
}
}
/// MapRoom struct stuff ///
MapRoom::MapRoom(int dx, int dy, int dwidth, int dheight)
: x{ dx }
, y{ dy }
, width{ dwidth }
, height{ dheight }
, amountDoors{ utils::RandomBetween(1, 4) }
, doors{ new MapDoor[amountDoors] }
{
//doors = new Position[amountDoors];
//std::wcout << "ROOM:\nx: " << std::to_wstring(x) << "\ny: " << std::to_wstring(y) << "\nwidth: " << std::to_wstring(width) << "\nheight: " << std::to_wstring(height);
//std::wcout << "\namount of doors: " << std::to_wstring(amountDoors) << '\n';
int doorPos{};
int tempPos{};
std::vector<int> positions{};
for(int i{}; i < amountDoors; ++i)
{
// this is to prevent multiple doors from being placed in the same spot
do
{
tempPos = utils::RandomBetween(0, 3);
}
while(std::find(positions.begin(), positions.end(), tempPos) != positions.end());
positions.push_back(tempPos);
doorPos = tempPos;
doors[i].direction = doorPos;
switch(doorPos)
{
// changing these to RandomBetweenSimple would barely speed up room level generation
case 0: // up
doors[i].pos.x = utils::RandomBetween(1, width - 2) + x;
doors[i].pos.y = y;
break;
case 1: // down
doors[i].pos.x = utils::RandomBetween(1, width - 2) + x;
doors[i].pos.y = y + height - 1;
break;
case 2: // left
doors[i].pos.x = x;
doors[i].pos.y = utils::RandomBetween(1, height - 2) + y;
break;
case 3: // right
doors[i].pos.x = x + width - 1;
doors[i].pos.y = utils::RandomBetween(1, height - 2) + y;
break;
}
}
positions.clear();
}
MapRoom::MapRoom(MapRoom&& other) noexcept
: x{ other.x }
, y{ other.y }
, width{ other.width }
, height{ other.height }
, amountDoors{ other.amountDoors }
, doors{ other.doors }
{
other.doors = nullptr;
}
MapRoom::~MapRoom()
{
delete[] doors;
}