From 5e3a2492c7bb73daa4e27398daaf490d09980ff3 Mon Sep 17 00:00:00 2001 From: Jon Santmyer Date: Wed, 22 Jun 2022 17:41:59 -0400 Subject: Base system viewer with data loaded from csv files --- src/camera.cpp | 197 +++++++++++++++++++++ src/game.cpp | 115 +++++++++++++ src/input.cpp | 37 ++++ src/keybind.cpp | 64 +++++++ src/main.cpp | 26 +++ src/system.cpp | 517 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/timeman.cpp | 49 ++++++ src/units.cpp | 61 +++++++ src/window.cpp | 72 ++++++++ 9 files changed, 1138 insertions(+) create mode 100644 src/camera.cpp create mode 100644 src/game.cpp create mode 100644 src/input.cpp create mode 100644 src/keybind.cpp create mode 100644 src/main.cpp create mode 100644 src/system.cpp create mode 100644 src/timeman.cpp create mode 100644 src/units.cpp create mode 100644 src/window.cpp (limited to 'src') diff --git a/src/camera.cpp b/src/camera.cpp new file mode 100644 index 0000000..59e99a0 --- /dev/null +++ b/src/camera.cpp @@ -0,0 +1,197 @@ +#include "camera.hpp" +#include "shape.hpp" +#include +#include +#include +#include +#include + +void +Camera::updateFrustum(){ + if(m_dirty) { + *m_viewport << straw::clear(' '); + m_frustum = shapes::rectangle(0, 0, (long)m_viewport->getwidth(), (long)m_viewport->getheight()); + m_frustum.position -= (m_frustum.bounds / 2); + m_frustum.position += m_position + m_origin; + m_dirty = false; + } +} + +void +RenderBatchEntry::translate(Camera *camera) +{ + std::visit([camera](auto &shapeval) { + shapeval.translate(-camera->m_frustum.position); + shapeval.translate((camera->m_frustum.bounds * camera->m_scale) / 2); + shapeval.position = vex::vec2( + (long)std::floor((double)shapeval.position[0] / (double)camera->m_scale), + (long)std::floor((double)shapeval.position[1] / (double)camera->m_scale)); + shapeval.scale(camera->m_scale); + }, shape); +} + +void +Camera::draw() +{ + if(!m_dirty) return; + updateFrustum(); + shapes::rectangle ssfrustum{vex::vec2(0, 0), vex::vec2(m_viewport->getwidth(), m_viewport->getheight())}; + + //Remove all batches not intersecting the screenspace frustum + m_shapeBatch.erase( + std::remove_if( + m_shapeBatch.begin(), + m_shapeBatch.end(), + [ssfrustum, this](RenderBatchEntry &entry){ + entry.translate(this); + return std::visit([ssfrustum](auto &&shape) { + return shapes::intersects(shape, ssfrustum) == 0; + }, entry.shape); + }), + m_shapeBatch.end()); + for(RenderBatchEntry &entry : m_shapeBatch) entry.plot(this); + m_shapeBatch.clear(); +} + +void +RenderBatchEntry::plotPoint(shapes::point point, Camera *camera) { + long ploty = camera->m_viewport->getheight() - point.position[1]; + if(point.position[0] < 0 || point.position[0] >= camera->m_viewport->getwidth() || + ploty < 0 || ploty >= camera->m_viewport->getheight()) return; + *camera->m_viewport << straw::setcolor(fg, bg) << straw::plot(point.position[0], ploty, c); +} + +void +RenderBatchEntry::plotLine(shapes::line line, Camera *camera) { + if(line.end == line.position) { + plotPoint(shapes::point(line.position), camera); + } + vex::vec2 vpdim(camera->m_viewport->getwidth(), + camera->m_viewport->getheight()); + vex::vec2 d(line.end - line.position); + long adx = std::abs(d[0]); + long ady = std::abs(d[1]); + + long offX= d[0] > 0 ? 1 : -1; + long offY = d[1] > 0 ? 1 : -1; + + *camera->m_viewport << straw::setcolor(fg, bg); + if(adx < ady) { + long err = ady / 2; + long x = line.position[0]; + long y = line.position[1]; + for(long i = 0; i < ady; i++) { + if(x > 0 && x < vpdim[0] && y > 0 && y < vpdim[1]) { + long ploty = vpdim[1] - y; + *camera->m_viewport << straw::plot(x, ploty, c); + } + if(err >= ady) { + x += offX; + y += offY; + err += adx - ady; + }else { + y += offY; + err += adx; + } + } + + }else{ + long err = adx / 2; + long x = line.position[0]; + long y = line.position[1]; + for(long i = 0; i < adx; i++) { + if(x > 0 && x < vpdim[0] && y > 0 && y < vpdim[1]) { + long ploty = vpdim[1] - y; + *camera->m_viewport << straw::plot(x, ploty, c); + } + if(err >= adx) { + x += offX; + y += offY; + err += ady - adx; + }else { + x += offX; + err += ady; + } + } + } +} + +void +RenderBatchEntry::plotCircle(shapes::circle circle, Camera *camera) { + if(circle.radius == 1) { + plotPoint(shapes::point(circle.position), camera); + return; + } + long sy = std::max(1, circle.position[1] - circle.radius); + long ey = std::min(camera->m_viewport->getheight(), circle.position[1] + circle.radius); + *camera->m_viewport << straw::setcolor(fg, bg); + for(long y = sy; y <= ey; y++) { + long ploty = camera->m_viewport->getheight() - y; + long r2 = circle.radius * circle.radius; + long dy = circle.position[1] - y; + long dx = (long)std::sqrt(r2 - (dy * dy)); + long sx = std::max(0, circle.position[0] - dx + 1); + long ex = std::min(camera->m_viewport->getwidth(), circle.position[0] + dx); + for(unsigned x = (unsigned)sx; x < ex; x++) { + *camera->m_viewport << straw::plot(x, ploty, c); + } + } +} + +void +RenderBatchEntry::plotRectangle(shapes::rectangle rectangle, Camera *camera) +{ + if(rectangle.bounds[1] == 1) { + plotPoint(shapes::point(rectangle.position), camera); + return; + } + long sy = std::max(1, rectangle.position[1]); + long ey = std::min(camera->m_viewport->getheight(), rectangle.position[1] + rectangle.bounds[1]); + *camera->m_viewport << straw::setcolor(fg, bg); + for(long y = sy; y < ey; y++) { + long ploty = camera->m_viewport->getheight() - y; + long sx = std::max(0, rectangle.position[1]); + long ex = std::min(camera->m_viewport->getwidth(), rectangle.position[1] + rectangle.bounds[1]); + for(unsigned x = (unsigned)sx; x < ex; x++) { + *camera->m_viewport << straw::plot(x, ploty, c); + } + } +} + +void +RenderBatchEntry::plotEllipse(shapes::ellipse shapeval, Camera *camera) { + if(shapeval.a == 1) { + plotPoint(shapes::point(shapeval.position), camera); + return; + } + long sy = std::max(1, shapeval.position[1] - shapeval.b); + long ey = std::min(camera->m_viewport->getheight(), shapeval.position[1] + shapeval.b); + *camera->m_viewport << straw::setcolor(fg, bg); + for(long y = sy; y <= ey; y++) { + long ploty = camera->m_viewport->getheight() - y; + long dy = shapeval.position[1] - y; + long dy2 = dy * dy; + long a2 = shapeval.a * shapeval.a; + long b2 = shapeval.b * shapeval.b; + long dx = (long)(((2.0 * (double)shapeval.a) / + (double)shapeval.b) * std::sqrt(b2 - dy2) / 2.0); + long sx = std::max(0, shapeval.position[0] - dx + 1); + long ex = std::min(camera->m_viewport->getwidth(), shapeval.position[0] + dx); + + for(unsigned x = (unsigned)sx; x < ex; x++) { + *camera->m_viewport << straw::plot(x, ploty, c); + } + } +} +void +RenderBatchEntry::plot(Camera *camera) +{ + std::visit([this, camera](auto &shapeval) -> void{ + using T = std::decay_t; + if constexpr(std::is_same_v>) plotPoint(shapeval, camera); + if constexpr(std::is_same_v>) plotLine(shapeval, camera); + if constexpr(std::is_same_v>) plotEllipse(shapeval, camera); + if constexpr(std::is_same_v>) plotCircle(shapeval, camera); + if constexpr(std::is_same_v>) plotRectangle(shapeval, camera); + }, shape); +} diff --git a/src/game.cpp b/src/game.cpp new file mode 100644 index 0000000..b1d0b00 --- /dev/null +++ b/src/game.cpp @@ -0,0 +1,115 @@ +#include "game.hpp" +#include "keybind.hpp" +#include "input.hpp" +#include +#include + +constexpr static double REQUESTED_FPS = 60.0; +constexpr static double REQ_FPS_MSPT = 1000.0 / REQUESTED_FPS; + +std::unordered_map Game::m_contexts; +std::string Game::m_currentContext; + +std::unique_ptr Game::m_camera; +std::unique_ptr Game::m_system; +SystemView Game::m_systemView(nullptr); + +input::Context Game::m_inputContext; + +double Game::m_delta; +Game::State Game::m_state = State::RUNNING; +Game::WindowContexts Game::contexts; + +void +Game::setup(unsigned w, unsigned h) +{ + KeyMan::loadKeybindsFrom("keybinds.csv"); + + m_inputContext.echo(false); + m_inputContext.canon(false); + m_inputContext.cbreak(false); + + unsigned int viewh = h - 1; + unsigned int infow = 24; + unsigned int infoh = 12; + unsigned int timeh = 10; + + TimeMan::init(); + m_contexts.emplace(WINCTX_GAME, WindowContext()); + WindowContext *gameContext = &m_contexts[WINCTX_GAME]; + + gameContext->registerWindow(WINDOW_SYSTEMVIEW_ID, "System View", infow, 0, w - infow, viewh); + gameContext->registerWindow(WINDOW_BODYINFO_ID, "Body Info", 0, 0, infow, infoh); + gameContext->registerWindow(WINDOW_EVENTS_ID, "Events", 0, infoh, infow, viewh - infoh - timeh); + gameContext->registerWindow(WINDOW_TIMEMAN_ID, "Time", 0, viewh - timeh, infow, timeh); + + gameContext->registerWindow(WINDOW_SYSTEMVIEW_SEARCH_ID, "Search", infow, 0, (w - infow) / 4, viewh, true); + m_currentContext = WINCTX_GAME; + + m_camera = std::make_unique((*gameContext)[WINDOW_SYSTEMVIEW_ID].screen()); + m_system = std::make_unique(); + m_systemView.view(m_system.get()); + + KeyMan::registerBind('\x1B', BIND_G_ESCAPE, CTX_GLOBAL, "Escape from focused searchbox / window"); + KeyMan::registerBind('\n', BIND_G_SELECT, CTX_GLOBAL, "Select something"); + KeyMan::registerBind('x', BIND_G_NEXTWIN, CTX_GLOBAL, "Change the focused window to the next in the stack"); + KeyMan::registerBind('X', BIND_G_PREVWIN, CTX_GLOBAL, "Change the focused window to the previous in the stack"); + KeyMan::registerBind('y', BIND_G_QUIT, CTX_GLOBAL, "Terminate the game"); + + KeyMan::registerBind('w', BIND_SYSTEMVIEW_PANUP, CTX_SYSTEMVIEW, "Moves the camera upwards on the system viewer"); + KeyMan::registerBind('a', BIND_SYSTEMVIEW_PANLEFT, CTX_SYSTEMVIEW, "Moves the camera to the left on the system viewer"); + KeyMan::registerBind('s', BIND_SYSTEMVIEW_PANDOWN, CTX_SYSTEMVIEW, "Moves the camera downwards on the system viewer"); + KeyMan::registerBind('d', BIND_SYSTEMVIEW_PANRIGHT, CTX_SYSTEMVIEW, "Moves the camera to the right on the system viewer"); + KeyMan::registerBind('-', BIND_SYSTEMVIEW_INCSCALE, CTX_SYSTEMVIEW, "Decreases zoom from center of screen"); + KeyMan::registerBind('+', BIND_SYSTEMVIEW_DECSCALE, CTX_SYSTEMVIEW, "Increases zoom into center of screen"); + KeyMan::registerBind('/', BIND_SYSTEMVIEW_SEARCH, CTX_SYSTEMVIEW, "Search through bodies in the system"); + + KeyMan::registerBind(input::CTRL_KEY_ARROWUP, BIND_SYSTEMVIEW_SEARCH_PREV, CTX_SYSTEMVIEW, "Move the cursor up in the search view"); + KeyMan::registerBind(input::CTRL_KEY_ARROWDOWN, BIND_SYSTEMVIEW_SEARCH_NEXT, CTX_SYSTEMVIEW, "Move the cursor down in the search view"); + KeyMan::registerBind(input::CTRL_KEY_HOME, BIND_SYSTEMVIEW_SEARCH_TOP, CTX_SYSTEMVIEW, "Move to first entry in search view"); + KeyMan::registerBind(input::CTRL_KEY_END, BIND_SYSTEMVIEW_SEARCH_BOTTOM, CTX_SYSTEMVIEW, "Move to last entry in search view"); + KeyMan::registerBind(input::CTRL_KEY_ARROWRIGHT, BIND_SYSTEMVIEW_SEARCH_COLLAPSE, CTX_SYSTEMVIEW, "Toggle collapsed entry in search view"); +} + +void +Game::cleanup() +{ + KeyMan::writeKeybindsTo("keybinds.csv"); +} + +void +Game::turn() +{ + auto start = std::chrono::steady_clock::now(); + auto end = start + std::chrono::milliseconds(16); + int c = input::getcode(); + WindowContext &context = m_contexts.at(m_currentContext); + + /*Global keybinds*/ + if(!inputMode()) { + if(c == KeyMan::binds[BIND_G_QUIT].code) m_state = State::STOPPED; + context.update(c); + } + + if(m_currentContext == WINCTX_GAME) { /*Game Context*/ + m_systemView.keypress(m_camera.get(), c); + + m_system->update(); + m_systemView.update(m_camera.get()); + TimeMan::update(c); + if(TimeMan::changed()) { + m_camera->markDirty(); + } + + m_systemView.draw(m_camera.get()); + TimeMan::draw(); + m_camera->draw(); + m_systemView.drawOver(m_camera.get()); + context.draw(); + } + + std::this_thread::sleep_until(end); + end = std::chrono::steady_clock::now(); + auto diff = end - start; + m_delta = (double)(std::chrono::duration_cast(diff).count()) / 1000.0; +} diff --git a/src/input.cpp b/src/input.cpp new file mode 100644 index 0000000..5cbedb2 --- /dev/null +++ b/src/input.cpp @@ -0,0 +1,37 @@ +#include "input.hpp" +namespace input { + +int getcode() { + int r = 0; + char c; + if(read(STDIN_FILENO, &c, 1) != 1) return -1; + if(c == '\x1b') { + char seq[3]; + if(read(STDIN_FILENO, &seq[0], 1) != 1) return '\x1b'; + if(read(STDIN_FILENO, &seq[1], 1) != 1) return '\x1b'; + if(seq[0] == '[') { + if(seq[1] >= 0 && seq[1] <= '9') { + if(read(STDIN_FILENO, &seq[2], 1) != 1) return '\x1b'; + if(seq[2] == '~') { + switch(seq[1]) { + case '1': + case '7': return CTRL_KEY_HOME; + case '4': + case '8': return CTRL_KEY_END; + default: return CTRL_RANGE_START + seq[1] + 20; + } + } + }else{ + switch(seq[1]) { + default: return CTRL_RANGE_START + seq[1]; + } + } + }else if(seq[0] == 'O') { + return CTRL_RANGE_START + seq[1]; + } + return '\x1b'; + }else{ + return c; + } +} +} diff --git a/src/keybind.cpp b/src/keybind.cpp new file mode 100644 index 0000000..8283c4c --- /dev/null +++ b/src/keybind.cpp @@ -0,0 +1,64 @@ +#include "keybind.hpp" +#include "csv.hpp" +#include "input.hpp" + +#include + +std::unordered_map KeyMan::m_keybinds; +std::unordered_map KeyMan::m_keybindContexts; + +KeyMan::Binds KeyMan::binds; + +static std::unordered_map CODENAMES = +{ + { input::CTRL_KEY_ARROWUP, "Up arrow" }, + { input::CTRL_KEY_ARROWDOWN, "Down arrow" }, + { input::CTRL_KEY_ARROWRIGHT, "Right arrow" }, + { input::CTRL_KEY_ARROWLEFT, "Left arrow" } +}; + +void +KeyMan::registerBind(int def, + const std::string &name, + const std::string &context, + const std::string &desc) +{ + auto find = m_keybinds.find(name); + if(find != m_keybinds.end()) { + find->second.ctx = context; + find->second.desc = desc; + }else{ + Bind bind = { def, name, context, desc}; + m_keybinds[name] = bind; + } + m_keybindContexts[name] = context; +} + +void +KeyMan::loadKeybindsFrom(const std::string &csvPath) +{ + csv::CSVFile<',', int, std::string> keybindData(csvPath); + for(auto &bind : keybindData.get()) { + int code = std::get<0>(bind); + std::string name = std::get<1>(bind); + + m_keybinds[name] = { .code = code, .name = name, .ctx = "", .desc = ""}; + } +} + +void +KeyMan::writeKeybindsTo(const std::string &csvPath) +{ + csv::CSVFile<',', int, std::string> keybindData(csvPath, true); + for(Bind &bind : KeyMan::binds()) { + keybindData.put({bind.code, bind.name}); + } + keybindData.write(); +} + +std::string +KeyMan::translateCode(int code) +{ + if(code < 256) return std::string(1, (char)code); + return CODENAMES[code]; +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..9ff09a6 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,26 @@ +#include +#include "game.hpp" + +#include +#include +#include + +int +main(int argc, char **argv) +{ + (void)argc; + (void)argv; + + struct winsize w; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); + fcntl(STDIN_FILENO, F_SETFL, fcntl(0, F_GETFL) | O_NONBLOCK); + + Game::setup(w.ws_col, w.ws_row); + + while(Game::running()) { + Game::turn(); + } + + Game::cleanup(); + return 0; +} diff --git a/src/system.cpp b/src/system.cpp new file mode 100644 index 0000000..974d0dc --- /dev/null +++ b/src/system.cpp @@ -0,0 +1,517 @@ +#include "system.hpp" +#include "timeman.hpp" +#include "ecs.hpp" +#include "vex.hpp" +#include "units.hpp" +#include "csv.hpp" +#include "keybind.hpp" +#include "game.hpp" +#include +#include + +static double G = 6.6743 * std::pow(10, -11); + +ecs::Entity & +System::addOrbital(const std::string &name, + const std::string &orbitingName, + unsigned long a, + double e, + unit::Mass m, + unsigned r, + double M, + double w) +{ + M *= (std::numbers::pi / 180.0); + w *= (std::numbers::pi / 180.0); + + SystemTreeNode *treeNode = getNode(orbitingName); + ecs::Entity &newOrbital = m_entityMan.newEntity() + .addComponent(ecs::PositionComponent{vex::vec2{0, 0}}) + .addComponent(ecs::MassComponent{m}) + .addComponent(ecs::OrbitalComponent{.origin = treeNode->entityId, .a = (long)a, .e = e, .w = w, .M = M, .T = 0, .v = 0}) + .addComponent(ecs::RenderCircleComponent{r}) + .addComponent(ecs::NameComponent{name}); + treeNode->children.push_back({newOrbital.id, {}}); + return newOrbital; +} + +System::System() +{ + auto sol = m_entityMan.newEntity() + .addComponent(ecs::PositionComponent{vex::vec2{0, 0}}) + .addComponent(ecs::MassComponent{unit::solMass}) + .addComponent(ecs::RenderCircleComponent{695700}) + .addComponent(ecs::NameComponent{"Sol"}); + m_systemTree.entityId = sol.id; + + csv::CSVFile<',', std::string, std::string, double, double, double, double, double, double> planetData("data/sol_planets.csv"); + for(auto &planet : planetData.get()) { + std::string name = std::get<0>(planet); + std::string orbiting = std::get<1>(planet); + double sma = std::get<2>(planet) * unit::AU; + double e = std::get<3>(planet); + unit::Mass m = unit::earthMass * std::get<4>(planet); + double r = std::get<5>(planet) * unit::earthRad; + double M = std::get<6>(planet); + double w = std::get<7>(planet); + addOrbital(name, orbiting, sma, e, m, r, M, w); + } + + csv::CSVFile<',', std::string, std::string, double, double, double, double, double, double> satelliteData("data/sol_satellites.csv"); + for(auto &satellite : satelliteData.get()) { + std::string name = std::get<0>(satellite); + std::string orbiting = std::get<1>(satellite); + double sma = std::get<2>(satellite) * unit::AU; + double e = std::get<3>(satellite); + unit::Mass m = unit::earthMass * std::get<4>(satellite); + double r = std::get<5>(satellite) * unit::earthRad; + double M = std::get<6>(satellite); + double w = std::get<7>(satellite); + addOrbital(name, orbiting, sma, e, m, r, M, w); + } + + csv::CSVFile<',', std::string, double, double, double, double, double, std::string> asteroidData("data/sol_asteroids.csv"); + for(auto &asteroid : asteroidData.get()) { + std::string name = std::get<0>(asteroid); + if(name == "Missing") name = std::get<6>(asteroid); + addOrbital(name, + "Sol", + (unsigned long)(std::get<1>(asteroid) * unit::AU), + std::get<2>(asteroid), + unit::Mass(0), + (unsigned)std::get<3>(asteroid), + std::get<4>(asteroid), + std::get<5>(asteroid)); + } +} + +constexpr static double tau = std::numbers::pi * 2; + +void +System::tickOrbitals(unit::Time time) +{ + for(ecs::Entity &e : m_entityMan.getWith()) { + auto &oc = e.get(); + auto &pc = e.get(); + auto &mc = e.get(); + + ecs::Entity &o = m_entityMan[oc.origin]; + auto &opc = o.get(); + auto &om = o.get(); + + double e2 = oc.e * oc.e; + double td = (double)time(); + if(oc.T == 0) { + double u = G * (om.mass() + mc.mass()); + double am = (double)oc.a * 1000.0; + oc.T = tau * std::sqrt((am * am * am) / u); + } + double n = tau / oc.T; + double M = oc.M + (n * td); + double E = M; + + int its = 0; + while(true) { + double dE = (E - oc.e * std::sin(E) - M) / (1 - oc.e * std::cos(E)); + E -= dE; + its++; + if(std::abs(dE) < 1e-6) break; + } + + double x = std::cos(E) - oc.e; + double y = std::sin(E) * std::sqrt(1 - e2); + + double v = std::atan2(y, x) + oc.w; + oc.v = v; + + double r = std::sqrt(x*x + y*y) * (double)oc.a; + + vex::vec2 polar(r, v); + pc.position = vex::cartesian(polar) + opc.position; + } + +} + +void +System::update() +{ + tickOrbitals(TimeMan::time()); +} + +ecs::Entity & +System::getBody(std::size_t id) { + return m_entityMan[id]; +} + +System::SystemTreeNode * +System::traverseSystemTree(SystemTreeNode &node, const std::string &name) +{ + SystemTreeNode *found = nullptr; + ecs::Entity &entity = getBody(node.entityId); + auto &namecomp = entity.get(); + if(namecomp.name == name) found = &node; + + for(SystemTreeNode &child : node.children) { + if(found != nullptr) break; + found = traverseSystemTree(child, name); + } + return found; +} + +System::SystemTreeNode * +System::getNode(const std::string &name) +{ + SystemTreeNode *treeRes = traverseSystemTree(m_systemTree, name); + if(treeRes == nullptr) + treeRes = &m_systemTree; + return treeRes; +} + +void +SystemView::view(System *system) +{ + m_system = system; + m_focus = &system->m_systemTree; +} + +void +SystemView::keypress(Camera *camera, int key) +{ + WindowContext &context = Game::contexts(); + if(context.getFocusedString() == WINDOW_SYSTEMVIEW_SEARCH_ID) { + m_focusSearch->keypress(key); + if(!Game::paused()) { + Search *fp = m_focusSearch.release(); + delete fp; + } + } + if(context.getFocusedString() != WINDOW_SYSTEMVIEW_ID) return; + if(key == KeyMan::binds[BIND_SYSTEMVIEW_PANUP].code) camera->move(0, camera->getscale()); + if(key == KeyMan::binds[BIND_SYSTEMVIEW_PANDOWN].code) camera->move(0, -(long)camera->getscale()); + if(key == KeyMan::binds[BIND_SYSTEMVIEW_PANRIGHT].code) camera->move(camera->getscale(), 0); + if(key == KeyMan::binds[BIND_SYSTEMVIEW_PANLEFT].code) camera->move(-(long)camera->getscale(), 0); + if(key == KeyMan::binds[BIND_SYSTEMVIEW_INCSCALE].code) camera->setscale(camera->getscale() * 2); + if(key == KeyMan::binds[BIND_SYSTEMVIEW_DECSCALE].code) camera->setscale(camera->getscale() / 2); + if(key == KeyMan::binds[BIND_SYSTEMVIEW_SEARCH].code) { + Game::setState(Game::State::PAUSED_INPUT); + context.focus(WINDOW_SYSTEMVIEW_SEARCH_ID); + context.setWindowHidden(WINDOW_SYSTEMVIEW_SEARCH_ID, false); + m_focusSearch = std::make_unique(this); + } +} + +void +SystemView::update(Camera *camera) +{ + if(Game::paused()) return; + auto &efoc = m_system->m_entityMan[m_focus->entityId]; + auto &efocp = efoc.get(); + if(efocp.position != camera->getorigin()) { + camera->setorigin(efocp.position); + } +} + +void +SystemView::drawOver(Camera *camera) { + auto &efoc = m_system->m_entityMan[m_focus->entityId]; + auto &efocp = efoc.get(); + auto &efocm = efoc.get(); + + WindowContext &context = Game::contexts(); + Window &infoWindow = context[WINDOW_BODYINFO_ID]; + Window &viewWindow = context[WINDOW_SYSTEMVIEW_ID]; + + infoWindow << straw::clear(' '); + infoWindow << straw::move(0, 0) << "Focus: " << efoc.get().name << '\n'; + if(efoc.contains()) { + auto &efoco = efoc.get(); + ecs::Entity &efoc_origin = m_system->m_entityMan[efoco.origin]; + + infoWindow << "Orbiting: " << efoc_origin.get().name << '\n'; + infoWindow << "Distance: " << std::abs((double)(efocp.position - efoc_origin.get().position).magnitude()) << "km\n"; + infoWindow << "Period: " << efoco.T / unit::DAY_SECONDS << " days\n"; + infoWindow << "Angle: " << efoco.v * (180.0 / std::numbers::pi) << '\n'; + infoWindow << "Eccentricity: " << efoco.e << '\n'; + infoWindow << "Mass: " << efocm.mass() << '\n'; + } + vex::vec2 viewdims( + viewWindow.screen()->getwidth(), + viewWindow.screen()->getheight()); + + viewWindow << straw::move(0, 0) << + straw::setcolor(straw::WHITE, straw::BLACK) << + "Press '" << + KeyMan::translateCode(KeyMan::binds[BIND_SYSTEMVIEW_SEARCH].code) << + "' to change focus"; + + double scale = camera->getscale() * (viewdims[0] / 2.0); + if(scale > 1e7) { + viewWindow << straw::move(0, viewdims[1] - 1) << scale / unit::AU << " AU"; + }else{ + viewWindow << straw::move(0, viewdims[1] - 1) << scale << " km"; + } + +} + +void +SystemView::draw(Camera *camera) +{ + if(m_focusSearch != nullptr) m_focusSearch->draw(); + if(!camera->dirty()) return; + auto &efoc = m_system->m_entityMan[m_focus->entityId]; + auto &efocp = efoc.get(); + auto &efocm = efoc.get(); + + if(efoc.contains()) { + auto &oc = efoc.get(); + auto &om = efoc.get(); + ecs::Entity &origin = m_system->m_entityMan[oc.origin]; + auto pc = origin.get(); + + std::vector> points; + float i = 0.0; + while(i < tau){ + double e2 = oc.e * oc.e; + double M = oc.M + i; + double E = M; + + int its = 0; + while(its < 512) { + double dE = (E - oc.e * std::sin(E) - M) / (1 - oc.e * std::cos(E)); + E -= dE; + its++; + if(std::abs(dE) < 1e-6) break; + } + + double x = (std::cos(E) - oc.e); + double y = (std::sin(E) * std::sqrt(1 - e2)); + + double v = std::atan2(y, x) + oc.w; + double r = std::sqrt(x*x + y*y) * (double)oc.a; + + + vex::vec2 polar{r, v}; + vex::vec2 cart = vex::cartesian(polar) + pc.position; + + points.push_back(cart); + i += 0.01; + } + for(unsigned i = 0; i < points.size(); i++) { + if(i == 0) { + camera->batchShape(shapes::line(points[points.size() - 1], points[i]), straw::color(0, 0, 255), '#'); + }else{ + camera->batchShape(shapes::line(points[i-1], points[i]), straw::color(0, 0, 255), '#'); + } + } + } + + for(ecs::Entity &e : m_system->m_entityMan.getWith()) { + auto &pc = e.get(); + auto &cc = e.get(); + unsigned id = e.id; + + long cr = cc.radius; + if(cr < camera->getscale()) cr = camera->getscale(); + shapes::ellipse circle(pc.position, cr, cr); + + straw::color color = id == m_focus->entityId ? straw::color{255, 255, 0} : straw::WHITE; + if(cc.radius < cr) { + camera->batchShape(circle, color, '*'); + }else{ + camera->batchShape(circle, color, '#'); + } + } +} + +ecs::Entity & +SystemView::getBody(int id) const +{ + return m_system->m_entityMan[id]; +} + +int +SystemView::getBodyIdByName(const std::string &name) +{ + for(ecs::Entity &entity : m_system->m_entityMan.all()) { + int id = entity.id; + if(entity.contains()) { + auto &namecomp = entity.get(); + if(namecomp.name == name) return id; + } + } + return -1; +} + +void +SystemView::Search::addNodeToTree(SystemTreeDisplayNode &root, System::SystemTreeNode *node) +{ + m_displayTreeFlat.emplace_back(&root); + for(auto &child : node->children) + { + ecs::NameComponent &namecomp = m_systemView->m_system->m_entityMan[child.entityId].get(); + root.children.push_back( + {&child, + {}, + &root, + (unsigned)m_displayTreeFlat.size(), + m_query.empty(), + m_query.empty() ? false : namecomp.name.find(m_query) == std::string::npos}); + addNodeToTree(root.children.back(), &child); + } +} + +void +SystemView::Search::rebuild() +{ + System::SystemTreeNode *systemRoot = &m_systemView->m_system->m_systemTree; + m_displayTree = {systemRoot, {}, nullptr, 0, false, false}; + m_displayTreeFlat.clear(); + m_selectionIndex = 0; + + addNodeToTree(m_displayTree, systemRoot); + for(unsigned i = 0; auto *node : m_displayTreeFlat) { + if(m_systemView->m_focus == node->node) { + m_selectionIndex = i; + break; + } + i++; + } + SystemTreeDisplayNode *recurse = m_displayTreeFlat[m_selectionIndex]; + while(recurse->parent != nullptr) { + recurse->parent->collapsed = false; + recurse->parent->hidden = false; + recurse = recurse->parent; + } +} + +void +SystemView::Search::drawNode( + SystemTreeDisplayNode &root, + Window &searchWindow, + unsigned indent) +{ + unsigned windowH = searchWindow.screen()->getheight(); + unsigned cursorY = searchWindow.screen()->getcursory(); + if(cursorY == windowH && root.index > m_selectionIndex) return; + + ecs::NameComponent &namecomp = m_systemView->m_system->m_entityMan[root.node->entityId].get(); + + if(root.index == m_selectionIndex) { + searchWindow << straw::setcolor(straw::BLACK, straw::WHITE); + }else searchWindow << straw::setcolor(straw::WHITE, straw::BLACK); + + + if(!root.hidden) { + if(m_query.empty()) searchWindow << std::string((indent * 4), ' '); + if(root.children.size() > 0) + searchWindow << '[' << (root.collapsed ? '+' : '-') << "] "; + searchWindow << namecomp.name << straw::setcolor(straw::WHITE, straw::BLACK) << '\n'; + } + + if(!root.collapsed) { + for(auto &child : root.children) drawNode(child, searchWindow, indent + 1); + } +} + +SystemView::Search::Search(SystemView *systemView) : + m_systemView(systemView), m_selectionIndex(0), m_dirty(true) +{ + rebuild(); +} + +void +SystemView::Search::finish() +{ + WindowContext &context = Game::contexts(); + Game::setState(Game::State::RUNNING); + context.setWindowHidden(WINDOW_SYSTEMVIEW_SEARCH_ID, true); + context.focus(WINDOW_SYSTEMVIEW_ID); +} + +void +SystemView::Search::keypress(int key) +{ + if(key == KeyMan::binds[BIND_G_ESCAPE].code) { + finish(); + }else + if(key == KeyMan::binds[BIND_SYSTEMVIEW_SEARCH_NEXT].code) { + if(m_selectionIndex == m_displayTreeFlat.size() - 1) return; + for(m_selectionIndex++; m_selectionIndex < m_displayTreeFlat.size() - 1; ++m_selectionIndex) { + if(!m_displayTreeFlat[m_selectionIndex]->hidden) break; + } + if(m_displayTreeFlat[m_selectionIndex]->hidden) m_selectionIndex = 0; + SystemTreeDisplayNode *node = m_displayTreeFlat[m_selectionIndex]; + while(node->parent != nullptr) { + if(!node->parent->collapsed) break; + m_selectionIndex += node->parent->children.size(); + node = node->parent; + } + + m_dirty = true; + }else + if(key == KeyMan::binds[BIND_SYSTEMVIEW_SEARCH_PREV].code) { + if(m_selectionIndex == 0) return; + for(m_selectionIndex--; m_selectionIndex > 0; --m_selectionIndex) { + if(!m_displayTreeFlat[m_selectionIndex]->hidden) break; + } + SystemTreeDisplayNode *node = m_displayTreeFlat[m_selectionIndex]; + while(node->parent != nullptr) { + if(!node->parent->collapsed) break; + m_selectionIndex -= node->parent->children.size(); + node = node->parent; + } + + m_dirty = true; + }else + if(key == KeyMan::binds[BIND_SYSTEMVIEW_SEARCH_TOP].code) { + m_selectionIndex = 0; + for(; m_selectionIndex < m_displayTreeFlat.size(); ++m_selectionIndex) { + if(!m_displayTreeFlat[m_selectionIndex]->hidden) break; + } + m_dirty = true; + }else + if(key == KeyMan::binds[BIND_SYSTEMVIEW_SEARCH_BOTTOM].code) { + m_selectionIndex = m_displayTreeFlat.size() - 1; + for(; m_selectionIndex > 0; --m_selectionIndex) { + if(!m_displayTreeFlat[m_selectionIndex]->hidden) break; + } + m_dirty = true; + }else + if(key == KeyMan::binds[BIND_SYSTEMVIEW_SEARCH_COLLAPSE].code) { + m_displayTreeFlat[m_selectionIndex]->collapsed = !m_displayTreeFlat[m_selectionIndex]->collapsed; + m_dirty = true; + }else + if(key == KeyMan::binds[BIND_G_SELECT].code) { + m_systemView->m_focus = m_displayTreeFlat[m_selectionIndex]->node; + finish(); + }else + { + if(key < 0) return; + switch(key) { + case 127: + case '\b': { + if(!m_query.empty()) m_query.pop_back(); + } break; + default: m_query.push_back((char)key); break; + } + rebuild(); + m_dirty = true; + } +} + +void +SystemView::Search::draw() +{ + if(!m_dirty) return; + + WindowContext &windowContext = Game::contexts(); + Window &searchWindow = windowContext[WINDOW_SYSTEMVIEW_SEARCH_ID]; + + searchWindow << straw::clear(' ') << straw::move(0, 0); + searchWindow << "Query: " << m_query << '\n'; + + drawNode(m_displayTree, searchWindow, 0); + + searchWindow << straw::flush(); + m_dirty = false; +} diff --git a/src/timeman.cpp b/src/timeman.cpp new file mode 100644 index 0000000..f043894 --- /dev/null +++ b/src/timeman.cpp @@ -0,0 +1,49 @@ +#include "timeman.hpp" +#include "keybind.hpp" +#include "window.hpp" +#include "game.hpp" + +unit::Time TimeMan::m_time(0); +unit::Time TimeMan::m_step(unit::DAY_SECONDS); +bool TimeMan::m_auto; +bool TimeMan::m_changed; + +void +TimeMan::init() +{ + KeyMan::registerBind('.', BIND_TIMEMAN_STEP, CTX_TIMEMAN, "Move time ahead by a step"); + KeyMan::registerBind('+', BIND_TIMEMAN_INCSTEP, CTX_TIMEMAN, "Increase the timestep"); + KeyMan::registerBind('-', BIND_TIMEMAN_DECSTEP, CTX_TIMEMAN, "Decrease the timestep"); + KeyMan::registerBind('a', BIND_TIMEMAN_TOGGLEAUTO, CTX_TIMEMAN, "Toggle if time will move automatically"); + m_changed = true; +} + +void +TimeMan::update(int c) +{ + WindowContext &context = Game::contexts(); + m_changed = false; + if(!Game::paused()) { + if(m_auto) { + m_time += (m_step); + m_changed = true; + } + } + if(context.getFocusedString() != WINDOW_TIMEMAN_ID) return; + if(c == KeyMan::binds[BIND_TIMEMAN_INCSTEP].code) m_step = unit::Time(std::max(1, m_step() * 2)); + if(c == KeyMan::binds[BIND_TIMEMAN_DECSTEP].code) m_step = unit::Time(std::max(1, m_step() / 2)); + if(c == KeyMan::binds[BIND_TIMEMAN_TOGGLEAUTO].code) m_auto = !m_auto; + if(c == KeyMan::binds[BIND_TIMEMAN_STEP].code && !Game::paused()) { m_time += m_step; m_changed = true; } +} + +void +TimeMan::draw() +{ + WindowContext &context = Game::contexts(); + Window &timeWindow = context[WINDOW_TIMEMAN_ID]; + timeWindow << straw::clear(' '); + + timeWindow << straw::move(0, 0) << straw::resetcolor() << m_time.format("%S %D, %C \n%H:%m\n\n"); + timeWindow << m_step.format("Step:\n%Y Years, %M Months\n%D Days, %H Hours\n%m Minutes, %s Seconds\n\n"); + if(m_auto) timeWindow << "Auto"; +} diff --git a/src/units.cpp b/src/units.cpp new file mode 100644 index 0000000..7cc7d61 --- /dev/null +++ b/src/units.cpp @@ -0,0 +1,61 @@ +#include "units.hpp" +#include +#include +#include + +namespace unit { + +std::string +Time::format(const char *fmt) +{ + std::stringstream ss; + for(; *fmt; fmt++) { + if(*fmt != '%') { + ss << *fmt; + continue; + } + fmt++; + switch(*fmt) { + case '%': + ss << '%'; + break; + case 'Y': + ss << real_years(); + break; + case 'C': + ss << years(); + break; + case 'S': { + Time year = current_year(); + ss << month_str[year.months()]; + break; } + case 'M': { + Time year = current_year(); + ss << year.months(); + break; } + case 'W': { + Time month = current_month(); + ss << month.weeks(); + break; } + case 'D': { + Time month = current_month(); + ss << month.days(); + break; } + case 'H': { + Time day = current_day(); + ss << day.hours(); + break; } + case 'm': { + Time hour = current_hour(); + ss << hour.minutes(); + break; } + case 's': { + Time minute = current_minute(); + ss << minute.seconds(); + break; } + } + } + return ss.str(); +} + +} diff --git a/src/window.cpp b/src/window.cpp new file mode 100644 index 0000000..4e487f3 --- /dev/null +++ b/src/window.cpp @@ -0,0 +1,72 @@ +#include "window.hpp" +#include "keybind.hpp" + +void +Window::draw(bool focus) +{ + if(m_hidden) return; + if(focus) { + m_border << straw::setcolor(straw::BLACK, straw::WHITE) << straw::clear(' ') << straw::move(0, 0) << m_title << straw::resetcolor(); + }else{ + m_border << straw::setcolor(straw::WHITE, straw::BLACK) << straw::clear(' ') << straw::move(0, 0) << m_title; + } + m_border.flush(); + m_screen.flush(); +} + +void +WindowContext::registerWindow(const std::string &id, + const std::string &title, + unsigned x, unsigned y, + unsigned w, unsigned h, + bool hidden) +{ + m_windows.emplace(id, Window(title, x, y, w, h, hidden)); + m_windowOrder.push_back(id); +} + +void +WindowContext::update(int code) +{ + if(code == KeyMan::binds[BIND_G_NEXTWIN].code) { + m_focus = (m_focus + 1) % m_windows.size(); + while(m_windows.at(m_windowOrder[m_focus]).hidden()) { + m_focus = (m_focus + 1) % m_windows.size(); + } + } + if(code == KeyMan::binds[BIND_G_PREVWIN].code) { + m_focus = (m_focus == 0 ? m_windows.size() - 1 : m_focus - 1); + while(m_windows.at(m_windowOrder[m_focus]).hidden()) { + m_focus = (m_focus == 0 ? m_windows.size() - 1 : m_focus - 1); + } + } +} + +void +WindowContext::draw() +{ + for(unsigned i = 0; i < m_windows.size(); i++) { + m_windows.at(m_windowOrder[i]).draw(i == m_focus); + } +} + +void +WindowContext::focus(const std::string &id) +{ + for(unsigned i = 0; i < m_windowOrder.size(); i++) { + if(m_windowOrder[i] == id) { + m_focus = i; + return; + } + } +} + +void +WindowContext::setWindowHidden(const std::string &id, bool mode) +{ + m_windows.at(id).setHidden(mode); + for(unsigned i = 0; i < m_windows.size(); i++) { + if(m_windowOrder[i] == id && mode) continue; + m_windows.at(m_windowOrder[i]) << straw::redraw(); + } +} -- cgit v1.2.1