Skip to content
Snippets Groups Projects
Commit a9e168c9 authored by Frederik Alexander Keens's avatar Frederik Alexander Keens
Browse files

Merge branch 'leveleditor' into 'main'

Merge the level editor

See merge request !38
parents c3814c85 20b137f9
No related branches found
No related tags found
2 merge requests!38Merge the level editor,!29Merge main into box2d to implement physics
Showing
with 1138 additions and 4 deletions
......@@ -41,8 +41,8 @@ find_package(Boost 1.71.0 REQUIRED COMPONENTS graph)
# Quellen sammeln
file(GLOB_RECURSE ADVANCED_WARS_SOURCES
"${PROJECT_SOURCE_DIR}/src/*.cpp"
"${PROJECT_SOURCE_DIR}/src/*.hpp"
"${PROJECT_SOURCE_DIR}/src/game/*.cpp"
"${PROJECT_SOURCE_DIR}/src/game/*.hpp"
)
# C++ Standard festlegen
......@@ -132,9 +132,40 @@ else()
box2d
m
)
endif()
# leveleditor
# Find Qt
find_package(Qt6 REQUIRED COMPONENTS Widgets)
file(GLOB_RECURSE LEVELEDITOR_SOURCES
"${PROJECT_SOURCE_DIR}/src/editor/*.cpp"
"${PROJECT_SOURCE_DIR}/src/editor/*.hpp"
)
# Add executable
add_executable(editor ${LEVELEDITOR_SOURCES})
# Link Qt libraries
target_link_libraries(editor PRIVATE Qt6::Widgets ${HDF5_LIBRARIES})
target_include_directories(editor
PRIVATE
${highfive_SOURCE_DIR}/include
)
# writelevel
add_executable(writelevel ${PROJECT_SOURCE_DIR}/src/util/writelevel.cpp)
target_include_directories(writelevel
PRIVATE
${highfive_SOURCE_DIR}/include
)
target_link_libraries(writelevel
${HDF5_LIBRARIES}
)
target_include_directories(advanced_wars PRIVATE ${highfive_SOURCE_DIR}/include)
endif()
# Ressourcen kopieren (plattformübergreifend)
foreach(FONT ${FONT_FILES})
......
......@@ -91,3 +91,6 @@ CMake kann mit verschiedenen Optionen konfiguriert werden:
cmake .. -DCMAKE_BUILD_TYPE=Release # Release-Build
cmake .. -DCMAKE_BUILD_TYPE=Debug # Debug-Build
```
## Prerequisites for Leveleditor
- QT 6: ```sudo apt install qt6-base-dev```
\ No newline at end of file
# Level Editor Doku
## System Architecture
The LevelEditor UI is built of many classes that inherit from a QWidget.
The Composition of these classes forms the Application.
Appart from main.cpp and the QWidget classes, level editor also makes use of
the SpriteProvider and EventHandler class.
The SpriteProvider is initialized at the beginning of the program in the main function
and can later be used to retrieve any sprite (QPixmap) by tile id from anywhere in the application.
The EventHandler class is the heart of the application. It allows any Class to emit or receive events from anywhere in the program.
To receive Events from EventHandler a class needs to inherit from EventHandler and overwrite the event method for the event it wants to handle.
The available event methods, as well as the info on which class emits which event method and which class overwrites which event method can be seen in the below diagramm.
The formless diagramm below demonstrates how the classes form the Application.
For more details on the individual classes, take a look at the documentation in their header files.
![Formless System Architecture Diagramm](./architecture.svg)
## Level Specification
The level is stored in two datasets named 'metadata', 'tilesarray' in the level hdf5 file (level.h5).
The metadata dataset is in XML format. It provides the level height, width and name.
The tilesarray dataset is a giant array<uint8_t> which defines the level map through an array of tile ids.
### 1. metadata dataset (XML)
```xml
<?xml version="1.0" encoding="ASCII"?>
<level>
<width>20</width> <!-- Breite des Levels -->
<height>20</height> <!-- Hoehe des Levels -->
<name>Geiles Level<name><!-- Name des Levels -->
</level>
```
### 2. tilesarray dataset
The length of the tiles array is level width x level height (see level metadata).
From the individual values inside the tiles array you can determine the IDs of the buildings and terrains.
0 - 29 are terrain IDs => Mapping see enum in src/game/Tile.hpp
30-49 are undefined
50-79 are building/faction IDs:
50 => faction ID 0, building ID 0
51 => faction ID 0, building ID 1
...
55 => faction ID 1, building ID 0
56 => faction ID 1, building ID 1
57 => faction ID 1, building ID 2
...
In General:
Let t be a value from the tiles array,
in case t < 30: terrain ID = t
in case t >= 50: faction ID = (t - 50) / 5 building ID = (t - 50) % 5
t is defined as t = terrain ID for terrains
and t = 50 + 5*faction Id + building ID for buildings
Which faction id belongs to which faction and which building id belongs to
which building can be found in the enum inside src/game/building.hpp
This diff is collapsed.
No preview for this file type
/**
* TileSelector.cpp
*
* @date 06.02.2025
* @author Nils Jonathan Friedrich Eckardt implementation
*/
#include "AutomateButton.hpp"
#include "EventHandler.hpp"
#include <QMessageBox>
#include <QEvent>
namespace editor
{
AutomateButton::AutomateButton(const QString text, QWidget* parent) : QCheckBox(text,parent)
{
setChecked(false);
}
void AutomateButton::mousePressEvent(QMouseEvent *event)
{
QCheckBox::mousePressEvent(event);
EventHandler::send([](EventHandler* e) { e->onCheckBoxToggled(); });
}
} // namespace editor
\ No newline at end of file
/**
* TileSelector.hpp
*
* @date 06.02.2025
* @author Nils Jonathan Friedrich Eckardt implementation
*/
#pragma once
#include <QCheckBox>
#include <QString>
#include <QEvent>
namespace editor
{
class AutomateButton : public QCheckBox {
public:
AutomateButton(const QString text, QWidget *parent = nullptr);
protected:
void mousePressEvent(QMouseEvent *event) override;
};
} // namespace editor
\ No newline at end of file
/**
* EventHandler.cpp
*
* @date 29.01.2025
* @author Jonathan Dueck (jonathan.dueck@informatik.hs-fulda.de)
*/
#include "EventHandler.hpp"
namespace editor
{
std::vector<EventHandler*> EventHandler::instances = {};
EventHandler::EventHandler()
{
// register event handler
instances.push_back(this);
}
EventHandler::~EventHandler()
{
// deregister event handler
instances.erase(std::remove(instances.begin(), instances.end(), this), instances.end());
}
void EventHandler::send(std::function<void(EventHandler*)> callback)
{
// Execute the provided callaback on every EventHandler instance.
for (EventHandler* instance : instances)
{
callback(instance);
}
}
} // namespace editor
\ No newline at end of file
/**
* EventHandler.cpp
*
* @date 29.01.2025
* @author Jonathan Dueck (jonathan.dueck@informatik.hs-fulda.de)
* @author Nils Jonathan Friedrich Eckardt onCheckBoxToggle added
*/
#pragma once
#include <cstdint>
#include <vector>
#include <algorithm>
#include <iostream>
#include <functional>
#include <QString>
namespace editor
{
// forward declaration of Tile
class Tile;
/**
* EventHandler is the backbone in the architecture that makes interaction possible.
*
* Classes that want to act on specific LevelEditor Events can subclass EventHandler
* and overwrite the methods for the events they want to handle. If they don't overwrite
* an event method the default implemenatation (doing nothing) is provided by EventHandler.
*
* EventHandler stores all instances of EventHandler subclasses in a static variable.
* Events can be emitted from anywhere in the program by using the static send method.
* Emitting an event can be achieved by providing a callback to the send method that
* receives an EventHandler instance as an argument and invokes the desired event method
* on it. The static send method will invoke that event method for every EventHandler instance
* from the static instances variable.
*/
class EventHandler {
public:
/**
* Saves a pointer to itself in the instances variable at construction.
*/
EventHandler();
/**
* Removes the pointer to itself from the instances variable at deconstruction.
*/
virtual ~EventHandler();
/**
* Executes the provided callback for every EventHandler instance from the static instances variable.
* @param callback The callback that executes the desired event method.
* It will be executed for every EventHandler instance.
*/
static void send(std::function<void(EventHandler*)> callback);
/**
* Overwrite this event method to handle the dispatch of a new level name
* by the level name edit field.
* @param new_name The new level name.
*/
virtual void onLevelNameUpdated(std::string new_name){};
/**
* Overwrite this event method to handle the dispatch of a request to write
* the level to disk by the save button.
* @param file_path The path to the file into which the level should be written.
*/
virtual void onLevelWriteRequested(QString file_path){};
/**
* Overwrite this event method to handle the dispatch of a new tile index due
* to the mouse entering its boundaries.
* @param index The index of the entered tile.
*/
virtual void onTileEntered(int index){};
/**
* Overwrite this event method to handle the dispatch of a new tile index due
* to the mouse leaving its boundaries.
* @param index The index of the exited tile.
*/
virtual void onTileExited(int index){};
/**
* Overwrite this event method to handle the dispatch of a new tile index due
* to the user clicking it.
* @param index The index of the clicked tile.
*/
virtual void onTileClicked(int index){};
/**
* Overwrite this event method to handle the dispatch of a new tile_id due to
* the user selecting a different tile type in the TileSelector on the right.
* @param tile_id Id of the tile to use when an existing tile is clicked on the levelmap.
*/
virtual void onNewTileIdSelected(uint8_t tile_id){};
/**
* Overwrite this event method to handle the change of tile placement method due
* to the user selecting automatic tile placement assisstance.
* @param isToggled Boolean that enables tile placing assisstment.
*/
virtual void onCheckBoxToggled(){};
/**
* Overwrite this event method to handle the dispatch of a new delta due to
* the user pressing the zoom in or zoom out button.
* @param delta Amount to zoom in or out, e.g 0.2 => 20% in, -0.2 => 20% out.
*/
virtual void onZoomed(double delta){}
private:
/**
* Stores all existing EventHandler instances so that the send method can
* invoke the emiited event method on every existing instance.
*/
static std::vector<EventHandler*> instances ;
};
} // namespace editor
\ No newline at end of file
/**
* EventHandler.cpp
*
* @date 29.01.2025
* @author Jonathan Dueck (jonathan.dueck@informatik.hs-fulda.de)
*/
#include "LevelNameEdit.hpp"
#include <QKeyEvent>
#include "EventHandler.hpp"
namespace editor
{
LevelNameEdit::LevelNameEdit(const std::string& level_name, QWidget* parent) : QLineEdit(parent)
{
setFixedWidth(150);
setText(level_name.c_str()); // prefill level name
}
void LevelNameEdit::keyPressEvent(QKeyEvent* event)
{
QLineEdit::keyPressEvent(event);
std::string new_name = text().toStdString();
// dispatch the level name updated event method to allow all depending Widgets to take action
EventHandler::send([new_name](EventHandler* e) { e->onLevelNameUpdated(new_name); });
}
} // namespace editor
\ No newline at end of file
/**
* EventHandler.cpp
*
* @date 29.01.2025
* @author Jonathan Dueck (jonathan.dueck@informatik.hs-fulda.de)
*/
#pragma once
#include <QLineEdit>
namespace editor
{
/**
* The LevelNameEdit Widget is displayed on the left of the TopBar.
* It allows the user to set a custom level name.
*/
class LevelNameEdit : public QLineEdit {
public:
/**
* Creates a LevelNameEdit Widget prefilling the provided level_name.
* The parent of the LevelNameEdit Widget is set to the provided parent.
* @param level_name The initial name of the level.
* @param parent The Widget that should be set as the parent of this Widget.
*/
LevelNameEdit(const std::string& level_name, QWidget* parent = nullptr);
protected:
void keyPressEvent(QKeyEvent* event) override;
};
} // namespace editor
\ No newline at end of file
/**
* LevelScene.cpp
*
* @date 28.01.2025
* @author Jonathan Dueck (jonathan.dueck@informatik.hs-fulda.de)
* @author Nils Jonathan Friedrich Eckardt implemented advanced placement
*/
#include "LevelScene.hpp"
#include <QGraphicsPixmapItem>
#include <QPixmap>
#include <QPoint>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include "highfive/H5File.hpp"
#include "SpriteProvider.hpp"
namespace editor
{
LevelScene::LevelScene(
const std::string& name, int width, int height, std::vector<uint8_t> tile_ids,
const std::string& file_path, QWidget* parent)
: QGraphicsScene(parent), m_name(name), m_width(width), m_height(height), m_tile_ids(tile_ids),
m_file_path(file_path), m_selected_tile_id(2)
{
setSceneRect(0, 0, m_width * 16, m_height * 16);
m_advanced_tile_placement = false;
m_tile_occupants = {};
m_tile_occupants.reserve(tile_ids.size());
for (int index = 0; index < tile_ids.size(); index++)
{
int x = (index % m_width) * 16;
int y = (index / m_width) * 16;
Tile* tile = new Tile(index, is_border(index) ? tile_ids[index] : 0);
addItem(tile);
tile->setZValue(0);
tile->setPos(x, y);
if (!is_border(index) && tile_ids[index] > 0)
{
m_tile_occupants.push_back(occupy_tile(index, tile_ids[index]));
}
else
{
m_tile_occupants.push_back(nullptr);
}
}
}
LevelScene* LevelScene::empty(const std::string& name, int width, int height, QWidget* parent)
{
// + 2 because of surrounding the map with cliffs
std::vector<uint8_t> tile_ids;
tile_ids.reserve(width*height);
// create top row with cliffs
tile_ids.push_back(22); // cliff corner top left
for (int i = 0; i < width - 2; i++) {
tile_ids.push_back(19); // cliff bottom
}
tile_ids.push_back(23); // cliff corner top right
// create main rows with cliff at start and end
for (int i = 0; i < height - 2; i++) {
tile_ids.push_back(21); // cliff right
for (int j = 0; j < width - 2; j++) {
tile_ids.push_back(0); // pleins
}
tile_ids.push_back(20); // cliff left
}
// create bottom row with cliffs
tile_ids.push_back(24); // cliff corner bottom left
for (int i = 0; i < width - 2; i++) {
tile_ids.push_back(18); // cliff top
}
tile_ids.push_back(25); // cliff corner bottom right
return new LevelScene(name, width, height, tile_ids, "../res/level_new.h5", parent);
}
LevelScene* LevelScene::fromFile(const std::string& file_path, QWidget* parent)
{
HighFive::File file(file_path, HighFive::File::ReadOnly);
// read level metadata
std::string level_metadata;
file.getDataSet("metadata").read(level_metadata);
// read tilesarray
std::vector<uint8_t> level_tilesarray;
file.getDataSet("tilesarray").read(level_tilesarray);
// extract metadata from xml
std::istringstream xmlStream(level_metadata);
boost::property_tree::ptree pt;
boost::property_tree::read_xml(xmlStream, pt);
int width = pt.get<int>("level.width");
int height = pt.get<int>("level.height");
std::string name = pt.get<std::string>("level.name");
return new LevelScene(name, width, height, level_tilesarray, file_path, parent);
}
std::string LevelScene::getName()
{
return m_name;
}
int LevelScene::getWidth()
{
return m_width;
}
int LevelScene::getHeight()
{
return m_height;
}
bool LevelScene::is_border(int index)
{
return (index / m_width) == 0
|| (index / m_width) == (m_height -1)
|| (index % m_width) == 0
|| (index % m_width) == (m_width - 1);
}
bool LevelScene::is_water_tile(uint8_t id)
{
if (id == 1)
{
return true;
}
if (id >= 17 && id <= 29)
{
return true;
}
return false;
}
void LevelScene::onLevelNameUpdated(std::string new_name)
{
m_name = new_name;
}
void LevelScene::onLevelWriteRequested(QString file_path)
{
boost::property_tree::ptree pt;
// Add data to the property tree
pt.put("level.width", m_width);
pt.put("level.height", m_height);
pt.put("level.name", m_name);
// convert property tree to xml string
std::ostringstream xmlStream;
boost::property_tree::write_xml(xmlStream, pt);
std::string xml_data = xmlStream.str();
// write level to hdf5
HighFive::File file(file_path.toStdString(), HighFive::File::Truncate);
file.createDataSet<std::string>("metadata", HighFive::DataSpace::From(xmlStream))
.write(xml_data);
file.createDataSet<uint8_t>("tilesarray", HighFive::DataSpace::From(m_tile_ids))
.write(m_tile_ids);
}
void LevelScene::onTileEntered(int index)
{
if (m_selected_tile_id == m_tile_ids[index])
{
return;
}
if (is_border(index) && !is_water_tile(m_selected_tile_id))
{
return;
}
if (m_tile_occupants[index] != nullptr)
{
removeItem(m_tile_occupants[index]);
delete m_tile_occupants[index];
m_tile_occupants[index] = nullptr;
}
if (!is_border(index) && m_selected_tile_id > 0 || is_border(index))
{
m_tile_occupants[index] = occupy_tile(index, m_selected_tile_id);
}
}
void LevelScene::onTileExited(int index)
{
if (m_selected_tile_id == m_tile_ids[index])
{
return;
}
if (m_tile_occupants[index] != nullptr)
{
removeItem(m_tile_occupants[index]);
delete m_tile_occupants[index];
m_tile_occupants[index] = nullptr;
}
if (m_tile_ids[index] > 0)
{
m_tile_occupants[index] = occupy_tile(index, m_tile_ids[index]);
}
}
void LevelScene::setTile(int index, uint8_t id)
{
m_tile_ids[index]=id;
if (m_tile_occupants[index] != nullptr)
{
removeItem(m_tile_occupants[index]);
delete m_tile_occupants[index];
m_tile_occupants[index] = nullptr;
}
if (m_tile_ids[index] > 0)
{
m_tile_occupants[index] = occupy_tile(index, id);
}
/* gespiegeltes Setzen
index = m_width * m_height - 1 -index;
int swapID[22] ={11,10,9,8,12,16,15,14,13,17,19,18,21,20,25,24,23,22,29,28,27,26};
if(id > 7 && id < 30) id = swapID[ id-8 ];
m_tile_ids[index]=id;
if (m_tile_occupants[index] != nullptr)
{
removeItem(m_tile_occupants[index]);
delete m_tile_occupants[index];
m_tile_occupants[index] = nullptr;
}
if (m_tile_ids[index] > 0)
{
m_tile_occupants[index] = occupy_tile(index, id);
}
*/
}
void LevelScene::onTileClicked(int index)
{
if (is_border(index) && !is_water_tile(m_selected_tile_id)) {
return;
}
if (!m_advanced_tile_placement) {
m_tile_ids[index] = m_selected_tile_id;
return;
}
if(m_selected_tile_id > 5 && m_selected_tile_id < 17){ //Straße plaziert
placeRoad(index, true);
return;
}
if((m_selected_tile_id > 16 && m_selected_tile_id < 30) || m_selected_tile_id == 1){ //Wasser plaziert
if(m_selected_tile_id == 17){
setTile(index,17);
} else {
setTile(index,1);
}
placeCliff(false, index);
return;
}
if(m_selected_tile_id == 0 || m_selected_tile_id == 2 || m_selected_tile_id == 3){ //Land plaziert
setTile(index,m_selected_tile_id);
placeCliff(true, index);
return;
}
setTile(index, m_selected_tile_id); //Gebäude plaziert
}
void LevelScene::onNewTileIdSelected(uint8_t tile_id)
{
m_selected_tile_id = tile_id;
}
void LevelScene::onCheckBoxToggled()
{
m_advanced_tile_placement = !m_advanced_tile_placement;
}
QGraphicsPixmapItem* LevelScene::occupy_tile(int index, uint8_t tile_id)
{
int x = (index % m_width) * 16;
int y = (index / m_width) * 16;
QPixmap tile_occupant = SpriteProvider::get_sprite(tile_id);
QGraphicsPixmapItem* tile_occupant_item = addPixmap(tile_occupant);
tile_occupant_item->setZValue(tile_id < 50 ? 1 : 2 + index);
tile_occupant_item->setPos(x, tile_id < 50 ? y : y - 16);
return tile_occupant_item;
}
void LevelScene::placeCliff(bool placedLand, int index){
const int16_t IdToSum[12] = {12,3,9,6,2,1,4,8,13,14,11,7};
const int16_t SumToId[16] = {1,23,22,19,24,24,21,29,25,20,25,28,18,26,27,0};
int16_t surroundingIDs[8] = {-2,-2,-2,-2,-2,-2,-2,-2}; //-2 = uninitialisiert, -1 = kartenrand
bool marchingSquares[8][4] = {false};
//Überprüfe Kartenränder, setze diese auf -1
if(index < m_width){ //oberer Kartenrand
surroundingIDs[7] = -1;
surroundingIDs[0] = -1;
surroundingIDs[1] = -1;
}
if((index + 1) % m_width == 0){ //rechter Kartenrand
surroundingIDs[1] = -1;
surroundingIDs[2] = -1;
surroundingIDs[3] = -1;
}
if(index >= m_width * (m_height - 1)){ //unterer Kartenrand
surroundingIDs[3] = -1;
surroundingIDs[4] = -1;
surroundingIDs[5] = -1;
}
if(index % m_width == 0){ //linker Kartenrand
surroundingIDs[5] = -1;
surroundingIDs[6] = -1;
surroundingIDs[7] = -1;
}
//Erhalte IDs von den umliegenden 8 Feldern, die kein Kartenrand sind
//startet in der mitte Oben, gehe im Uhrzeigersinn
if(surroundingIDs[0] == -2) surroundingIDs[0] = (int16_t) m_tile_ids[index - m_width];
if(surroundingIDs[1] == -2) surroundingIDs[1] = (int16_t) m_tile_ids[index - m_width + 1];
if(surroundingIDs[2] == -2) surroundingIDs[2] = (int16_t) m_tile_ids[index + 1];
if(surroundingIDs[3] == -2) surroundingIDs[3] = (int16_t) m_tile_ids[index + m_width + 1];
if(surroundingIDs[4] == -2) surroundingIDs[4] = (int16_t) m_tile_ids[index + m_width];
if(surroundingIDs[5] == -2) surroundingIDs[5] = (int16_t) m_tile_ids[index + m_width - 1];
if(surroundingIDs[6] == -2) surroundingIDs[6] = (int16_t) m_tile_ids[index - 1];
if(surroundingIDs[7] == -2) surroundingIDs[7] = (int16_t) m_tile_ids[index - m_width - 1];
//Ids können nicht lesbare Tiles enthalten, diese Herausfiltern
for(int i = 0; i < 8; i++){
if(surroundingIDs[i] == 17) surroundingIDs[i] = 1; //Riff als Wasser angesehen
if(surroundingIDs[i] == 2 || surroundingIDs[i] == 3) surroundingIDs[i] = 0; //Wald, Gebirge als Land angesehen
if(surroundingIDs[i] > 3 && surroundingIDs[i] < 17) surroundingIDs[i] = -1; //Straßen werden nicht verändert
if(surroundingIDs[i] > 29) surroundingIDs[i] = 0; //Gebäude werden als Land angesehen
}
//ID remapping um damit arbeiten zu können
for(int i = 0; i < 8; i++){
if(surroundingIDs[i] == -1){
} else if(surroundingIDs[i] == 0){
surroundingIDs[i] = 15;
} else if(surroundingIDs[i] == 1){
surroundingIDs[i] = 0;
} else{
surroundingIDs[i]= IdToSum[surroundingIDs[i] - 18];
}
}
//Berechne Marching Squares aus der remapped Id
for(int i = 0; i < 8; i++){
if(surroundingIDs[i] / 8 == 1){
marchingSquares[i][0] = true; //top left
surroundingIDs[i] -= 8;
}
if(surroundingIDs[i] / 4 == 1){
marchingSquares[i][1] = true; //top right
surroundingIDs[i] -= 4;
}
if(surroundingIDs[i] / 2 == 1){
marchingSquares[i][2] = true; //bottom right
surroundingIDs[i] -= 2;
}
if(surroundingIDs[i] == 1){
marchingSquares[i][3] = true; //bottom left
}
}
//Einfügen/Abziehen des Landes
marchingSquares[0][2] = placedLand; //oben
marchingSquares[0][3] = placedLand;
marchingSquares[1][3] = placedLand; //oben rechts
marchingSquares[2][3] = placedLand; //rechts
marchingSquares[2][0] = placedLand;
marchingSquares[3][0] = placedLand; //untern rechts
marchingSquares[4][0] = placedLand; //unten
marchingSquares[4][1] = placedLand;
marchingSquares[5][1] = placedLand; //unten links
marchingSquares[6][1] = placedLand; //links
marchingSquares[6][2] = placedLand;
marchingSquares[7][2] = placedLand; //oben links
//Berechne remapped ID aus Marching Squares
for(int i = 0; i < 8; i++){
if(surroundingIDs[i] > -1){
surroundingIDs[i] = 0;
if(marchingSquares[i][0]) surroundingIDs[i] += 8;
if(marchingSquares[i][1]) surroundingIDs[i] += 4;
if(marchingSquares[i][2]) surroundingIDs[i] += 2;
if(marchingSquares[i][3]) surroundingIDs[i] += 1;
}
}
//Remappe ID für setzbare Tiles
for(int i = 0; i < 8; i++){
if(surroundingIDs[i] > -1) surroundingIDs[i] = SumToId[ surroundingIDs[i] ];
}
//Plaziere Tiles
for(int i = 0; i < 8; i++){
if(isntIdentical(surroundingIDs[i], calcDir(i, index)))
setTile( calcDir(i, index),(uint8_t)surroundingIDs[i] );
}
}
void LevelScene::placeRoad(int index, bool updateFlag){
const int16_t IdToSum[] = {5,10,13,14,11,7,15,6,3,12,9};
const uint8_t SumToId[] = {6,6,7,14,6,6,13,11,7,16,7,10,15,8,9,12};
bool tileDirections[] = {false,false,false,false};
int16_t surroundingIDs[] = {-1,-1,-1,-1}; //-1 = kartenrand oder keine strasse
uint8_t placedID = 0;
//Erhalte IDs der umliegenden Felder - im Uhrzeigersinn, start: oben
//Vorgehen: überprüfe, ob die obere Zelle im Kartenrand ist. Wenn nicht erhalte index der oberen Zelle
if(!(index < m_width)) surroundingIDs[0] = (int16_t) m_tile_ids[index - m_width];
if(!((index + 1) % m_width == 0)) surroundingIDs[1] = (int16_t) m_tile_ids[index + 1];
if(!(index > m_width * (m_height-1))) surroundingIDs[2] = (int16_t) m_tile_ids[index + m_width];
if(!(index % m_width == 0)) surroundingIDs[3] = (int16_t) m_tile_ids[index - 1];
//Umformen der IDs oder ID ist keine Straße
for(int i = 0; i < 4; i++){
if(surroundingIDs[i] < 6 || surroundingIDs[i] > 16){
surroundingIDs[i] = -1; //ID ist keine Strasse
} else {
surroundingIDs[i] = IdToSum[ surroundingIDs[i] - 6]; //id remapping
}
}
//Berechne in welche Richtungen das neue Tile Zeigt
for(int i = 0; i < 4; i++){
if(surroundingIDs[i] > -1) tileDirections[i] = true;
}
//Berechne die Remapped ID
placedID = 0;
if(tileDirections[0]) placedID += 8;
if(tileDirections[1]) placedID += 4;
if(tileDirections[2]) placedID += 2;
if(tileDirections[3]) placedID += 1;
if(placedID == 0) updateFlag = false; //Umliegende Tiles müssen nicht geupdated werden, da keine Strassen
placedID = SumToId[placedID]; //Berechnete Summe in Valide ID umformen
setTile(index, placedID); //Tile setzen
//Update umliegende Tiles
if(updateFlag){
if(tileDirections[0]) placeRoad((index - m_width), false); //update oben
if(tileDirections[1]) placeRoad((index + 1), false); //update rechts
if(tileDirections[2]) placeRoad((index + m_width), false); //update unten
if(tileDirections[3]) placeRoad((index - 1), false); //update links
}
}
int LevelScene::calcDir(int i, int index){
if(i==0) return (index - m_width);
if(i==1) return (index - m_width + 1);
if(i==2) return (index + 1);
if(i==3) return (index + m_width + 1);
if(i==4) return (index + m_width);
if(i==5) return (index + m_width - 1);
if(i==6) return (index - 1);
if(i==7) return (index - m_width - 1);
}
bool LevelScene::isntIdentical(int16_t id, int index){
bool flag = (!(id == -1 || //kartenrand
(id == 0 && m_tile_ids[index]==2) || //wald
(id == 0 && m_tile_ids[index]==3) || //gebirge
(id == 1 && m_tile_ids[index]==17)) ); //riff
return flag;
}
} // namespace editor
\ No newline at end of file
/**
* LevelScene.hpp
*
* @date 28.01.2025
* @author Jonathan Dueck (jonathan.dueck@informatik.hs-fulda.de)
* @author Nils Jonathan Friedrich Eckardt minor changes
*/
#pragma once
#include <QGraphicsScene>
#include <QMouseEvent>
#include <QWidget>
#include <QGraphicsRectItem>
#include <QGraphicsSceneMouseEvent>
#include "EventHandler.hpp"
#include "Tile.hpp"
namespace editor
{
class LevelScene : public QGraphicsScene, public EventHandler {
public:
LevelScene(const std::string& name, int width, int height, std::vector<uint8_t> tile_ids, const std::string& file_path, QWidget *parent = nullptr);
static LevelScene* empty(const std::string& name, int width, int height, QWidget *parent = nullptr);
static LevelScene* fromFile(const std::string& file_path, QWidget *parent = nullptr);
std::string getName();
int getWidth();
int getHeight();
private:
bool is_border(int index);
bool is_water_tile(uint8_t id);
bool isntIdentical(int16_t id, int index);
void onLevelNameUpdated(std::string new_name) override;
void onLevelWriteRequested(QString file_path) override;
void onTileEntered(int index) override;
void onTileExited(int index) override;
void onTileClicked(int index) override;
void onNewTileIdSelected(uint8_t tile_id) override;
void onCheckBoxToggled() override;
void setTile(int index, uint8_t id);
void placeCliff(bool placedLand, int index);
void placeRoad(int index, bool updateFlag);
int calcDir(int i, int index);
QGraphicsPixmapItem* occupy_tile(int index, uint8_t tile_id);
uint8_t m_selected_tile_id;
std::string m_name;
bool m_advanced_tile_placement;
int m_width;
int m_height;
std::vector<uint8_t> m_tile_ids;
std::vector<QGraphicsPixmapItem*> m_tile_occupants;
std::string m_file_path;
};
} // namespace editor
\ No newline at end of file
/**
* LevelView.cpp
*
* @date 02.02.2025
* @author Jonathan Dueck (jonathan.dueck@informatik.hs-fulda.de)
*/
#include "LevelView.hpp"
#include "SpriteProvider.hpp"
namespace editor
{
LevelView::LevelView(LevelScene* scene, QWidget* parent) : QGraphicsView(parent), scale_val(2)
{
scene->setParent(this);
setScene(scene);
setAlignment(Qt::AlignCenter);
scale(scale_val, scale_val); // view starts at 2x zoom by default
setBackgroundBrush(QBrush(SpriteProvider::get_sprite(1)));
}
void LevelView::onZoomed(double delta)
{
// amount by which to scale has to be calculated using scale_vale (how much was already zoomed before)
// and delta (desired zoom percentage)
double scale_by = (scale_val + delta) / scale_val;
if (scale_by <= 0)
{
return;
}
scale_val += delta;
scale(scale_by, scale_by);
}
} // namespace editor
\ No newline at end of file
/**
* LevelView.hpp
*
* @date 02.02.2025
* @author Jonathan Dueck (jonathan.dueck@informatik.hs-fulda.de)
*/
#pragma once
#include <QGraphicsView>
#include "LevelScene.hpp"
#include "EventHandler.hpp"
namespace editor
{
/**
* The LevelView Widget is responsible for holding the LevelScene.
* It enables scrolling by default in case the LevelScene does not fit on
* the entire screen.
* It's also responsible for zooming the LevelScene in case a Zoom Event is received.
* To receive Zoom Events LevelView inherits from EventHandler and overwrites onZoomed
*/
class LevelView : public QGraphicsView, public EventHandler{
public:
/**
* Constructs a LevelView widget from the given scene and parent.
* @param scene The level scene that should be contained in this view.
* @param parent The widget to set as this widget's parent.
*/
LevelView(LevelScene* scene, QWidget* parent = nullptr);
private:
/**
* The current value by which the contained scene is scaled up or down.
* E.g 2.0 => twice the size, 0.5 => half the size
*/
double scale_val;
/**
* Zoom by the specified delta.
*/
void onZoomed(double delta) override;
};
} // namespace editor
\ No newline at end of file
/**
* MainWindow.cpp
*
* @date 27.01.2025
* @author Jonathan Dueck (jonathan.dueck@informatik.hs-fulda.de)
*/
#include "MainWindow.hpp"
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QHBoxLayout>
#include <iostream>
#include "LevelView.hpp"
#include "SpriteProvider.hpp"
#include "TileSelector.hpp"
#include "TopBar.hpp"
namespace editor
{
MainWindow::MainWindow(LevelScene* level, QWidget* parent)
: QMainWindow(parent), m_level_width(level->getWidth()), level_height(level->getHeight())
{
// CREATE MAIN WINDOW ------------------------------------------
QWidget* mainWidget = new QWidget(this);
addToolBar(new TopBar(level->getName(), this));
// CREATE TOOLBOX-----------------------------------------------
TileSelector* tile_selector = new TileSelector(mainWidget);
// CREATE LEVELMAP
LevelView* level_map = new LevelView(level, this);
level->setParent(level_map); // allow Qt to handle the memory
// LAYOUT-------------------------------------------------------
QHBoxLayout* layout = new QHBoxLayout(mainWidget);
layout->addWidget(level_map);
layout->addWidget(tile_selector);
setCentralWidget(mainWidget);
onLevelNameUpdated(level->getName());
}
void MainWindow::onLevelNameUpdated(std::string new_name)
{
std::string dim_text =
"(" + std::to_string(m_level_width) + " X " + std::to_string(level_height) + ")";
setWindowTitle((new_name + " " + dim_text).c_str());
}
} // namespace editor
\ No newline at end of file
/**
* MainWindow.hpp
*
* @date 27.01.2025
* @author Jonathan Dueck (jonathan.dueck@informatik.hs-fulda.de)
*/
#pragma once
#include <QMainWindow>
#include "LevelScene.hpp"
#include "EventHandler.hpp"
namespace editor
{
class MainWindow : public QMainWindow, public EventHandler {
public:
MainWindow(LevelScene* level, QWidget *parent = nullptr);
private:
void onLevelNameUpdated(std::string new_name) override;
int m_level_width;
int level_height;
};
} // namespace editor
\ No newline at end of file
/**
* SaveButton.cpp
*
* @date 29.01.2025
* @author Jonathan Dueck (jonathan.dueck@informatik.hs-fulda.de)
*/
#include "SaveButton.hpp"
#include <QFileDialog>
#include "EventHandler.hpp"
namespace editor
{
SaveButton::SaveButton(const std::string& title, QWidget* parent)
: QPushButton(title.c_str(), parent)
{
}
void SaveButton::mousePressEvent(QMouseEvent* event)
{
QPushButton::mousePressEvent(event);
QString file_path = QFileDialog::getSaveFileName(
this, "Level Speichern", QDir::currentPath(), "HDF5 Files (*.h5)");
if (!file_path.isEmpty())
{
EventHandler::send([file_path](EventHandler* e) { e->onLevelWriteRequested(file_path); });
}
}
} // namespace editor
\ No newline at end of file
/**
* SaveButton.cpp
*
* @date 29.01.2025
* @author Jonathan Dueck (jonathan.dueck@informatik.hs-fulda.de)
*/
#pragma once
#include <QPushButton>
namespace editor
{
class SaveButton : public QPushButton {
public:
SaveButton(const std::string& title, QWidget *parent = nullptr);
protected:
void mousePressEvent(QMouseEvent* event) override;
};
} // namespace editor
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment