under_the_ashland_dome/src/game/worldbuilder.cpp

263 lines
8.1 KiB
C++

#include "game/worldbuilder.hpp"
#include "algos/rand.hpp"
#include <fmt/core.h>
#include <iostream>
#include "game/components.hpp"
#include "algos/maze.hpp"
#include "graphics/textures.hpp"
#include "game/inventory.hpp"
#include "game/systems.hpp"
#include "graphics/animation.hpp"
using namespace fmt;
using namespace components;
void WorldBuilder::stylize_rooms() {
auto& tiles = $map.tiles();
auto style_config = settings::get("room_themes");
json& styles = style_config.json();
for(auto& room : $map.rooms()) {
auto& style = styles[Random::uniform(size_t(0), styles.size() - 1)];
dbc::check(style.contains("floor"),
$F("no floor spec in style {}", (std::string)style["name"]));
dbc::check(style.contains("walls"),
$F("no walls spec in style {}", (std::string)style["name"]));
auto& floor_name = style["floor"];
auto& wall_name = style["walls"];
size_t floor_id = textures::get_id(floor_name);
size_t wall_id = textures::get_id(wall_name);
for(matrix::box it{tiles, room.x, room.y, room.width+1, room.height+1}; it.next();) {
if(tiles[it.y][it.x] == 1) {
tiles[it.y][it.x] = wall_id;
} else if(tiles[it.y][it.x] == 0) {
tiles[it.y][it.x] = floor_id;
}
}
}
}
void WorldBuilder::generate_map() {
auto script = R"(
[
{"action": "hunt_and_kill"},
{"action": "clear"},
{"action": "randomize_rooms", "data": [3]},
{"action": "hunt_and_kill"},
{"action": "place_doors"}
]
)"_json;
int i = 0;
for(; i < 10; i++) {
auto [maze, valid] = maze::script($map, script);
if(valid) {
break;
} else {
maze.dump(fmt::format("FAILED width={}", $map.width()), true);
}
}
dbc::check(i < 10, "failed to find a valid map after 10 attempts");
$map.init_tiles();
stylize_rooms();
}
bool WorldBuilder::find_open_spot(Point& pos_out) {
size_t i = 0;
// horribly bad but I need to place things _somewhere_ so just fan out
for(i = 2; i < $map.width(); i++) {
// rando_rect starts at the top/left corner not center
for(matrix::rando_box it{$map.walls(), pos_out.x, pos_out.y, i}; it.next();) {
Point test{size_t(it.x), size_t(it.y)};
if($map.can_move(test) && !$collision.something_there(test)) {
pos_out = test;
return true;
}
}
}
matrix::dump("FAIL PLACE!", $map.walls(), pos_out.x, pos_out.y);
dbc::sentinel($F("failed to place entity in the entire map?: i={}; width={};", i, $map.width()));
return false;
}
DinkyECS::Entity WorldBuilder::configure_entity_in_map(DinkyECS::World &world, json &entity_data, Point pos) {
bool found = find_open_spot(pos);
dbc::check(found, "Failed to find a place for this thing.");
auto item = world.entity();
int inv_count = entity_data.contains("inventory_count") ? (int)entity_data["inventory_count"] : 0;
if(inv_count > 0) {
world.set<InventoryItem>(item, {entity_data["inventory_count"], entity_data});
}
if(entity_data.contains("components")) {
components::configure_entity(world, item, entity_data["components"]);
}
System::set_position(world, $collision, item, {pos.x, pos.y});
animation::configure(world, item);
return item;
}
DinkyECS::Entity WorldBuilder::configure_entity_in_room(DinkyECS::World &world, json &entity_data, int in_room) {
Point pos_out;
bool placed = $map.place_entity(in_room, pos_out);
dbc::check(placed, "failed to randomly place item in room");
// don't place anything inside a door
if($map.$doors.contains(pos_out)) return DinkyECS::NONE;
auto entity = configure_entity_in_map(world, entity_data, pos_out);
return entity;
}
inline json &select_entity_type(GameConfig &config, json &gen_config) {
int enemy_test = Random::uniform<int>(0,100);
int device_test = Random::uniform<int>(0, 100);
if(enemy_test < gen_config["enemy_probability"]) {
return config.enemies.json();
} else if(device_test < gen_config["device_probability"]) {
return config.devices.json();
} else {
return config.items.json();
}
}
inline json& random_entity_data(GameConfig& config, json& gen_config) {
json& entity_db = select_entity_type(config, gen_config);
std::vector<std::string> keys;
for(auto& el : entity_db.items()) {
auto& data = el.value();
if(data["placement"] == nullptr) {
keys.push_back(el.key());
}
}
int rand_entity = Random::uniform<int>(0, keys.size() - 1);
std::string key = keys[rand_entity];
return entity_db[key];
}
void WorldBuilder::randomize_entities(DinkyECS::World &world, GameConfig &config) {
auto& gen_config = config.game["worldgen"];
for(int room_num = $map.room_count() - 1; room_num > 0; room_num--) {
// pass that to the config as it'll be a generic json
auto& entity_data = random_entity_data(config, gen_config);
configure_entity_in_room(world, entity_data, room_num);
}
for(auto& at : $map.$dead_ends) {
if($map.$doors.contains(at)) continue;
auto& entity_data = random_entity_data(config, gen_config);
configure_entity_in_map(world, entity_data, at);
}
}
void WorldBuilder::place_doors(DinkyECS::World& world, GameConfig& config) {
auto& device_config = config.devices.json();
auto entity_data = device_config["DOOR_PLAIN"];
auto& tiles = $map.tiles();
auto& walls = $map.walls();
for(auto [door_at, _] : $map.$doors) {
// note, we set this to WALL_VALUE so it renders as a wall but map.iswall will check if its a door for collision
walls[door_at.y][door_at.x] = WALL_VALUE;
for(matrix::compass it{tiles, door_at.x, door_at.y}; it.next();) {
if(walls[it.y][it.x] == WALL_VALUE) {
// found a wall near the door, and since doors always have n/s/e/w walls it should be the one to use
size_t wall_id = tiles[it.y][it.x]; // this is wall to use
tiles[door_at.y][door_at.x] = textures::door_for_wall(wall_id);
break;
}
}
}
}
void WorldBuilder::place_stairs(DinkyECS::World& world, GameConfig& config) {
auto player = world.get_the<Player>();
auto player_pos = world.get<Position>(player.entity);
auto& device_config = config.devices.json();
auto entity_data = device_config["STAIRS_DOWN"];
auto at_end = $map.$dead_ends.back();
configure_entity_in_map(world, entity_data, at_end);
}
void WorldBuilder::configure_starting_items(DinkyECS::World &world) {
auto& player = world.get_the<Player>();
auto &inventory = world.get<inventory::Model>(player.entity);
auto healing = System::spawn_item(world, "REPAIR_KIT");
inventory.add("inv0", healing);
world.make_constant(healing);
auto sword = System::spawn_item(world, "SWORD_1");
inventory.add("weapon", sword);
world.make_constant(sword);
}
void WorldBuilder::place_entities(DinkyECS::World &world) {
auto &config = world.get_the<GameConfig>();
// configure a player as a fact of the world
Position player_pos{0,0};
if(world.has_the<Player>()) {
auto& player = world.get_the<Player>();
// first get a guess from the map
bool placed = $map.place_entity(0, player_pos.location);
dbc::check(placed, "map.place_entity failed to position player");
// then use the collision map to place the player safely
placed = find_open_spot(player_pos.location);
dbc::check(placed, "WorldBuild.find_open_spot also failed to position player");
System::set_position(world, $collision, player.entity, player_pos);
} else {
auto player_data = config.enemies["PLAYER_TILE"];
auto player_ent = configure_entity_in_room(world, player_data, 0);
dbc::check(player_ent != DinkyECS::NONE, "placed the player in a door!");
player_pos = world.get<Position>(player_ent);
// configure player in the world
Player player{player_ent};
world.set_the<Player>(player);
world.set<inventory::Model>(player_ent, {});
configure_starting_items(world);
world.make_constant(player.entity);
}
dbc::check(player_pos.location.x != 0 && player_pos.location.y != 0,
"failed to place the player correctly");
place_doors(world, config);
randomize_entities(world, config);
place_stairs(world, config);
}
void WorldBuilder::generate(DinkyECS::World &world) {
generate_map();
place_entities(world);
}