|
|
|
#include <chrono> // for operator""s, chrono_literals
|
|
|
|
#include <iostream> // for cout, ostream
|
|
|
|
#include <fstream>
|
|
|
|
#include <memory> // for allocator, shared_ptr
|
|
|
|
#include <string> // for string, operator<<
|
|
|
|
#include <thread> // for sleep_for
|
|
|
|
#include <array>
|
|
|
|
#include <algorithm>
|
|
|
|
|
|
|
|
#include <ftxui/dom/elements.hpp> // for hflow, paragraph, separator, hbox, vbox, filler, operator|, border, Element
|
|
|
|
#include <ftxui/dom/node.hpp> // for Render
|
|
|
|
#include <ftxui/screen/box.hpp> // for ftxui
|
|
|
|
#include <ftxui/component/loop.hpp>
|
|
|
|
#include <ftxui/screen/color.hpp>
|
|
|
|
#include <ftxui/dom/table.hpp>
|
|
|
|
|
|
|
|
#include <fmt/core.h>
|
|
|
|
#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() {
|
|
|
|
$inventory_list = {
|
|
|
|
"Sword (10)",
|
|
|
|
"Hat (1)",
|
|
|
|
"Fruit (4)",
|
|
|
|
"Dead Rabbit (1)"
|
|
|
|
};
|
|
|
|
|
|
|
|
$fake_text = R"(
|
|
|
|
Franzen kickstarter keffiyeh poutine actually master cleanse, irony butcher live-edge glossier stumptown organic PBR&B. Cloud bread cupping praxis hammock, offal tilde fam ennui vexillologist chartreuse chillwave. Chambray swag umami chartreuse cupping photo booth butcher fashion axe godard banh mi portland sartorial banjo shabby chic intelligentsia. Praxis synth helvetica 3 wolf moon raclette tousled. Tousled cliche jawn, coloring book meh pitchfork chartreuse. Kitsch try-hard poke celiac tote bag listicle photo booth.
|
|
|
|
)";
|
|
|
|
|
|
|
|
|
|
|
|
$inventory_table = Renderer([&]{
|
|
|
|
auto table = Table({
|
|
|
|
{"Version", "Marketing name", "Release date", "API level", "Runtime"},
|
|
|
|
{"2.3", "Gingerbread", "February 9 2011", "10", "Dalvik 1.4.0"},
|
|
|
|
{"4.0", "Ice Cream Sandwich", "October 19 2011", "15", "Dalvik"},
|
|
|
|
{"4.1", "Jelly Bean", "July 9 2012", "16", "Dalvik"},
|
|
|
|
{"4.2", "Jelly Bean", "November 13 2012", "17", "Dalvik"},
|
|
|
|
{"4.3", "Jelly Bean", "July 24 2013", "18", "Dalvik"},
|
|
|
|
{"4.4", "KitKat", "October 31 2013", "19", "Dalvik and ART"},
|
|
|
|
{"5.0", "Lollipop", "November 3 2014", "21", "ART"},
|
|
|
|
{"5.1", "Lollipop", "March 9 2015", "22", "ART"},
|
|
|
|
});
|
|
|
|
|
|
|
|
table.SelectAll().Border(LIGHT);
|
|
|
|
table.SelectRow(0).Border(DOUBLE);
|
|
|
|
return table.Render();
|
|
|
|
});
|
|
|
|
|
|
|
|
MenuOption option;
|
|
|
|
$inventory_box = Menu(&$inventory_list, &$selected, option);
|
|
|
|
|
|
|
|
auto inventory_test = Renderer([&] {
|
|
|
|
return hbox({
|
|
|
|
$inventory_box->Render() | frame | size(WIDTH, EQUAL, 35) | yflex_grow,
|
|
|
|
separator() | yflex_grow,
|
|
|
|
vflow({
|
|
|
|
paragraph($fake_text) | border,
|
|
|
|
$inventory_table->Render()
|
|
|
|
}) | flex
|
|
|
|
}) | border | flex;
|
|
|
|
});
|
|
|
|
|
|
|
|
set_renderer(inventory_test);
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatusUI::create_render() {
|
|
|
|
auto player = $world.get_the<Player>();
|
|
|
|
|
|
|
|
auto status_rend = Renderer([&, player]{
|
|
|
|
const auto& player_combat = $world.get<Combat>(player.entity);
|
|
|
|
const auto& inventory = $world.get<Inventory>(player.entity);
|
|
|
|
const auto& combat = $world.get<Combat>(player.entity);
|
|
|
|
$status_text = player_combat.hp > 0 ? "NOT DEAD" : "DEAD!!!!!!";
|
|
|
|
|
|
|
|
std::vector<Element> 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} DMG: {: >3}",
|
|
|
|
player_combat.hp,
|
|
|
|
inventory.gold,
|
|
|
|
combat.damage)) | 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<Debug>();
|
|
|
|
const auto& player = $world.get_the<Player>();
|
|
|
|
const auto& player_position = $world.get<Position>(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<Player>();
|
|
|
|
|
|
|
|
while($world.has_event<eGUI>()) {
|
|
|
|
auto [evt, entity, data] = $world.recv<eGUI>();
|
|
|
|
|
|
|
|
switch(evt) {
|
|
|
|
case eGUI::COMBAT: {
|
|
|
|
auto &damage = std::any_cast<Events::Combat&>(data);
|
|
|
|
auto enemy_combat = $world.get<Combat>(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<Loot&>(data);
|
|
|
|
auto inventory = $world.get<Inventory>(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<Player>();
|
|
|
|
int map_font_size = $renderer.font_size();
|
|
|
|
auto& player_motion = $world.get<Motion>(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>();
|
|
|
|
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>();
|
|
|
|
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<Player>();
|
|
|
|
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<int>(-10,10);
|
|
|
|
int y = Random::uniform<int>(-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<Debug>({});
|
|
|
|
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;
|
|
|
|
}
|