#include // for operator""s, chrono_literals #include // for cout, ostream #include #include // for allocator, shared_ptr #include // for string, operator<< #include // for sleep_for #include #include #include // for hflow, paragraph, separator, hbox, vbox, filler, operator|, border, Element #include // for Render #include // for ftxui #include #include #include #include "dbc.hpp" #include "gui.hpp" #include "rand.hpp" #include "systems.hpp" #include "events.hpp" #include "render.hpp" #include "save.hpp" #include "constants.hpp" using std::string; using namespace fmt; using namespace std::chrono_literals; using namespace ftxui; using namespace components; void InventoryUI::create_render() { auto cell = [](const char* t) { return text(t) | border; }; auto inventory_test = Renderer([cell] { return hflow({ gridbox({ {cell("one"), cell("two"), cell("three")}, {cell("four"), cell("five"), cell("six")}, {cell("seven"), cell("eight"), cell("nine")}, }) | yflex_grow, separator() | yflex_grow, paragraph("Item UI Goes Here") | yflex_grow }) | border | flex; }); set_renderer(inventory_test); } void StatusUI::create_render() { auto player = $world.get_the(); auto status_rend = Renderer([&, player]{ const auto& player_combat = $world.get(player.entity); const auto& inventory = $world.get(player.entity); $status_text = player_combat.hp > 0 ? "NOT DEAD" : "DEAD!!!!!!"; std::vector log_list; for(auto msg : $log.messages) { log_list.push_back(text(msg)); } auto log_box = vbox(log_list) | yflex_grow | border; return hbox({ hflow( vbox( text(format("HP: {: >3} GOLD: {: >3}", player_combat.hp, inventory.gold)) | border, text($status_text) | border, separator(), log_box ) | flex_grow ), separator(), hbox(), }); }); set_renderer(status_rend); } MapViewUI::MapViewUI(DinkyECS::World& world, LightRender& lights, Map& game_map) : Panel(GAME_MAP_PIXEL_POS, 0, 0, 0, true), $world(world), $lights(lights), $game_map(game_map) {} void MapViewUI::draw_map() { const auto& debug = $world.get_the(); const auto& player = $world.get_the(); const auto& player_position = $world.get(player.entity); Point start = $game_map.center_camera(player_position.location, width, height); auto &tiles = $game_map.tiles(); auto &paths = $game_map.paths(); auto &lighting = $lights.lighting(); // WARN: this is exploiting that -1 in size_t becomes largest size_t end_x = std::min(size_t(width), $game_map.width() - start.x); size_t end_y = std::min(size_t(height), $game_map.height() - start.y); for(size_t y = 0; y < end_y; ++y) { for(size_t x = 0; x < end_x; ++x) { const TileCell& tile = tiles.at(start.x+x, start.y+y); // light value is an integer that's a percent float light_value = debug.LIGHT ? 80 * PERCENT : lighting[start.y+y][start.x+x] * PERCENT; int dnum = debug.PATHS ? paths[start.y+y][start.x+x] : WALL_PATH_LIMIT; if(debug.PATHS && dnum != WALL_PATH_LIMIT) { string num = dnum > 15 ? "*" : format("{:x}", dnum); $canvas.DrawText(x * 2, y * 4, num, [dnum, tile, light_value](auto &pixel) { pixel.foreground_color = Color::HSV(dnum * 20, 150, 200); pixel.background_color = Color::HSV(30, 20, tile.bg_v * 50 * PERCENT); }); } else { $canvas.DrawText(x * 2, y * 4, tile.display, [tile, light_value](auto &pixel) { pixel.foreground_color = Color::HSV(tile.fg_h, tile.fg_s, tile.fg_v * light_value); pixel.background_color = Color::HSV(tile.bg_h, tile.bg_s, tile.bg_v * light_value); }); } } } System::draw_entities($world, $game_map, lighting, $canvas, start, width, height); } void MapViewUI::create_render() { set_renderer(Renderer([&] { draw_map(); return canvas($canvas); })); } void MapViewUI::resize_canvas() { // set canvas to best size $canvas = Canvas(width * 2, height * 4); } GUI::GUI(DinkyECS::World &world, Map& game_map) : $world(world), $game_map(game_map), $status_ui(world), $lights(game_map.width(), game_map.height()), $map_view($world, $lights, $game_map), $sounds("./assets") { // this needs a config file soon // $sounds.load("ambient", "ambient_sound.mp3"); $sounds.load("loot_gold", "loot_gold.mp3"); $sounds.load("combat_player_hit", "combat_player_hit.mp3"); $sounds.load("combat_enemy_hit", "combat_enemy_hit.mp3"); $sounds.load("combat_miss", "combat_miss.mp3"); resize_map(MAX_FONT_SIZE); init_shaders(); } void GUI::resize_map(int new_size) { $renderer.resize_grid(new_size, $map_view); $map_view.resize_canvas(); } void GUI::save_world() { $status_ui.log("Game saved!"); save::to_file("./savefile.world", $world, $game_map); } void GUI::create_renderer() { $renderer.init_terminal(); $map_view.create_render(); $status_ui.create_render(); $inventory_ui.create_render(); // don't activate this one $panels = {&$map_view, &$status_ui}; } void GUI::handle_world_events() { using eGUI = Events::GUI; auto player = $world.get_the(); while($world.has_event()) { auto [evt, entity, data] = $world.recv(); switch(evt) { case eGUI::COMBAT: { auto &damage = std::any_cast(data); auto enemy_combat = $world.get(entity); if(damage.enemy_did > 0) { $status_ui.log(format("Enemy HIT YOU for {} damage!", damage.enemy_did)); $status_ui.log(format("-- Enemy has {} HP left.", enemy_combat.hp)); $sounds.play("combat_enemy_hit"); shake(); } else { $status_ui.log("Enemy MISSED YOU."); } if(damage.player_did > 0) { $status_ui.log(format("You HIT enemy for {} damage!", damage.player_did)); $sounds.play("combat_player_hit"); } else { $sounds.play("combat_miss"); $status_ui.log("You MISSED the enemy."); } } break; case eGUI::LOOT: { auto &loot = std::any_cast(data); auto inventory = $world.get(player.entity); $sounds.play("loot_gold"); $status_ui.log(format("You found {} gold. You have {} now.", loot.amount, inventory.gold)); } break; default: $status_ui.log(format("INVALID EVENT! {},{}", evt, entity)); } } } void GUI::shutdown() { $renderer.close(); } bool GUI::handle_ui_events() { using KB = sf::Keyboard; using MOUSE = sf::Mouse; bool event_happened = false; sf::Event event; auto player = $world.get_the(); int map_font_size = $renderer.font_size(); auto& player_motion = $world.get(player.entity); Point pos; while($renderer.poll_event(event)) { if(event.type == sf::Event::Closed) { shutdown(); } else if(event.type == sf::Event::KeyPressed) { if(KB::isKeyPressed(KB::Left)) { player_motion.dx = -1; event_happened = true; } else if(KB::isKeyPressed(KB::Right)) { player_motion.dx = 1; event_happened = true; } else if(KB::isKeyPressed(KB::Up)) { player_motion.dy = -1; event_happened = true; } else if(KB::isKeyPressed(KB::Down)) { player_motion.dy = 1; event_happened = true; } else if(KB::isKeyPressed(KB::Equal)) { resize_map(map_font_size + 10); } else if(KB::isKeyPressed(KB::Hyphen)) { resize_map(map_font_size - 10); } else if(KB::isKeyPressed(KB::L)) { auto &debug = $world.get_the(); debug.LIGHT = !debug.LIGHT; } else if(KB::isKeyPressed(KB::I)) { // yes, using an if to avoid double grabbing screen if($show_modal) { $panels = {&$map_view, &$status_ui}; $show_modal = false; } else { pause_screen(); $panels = {&$inventory_ui}; $show_modal = true; } } else if(KB::isKeyPressed(KB::P)) { auto &debug = $world.get_the(); debug.PATHS = !debug.PATHS; } else if(KB::isKeyPressed(KB::S)) { save_world(); } else if(KB::isKeyPressed(KB::Tab)) { $status_ui.key_press(Event::Tab); } else if(KB::isKeyPressed(KB::Enter)) { $status_ui.key_press(Event::Return); } } else { for(Panel *panel : $panels) { if($renderer.mouse_position(*panel, pos)) { if(MOUSE::isButtonPressed(MOUSE::Left)) { panel->mouse_click(Mouse::Button::Left, pos); event_happened = true; } else { panel->mouse_release(Mouse::Button::Left, pos); } } } } } return event_happened; } void GUI::init_shaders() { auto& shader = $paused.load_shader("./shaders/modal.frag"); shader.setUniform("offsetFactor", sf::Glsl::Vec2{0.001f, 0.001f}); shader.setUniform("darkness", 0.05f); } void GUI::pause_screen() { auto &window = $renderer.$window; auto size = window.getSize(); $paused.texture.create(size.x, size.y); $paused.texture.update(window); $paused.sprite.setTexture($paused.texture); $paused.sprite.setPosition(0,0); } void GUI::draw_paused() { $renderer.draw_sprite($paused.sprite, &$paused.shader); } void GUI::run_systems() { auto player = $world.get_the(); System::motion($world, $game_map); System::enemy_pathing($world, $game_map, player); System::lighting($world, $game_map, $lights, player); System::collision($world, player); System::death($world); } void GUI::shake() { for(int i = 0; i < 10; ++i) { int x = Random::uniform(-10,10); int y = Random::uniform(-10,10); // add x/y back to draw screen $renderer.draw($map_view, x, y); $renderer.display(); std::this_thread::sleep_for(1ms); } } void GUI::render_scene() { $renderer.clear(); if($show_modal) { draw_paused(); $inventory_ui.render(); $renderer.draw($inventory_ui); } else { $map_view.render(); $renderer.draw($map_view); $status_ui.render(); $renderer.draw($status_ui); } $renderer.display(); } int GUI::main(bool run_once) { $world.set_the({}); create_renderer(); run_systems(); do { render_scene(); if(handle_ui_events()) { run_systems(); handle_world_events(); } std::this_thread::sleep_for(10ms); } while(!run_once && $renderer.is_open()); return 0; }