#include "map.hpp" #include "dbc.hpp" #include #include #include #include "rand.hpp" #include using std::vector, std::pair; using namespace fmt; void dump_map(const std::string &msg, Matrix &map) { println("----------------- {}", msg); for(auto row : map) { for(auto col : row) { print("{} ", col); } print("\n"); } } inline void add_neighbors(PointList &neighbors, Matrix &closed, size_t y, size_t x) { size_t h = closed.size(); size_t w = closed[0].size(); vector rows{y - 1, y, y + 1}; vector cols{x - 1, x, x + 1}; for(size_t row : rows) { for(size_t col : cols) { if((0 <= row && row < h) && (0 <= col && col < w) && closed[row][col] == 0) { closed[row][col] = 1; neighbors.push_back({.x=col, .y=row}); } } } } /** * This will create an _inverted_ map that you * can run make_rooms and generate on. It will * NOT be valid until you actually run generate. */ Map::Map(size_t width, size_t height) : $limit(1000), $width(width), $height(height), $input_map(height, MatrixRow(width, 1)), $walls(height, MatrixRow(width, INV_WALL)), $paths(height, MatrixRow(width, 1)), $lightmap(height, MatrixRow(width, 0)) { } // make explicit Map::Map(Matrix input_map, Matrix walls_map, int limit) : $limit(limit), $input_map(input_map), $walls(walls_map) { $width = $walls[0].size(); $height = $walls.size(); } void Map::make_paths() { size_t h = $input_map.size(); size_t w = $input_map[0].size(); // Initialize the new array with every pixel at limit distance // NOTE: this is normally ones() * limit int limit = $limit == 0 ? h * w : $limit; Matrix new_arr = Matrix(h, MatrixRow(w, limit)); Matrix closed = $walls; PointList starting_pixels; PointList open_pixels; // First pass: Add starting pixels and put them in closed for(size_t counter = 0; counter < h * w; counter++) { size_t x = counter % w; size_t y = counter / w; if($input_map[y][x] == 0) { new_arr[y][x] = 0; closed[y][x] = 1; starting_pixels.push_back({.x=x,.y=y}); } } // Second pass: Add border to open for(auto sp : starting_pixels) { add_neighbors(open_pixels, closed, sp.y, sp.x); } // Third pass: Iterate filling in the open list int counter = 1; // leave this here so it's available below for(; counter < limit && !open_pixels.empty(); ++counter) { PointList next_open; for(auto sp : open_pixels) { new_arr[sp.y][sp.x] = counter; add_neighbors(next_open, closed, sp.y, sp.x); } open_pixels = next_open; } // Last pass: flood last pixels for(auto sp : open_pixels) { new_arr[sp.y][sp.x] = counter; } $paths = new_arr; } void Map::make_room(size_t origin_x, size_t origin_y, size_t w, size_t h) { dbc::pre("x out of bounds", origin_x < $width); dbc::pre("y out of bounds", origin_y < $height); dbc::pre("w out of bounds", w <= $width); dbc::pre("h out of bounds", h <= $height); for(size_t y = origin_y; y < origin_y + h; ++y) { dbc::check(y < $walls.size(), "y is out of bounds"); for(size_t x = origin_x; x < origin_x + w; ++x) { dbc::check(x < $walls[y].size(), "x is out of bounds"); $walls[y][x] = INV_SPACE; } } } inline int make_split(Room &cur, bool horiz) { size_t dimension = horiz ? cur.height : cur.width; int min = dimension / 4; int max = dimension - min; return Random::uniform(min, max); } void Map::partition_map(Room &cur, int depth) { if(cur.width >= 5 && cur.width <= 10 && cur.height >= 5 && cur.height <= 10) { $rooms.push_back(cur); return; } bool horiz = cur.width > cur.height ? false : true; int split = make_split(cur, horiz); Room left = cur; Room right = cur; if(horiz) { dbc::check(split > 0, "split is not > 0"); dbc::check(split < int(cur.height), "split is too big!"); left.height = size_t(split - 1); right.y = cur.y + split; right.height = size_t(cur.height - split); } else { dbc::check(split > 0, "split is not > 0"); dbc::check(split < int(cur.width), "split is too big!"); left.width = size_t(split-1); right.x = cur.x + split, right.width = size_t(cur.width - split); } if(depth > 0 && left.width > 5 && left.height > 5) { partition_map(left, depth-1); } if(depth > 0 && right.width > 5 && right.height > 5) { partition_map(right, depth-1); } } void Map::place_rooms(Room &cur) { for(auto &cur : $rooms) { cur.x += 2; cur.y += 2; cur.width -= 4; cur.height -= 4; add_door(cur); set_door(cur, INV_SPACE); make_room(cur.x, cur.y, cur.width, cur.height); } } bool Map::neighbors(Point &out, bool greater) { std::array dirs{{ {out.x,out.y-1}, {out.x+1,out.y}, {out.x,out.y+1}, {out.x-1,out.y} }}; int zero_i = -1; int cur = $paths[out.y][out.x]; // BUG: sometimes cur is in a wall so finding neighbors fails for(size_t i = 0; i < dirs.size(); ++i) { Point dir = dirs[i]; int target = inmap(dir.x, dir.y) ? $paths[dir.y][dir.x] : $limit; if(target == $limit) continue; // skip unpathable stuff int diff = cur - target; if(diff == 1) { out = {.x=dir.x, .y=dir.y}; return true; } else if(diff == 0) { zero_i = i; } } if(zero_i != -1) { out = {.x=dirs[zero_i].x, .y=dirs[zero_i].y}; return true; } else { return false; } } bool Map::inmap(size_t x, size_t y) { return x < $width && y < $height; } void Map::set_door(Room &room, int value) { $walls[room.entry.y][room.entry.x] = value; $walls[room.exit.y][room.exit.x] = value; } void rand_side(Room &room, Point &door) { int rand_x = Random::uniform(0, room.width - 1); int rand_y = Random::uniform(0, room.height - 1); switch(Random::uniform(0,3)) { case 0: // north door.x = room.x + rand_x; door.y = room.y-1; break; case 1: // south door.x = room.x + rand_x; door.y = room.y + room.height; break; case 2: // east door.x = room.x + room.width; door.y = room.y + rand_y; break; case 3: // west door.x = room.x - 1; door.y = room.y + rand_y; break; default: dbc::sentinel("impossible side"); } } void Map::add_door(Room &room) { rand_side(room, room.entry); rand_side(room, room.exit); } bool Map::walk(Point &src, Point &target) { // this sets the target for the path dbc::check($input_map[target.y][target.x] == 0, "target point not set to 0"); $walls[src.y][src.x] = INV_WALL; $walls[target.y][target.x] = INV_WALL; // for the walk this needs to be walls since it's inverted? dbc::check($walls[src.y][src.x] == INV_WALL, "src room has a wall at exit door"); dbc::check($walls[target.y][target.x] == INV_WALL, "target room has a wall at entry door"); make_paths(); bool found = false; Point out{src.x, src.y}; int count = 0; do { $walls[out.y][out.x] = INV_SPACE; found = neighbors(out, true); if($paths[out.y][out.x] == 0) { $walls[out.y][out.x] = INV_SPACE; return true; } } while(found && out.x > 0 && out.y > 0 && ++count < 100); return false; } void Map::set_target(const Point &at, int value) { $input_map[at.y][at.x] = 0; } void Map::clear_light_target(const Point &at) { $input_map[at.y][at.x] = 1; } void Map::clear_target(const Point &at) { $input_map[at.y][at.x] = 1; } Point Map::place_entity(size_t room_index) { dbc::check(room_index < $rooms.size(), "room_index is out of bounds, not enough rooms"); Room &start = $rooms[room_index]; return {start.x+1, start.y+1}; } void Map::generate() { Room root{ .x = 0, .y = 0, .width = $width, .height = $height }; partition_map(root, 10); place_rooms(root); for(size_t i = 0; i < $rooms.size() - 1; i++) { Room &src = $rooms[i]; Room &target = $rooms[i+1]; set_target(target.entry); bool found = walk(src.exit, target.entry); if(!found) { println("ROOM NOT FOUND!"); } clear_target(target.entry); } Room &src = $rooms.back(); Room &target = $rooms.front(); set_target(target.entry); walk(src.exit, target.entry); clear_target(target.entry); for(size_t y = 0; y < $height; ++y) { for(size_t x = 0; x < $width; ++x) { $walls[y][x] = !$walls[y][x]; } } } bool Map::iswall(size_t x, size_t y) { return $walls[y][x] == WALL_VALUE; } void Map::dump() { dump_map("PATHS", $paths); dump_map("WALLS", $walls); dump_map("INPUT", $input_map); } bool Map::can_move(Point move_to) { return inmap(move_to.x, move_to.y) && !iswall(move_to.x, move_to.y); } Point Map::map_to_camera(const Point &loc, const Point &cam_orig) { return {loc.x - cam_orig.x, loc.y - cam_orig.y}; } Point Map::center_camera(const Point &around, size_t view_x, size_t view_y) { int high_x = int(width() - view_x); int high_y = int(height() - view_y); int center_x = int(around.x - view_x / 2); int center_y = int(around.y - view_y / 2); // BUG: is clamp really the best thing here? this seems wrong. size_t start_x = high_x > 0 ? std::clamp(center_x, 0, high_x) : 0; size_t start_y = high_y > 0 ? std::clamp(center_y, 0, high_y) : 0; return {start_x, start_y}; } void Map::reset_light() { for(auto &row : $lightmap) { for(size_t i = 0; i < row.size(); i++) { row[i] = lighting::MIN; } } } void Map::set_light_target(const Point &at, int value) { set_target(at, value); } void Map::path_light() { make_paths(); } void Map::light_box(LightSource source, Point from, Point &min_out, Point &max_out) { using std::min, std::max; min_out.x = max(int(from.x), source.distance) - source.distance; max_out.x = min(from.x + source.distance, width() - 1); min_out.y = max(int(from.y), source.distance) - source.distance; max_out.y = min(from.y + source.distance, width() - 1); } int Map::light_level(int level, size_t x, size_t y) { size_t at = level + $paths[y][x]; int cur_level = $lightmap[y][x]; int new_level = at < lighting::LEVELS.size() ? lighting::LEVELS[at] : lighting::MIN; return cur_level < new_level ? new_level : cur_level; } void Map::render_light(LightSource source, Point at) { const int UNPATH = $limit; Point min, max; light_box(source, at, min, max); clear_light_target(at); vector has_light; for(size_t x = min.x; x <= max.x; ++x) { for(size_t y = min.y; y <= max.y; ++y) { if($paths[y][x] != UNPATH) { $lightmap[y][x] = light_level(source.strength, x, y); has_light.push_back({x,y}); } } } const int wall_light = source.strength+3; for(auto point : has_light) { for(int i = -1; i <= 1; i++) { for(int j = -1; j <= 1; j++) { if(!inmap(point.x+i, point.y+j)) continue; if($paths[point.y+j][point.x+i] == UNPATH) { $lightmap[point.y+j][point.x+i] = light_level(wall_light, point.x, point.y); } } } } }