We now have a full map that's basically the same mapping system from Roguish. There's a bug right now where it needs you to move once to calc the light and it's not being centered, but it does work.

master
Zed A. Shaw 10 months ago
parent 55b67dcf5d
commit d798d154ae
  1. 2
      Makefile
  2. 371
      ansi_parser.cpp
  3. 23
      ansi_parser.hpp
  4. 163
      ansi_parser.rl
  5. 6
      assets/tiles.json
  6. 14
      color.hpp
  7. 10
      components.cpp
  8. 7
      components.hpp
  9. 11
      constants.hpp
  10. 133
      gui.cpp
  11. 27
      gui.hpp
  12. 2
      main.cpp
  13. 8
      meson.build
  14. 64
      panel.cpp
  15. 60
      panel.hpp
  16. 275
      render.cpp
  17. 80
      render.hpp
  18. 25
      systems.cpp
  19. 2
      systems.hpp
  20. 18
      tilemap.cpp
  21. 1
      tilemap.hpp
  22. 15
      wraps/ftxui.wrap

@ -6,7 +6,7 @@ reset:
%.cpp : %.rl
ragel -o $@ $<
build:
build: ansi_parser.cpp
meson compile -j 10 -C builddir
release_build:

@ -0,0 +1,371 @@
#line 1 "ansi_parser.rl"
#include <fmt/core.h>
#include <string_view>
#include "dbc.hpp"
#include <SFML/Graphics.hpp>
#include "ansi_parser.hpp"
#include <iostream>
using namespace fmt;
#line 122 "ansi_parser.rl"
#line 13 "ansi_parser.cpp"
static const char _ansi_parser_actions[] = {
0, 1, 0, 1, 3, 1, 4, 1,
5, 1, 6, 1, 7, 1, 8, 1,
9, 1, 10, 1, 11, 1, 15, 1,
16, 2, 1, 12, 2, 1, 13, 2,
6, 7, 2, 16, 5, 3, 1, 14,
2
};
static const char _ansi_parser_key_offsets[] = {
0, 0, 1, 2, 11, 12, 14, 17,
18, 22, 23, 27, 28, 29, 30, 31,
33, 36, 38, 41, 43, 46, 47, 50,
51, 52, 53, 54, 55
};
static const int _ansi_parser_trans_keys[] = {
27, 91, 48, 49, 50, 51, 52, 55,
57, 53, 54, 109, 48, 109, 34, 48,
55, 109, 50, 52, 55, 109, 109, 49,
56, 57, 109, 109, 59, 50, 59, 48,
57, 59, 48, 57, 48, 57, 59, 48,
57, 48, 57, 109, 48, 57, 109, 56,
57, 109, 59, 50, 109, 109, 27, 27,
0
};
static const char _ansi_parser_single_lengths[] = {
0, 1, 1, 7, 1, 2, 3, 1,
4, 1, 4, 1, 1, 1, 1, 0,
1, 0, 1, 0, 1, 1, 3, 1,
1, 1, 1, 1, 1
};
static const char _ansi_parser_range_lengths[] = {
0, 0, 0, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1,
1, 1, 1, 1, 1, 0, 0, 0,
0, 0, 0, 0, 0
};
static const char _ansi_parser_index_offsets[] = {
0, 0, 2, 4, 13, 15, 18, 22,
24, 29, 31, 36, 38, 40, 42, 44,
46, 49, 51, 54, 56, 59, 61, 65,
67, 69, 71, 73, 75
};
static const char _ansi_parser_trans_targs[] = {
2, 1, 3, 0, 4, 5, 8, 10,
22, 26, 6, 7, 0, 28, 0, 6,
28, 0, 7, 7, 7, 0, 28, 0,
7, 7, 9, 28, 0, 28, 0, 11,
12, 21, 28, 0, 28, 0, 13, 0,
14, 0, 15, 0, 16, 0, 17, 16,
0, 18, 0, 19, 18, 0, 20, 0,
28, 20, 0, 28, 0, 23, 25, 28,
0, 24, 0, 14, 0, 28, 0, 28,
0, 2, 1, 2, 1, 0
};
static const char _ansi_parser_trans_actions[] = {
0, 7, 0, 0, 21, 21, 21, 21,
21, 21, 21, 21, 0, 31, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 17, 0, 15, 0, 0,
0, 0, 0, 0, 19, 0, 0, 0,
3, 0, 0, 0, 1, 0, 25, 0,
0, 1, 0, 28, 0, 0, 1, 0,
37, 0, 0, 9, 0, 0, 0, 0,
0, 0, 0, 5, 0, 11, 0, 13,
0, 0, 7, 23, 34, 0
};
static const char _ansi_parser_eof_actions[] = {
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 23
};
static const int ansi_parser_start = 27;
static const int ansi_parser_first_final = 27;
static const int ansi_parser_error = 0;
static const int ansi_parser_en_main = 27;
#line 125 "ansi_parser.rl"
#include <ftxui/screen/terminal.hpp>
ANSIParser::ANSIParser(sf::Color default_fg, sf::Color default_bg) :
$default_fg(default_fg),
$default_bg(default_bg)
{
}
bool ANSIParser::parse(std::wstring_view codes, ColorCB color_cb, WriteCB write_cb) {
const wchar_t *start = NULL;
int cs = 0;
unsigned int value = 0;
const wchar_t *p = codes.data();
const wchar_t *pe = p + codes.size();
const wchar_t *eof = pe;
sf::Color bgcolor($default_bg);
sf::Color color($default_fg);
sf::Color* target = &color;
#line 120 "ansi_parser.cpp"
{
cs = ansi_parser_start;
}
#line 146 "ansi_parser.rl"
#line 123 "ansi_parser.cpp"
{
int _klen;
unsigned int _trans;
const char *_acts;
unsigned int _nacts;
const int *_keys;
if ( p == pe )
goto _test_eof;
if ( cs == 0 )
goto _out;
_resume:
_keys = _ansi_parser_trans_keys + _ansi_parser_key_offsets[cs];
_trans = _ansi_parser_index_offsets[cs];
_klen = _ansi_parser_single_lengths[cs];
if ( _klen > 0 ) {
const int *_lower = _keys;
const int *_mid;
const int *_upper = _keys + _klen - 1;
while (1) {
if ( _upper < _lower )
break;
_mid = _lower + ((_upper-_lower) >> 1);
if ( (*p) < *_mid )
_upper = _mid - 1;
else if ( (*p) > *_mid )
_lower = _mid + 1;
else {
_trans += (unsigned int)(_mid - _keys);
goto _match;
}
}
_keys += _klen;
_trans += _klen;
}
_klen = _ansi_parser_range_lengths[cs];
if ( _klen > 0 ) {
const int *_lower = _keys;
const int *_mid;
const int *_upper = _keys + (_klen<<1) - 2;
while (1) {
if ( _upper < _lower )
break;
_mid = _lower + (((_upper-_lower) >> 1) & ~1);
if ( (*p) < _mid[0] )
_upper = _mid - 2;
else if ( (*p) > _mid[1] )
_lower = _mid + 2;
else {
_trans += (unsigned int)((_mid - _keys)>>1);
goto _match;
}
}
_trans += _klen;
}
_match:
cs = _ansi_parser_trans_targs[_trans];
if ( _ansi_parser_trans_actions[_trans] == 0 )
goto _again;
_acts = _ansi_parser_actions + _ansi_parser_trans_actions[_trans];
_nacts = (unsigned int) *_acts++;
while ( _nacts-- > 0 )
{
switch ( *_acts++ )
{
case 0:
#line 14 "ansi_parser.rl"
{
start = p;
}
break;
case 1:
#line 18 "ansi_parser.rl"
{
value = 0;
size_t len = p - start;
dbc::check(start[0] != '-', "negative numbers not supported");
switch(len) {
case 10: value += (start[len-10] - '0') * 1000000000; [[fallthrough]];
case 9: value += (start[len- 9] - '0') * 100000000; [[fallthrough]];
case 8: value += (start[len- 8] - '0') * 10000000; [[fallthrough]];
case 7: value += (start[len- 7] - '0') * 1000000; [[fallthrough]];
case 6: value += (start[len- 6] - '0') * 100000; [[fallthrough]];
case 5: value += (start[len- 5] - '0') * 10000; [[fallthrough]];
case 4: value += (start[len- 4] - '0') * 1000; [[fallthrough]];
case 3: value += (start[len- 3] - '0') * 100; [[fallthrough]];
case 2: value += (start[len- 2] - '0') * 10; [[fallthrough]];
case 1: value += (start[len- 1] - '0');
break;
default:
dbc::sentinel("can't process > 10 digits");
}
}
break;
case 2:
#line 40 "ansi_parser.rl"
{
color_cb(color, bgcolor);
}
break;
case 3:
#line 43 "ansi_parser.rl"
{
target = &color;
}
break;
case 4:
#line 46 "ansi_parser.rl"
{
target = &bgcolor;
}
break;
case 5:
#line 50 "ansi_parser.rl"
{
write_cb((*p));
}
break;
case 6:
#line 54 "ansi_parser.rl"
{
color = $default_fg;
color_cb(color, bgcolor);
}
break;
case 7:
#line 58 "ansi_parser.rl"
{
bgcolor = $default_bg;
color_cb(color, bgcolor);
}
break;
case 8:
#line 62 "ansi_parser.rl"
{
color = $default_bg;
bgcolor = $default_fg;
color_cb(color, bgcolor);
}
break;
case 9:
#line 67 "ansi_parser.rl"
{
color = $default_fg;
bgcolor = $default_bg;
color_cb(color, bgcolor);
}
break;
case 10:
#line 72 "ansi_parser.rl"
{
color = sf::Color(100,100,100);
color_cb(color, bgcolor);
}
break;
case 11:
#line 76 "ansi_parser.rl"
{
color = sf::Color::Red;
color_cb(color, bgcolor);
}
break;
case 12:
#line 81 "ansi_parser.rl"
{ target->r = value; }
break;
case 13:
#line 82 "ansi_parser.rl"
{ target->g = value; }
break;
case 14:
#line 83 "ansi_parser.rl"
{ target->b = value; }
break;
case 15:
#line 84 "ansi_parser.rl"
{ value = 0; }
break;
case 16:
#line 85 "ansi_parser.rl"
{}
break;
#line 296 "ansi_parser.cpp"
}
}
_again:
if ( cs == 0 )
goto _out;
if ( ++p != pe )
goto _resume;
_test_eof: {}
if ( p == eof )
{
const char *__acts = _ansi_parser_actions + _ansi_parser_eof_actions[cs];
unsigned int __nacts = (unsigned int) *__acts++;
while ( __nacts-- > 0 ) {
switch ( *__acts++ ) {
case 16:
#line 85 "ansi_parser.rl"
{}
break;
#line 314 "ansi_parser.cpp"
}
}
}
_out: {}
}
#line 147 "ansi_parser.rl"
bool good = pe - p == 0;
if(!good) {
p -= 10;
// dear cthuhlu, save me from the pain that is wstring
for(int i = 0; i < 100; i++) {
try {
print("{}", p[i] == 0x1B ? '^' : char(p[i]));
} catch(...) {
print("?=", int(p[i]));
}
}
}
return good;
}

@ -0,0 +1,23 @@
#pragma once
#include <string_view>
#include <SFML/Graphics.hpp>
#include <codecvt>
#include <functional>
typedef std::function<void(sf::Color bgcolor, sf::Color color)> ColorCB;
typedef std::function<void(wchar_t ch)> WriteCB;
class ANSIParser {
sf::Color $default_fg;
sf::Color $default_bg;
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> $converter;
public:
ANSIParser(sf::Color default_fg, sf::Color default_bg);
// disable copying
ANSIParser(ANSIParser& ap) = delete;
bool parse(std::wstring_view codes, ColorCB color_cb, WriteCB write_cb);
};

@ -0,0 +1,163 @@
#include <fmt/core.h>
#include <string_view>
#include "dbc.hpp"
#include <SFML/Graphics.hpp>
#include "ansi_parser.hpp"
#include <iostream>
using namespace fmt;
%%{
machine ansi_parser;
alphtype int;
action tstart {
start = fpc;
}
action number {
value = 0;
size_t len = fpc - start;
dbc::check(start[0] != '-', "negative numbers not supported");
switch(len) {
case 10: value += (start[len-10] - '0') * 1000000000; [[fallthrough]];
case 9: value += (start[len- 9] - '0') * 100000000; [[fallthrough]];
case 8: value += (start[len- 8] - '0') * 10000000; [[fallthrough]];
case 7: value += (start[len- 7] - '0') * 1000000; [[fallthrough]];
case 6: value += (start[len- 6] - '0') * 100000; [[fallthrough]];
case 5: value += (start[len- 5] - '0') * 10000; [[fallthrough]];
case 4: value += (start[len- 4] - '0') * 1000; [[fallthrough]];
case 3: value += (start[len- 3] - '0') * 100; [[fallthrough]];
case 2: value += (start[len- 2] - '0') * 10; [[fallthrough]];
case 1: value += (start[len- 1] - '0');
break;
default:
dbc::sentinel("can't process > 10 digits");
}
}
action color_out {
color_cb(color, bgcolor);
}
action is_fg {
target = &color;
}
action is_bg {
target = &bgcolor;
}
action out {
write_cb(fc);
}
action reset_fg {
color = $default_fg;
color_cb(color, bgcolor);
}
action reset_bg {
bgcolor = $default_bg;
color_cb(color, bgcolor);
}
action invert {
color = $default_bg;
bgcolor = $default_fg;
color_cb(color, bgcolor);
}
action reset_invert {
color = $default_fg;
bgcolor = $default_bg;
color_cb(color, bgcolor);
}
action half_bright {
color = sf::Color(100,100,100);
color_cb(color, bgcolor);
}
action red_text {
color = sf::Color::Red;
color_cb(color, bgcolor);
}
action red { target->r = value; }
action blue { target->g = value; }
action green { target->b = value; }
action start { value = 0; }
action end {}
action log { println("command {}", (char)fc); }
ESC = 0x1B;
start = ESC "[";
fg = "38;" %is_fg;
bg = "48;" %is_bg;
reset = ("39" %reset_fg | "49" %reset_bg);
num = digit+ >tstart %number;
color256 = "5;";
color24b = "2;";
ansi = (
start %start
(
reset |
"0" %reset_fg %reset_bg |
"1" |
"2" %half_bright |
"3" |
"4" |
"5" |
"6" |
"7" %invert |
"31" %red_text |
"22" |
"24" |
"27" %reset_invert |
"9" ["0"-"7"] |
"10" ["0"-"7"] |
(fg|bg) (color24b num %red ";" num %blue ";" num %green ) %color_out
) "m" %end
);
other = (any+ @out -- ESC)*;
main := (other :> ansi)**;
}%%
%% write data;
#include <ftxui/screen/terminal.hpp>
ANSIParser::ANSIParser(sf::Color default_fg, sf::Color default_bg) :
$default_fg(default_fg),
$default_bg(default_bg)
{
}
bool ANSIParser::parse(std::wstring_view codes, ColorCB color_cb, WriteCB write_cb) {
const wchar_t *start = NULL;
int cs = 0;
unsigned int value = 0;
const wchar_t *p = codes.data();
const wchar_t *pe = p + codes.size();
const wchar_t *eof = pe;
sf::Color bgcolor($default_bg);
sf::Color color($default_fg);
sf::Color* target = &color;
%% write init;
%% write exec;
bool good = pe - p == 0;
if(!good) {
p -= 10;
// dear cthuhlu, save me from the pain that is wstring
for(int i = 0; i < 100; i++) {
try {
print("{}", p[i] == 0x1B ? '^' : char(p[i]));
} catch(...) {
print("?=", int(p[i]));
}
}
}
return good;
}

@ -11,20 +11,20 @@
"foreground": [230, 20, 30],
"background": [230, 20, 120],
"collision": true,
"display": ""
"display": "\ua5b8"
},
"WALL_VINES": {
"texture": "assets/wall_with_vines-256.png",
"foreground": [40, 15, 125],
"background": [200, 29, 75],
"collision": false,
"display":"#"
"display":"\u0799"
},
"WALL_PILLAR": {
"texture": "assets/wall_with_pillars-256.png",
"foreground": [40, 15, 125],
"background": [200, 29, 75],
"collision": false,
"display":"%"
"display":"\u2274"
}
}

@ -0,0 +1,14 @@
#pragma once
namespace ColorValue {
const sf::Color BLACK{1, 4, 2};
const sf::Color DARK_DARK{9, 29, 16};
const sf::Color DARK_MID{14, 50, 26};
const sf::Color DARK_LIGHT{0, 109, 44};
const sf::Color MID{63, 171, 92};
const sf::Color LIGHT_DARK{161, 217, 155};
const sf::Color LIGHT_MID{199, 233, 192};
const sf::Color LIGHT_LIGHT{229, 245, 224};
const sf::Color WHITE{255, 255, 255};
const sf::Color TRANSPARENT = sf::Color::Transparent;
}

@ -13,7 +13,15 @@ namespace components {
} else if(comp_type == "Loot") {
world.set<Loot>(entity, {config["amount"]});
} else if(comp_type == "Tile") {
world.set<Tile>(entity, {config["chr"]});
world.set<Tile>(entity, {
config["chr"],
entity_data["foreground"][0],
entity_data["foreground"][1],
entity_data["foreground"][2],
entity_data["background"][0],
entity_data["background"][1],
entity_data["background"][2]});
} else if(comp_type == "EnemyConfig") {
world.set<EnemyConfig>(entity, {config["hearing_distance"]});
} else if(comp_type == "Combat") {

@ -32,6 +32,13 @@ namespace components {
struct Tile {
std::string chr;
uint8_t fg_h = 200;
uint8_t fg_s = 20;
uint8_t fg_v = 200;
uint8_t bg_h = 100;
uint8_t bg_s = 20;
uint8_t bg_v = 0;
DEFINE_SERIALIZABLE(Tile, chr);
};

@ -6,10 +6,10 @@ constexpr const int TEXTURE_WIDTH=256;
constexpr const int TEXTURE_HEIGHT=256;
constexpr const int RAY_VIEW_WIDTH=960;
constexpr const int RAY_VIEW_HEIGHT=720;
constexpr const int RAY_VIEW_X=(1280 - RAY_VIEW_WIDTH);
constexpr const int SCREEN_WIDTH=1280;
constexpr const int RAY_VIEW_X=(SCREEN_WIDTH - RAY_VIEW_WIDTH);
constexpr const int RAY_VIEW_Y=0;
constexpr const int SCREEN_HEIGHT=720;
constexpr const int SCREEN_WIDTH=1280;
constexpr const bool VSYNC=false;
constexpr const int FRAME_LIMIT=60;
constexpr const int NUM_SPRITES=1;
@ -42,3 +42,10 @@ constexpr int MIN_FONT_SIZE = 20;
constexpr int STATUS_UI_WIDTH = 40;
constexpr int STATUS_UI_HEIGHT = 30;
constexpr float PERCENT = 0.01f;
// for the panels/renderer
constexpr wchar_t BG_TILE = L'';
constexpr wchar_t UI_BASE_CHAR = L'';
constexpr int BG_BOX_OFFSET=5;
constexpr const char *FONT_FILE_NAME="./assets/text.otf";

@ -10,21 +10,82 @@
using namespace components;
namespace gui {
using ftxui::Color;
MapViewUI::MapViewUI(GameLevel &level) :
Panel(RAY_VIEW_X, 0, 0, 0, true),
$level(level)
{}
void MapViewUI::update_level(GameLevel &level) {
$level = level;
}
void MapViewUI::draw_map() {
const auto& debug = $level.world->get_the<Debug>();
const auto& player = $level.world->get_the<Player>();
const auto& player_position = $level.world->get<Position>(player.entity);
Point start = $level.map->center_camera(player_position.location, width, height);
auto &tiles = $level.map->tiles();
auto &paths = $level.map->paths();
auto &lighting = $level.lights->lighting();
// WARN: this is exploiting that -1 in size_t becomes largest
size_t end_x = std::min(size_t(width), $level.map->width() - start.x);
size_t end_y = std::min(size_t(height), $level.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 ? "*" : fmt::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(*$level.world, *$level.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);
}
FSM::FSM() :
$window(sf::VideoMode({SCREEN_WIDTH, SCREEN_HEIGHT}), "Zed's Raycaster Thing"),
$font{"./assets/text.otf"},
$renderer($window),
$level($levels.current()),
$map_view($level),
$font{FONT_FILE_NAME},
$text{$font},
$map_display{$font},
$rayview($textures, RAY_VIEW_WIDTH, RAY_VIEW_HEIGHT)
{
$window.setVerticalSyncEnabled(VSYNC);
$window.setFramerateLimit(FRAME_LIMIT);
$text.setPosition({10,10});
$text.setFillColor({255,255,255});
$map_display.setPosition({10, SCREEN_HEIGHT-300});
$map_display.setFillColor({255,255,255});
$map_display.setLetterSpacing(0.5);
$map_display.setLineSpacing(0.9);
$textures.load_tiles();
$textures.load_sprites();
}
@ -33,6 +94,7 @@ namespace gui {
switch($state) {
FSM_STATE(State, START, ev);
FSM_STATE(State, MOVING, ev);
FSM_STATE(State, MAPPING, ev);
FSM_STATE(State, ROTATING, ev);
FSM_STATE(State, IDLE, ev);
FSM_STATE(State, END, ev);
@ -41,12 +103,36 @@ namespace gui {
void FSM::START(Event ) {
generate_map();
$level.world->set_the<Debug>({});
$rayview.init_shaders();
$rayview.set_position(RAY_VIEW_X, RAY_VIEW_Y);
$rayview.position_camera($player.x + 0.5, $player.y + 0.5);
$renderer.init_terminal();
$map_view.create_render();
$renderer.resize_grid(MAX_FONT_SIZE, $map_view);
$map_view.resize_canvas();
state(State::IDLE);
}
void FSM::MAPPING(Event ev) {
// BUG: can't close window when in mapping
switch(ev) {
case Event::MAP_OPEN:
state(State::IDLE);
break;
case Event::CLOSE:
state(State::IDLE);
break;
case Event::TICK:
break;
default:
dbc::log("invalid event sent to MAPPING");
}
}
void FSM::MOVING(Event ) {
if($camera.play_move($rayview)) {
System::plan_motion(*$level.world, {size_t($camera.targetX), size_t($camera.targetY)});
@ -89,6 +175,12 @@ namespace gui {
$camera.plan_rotate($rayview, -1);
state(State::ROTATING);
break;
case FU::MAP_OPEN:
state(State::MAPPING);
break;
case FU::CLOSE:
dbc::log("Nothing to close.");
break;
default:
dbc::sentinel("unhandled event in IDLE");
}
@ -140,6 +232,12 @@ namespace gui {
case KEY::R:
$stats.reset();
break;
case KEY::M:
event(Event::MAP_OPEN);
break;
case KEY::Escape:
event(Event::CLOSE);
break;
default:
break; // ignored
}
@ -156,15 +254,9 @@ namespace gui {
void FSM::draw_gui() {
sf::RectangleShape rect({SCREEN_WIDTH - RAY_VIEW_WIDTH, SCREEN_HEIGHT});
sf::RectangleShape map_rect({SCREEN_WIDTH - RAY_VIEW_WIDTH - 10, 300});
rect.setPosition({0,0});
rect.setFillColor({50, 50, 50});
map_rect.setPosition({0, SCREEN_HEIGHT-300});
map_rect.setFillColor({20, 20, 20});
$window.draw(rect);
$window.draw(map_rect);
$text.setString(
fmt::format("FPS\n"
@ -184,15 +276,19 @@ namespace gui {
$rayview.$dirY, $rayview.$posX, $rayview.$posY));
$window.draw($text);
std::wstring map = $level.map->tiles().minimap(int($rayview.$posX), int($rayview.$posY));
$map_display.setString(map);
$window.draw($map_display);
}
void FSM::render() {
auto start = std::chrono::high_resolution_clock::now();
$rayview.draw($window);
if(in_state(State::MAPPING)) {
$window.clear();
$map_view.render();
$renderer.draw($map_view);
} else {
$rayview.draw($window);
}
auto end = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration<double>(end - start);
$stats.sample(1/elapsed.count());
@ -211,7 +307,7 @@ namespace gui {
}
void FSM::generate_map() {
$level = $levels.current();
// ZED: this should eventually go away now that level manager is in play
auto& player = $level.world->get_the<Player>();
auto& player_position = $level.world->get<Position>(player.entity);
$player = player_position.location;
@ -222,6 +318,7 @@ namespace gui {
System::enemy_pathing($level);
System::collision($level);
System::motion($level);
System::lighting($level);
System::death($level);
}

@ -5,11 +5,30 @@
#include "levelmanager.hpp"
#include "camera.hpp"
#include "fsm.hpp"
#include "render.hpp"
#include "panel.hpp"
#include <ftxui/dom/canvas.hpp>
using ftxui::Canvas;
namespace gui {
class MapViewUI : public Panel {
public:
Canvas $canvas;
GameLevel $level;
MapViewUI(GameLevel &level);
void create_render();
void resize_canvas();
void draw_map();
void update_level(GameLevel &level);
};
enum class State {
START,
MOVING,
MAPPING,
ROTATING,
IDLE,
END
@ -22,6 +41,8 @@ namespace gui {
MOVE_BACK,
MOVE_LEFT,
MOVE_RIGHT,
MAP_OPEN,
CLOSE,
ROTATE_LEFT,
ROTATE_RIGHT,
QUIT
@ -29,15 +50,16 @@ namespace gui {
class FSM : public DeadSimpleFSM<State, Event> {
public:
GameLevel $level;
float $rotation = -30.0f;
Point $player{0,0};
LevelManager $levels;
sf::RenderWindow $window;
SFMLRender $renderer;
GameLevel $level;
MapViewUI $map_view;
CameraLOL $camera;
sf::Font $font;
sf::Text $text;
sf::Text $map_display;
Stats $stats;
TexturePack $textures;
Raycaster $rayview;
@ -48,6 +70,7 @@ namespace gui {
void START(Event );
void MOVING(Event );
void MAPPING(Event);
void ROTATING(Event );
void IDLE(Event ev);
void END(Event ev);

@ -10,6 +10,8 @@ int main() {
// ZED: need to sort out how to deal with this in the FSM
if(main.in_state(gui::State::IDLE)) {
main.keyboard();
} else if(main.in_state(gui::State::MAPPING)) {
main.keyboard();
} else{
main.event(gui::Event::TICK);
}

@ -30,17 +30,21 @@ sfml_main = dependency('sfml_main')
sfml_network = dependency('sfml_network')
sfml_system = dependency('sfml_system')
sfml_window = dependency('sfml_window')
ftxui_screen = dependency('ftxui-screen')
ftxui_dom = dependency('ftxui-dom')
ftxui_component = dependency('ftxui-component')
dependencies = [
fmt, json, opengl32, freetype2,
flac, ogg, vorbis, vorbisfile, vorbisenc,
winmm, gdi32, sfml_audio, sfml_graphics,
sfml_main, sfml_network, sfml_system,
sfml_window
sfml_window, ftxui_screen, ftxui_dom, ftxui_component
]
sources = [
'animator.cpp',
'ansi_parser.cpp',
'camera.cpp',
'combat.cpp',
'components.cpp',
@ -54,9 +58,11 @@ sources = [
'map.cpp',
'matrix.cpp',
'matrix.cpp',
'panel.cpp',
'pathing.cpp',
'rand.cpp',
'raycaster.cpp',
'render.cpp',
'save.cpp',
'shiterator.hpp',
'spatialmap.cpp',

@ -0,0 +1,64 @@
#include "panel.hpp"
#include "dbc.hpp"
void Panel::resize(int w, int h) {
$dirty = true;
width = w;
height = h;
$screen = Screen(width, height);
}
void Panel::set_renderer(Component renderer) {
$dirty = true;
$component = renderer;
}
void Panel::add(Component child) {
dbc::pre("must set_renderer first", $component != nullptr);
$dirty = true;
$component->Add(child);
}
void Panel::render() {
$dirty = true;
if(must_clear) $screen.Clear();
Render($screen, $component->Render());
}
const std::wstring& Panel::to_string() {
if($dirty) {
std::string as_text = $screen.ToString();
$screenout = $converter.from_bytes(as_text);
$dirty = false;
}
return $screenout;
}
void Panel::mouse_click(ftxui::Mouse::Button btn, Point pos) {
ftxui::Mouse mev{
.button=btn,
.motion=ftxui::Mouse::Motion::Pressed,
.x=int(pos.x), .y=int(pos.y)
};
$component->OnEvent(ftxui::Event::Mouse("", mev));
}
void Panel::mouse_release(ftxui::Mouse::Button btn, Point pos) {
ftxui::Mouse mev{
.button=btn,
.motion=ftxui::Mouse::Motion::Released,
.x=int(pos.x), .y=int(pos.y)
};
$component->OnEvent(ftxui::Event::Mouse("", mev));
}
const Screen& Panel::screen() {
return $screen;
}
void Panel::key_press(ftxui::Event event) {
$component->OnEvent(event);
}

@ -0,0 +1,60 @@
#pragma once
#include <ftxui/dom/node.hpp> // for Render
#include <ftxui/component/component.hpp>
#include <ftxui/component/mouse.hpp>
#include <ftxui/dom/canvas.hpp>
#include <ftxui/screen/screen.hpp>
#include <ftxui/dom/canvas.hpp>
#include <ftxui/screen/screen.hpp>
#include <ftxui/dom/canvas.hpp>
#include <SFML/Graphics/Color.hpp>
#include <locale>
#include <codecvt>
#include "color.hpp"
#include "point.hpp"
const int UI_PANEL_BORDER_PX=5;
using ftxui::Renderer, ftxui::Component, ftxui::Element, ftxui::Screen;
class Panel {
public:
int x;
int y;
int width;
int height;
bool has_border = false;
bool must_clear = true;
bool grid = false;
sf::Color default_bg = ColorValue::BLACK;
sf::Color default_fg = ColorValue::LIGHT_LIGHT;
sf::Color border_color = ColorValue::MID;
int border_px = UI_PANEL_BORDER_PX;
bool $dirty = true;
Component $component = nullptr;
Screen $screen;
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> $converter;
std::wstring $screenout;
Panel(int x, int y, int width, int height, bool is_grid=false) :
x(x),
y(y),
width(width),
height(height),
grid(is_grid),
$screen(Screen(width, height))
{
must_clear = !is_grid;
};
void resize(int width, int height);
void set_renderer(Component renderer);
void add(Component child);
void render();
void mouse_click(ftxui::Mouse::Button btn, Point pos);
void mouse_release(ftxui::Mouse::Button btn, Point pos);
void key_press(ftxui::Event event);
const std::wstring &to_string();
const Screen &screen();
};

@ -0,0 +1,275 @@
#include "render.hpp"
#include "ansi_parser.hpp"
#include <cmath>
#include <fmt/core.h>
#include <array>
#include "map.hpp"
#include <iostream>
#include "color.hpp"
#if defined(_WIN64) || defined(_WIN32)
#include <stdio.h>
#include <fcntl.h>
#include <io.h>
#endif
using namespace fmt;
SFMLRender::SFMLRender(sf::RenderWindow &window) :
$window(window),
$map_font_size(0),
$line_spacing(0),
$default_fg(ColorValue::LIGHT_MID),
$default_bg(ColorValue::BLACK),
$bg_sprite($font_texture),
$font(FONT_FILE_NAME),
$ui_text($font),
$ansi($default_fg, $default_bg)
{
// force true color, but maybe I want to support different color sets
$font.setSmooth(false);
$ui_text.setPosition({0,0});
$ui_text.setCharacterSize($config.ui_font_size);
$ui_text.setFillColor(ColorValue::LIGHT_MID);
sf::Glyph glyph = $font.getGlyph($config.ui_base_char, $config.ui_font_size, false);
$text_bounds = glyph.bounds;
$cells_w = std::ceil($config.video_x / $text_bounds.size.x);
$cells_h = std::ceil($config.video_y / $text_bounds.size.y);
}
sf::Sprite &SFMLRender::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);
$sprites.try_emplace(tile, $font_texture, glyph.textureRect);
}
return $sprites.at(tile);
}
void SFMLRender::clear_cache() {
$sprites.clear();
bool good = $font.openFromFile(FONT_FILE_NAME);
dbc::check(good, "Failed to load the font.");
$font.setSmooth(false);
$ui_text.setFont($font);
}
void SFMLRender::center_panel(Panel &panel) {
int cell_center_x = ($cells_w - panel.width) / 2;
int cell_center_y = ($cells_h - panel.height) / 2;
panel.x = cell_center_x * $text_bounds.size.x;
panel.y = cell_center_y * $text_bounds.size.y;
}
void SFMLRender::resize_grid(int new_size, Panel &panel_out) {
auto glyph = $font.getGlyph($config.bg_tile, new_size, false);
int view_x = std::ceil(($config.video_x - panel_out.x) / glyph.bounds.size.x);
int view_y = std::ceil(($config.video_y - panel_out.y) / glyph.bounds.size.y);
// looks good, set 'em all
$base_glyph = glyph;
$map_font_size = new_size;
$sprites.clear(); // need to reset the sprites for the new size
$line_spacing = $font.getLineSpacing($map_font_size);
$bg_sprite = get_text_sprite($config.bg_tile);
$grid_bounds = $bg_sprite.getLocalBounds();
panel_out.resize(view_x, view_y);
}
inline void configure_tile(const sf::Sprite &sprite, sf::FloatRect &sp_bounds, sf::FloatRect grid_bounds, float &width_delta, float &height_delta) {
// BUG: I think I could create a struct that kept this info for all sprites loaded
// should look into caching all this instead of calcing it each time
sp_bounds = sprite.getLocalBounds();
// calculate where to center the sprite, but only if it's smaller
width_delta = grid_bounds.size.x > sp_bounds.size.x ? (grid_bounds.size.x - sp_bounds.size.x) / 2 : 0;
height_delta = grid_bounds.size.y > sp_bounds.size.x ? (grid_bounds.size.y - sp_bounds.size.y) / 2 : 0;
}
void SFMLRender::render_grid(const std::wstring &text, sf::Color default_fg, sf::Color default_bg, float x, float y) {
wchar_t last_tile = $config.bg_tile;
sf::FloatRect sp_bounds;
float width_delta = 0;
float height_delta = 0;
sf::Sprite &sprite = get_text_sprite(last_tile);
const float start_x = x;
sf::Color cur_fg = default_fg;
sf::Color cur_bg = default_bg;
$ansi.parse(text, [&](auto fg, auto bg) {
cur_fg = fg;
cur_bg = bg;
},
[&](wchar_t tile) {
switch(tile) {
case '\r': break; // ignore it
case '\n': {
// don't bother processing newlines, just skip
y += $line_spacing;
x = start_x;
}
break;
default: {
// only get a new sprite if the tile changed
if(last_tile != tile) {
sprite = get_text_sprite(tile);
configure_tile(sprite, sp_bounds, $grid_bounds, width_delta, height_delta);
last_tile = tile; // update last tile seen
}
sprite.setPosition({x+width_delta, y+height_delta});
sprite.setColor(cur_fg);
// only draw background char if it's different from default
if(cur_bg != default_bg) {
$bg_sprite.setPosition({x, y});
$bg_sprite.setColor(cur_bg);
$window.draw($bg_sprite);
}
$window.draw(sprite);
// next cell
x += $base_glyph.advance;
}
}
});
}
inline sf::FloatRect draw_chunk(sf::RenderWindow& window,
sf::FloatRect text_bounds, sf::Text& text, sf::Color default_bg,
sf::Color bgcolor, int bg_box_offset, float x, float y, std::wstring &out)
{
text.setString(out);
text.setPosition({x, y});
// get a base character for the cell size
sf::FloatRect bounds({x, y}, {text_bounds.size.x * out.size(), text_bounds.size.y});
if(default_bg != bgcolor) {
sf::RectangleShape backing(bounds.size);
backing.setFillColor(bgcolor);
backing.setPosition({bounds.position.x, bounds.position.y + bg_box_offset});
window.draw(backing);
}
window.draw(text);
out.clear();
return bounds;
}
void SFMLRender::render_text(const std::wstring &text, sf::Color default_fg, sf::Color default_bg, float start_x, float start_y) {
std::wstring out;
float x = start_x;
float y = start_y;
sf::Color cur_bg = default_bg;
// start with the default_fg until it's changed
$ui_text.setFillColor(default_fg);
$ansi.parse(text,
[&](auto fg, auto bg) {
if(out.size() > 0 ) {
auto bounds = draw_chunk($window,
$text_bounds, $ui_text,
default_bg, cur_bg, $config.bg_box_offset, x, y, out);
x += bounds.size.x;
}
cur_bg = bg;
$ui_text.setFillColor(fg);
},
[&](wchar_t tile) {
switch(tile) {
case '\r': break; // ignore it
case '\n': {
sf::FloatRect bounds;
if(out.size() > 0) {
bounds = draw_chunk($window, $text_bounds,
$ui_text, default_bg, cur_bg, $config.bg_box_offset, x, y, out);
} else {
bounds = $ui_text.getLocalBounds();
}
y += bounds.size.y;
x = start_x; // reset to the original position
}
break;
default:
out += tile;
break;
}
}
);
if(out.size() > 0) {
draw_chunk($window, $text_bounds, $ui_text, default_bg, cur_bg, $config.bg_box_offset, x, y, out);
}
}
void SFMLRender::draw_sprite(sf::Sprite &sprite, sf::Shader *shader) {
$window.draw(sprite, shader);
}
/*
* Does not render the panel, you have to do that so you can control
* when things render.
*/
void SFMLRender::draw(Panel &panel, float x_offset, float y_offset) {
const std::wstring &panelout = panel.to_string();
auto bounds = panel.grid ? $grid_bounds : $text_bounds;
sf::RectangleShape backing(
sf::Vector2f(bounds.size.x * panel.width + panel.border_px,
bounds.size.y * panel.height + panel.border_px));
backing.setFillColor(panel.default_bg);
if(panel.has_border) {
backing.setOutlineColor(panel.border_color);
backing.setOutlineThickness(panel.border_px);
}
backing.setPosition({panel.x + x_offset, panel.y + y_offset});
$window.draw(backing);
if(panel.grid) {
render_grid(panelout, panel.default_fg, panel.default_bg, panel.x + x_offset, panel.y + y_offset);
} else {
render_text(panelout, panel.default_fg, panel.default_bg, panel.x + x_offset, panel.y + y_offset);
}
}
bool SFMLRender::mouse_position(Panel &panel, Point &out) {
// yes, you have to do this in sfml
sf::Vector2f pos = $window.mapPixelToCoords(sf::Mouse::getPosition($window));
auto bounds = panel.grid ? $grid_bounds : $text_bounds;
if(pos.x >= panel.x && pos.y >= panel.y
&& pos.x <= (panel.x + panel.width * bounds.size.x)
&& pos.y <= (panel.y + panel.height * bounds.size.y))
{
out = {
size_t((pos.x - panel.x) / bounds.size.x),
size_t((pos.y - panel.y) / bounds.size.y)
};
return true;
}
return false;
}
void SFMLRender::init_terminal() {
#if defined(_WIN64) || defined(_WIN32)
_setmode(_fileno(stdout), _O_U16TEXT);
#endif
ftxui::Terminal::SetColorSupport(ftxui::Terminal::Color::TrueColor);
}

@ -0,0 +1,80 @@
#pragma once
#include <ftxui/screen/screen.hpp>
#include <ftxui/dom/canvas.hpp>
#include <SFML/Window.hpp>
#include <SFML/System.hpp>
#include <SFML/Graphics.hpp>
#include <SFML/Graphics/Rect.hpp>
#include "point.hpp"
#include <codecvt>
#include "ansi_parser.hpp"
#include "panel.hpp"
#include "constants.hpp"
#include <optional>
using ftxui::Canvas, ftxui::Screen;
/*
* BUG: This could be so much better.
*/
struct RenderConfig {
unsigned int video_x = VIDEO_WINDOW_X;
unsigned int video_y = VIDEO_WINDOW_Y;
int ui_font_size=UI_FONT_SIZE;
int base_map_font_size=BASE_MAP_FONT_SIZE;
wchar_t bg_tile = BG_TILE;
wchar_t ui_base_char = UI_BASE_CHAR;
int bg_box_offset=BG_BOX_OFFSET;
};
struct SFMLRender {
int $cells_w = 0;
int $cells_h = 0;
RenderConfig $config;
sf::RenderWindow& $window;
int $map_font_size;
float $line_spacing;
sf::Color $default_fg;
sf::Color $default_bg;
sf::Texture $font_texture;
sf::Sprite $bg_sprite;
sf::Font $font;
sf::Text $ui_text;
ANSIParser $ansi;
std::unordered_map<wchar_t, sf::Sprite> $sprites;
sf::Glyph $base_glyph;
sf::FloatRect $grid_bounds;
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> $converter;
sf::FloatRect $text_bounds;
SFMLRender(sf::RenderWindow& window);
// disable copy
SFMLRender(SFMLRender &other) = delete;
sf::Sprite &get_text_sprite(wchar_t tile);
void resize_grid(int new_size, Panel &panel_out);
void render_grid(const std::wstring &text, sf::Color default_fg, sf::Color default_bg, float x, float y);
void render_text(const std::wstring &text, sf::Color default_fg, sf::Color default_bg, float x, float y);
void draw(Panel &panel, float x_offset=0.0f, float y_offset=0.0f);
void draw_sprite(sf::Sprite &sprite, sf::Shader *shader);
void center_panel(Panel &panel);
std::optional<sf::Event> poll_event() {
return $window.pollEvent();
}
void close() { return $window.close(); }
bool is_open() { return $window.isOpen(); }
int font_size() { return $map_font_size; }
void clear() { $window.clear(); }
void display() { $window.display(); }
bool mouse_position(Panel &panel, Point &out);
void clear_cache();
static void init_terminal();
};

@ -12,6 +12,7 @@ using std::string;
using namespace fmt;
using namespace components;
using lighting::LightSource;
using ftxui::Color;
void System::lighting(GameLevel &level) {
auto &light = *level.lights;
@ -221,3 +222,27 @@ void System::plan_motion(DinkyECS::World& world, Point move_to) {
motion.dx = move_to.x - player_position.location.x;
motion.dy = move_to.y - player_position.location.y;
}
/*
* This one is called inside the MapViewUI very often so
* just avoide GameMap unlike the others.
*/
void System::draw_entities(DinkyECS::World &world, Map &map, const Matrix &lights, ftxui::Canvas &canvas, const Point &cam_orig, size_t view_x, size_t view_y) {
auto &tiles = map.tiles();
world.query<Position, Tile>([&](auto &ent[[maybe_unused]], auto &pos, auto &tile) {
if(pos.location.x >= cam_orig.x && pos.location.x <= cam_orig.x + view_x
&& pos.location.y >= cam_orig.y && pos.location.y <= cam_orig.y + view_y) {
Point loc = map.map_to_camera(pos.location, cam_orig);
float light_value = lights[pos.location.y][pos.location.x] * PERCENT;
const TileCell& cell = tiles.at(pos.location.x, pos.location.y);
// the 2 and 4 are from ftxui::Canvas since it does a kind of "subpixel" drawing
canvas.DrawText(loc.x*2, loc.y*4, tile.chr, [tile, light_value, cell](auto &pixel) {
pixel.foreground_color = Color::HSV(tile.fg_h, tile.fg_s, tile.fg_v * light_value);
pixel.background_color = Color::HSV(cell.bg_h, cell.bg_s, cell.bg_v * light_value);
});
}
});
}

@ -1,6 +1,7 @@
#pragma once
#include "components.hpp"
#include "levelmanager.hpp"
#include <ftxui/dom/canvas.hpp>
namespace System {
@ -16,4 +17,5 @@ namespace System {
void pickup(DinkyECS::World &world, DinkyECS::Entity actor, DinkyECS::Entity item);
void device(DinkyECS::World &world, DinkyECS::Entity actor, DinkyECS::Entity item);
void plan_motion(DinkyECS::World& world, Point move_to);
void draw_entities(DinkyECS::World &world, Map &map, const Matrix &lights, ftxui::Canvas &canvas, const Point &cam_orig, size_t view_x, size_t view_y);
}

@ -73,21 +73,3 @@ bool TileMap::INVARIANT() {
dbc::check(matrix::width($tile_ids) == $width, "$tile_ids has wrong width");
return true;
}
std::wstring TileMap::minimap(size_t x, size_t y) {
string result;
for(matrix::box it{$tile_ids, x, y, 5}; it.next();) {
const TileCell &cell = $display[it.y][it.x];
if(it.x == x && it.y == y) {
result += "@";
} else {
result += cell.display;
}
if(it.x == it.right - 1) result += "\n";
}
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> $converter;
return $converter.from_bytes(result);
}

@ -43,5 +43,4 @@ public:
void dump(int show_x=-1, int show_y=-1);
bool INVARIANT();
std::wstring minimap(size_t x, size_t y);
};

@ -0,0 +1,15 @@
[wrap-file]
directory = FTXUI-5.0.0
source_url = https://github.com/ArthurSonzogni/FTXUI/archive/refs/tags/v5.0.0.tar.gz
source_filename = FTXUI-5.0.0.tar.gz
source_hash = a2991cb222c944aee14397965d9f6b050245da849d8c5da7c72d112de2786b5b
patch_filename = ftxui_5.0.0-1_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/ftxui_5.0.0-1/get_patch
patch_hash = 21c654e82739b90b95bd98c1d321264608d37c50d29fbcc3487f790fd5412909
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/ftxui_5.0.0-1/FTXUI-5.0.0.tar.gz
wrapdb_version = 5.0.0-1
[provide]
ftxui-screen = screen_dep
ftxui-dom = dom_dep
ftxui-component = component_dep