#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 // for hflow, paragraph, separator, hbox, vbox, filler, operator|, border, Element #include // for Render #include // for ftxui #include #include #include #include #include #include #include "dbc.hpp" #include "gui.hpp" #include "rand.hpp" #include "components.hpp" #include "systems.hpp" #include "collider.hpp" #include "events.hpp" using std::string; using namespace fmt; using namespace std::chrono_literals; using namespace ftxui; using namespace Components; std::array VALUES{ sf::Color{1, 4, 2}, // black sf::Color{9, 29, 16}, // dark dark sf::Color{14, 50, 26}, // dark mid sf::Color{0, 109, 44}, // dark light sf::Color{63, 171, 92}, // mid sf::Color{161, 217, 155}, // light dark sf::Color{199, 233, 192}, // light mid sf::Color{229, 245, 224}, // light light sf::Color{255, 255, 255}, // white sf::Color::Transparent, // white }; sf::Color GUI::color(int val) { return VALUES[size_t(val)]; } sf::Color GUI::color(Value val) { return VALUES[size_t(val)]; } GUI::GUI() : $game_map(GAME_MAP_X, GAME_MAP_Y), $window(sf::VideoMode(VIDEO_X,VIDEO_Y), "Roguish"), $screen(SCREEN_X, SCREEN_Y), $map_screen(0,0), $view_port{0,0}, $map_font_size(BASE_MAP_FONT_SIZE), $line_spacing(0) { $font.loadFromFile("./assets/text.otf"); resize_map(BASE_MAP_FONT_SIZE); $ui_text.setFont($font); $ui_text.setPosition(0,0); $ui_text.setCharacterSize(UI_FONT_SIZE); $ui_text.setFillColor(color(Value::LIGHT_LIGHT)); $game_map.generate(); } void GUI::create_renderer() { auto player = $world.get_the(); $map_view = Renderer([&] { System::draw_map($world, $game_map, $canvas, $view_port.x, $view_port.y); return canvas($canvas); }); $document = Renderer([&, player]{ const auto& player_combat = $world.get(player.entity); const auto& log = $world.get_the(); $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}", player_combat.hp)) | border, text($status_text) | border, separator(), log_box ) | flex_grow ), separator(), hbox(), }); }); } bool GUI::handle_events() { sf::Event event; bool event_happened = false; auto& log = $world.get_the(); auto player = $world.get_the(); auto sounds = $world.get_the(); while($world.has_event()) { auto [evt, entity] = $world.recv(); switch(evt) { case GUIEvent::HIT: { auto combat = $world.get(entity); if(entity == player.entity) { log.log(format("Enemy HIT YOU, you have {} HP!", combat.hp)); sounds.play("hit"); shake(); } else { log.log(format("You HIT enemy, they have {} HP!", combat.hp)); sounds.play("hit"); shake(); } } break; case GUIEvent::MISS: if(entity == player.entity) { log.log("You MISSED the enemy."); } else { log.log("Enemy MISSED YOU."); } break; case GUIEvent::DEAD: log.log("--- ENEMY DEAD!"); break; default: log.log(format("INVALID EVENT! {},{}", evt, entity)); } } while($window.pollEvent(event)) { if(event.type == sf::Event::Closed) { $window.close(); } else if(event.type == sf::Event::KeyPressed) { auto& player_motion = $world.get(player.entity); if(sf::Keyboard::isKeyPressed(sf::Keyboard::Left)) { player_motion.dx = -1; event_happened = true; } else if(sf::Keyboard::isKeyPressed(sf::Keyboard::Right)) { player_motion.dx = 1; event_happened = true; } else if(sf::Keyboard::isKeyPressed(sf::Keyboard::Up)) { player_motion.dy = -1; event_happened = true; } else if(sf::Keyboard::isKeyPressed(sf::Keyboard::Down)) { player_motion.dy = 1; event_happened = true; } else if(sf::Keyboard::isKeyPressed(sf::Keyboard::Equal)) { resize_map($map_font_size + 10); } else if(sf::Keyboard::isKeyPressed(sf::Keyboard::Hyphen)) { resize_map($map_font_size - 10); } } } return event_happened; } sf::Sprite &GUI::get_text_sprite(wchar_t tile) { if(!$sprites.contains(tile)) { sf::Glyph glyph = $font.getGlyph(tile, $map_font_size, false); // WARNING! we actually have to do this here because SFML caches // the glyphs on the font texture, so this gets loaded each time // we get a new glyph from the font. $font_texture = $font.getTexture($map_font_size); sf::Sprite sprite($font_texture); sprite.setTextureRect(glyph.textureRect); $sprites[tile] = sprite; } return $sprites[tile]; } void GUI::run_systems() { auto player = $world.get_the(); System::enemy_pathing($world, $game_map, player); System::motion($world, $game_map); System::combat($world, player); System::death($world); } void GUI::resize_map(int new_size) { if(MIN_FONT_SIZE < new_size && new_size < MAX_FONT_SIZE) { $sprites.clear(); // need to reset the sprites for the new size $map_font_size = new_size; $base_glyph = $font.getGlyph(L'█', $map_font_size, false); auto bounds = $base_glyph.bounds; $line_spacing = $font.getLineSpacing($map_font_size); $view_port = { size_t(std::ceil((VIDEO_X - GAME_MAP_POS) / bounds.width)), size_t(std::ceil(VIDEO_Y / bounds.height)) }; // set canvas to best size $canvas = Canvas($view_port.x * 2, $view_port.y * 4); $map_screen = Screen($view_port.x, $view_port.y); } } void GUI::draw_screen(bool clear, float map_off_x, float map_off_y) { if(clear) $window.clear(); std::string screenout = $screen.ToString(); std::wstring main_screen_utf8 = $converter.from_bytes(screenout); $ui_text.setString(main_screen_utf8); $window.draw($ui_text); std::string map_screenout = $map_screen.ToString(); std::wstring map_screen_utf8 = $converter.from_bytes(map_screenout); float y = 0.0f; float x = GAME_MAP_POS; // make a copy so we don't modify the cached one auto bg_sprite = get_text_sprite(L'█'); auto bg_bounds = bg_sprite.getLocalBounds(); bg_sprite.setColor(sf::Color(20,20,20)); auto add_sprite = get_text_sprite(L'!'); bool has_add = false; for(size_t i = 0; i < map_screen_utf8.size(); i++) { wchar_t tile = map_screen_utf8[i]; if(tile == L'\n') { // don't bother processing newlines, just skip y += $line_spacing; x = GAME_MAP_POS; } else if(tile == L'\r') { continue; // skip these, just windows junk } else { // it's a visual cell bg_sprite.setPosition({x+map_off_x, y+map_off_y}); sf::Sprite &sprite = get_text_sprite(tile); // should look into caching all this instead of calcing it each time auto sp_bounds = sprite.getLocalBounds(); // calculate where to center the sprite, but only if it's smaller auto width_delta = bg_bounds.width > sp_bounds.width ? (bg_bounds.width - sp_bounds.width) / 2 : 0; auto height_delta = bg_bounds.height > sp_bounds.width ? (bg_bounds.height - sp_bounds.height) / 2 : 0; // TODO: need to center it inside the bg_sprite sprite.setPosition({x+width_delta+map_off_x, y+height_delta+map_off_y}); // get the entity combat and make them light gray if dead if(tile == L'█') { sprite.setColor(sf::Color(80,80,80)); } else if(tile == L'☺') { sprite.setColor(sf::Color::Blue); } else if(tile == L'Ω') { sprite.setColor(sf::Color::Red); // HACK: just playing with adding multiple characters for drawing add_sprite.setColor(sf::Color::Red); add_sprite.setPosition({x-3,y-3}); has_add = true; } else if(tile == L'#') { sprite.setColor(sf::Color(5,5,5)); } else { sprite.setColor(color(Value::MID)); } // now draw the background sprite and sprite // TODO: this can become a standard sprite description $window.draw(bg_sprite); $window.draw(sprite); if(has_add) { $window.draw(add_sprite); has_add = false; } // next cell x += $base_glyph.advance; } } $window.display(); } void GUI::shake() { for(int i = 0; i < 10; ++i) { int x = Random::uniform(-20,20); int y = Random::uniform(-20,20); // add x/y back to draw screen draw_screen(true, x, y); std::this_thread::sleep_for(1ms); } } void GUI::configure_world() { SoundManager sounds("./assets"); sounds.load("hit", "hit.wav"); $world.set_the(sounds); $world.set_the(GUIEvent::START); dbc::check($game_map.room_count() > 1, "not enough rooms in map."); // configure a player as a fact of the world Player player{$world.entity()}; $world.set_the(player); ActionLog log{{"Welcome to the game!"}}; $world.set_the(log); spatial_map collider; $world.set_the(collider); $world.set(player.entity, {$game_map.place_entity(0)}); $world.set(player.entity, {0, 0}); $world.set(player.entity, {100, 10}); $world.set(player.entity, {PLAYER_TILE}); auto enemy = $world.entity(); $world.set(enemy, {$game_map.place_entity(1)}); $world.set(enemy, {0,0}); $world.set(enemy, {20, 10}); $world.set(enemy, {ENEMY_TILE}); auto enemy2 = $world.entity(); $world.set(enemy2, {$game_map.place_entity(2)}); $world.set(enemy2, {0,0}); $world.set(enemy2, {20, 10}); $world.set(enemy2, {"*"}); auto gold = $world.entity(); $world.set(gold, {$game_map.place_entity($game_map.room_count() - 1)}); $world.set(gold, {100}); $world.set(gold, {"$"}); System::init_positions($world); } void GUI::render_scene() { $screen.Clear(); $map_screen.Clear(); Render($map_screen, $map_view->Render()); Render($screen, $document->Render()); draw_screen(); } int GUI::main() { configure_world(); create_renderer(); run_systems(); while($window.isOpen()) { render_scene(); if(handle_events()) { run_systems(); } std::this_thread::sleep_for(10ms); } return 0; }