The next little game in the series where I make a fancy rogue game.
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.
 
 
 
 
 
 
roguish/worldbuilder.cpp

327 lines
8.8 KiB

#include "worldbuilder.hpp"
#include "rand.hpp"
#include <fmt/core.h>
#include <iostream>
#include "components.hpp"
using namespace fmt;
using namespace components;
inline int make_split(Room &cur, bool horiz) {
size_t dimension = horiz ? cur.height : cur.width;
int min = dimension / WORLDBUILD_DIVISION;
int max = dimension - min;
return Random::uniform<int>(min, max);
}
void WorldBuilder::set_door(Room &room, int value) {
$map.$walls[room.entry.y][room.entry.x] = value;
$map.$walls[room.exit.y][room.exit.x] = value;
}
void rand_side(Room &room, Point &door) {
dbc::check(int(room.width) > 0 && int(room.height) > 0, "Weird room with 0 for height or width.");
int rand_x = Random::uniform<int>(0, room.width - 1);
int rand_y = Random::uniform<int>(0, room.height - 1);
switch(Random::uniform<int>(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 WorldBuilder::add_door(Room &room) {
rand_side(room, room.entry);
rand_side(room, room.exit);
}
void WorldBuilder::partition_map(Room &cur, int depth) {
if(cur.width >= 3 && cur.width <= 6 &&
cur.height >= 3 && cur.height <= 6)
{
$map.add_room(cur);
return;
}
bool horiz = cur.width > cur.height ? false : true;
int split = make_split(cur, horiz);
if(split <= 0) return; // end recursion
Room left = cur;
Room right = cur;
if(horiz) {
if(split >= int(cur.height)) return; // end recursion
left.height = size_t(split - 1);
right.y = cur.y + split;
right.height = size_t(cur.height - split);
} else {
if(split >= int(cur.width)) return; // end recursion
left.width = size_t(split-1);
right.x = cur.x + split,
right.width = size_t(cur.width - split);
}
// BUG: min room size should be configurable
if(depth > 0 && left.width > 2 && left.height > 2) {
partition_map(left, depth-1);
}
// BUG: min room size should be configurable
if(depth > 0 && right.width > 2 && right.height > 2) {
partition_map(right, depth-1);
}
}
void WorldBuilder::update_door(Point &at, int wall_or_space) {
$map.$walls[at.y][at.x] = wall_or_space;
}
void WorldBuilder::stylize_room(int room, string tile_name, float size) {
Point pos_out;
bool placed = $map.place_entity(room, pos_out);
dbc::check(placed, "failed to place style in room");
for(matrix::circle it{$map.$walls, pos_out, size}; it.next();) {
for(int x = it.left; x < it.right; x++) {
if(!$map.iswall(x, it.y)) {
$map.$tiles.set_tile(x, it.y, tile_name);
}
}
}
}
void WorldBuilder::generate_rooms() {
Room root{
.x = 0,
.y = 0,
.width = $map.$width,
.height = $map.$height
};
// BUG: depth should be configurable
partition_map(root, 10);
place_rooms();
dbc::check($map.room_count() > 0, "map generated zero rooms, map too small?");
}
void WorldBuilder::generate_map() {
generate_rooms();
PointList holes;
for(size_t i = 0; i < $map.$rooms.size() - 1; i++) {
tunnel_doors(holes, $map.$rooms[i], $map.$rooms[i+1]);
}
// one last connection from first room to last
tunnel_doors(holes, $map.$rooms.back(), $map.$rooms.front());
// place all the holes
for(auto hole : holes) {
if(!matrix::inbounds($map.$walls, hole.x, hole.y)) {
matrix::dump("MAP BEFORE CRASH", $map.$walls, hole.x, hole.y);
auto err = fmt::format("invalid hold target {},{} map is only {},{}",
hole.x, hole.y, matrix::width($map.$walls),
matrix::height($map.$walls));
dbc::sentinel(err);
}
$map.$walls[hole.y][hole.x] = INV_SPACE;
}
$map.invert_space();
$map.expand();
$map.load_tiles();
// get only the tiles with no collision to fill rooms
auto room_types = $map.$tiles.tile_names(false);
for(size_t i = 0; i < $map.$rooms.size() - 1; i++) {
size_t room_type = Random::uniform<size_t>(0, room_types.size() - 1);
int room_size = Random::uniform<int>(100, 800);
string tile_name = room_types[room_type];
stylize_room(i, tile_name, room_size * 0.01f);
}
}
DinkyECS::Entity place_item(DinkyECS::World &world, Map &game_map, std::string name, int in_room) {
auto &config = world.get_the<GameConfig>();
auto item = world.entity();
Point pos_out;
bool placed = game_map.place_entity(in_room, pos_out);
dbc::check(placed, "failed to randomly place item in room");
json item_data = config.items[name];
world.set<Position>(item, {pos_out.x+1, pos_out.y+1});
if(item_data["inventory_count"] > 0) {
world.set<InventoryItem>(item, {item_data["inventory_count"], item_data});
}
if(item_data.contains("components")) {
components::configure(world, item, item_data);
}
return item;
}
DinkyECS::Entity place_combatant(DinkyECS::World &world, Map &game_map, std::string name, int in_room) {
auto &config = world.get_the<GameConfig>();
auto enemy = world.entity();
auto enemy_data = config.enemies[name];
Point pos;
bool placed = game_map.place_entity(in_room, pos);
dbc::check(placed, "failed to place combatant in room");
world.set<Position>(enemy, {pos});
if(enemy_data.contains("components")) {
components::configure(world, enemy, enemy_data);
}
if(!world.has<Motion>(enemy)) {
world.set<Motion>(enemy, {0,0});
}
return enemy;
}
void WorldBuilder::place_entities(DinkyECS::World &world) {
auto &config = world.get_the<GameConfig>();
// configure a player as a fact of the world
auto player_ent = place_combatant(world, $map, "PLAYER_TILE", 0);
// configure player in the world
Player player{player_ent};
world.set_the<Player>(player);
world.set<LightSource>(player.entity, {50,1.0});
world.set<Inventory>(player.entity, {5});
{
std::vector<std::string> keys;
for(auto &el : config.items.json().items()) {
keys.push_back(el.key());
}
for(size_t room_num = 1; room_num < $map.room_count(); room_num++) {
std::string key = keys[room_num % keys.size()];
place_item(world, $map, key, room_num);
}
}
{
std::vector<std::string> keys;
for(auto &el : config.enemies.json().items()) {
keys.push_back(el.key());
}
for(size_t room_num = $map.room_count() - 1; room_num > 0; room_num--) {
if(room_num % 2 == 0) {
std::string key = keys[room_num % keys.size()];
place_combatant(world, $map, key, room_num);
}
}
}
}
void WorldBuilder::generate(DinkyECS::World &world) {
generate_map();
place_entities(world);
}
void WorldBuilder::make_room(size_t origin_x, size_t origin_y, size_t w, size_t h) {
$map.INVARIANT();
dbc::pre("y out of bounds", origin_y + h < $map.$height);
dbc::pre("x out of bounds", origin_x + w < $map.$width);
for(size_t y = origin_y; y < origin_y + h; ++y) {
for(size_t x = origin_x; x < origin_x + w; ++x) {
$map.$walls[y][x] = INV_SPACE;
}
}
}
void WorldBuilder::place_rooms() {
for(auto &cur : $map.$rooms) {
// println("ROOM x/y={},{}; w/h={},{}; map={},{}",
// cur.x, cur.y, cur.width, cur.height, $map.$width, $map.$height);
add_door(cur);
make_room(cur.x, cur.y, cur.width, cur.height);
}
}
inline bool random_path(Map &map, PointList &holes, Point src, Point target) {
bool keep_going = false;
bool target_found = false;
int count = 0;
map.set_target(target);
map.make_paths();
Matrix &paths = map.paths();
Point out{src.x, src.y};
do {
keep_going = map.neighbors(out, true);
holes.push_back(out);
target_found = paths[out.y][out.x] == 0;
} while(!target_found && keep_going && ++count < WORLDBUILD_MAX_PATH);
map.INVARIANT();
map.clear_target(target);
return target_found;
}
inline void straight_path(Map &map, PointList &holes, Point src, Point target) {
for(matrix::line dig{src, target}; dig.next();) {
holes.push_back({size_t(dig.x), size_t(dig.y)});
Point expand{(size_t)dig.x+1, (size_t)dig.y};
if(map.inmap(expand.x, expand.y)) {
// BUG? should really just move doors away from the edge
holes.push_back(expand);
}
}
}
void WorldBuilder::tunnel_doors(PointList &holes, Room &src, Room &target) {
int path_type = Random::uniform<int>(0, 3);
switch(path_type) {
case 0:
// for now do 25% as simple straight paths
straight_path($map, holes, src.exit, target.entry);
break;
case 1:
// for now do 25% as simple straight paths
straight_path($map, holes, src.exit, target.entry);
break;
default:
// then do the rest as random with fallback
if(!random_path($map, holes, src.exit, target.entry)) {
straight_path($map, holes, src.exit, target.entry);
}
}
}