diff --git a/demo/demo/hexboard/board.cpp b/demo/demo/hexboard/board.cpp index 174826585af85f9670d3f603be0fc5fac8928b50..9c12a820cfd966da6195c66402336e54be4d7225 100644 --- a/demo/demo/hexboard/board.cpp +++ b/demo/demo/hexboard/board.cpp @@ -25,10 +25,27 @@ namespace demo::hexboard { void Scene::activate() { m_board = std::make_unique<fggl::grid::HexGrid>(); + m_layout = std::make_unique<fggl::grid::Layout>( fggl::grid::Orientation::make_pointy(), 64.0F ); + m_selections = std::make_unique<SelectionModel>(); + + fggl::grid::TerrainType grass{ + .data = std::make_shared<fggl::grid::MaterialData>() + + }; + grass.data->name = "grass"; + grass.data->colour = {0.0F, 1.0F, 0.0}; + + fggl::grid::IntHex islandPoint{3,3}; + auto island = islandPoint.hexesInRange(2); + for ( auto& hex : island) { + m_board->setTerrain(hex, grass); + } + } void Scene::deactivate() { m_board = nullptr; + m_selections = nullptr; } void Scene::update(float delta) { @@ -38,45 +55,40 @@ namespace demo::hexboard { if ( m_board == nullptr ){ return; } - } - - // see https://www.redblobgames.com/grids/hexagons/#hex-to-pixel - const fggl::math::mat2 HEX_BASIS{ - std::sqrt(3.0F), std::sqrt(3.0F) / 2.0F, - 0.0F, 3.0F / 2.0F - }; - - static inline fggl::math::vec2 hexToScreen(fggl::grid::IntHex hexPos, float size, fggl::math::vec2 offset = {0,0}) { - return size * fggl::math::vec2{hexPos.q(), hexPos.r()} * HEX_BASIS + offset; - } - void Scene::render(fggl::gfx::Graphics &gfx) { - // if the board is not set, abort - if ( m_board == nullptr ){ - return; + // check if a button was pressed + auto& input = this->input(); + { + fggl::math::vec2 screenPos( + input.mouse.axis(fggl::input::MouseAxis::X), + input.mouse.axis(fggl::input::MouseAxis::Y) + ); + screenPos.x = fggl::math::rescale_ndc(screenPos.x, 0, 1920); + screenPos.y = fggl::math::rescale_ndc(screenPos.y, 0, 1080); + m_selections->hover = fggl::grid::round2( m_layout->toGrid(screenPos) ); + + if (input.mouse.pressed(fggl::input::MouseButton::LEFT)) { + m_selections->selected = m_selections->hover; + } } + } - // draw the grid - // FIXME don't hard-code the screen size + void Scene::drawGrid(fggl::gfx::Paint& paint) { const float hexRadius = 64.0F; const auto gridWidth = (int)( (1920 - hexRadius) / (hexRadius * std::sqrt(3.0F)) ); const auto gridHeight = (int)( (1080 - hexRadius) / (hexRadius * (3.0F / 2.0F) )); - const fggl::math::vec2 offset{ - ( 1920 - ( (float)gridWidth * hexRadius * std::sqrt(3.0F))) - (hexRadius / 2.0F), - ( (1080 - hexRadius / 2.0F) - ( (float)gridHeight * hexRadius * (3.0F / 2.0F) )), - }; + auto tiles = m_board->getAllTiles(); - fggl::gfx::Paint paint; fggl::grid::IntHex hexPos{0, 0}; auto rowBasis = hexPos; for(auto i=0; i<gridHeight; ++i) { for (auto j=0; j<gridWidth; ++j) { - if ( m_board->isValidPos(hexPos) ) { - auto pos = hexToScreen(hexPos, hexRadius, offset); - auto hexShape = fggl::gfx::make_shape(pos, hexRadius, 6); - paint.stroke(hexShape); + auto terrain = m_board->getTerrain(hexPos); + if ( terrain.has_value() ) { + const auto& value = terrain.value(); + m_layout->paintHex(paint, hexPos, value.colour); } // next hexagon @@ -86,7 +98,33 @@ namespace demo::hexboard { rowBasis = i % 2 == 0 ? rowBasis.neighbour(fggl::grid::HexDirPointy::BOTTOM_RIGHT) : rowBasis.neighbour(fggl::grid::HexDirPointy::BOTTOM_LEFT); hexPos = rowBasis; } + } + + void Scene::drawSelections(fggl::gfx::Paint& paint) { + if ( m_selections == nullptr ) { + return; + } + + if ( m_selections->selected.has_value() ) { + m_layout->paintHex( paint, m_selections->selected.value(), fggl::gfx::colours::YELLOW); + } + + if ( m_selections->hover.has_value() ) { + m_layout->paintHex( paint, m_selections->hover.value(), fggl::gfx::colours::BLANCHED_ALMOND); + } + } + void Scene::render(fggl::gfx::Graphics &gfx) { + // if the board is not set, abort + if ( m_board == nullptr ){ + return; + } + + // draw the grid + // FIXME don't hard-code the screen size + fggl::gfx::Paint paint; + drawGrid(paint); + drawSelections(paint); gfx.draw2D(paint); } diff --git a/demo/include/hexboard/scene.hpp b/demo/include/hexboard/scene.hpp index 0ded844c82ab02d80ce86b1dcef5f577a7c388e8..0fe38cd88783f63e763f588fa8d7f33505bf79a3 100644 --- a/demo/include/hexboard/scene.hpp +++ b/demo/include/hexboard/scene.hpp @@ -20,12 +20,19 @@ #define FGGL_DEMO_INCLUDE_HEXBOARD_SCENE_H #include <memory> +#include <optional> #include "fggl/scenes/game.hpp" #include "fggl/grid/hexgrid.hpp" +#include "fggl/grid/layout.hpp" namespace demo::hexboard { + struct SelectionModel { + std::optional<fggl::grid::IntHex> selected; + std::optional<fggl::grid::IntHex> hover; + }; + class Scene : public fggl::scenes::GameBase { public: explicit Scene(fggl::App& app); @@ -38,6 +45,11 @@ namespace demo::hexboard { private: std::unique_ptr<fggl::grid::HexGrid> m_board; + std::unique_ptr<fggl::grid::Layout> m_layout; + std::unique_ptr<SelectionModel> m_selections; + + void drawGrid(fggl::gfx::Paint&); + void drawSelections(fggl::gfx::Paint&); }; } // namespace demo::hexboard diff --git a/fggl/grid/hexagon.cpp b/fggl/grid/hexagon.cpp index 25a5d30f7b2d9a71c8ded733998c15aa416cd408..b07b598167f794aa34c3d611f7bde599a58a8baa 100644 --- a/fggl/grid/hexagon.cpp +++ b/fggl/grid/hexagon.cpp @@ -24,7 +24,7 @@ namespace fggl::grid { int distance = start.distance(end); std::vector<IntHex> line; for (auto i=0; i < distance; ++i) { - line.push_back( round(hexLerp(start, end, 1.0F/distance * i)) ); + line.push_back( round2(hexLerp(start, end, 1.0F/distance * i)) ); } return line; } diff --git a/include/fggl/gfx/paint.hpp b/include/fggl/gfx/paint.hpp index 6ffaad3ce4e2b529c7afd28a18bb861059f5e6e4..884cc498114dc0844011bfc03901f564af084120 100644 --- a/include/fggl/gfx/paint.hpp +++ b/include/fggl/gfx/paint.hpp @@ -225,21 +225,22 @@ namespace fggl::gfx { }; inline Path2D make_shape(math::vec2 center, float radius, int sides, math::vec3 colour = colours::WHITE, ShapeOpts opts = {}) { - float angle = (math::PI * 2.0F) / sides; + float angle = ((math::PI * 2.0F) / sides); fggl::gfx::Path2D tileGfx(center); tileGfx.colour(colour); for (int i=0; i < sides; ++i) { - float xPos = (float)(sinf(i * angle + opts.angleOffset) * radius) + center.x; - float yPos = (float)(cosf(i * angle + opts.angleOffset) * radius) + center.y; - if (!opts.sinFirst) { - std::swap(xPos, yPos); - } + math::vec2 pos ( + (float)(cosf(i * angle + opts.angleOffset) * radius), + (float)(sinf(i * angle + opts.angleOffset) * radius) + ); + pos += center; + if ( i == 0 ) { - tileGfx.moveTo( {xPos, yPos} ); + tileGfx.moveTo( pos ); } else { - tileGfx.pathTo({xPos, yPos}); + tileGfx.pathTo( pos ); } } tileGfx.close(); diff --git a/include/fggl/grid/hexagon.hpp b/include/fggl/grid/hexagon.hpp index b5bb3b14f84359644f9244bf47e1dbd57ed04f7b..7277d65d23eb70c293d9b1b4b0e7bdfb716f92af 100644 --- a/include/fggl/grid/hexagon.hpp +++ b/include/fggl/grid/hexagon.hpp @@ -111,7 +111,7 @@ namespace fggl::grid { for ( auto q = -range; q <= range; ++q ) { auto stopCount = std::min(range, -q+range); for ( auto r = std::max(-range, -q-range); r <= stopCount; ++r ) { - results.push_back( this + HexPointT<T>(q, r) ); + results.push_back( *this + HexPointT<T>(q, r) ); } } return results; @@ -132,15 +132,25 @@ namespace fggl::grid { }; } - constexpr IntHex round(const FloatHex& hex) { - // see https://observablehq.com/@jrus/hexround for original JS implementation - float xGrid = std::round( hex.r() ); - float yGrid = std::round( hex.q() ); - float x = hex.q() - xGrid; - float y = hex.r() - yGrid; - auto dx = std::round(x + 0.5F*y) * (float)(x*x >= y*y); - auto dy = std::round(y + 0.5F*x) * (float)(x*x < y*y); - return { (int)(xGrid + dx), (int)(yGrid + dy) }; + [[nodiscard]] + constexpr IntHex round2(const FloatHex& hex) { + auto q = std::round( hex.q() ); + auto r = std::round( hex.r() ); + auto s = std::round( hex.s() ); + + auto qDiff = std::abs( q - hex.q() ); + auto rDiff = std::abs( r - hex.r() ); + auto sDiff = std::abs( s - hex.r() ); + + if ( qDiff > rDiff && qDiff > sDiff) { + q = -r-s; + } else if ( rDiff > sDiff ) { + r = -q-s; + } else { + s = -q-r; + } + + return {(int)q, (int)r}; } std::vector<IntHex> lineTo(const IntHex& start, const IntHex& end); diff --git a/include/fggl/grid/hexgrid.hpp b/include/fggl/grid/hexgrid.hpp index ae948db54667985a4f172438950620ef119117c5..374641c86dbb137abf468814a1a0db41d5fdf476 100644 --- a/include/fggl/grid/hexgrid.hpp +++ b/include/fggl/grid/hexgrid.hpp @@ -19,15 +19,69 @@ #ifndef FGGL_GRID_HEXAGON_BOARD_HPP #define FGGL_GRID_HEXAGON_BOARD_HPP +#include <map> +#include <set> +#include <optional> + #include "fggl/grid/hexagon.hpp" +#include "fggl/math/types.hpp" namespace fggl::grid { + struct MaterialData { + std::string name; + math::vec3 colour; + }; + + struct TerrainType { + std::shared_ptr<MaterialData> data; + }; + + struct HexTile { + std::shared_ptr<MaterialData> terrain; + + [[nodiscard]] + inline std::optional<const MaterialData> data() const { + if (terrain == nullptr) { + {} + } + + return *terrain.get(); + } + }; + class HexGrid { public: - inline bool isValidPos(const IntHex& pos) { - return true; + inline bool isValidPos(const IntHex& pos) const { + return m_tiles.contains(pos); + } + + void setTerrain(const IntHex& pos, const TerrainType& terrain) { + auto& mapTile = m_tiles[pos]; + mapTile.terrain = terrain.data; + } + + std::optional<const MaterialData> getTerrain(const IntHex& pos) const { + const auto itr = m_tiles.find(pos); + if ( itr == m_tiles.end() ) { + return {}; + } + return itr->second.data(); } + + std::set<IntHex> getAllTiles() { + std::set<IntHex> posSet; + for ( auto& [pos,data] : m_tiles ) { + posSet.emplace( pos ); + } + return posSet; + } + + std::set<HexTile> tilesInRange(const IntHex& pos, int range) const; + std::set<HexTile> neighboursOf(const IntHex& pos) const; + + private: + std::map<IntHex, HexTile> m_tiles; }; } // namespace fggl::grid diff --git a/include/fggl/grid/layout.hpp b/include/fggl/grid/layout.hpp new file mode 100644 index 0000000000000000000000000000000000000000..96cfa7e15116c633a56965be08db378c2f7fa38b --- /dev/null +++ b/include/fggl/grid/layout.hpp @@ -0,0 +1,132 @@ +/* + * This file is part of FGGL. + * + * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with FGGL. + * If not, see <https://www.gnu.org/licenses/>. + */ + +/* + * This file is part of FGGL. + * + * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with FGGL. + * If not, see <https://www.gnu.org/licenses/>. + */ + +// +// Created by webpigeon on 18/12/22. +// + +#ifndef FGGL_FGGL_GRID_LAYOUT_HPP +#define FGGL_FGGL_GRID_LAYOUT_HPP + +#include "fggl/math/types.hpp" +#include "fggl/grid/hexagon.hpp" + +namespace fggl::grid { + + const math::mat2 MAT_HEX_POINTY { + std::sqrt(3.0F), 0.0F, + std::sqrt(3.0F) / 2.0F, 3.0F/2.0F + }; + + const math::mat2 MAT_HEX_FLAT { + 3.F/2.F, std::sqrt(3.F)/2.F, + 0.0F, std::sqrt(3.F) + }; + + struct Orientation { + math::mat2 m_forward; + math::mat2 m_backward; + int m_angle; + + inline Orientation(int angle, math::mat2 forward) : m_forward(forward), m_backward(glm::inverse(forward)), m_angle(angle) {}; + + static inline Orientation make_pointy() { + return { 30, MAT_HEX_POINTY}; + } + + static inline Orientation make_flat() { + return { 0, MAT_HEX_FLAT }; + } + }; + + + struct Layout { + Orientation m_orientation; + math::vec2 m_size; + math::vec2 m_origin; + + Layout(Orientation orientation, math::vec2 size, math::vec2 origin) : m_orientation(orientation), m_size(size), m_origin(origin){} + Layout(Orientation orientation, float size) : m_orientation(orientation), m_size(size, size), m_origin() {} + + [[nodiscard]] + inline math::vec2 origin() const { + return m_origin; + } + + [[nodiscard]] + inline math::vec2 size() const { + return m_size; + } + + [[nodiscard]] + inline math::vec2 toScreen(IntHex gridPos) const { + math::vec2 hexPoint{gridPos.q(), gridPos.r()}; + auto p = (m_orientation.m_forward * hexPoint) * m_size; + return p + m_origin; + } + + [[nodiscard]] + inline FloatHex toGrid(math::vec2 screen) const { + auto point = (screen - m_origin) / m_size; + auto p = m_orientation.m_backward * point; + return {p.x, p.y}; + } + + [[nodiscard]] + inline math::vec2 cornerOffset(int corner) const { + int angInc = (360 / 6) * corner; + float angle = (angInc + m_orientation.m_angle) * ( fggl::math::PI / 180.0F); + return { + m_size.x * cosf(angle), + m_size.y * sinf(angle) + }; + } + + void paintHex(fggl::gfx::Paint& paint, IntHex pos, math::vec3 colour) const { + const auto hexScreenCenter = toScreen(pos); + + gfx::Path2D path({0,0}); + path.colour(colour); + + for (int i=0; i < 6; ++i) { + auto cornerPos = hexScreenCenter + cornerOffset(i); + if ( i == 0) { + path.moveTo(cornerPos); + } else { + path.pathTo(cornerPos); + } + } + paint.stroke(path); + } + + + }; + +} + +#endif //FGGL_FGGL_GRID_LAYOUT_HPP