From 0b9f601d2c054da1cae85f8b3e006a623eb3f7f2 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 18 Sep 2022 03:23:17 +0400 Subject: [PATCH] Squashing editor --- .github/workflows/github.yml | 3 +- .travis.yml | 2 +- CMakeLists.txt | 8 +- config/schemas/settings.json | 2 +- launcher/modManager/cmodlist.cpp | 91 +- launcher/modManager/cmodlist.h | 4 - launcher/modManager/cmodlistmodel_moc.cpp | 1 - launcher/modManager/cmodmanager.cpp | 4 - lib/CModHandler.cpp | 15 +- lib/CModHandler.h | 61 +- mapeditor/Animation.cpp | 780 +++++++++++++ mapeditor/Animation.h | 87 ++ mapeditor/BitmapHandler.cpp | 169 +++ mapeditor/BitmapHandler.h | 20 + mapeditor/CGameInfo.cpp | 108 ++ mapeditor/CGameInfo.h | 93 ++ mapeditor/CMakeLists.txt | 139 +++ mapeditor/StdInc.cpp | 1 + mapeditor/StdInc.h | 32 + mapeditor/generatorprogress.cpp | 38 + mapeditor/generatorprogress.h | 26 + mapeditor/generatorprogress.ui | 46 + mapeditor/graphics.cpp | 378 +++++++ mapeditor/graphics.h | 84 ++ mapeditor/icons/menu-game.png | Bin 0 -> 5482 bytes mapeditor/icons/menu-mods.png | Bin 0 -> 2986 bytes mapeditor/icons/menu-settings.png | Bin 0 -> 4474 bytes mapeditor/icons/mod-delete.png | Bin 0 -> 1449 bytes mapeditor/icons/mod-disabled.png | Bin 0 -> 1604 bytes mapeditor/icons/mod-download.png | Bin 0 -> 895 bytes mapeditor/icons/mod-enabled.png | Bin 0 -> 1104 bytes mapeditor/icons/mod-update.png | Bin 0 -> 1341 bytes mapeditor/inspector/armywidget.cpp | 142 +++ mapeditor/inspector/armywidget.h | 49 + mapeditor/inspector/armywidget.ui | 298 +++++ mapeditor/inspector/inspector.cpp | 735 ++++++++++++ mapeditor/inspector/inspector.h | 154 +++ mapeditor/inspector/messagewidget.cpp | 53 + mapeditor/inspector/messagewidget.h | 37 + mapeditor/inspector/messagewidget.ui | 33 + mapeditor/inspector/rewardswidget.cpp | 329 ++++++ mapeditor/inspector/rewardswidget.h | 79 ++ mapeditor/inspector/rewardswidget.ui | 83 ++ mapeditor/inspector/townbulidingswidget.cpp | 243 ++++ mapeditor/inspector/townbulidingswidget.h | 53 + mapeditor/inspector/townbulidingswidget.ui | 49 + mapeditor/jsonutils.cpp | 125 +++ mapeditor/jsonutils.h | 22 + mapeditor/launcherdirs.cpp | 36 + mapeditor/launcherdirs.h | 22 + mapeditor/main.cpp | 19 + mapeditor/mainwindow.cpp | 1118 ++++++++++++++++++ mapeditor/mainwindow.h | 148 +++ mapeditor/mainwindow.ui | 1120 +++++++++++++++++++ mapeditor/mapcontroller.cpp | 466 ++++++++ mapeditor/mapcontroller.h | 62 + mapeditor/mapeditor.ico | Bin 0 -> 81276 bytes mapeditor/mapeditor.rc | 1 + mapeditor/maphandler.cpp | 538 +++++++++ mapeditor/maphandler.h | 107 ++ mapeditor/mapsettings.cpp | 102 ++ mapeditor/mapsettings.h | 27 + mapeditor/mapsettings.ui | 175 +++ mapeditor/mapview.cpp | 448 ++++++++ mapeditor/mapview.h | 133 +++ mapeditor/objectbrowser.cpp | 68 ++ mapeditor/objectbrowser.h | 20 + mapeditor/playerparams.cpp | 139 +++ mapeditor/playerparams.h | 42 + mapeditor/playerparams.ui | 142 +++ mapeditor/playersettings.cpp | 77 ++ mapeditor/playersettings.h | 34 + mapeditor/playersettings.ui | 117 ++ mapeditor/radiopushbutton.cpp | 7 + mapeditor/radiopushbutton.h | 12 + mapeditor/scenelayer.cpp | 564 ++++++++++ mapeditor/scenelayer.h | 178 +++ mapeditor/spoiler.cpp | 59 + mapeditor/spoiler.h | 27 + mapeditor/validator.cpp | 159 +++ mapeditor/validator.h | 33 + mapeditor/validator.ui | 72 ++ mapeditor/windownewmap.cpp | 405 +++++++ mapeditor/windownewmap.h | 96 ++ mapeditor/windownewmap.ui | 784 +++++++++++++ 85 files changed, 12109 insertions(+), 124 deletions(-) create mode 100644 mapeditor/Animation.cpp create mode 100644 mapeditor/Animation.h create mode 100644 mapeditor/BitmapHandler.cpp create mode 100644 mapeditor/BitmapHandler.h create mode 100644 mapeditor/CGameInfo.cpp create mode 100644 mapeditor/CGameInfo.h create mode 100644 mapeditor/CMakeLists.txt create mode 100644 mapeditor/StdInc.cpp create mode 100644 mapeditor/StdInc.h create mode 100644 mapeditor/generatorprogress.cpp create mode 100644 mapeditor/generatorprogress.h create mode 100644 mapeditor/generatorprogress.ui create mode 100644 mapeditor/graphics.cpp create mode 100644 mapeditor/graphics.h create mode 100644 mapeditor/icons/menu-game.png create mode 100644 mapeditor/icons/menu-mods.png create mode 100644 mapeditor/icons/menu-settings.png create mode 100644 mapeditor/icons/mod-delete.png create mode 100644 mapeditor/icons/mod-disabled.png create mode 100644 mapeditor/icons/mod-download.png create mode 100644 mapeditor/icons/mod-enabled.png create mode 100644 mapeditor/icons/mod-update.png create mode 100644 mapeditor/inspector/armywidget.cpp create mode 100644 mapeditor/inspector/armywidget.h create mode 100644 mapeditor/inspector/armywidget.ui create mode 100644 mapeditor/inspector/inspector.cpp create mode 100644 mapeditor/inspector/inspector.h create mode 100644 mapeditor/inspector/messagewidget.cpp create mode 100644 mapeditor/inspector/messagewidget.h create mode 100644 mapeditor/inspector/messagewidget.ui create mode 100644 mapeditor/inspector/rewardswidget.cpp create mode 100644 mapeditor/inspector/rewardswidget.h create mode 100644 mapeditor/inspector/rewardswidget.ui create mode 100644 mapeditor/inspector/townbulidingswidget.cpp create mode 100644 mapeditor/inspector/townbulidingswidget.h create mode 100644 mapeditor/inspector/townbulidingswidget.ui create mode 100644 mapeditor/jsonutils.cpp create mode 100644 mapeditor/jsonutils.h create mode 100644 mapeditor/launcherdirs.cpp create mode 100644 mapeditor/launcherdirs.h create mode 100644 mapeditor/main.cpp create mode 100644 mapeditor/mainwindow.cpp create mode 100644 mapeditor/mainwindow.h create mode 100644 mapeditor/mainwindow.ui create mode 100644 mapeditor/mapcontroller.cpp create mode 100644 mapeditor/mapcontroller.h create mode 100644 mapeditor/mapeditor.ico create mode 100644 mapeditor/mapeditor.rc create mode 100644 mapeditor/maphandler.cpp create mode 100644 mapeditor/maphandler.h create mode 100644 mapeditor/mapsettings.cpp create mode 100644 mapeditor/mapsettings.h create mode 100644 mapeditor/mapsettings.ui create mode 100644 mapeditor/mapview.cpp create mode 100644 mapeditor/mapview.h create mode 100644 mapeditor/objectbrowser.cpp create mode 100644 mapeditor/objectbrowser.h create mode 100644 mapeditor/playerparams.cpp create mode 100644 mapeditor/playerparams.h create mode 100644 mapeditor/playerparams.ui create mode 100644 mapeditor/playersettings.cpp create mode 100644 mapeditor/playersettings.h create mode 100644 mapeditor/playersettings.ui create mode 100644 mapeditor/radiopushbutton.cpp create mode 100644 mapeditor/radiopushbutton.h create mode 100644 mapeditor/scenelayer.cpp create mode 100644 mapeditor/scenelayer.h create mode 100644 mapeditor/spoiler.cpp create mode 100644 mapeditor/spoiler.h create mode 100644 mapeditor/validator.cpp create mode 100644 mapeditor/validator.h create mode 100644 mapeditor/validator.ui create mode 100644 mapeditor/windownewmap.cpp create mode 100644 mapeditor/windownewmap.h create mode 100644 mapeditor/windownewmap.ui diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 2c3114e25..53e91f4d1 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -5,6 +5,7 @@ on: branches: - features/* - develop + - cpp-map-editor pull_request: env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) @@ -46,7 +47,7 @@ jobs: pack: 1 cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis` extension: exe - cmake_args: -G Ninja + cmake_args: -G Ninja -DENABLE_EDITOR=0 - platform: msvc os: windows-latest test: 0 diff --git a/.travis.yml b/.travis.yml index 7fabdc1f0..492820a41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,7 +41,7 @@ addons: notification_email: coverity@arseniyshestakov.com build_command_prepend: cov-configure --compiler clang-3.6 --comptype clangcc && cov-configure --comptype clangcxx --compiler clang++-3.6 && cmake -G Ninja .. - -DCMAKE_BUILD_TYPE=DEBUG -DENABLE_LAUNCHER=0 + -DCMAKE_BUILD_TYPE=DEBUG -DENABLE_LAUNCHER=0 -DENABLE_EDITOR=0 build_command: ninja -j 3 branch_pattern: coverity_scan diff --git a/CMakeLists.txt b/CMakeLists.txt index 648de8e9f..9ae843236 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ set(VCMI_VERSION_PATCH 0) option(ENABLE_ERM "Enable compilation of ERM scripting module" ON) option(ENABLE_LUA "Enable compilation of LUA scripting module" ON) option(ENABLE_LAUNCHER "Enable compilation of launcher" ON) +option(ENABLE_EDITOR "Enable compilation of map editor" ON) option(ENABLE_TEST "Enable compilation of unit tests" ON) if(NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0") option(ENABLE_PCH "Enable compilation using precompiled headers" ON) @@ -247,7 +248,7 @@ if(TARGET SDL2_ttf::SDL2_ttf) endif() find_package(TBB REQUIRED) -if(ENABLE_LAUNCHER) +if(ENABLE_LAUNCHER OR ENABLE_EDITOR) # Widgets finds its own dependencies (QtGui and QtCore). find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Network) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Network) @@ -356,6 +357,9 @@ add_subdirectory_with_folder("AI" AI) if(ENABLE_LAUNCHER) add_subdirectory(launcher) endif() +if(ENABLE_EDITOR) + add_subdirectory(mapeditor) +endif() if(ENABLE_TEST) enable_testing() add_subdirectory(test) @@ -390,7 +394,7 @@ if(WIN32) set(debug_postfix d) endif() - if(ENABLE_LAUNCHER) + if(ENABLE_LAUNCHER OR ENABLE_EDITOR) get_target_property(QtCore_location Qt${QT_VERSION_MAJOR}::Core LOCATION) get_filename_component(Qtbin_folder ${QtCore_location} PATH) file(GLOB dep_files diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 66d39b630..648735aa6 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -388,7 +388,7 @@ }, "updateConfigUrl" : { "type" : "string", - "default" : "https://raw.githubusercontent.com/vcmi/vcmi-updates/master/vcmi-updates.json" + "default" : "https://raw.githubusercontent.com/Nordsoft91/vcmi-autoupdate/main/autoUpdate.json" } } } diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index 3cca86d1e..c0d617442 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -12,59 +12,11 @@ #include "../../lib/JsonNode.h" #include "../../lib/filesystem/CFileInputStream.h" -#include "../../lib/GameConstants.h" - -const int maxSections = 3; // versions consist from up to 3 sections, major.minor.patch - -bool isCompatible(const QString & verMin, const QString & verMax) -{ - QList vcmiVersionList = {GameConstants::VCMI_VERSION_MAJOR, - GameConstants::VCMI_VERSION_MINOR, - GameConstants::VCMI_VERSION_PATCH}; - - if(!verMin.isEmpty()) - { - QStringList verMinList = verMin.split("."); - assert(verMinList.size() == maxSections); - bool compatibleMin = true; - for(int i = 0; i < maxSections; i++) - { - if(verMinList[i].toInt() < vcmiVersionList[i]) - { - break; - } - if(verMinList[i].toInt() > vcmiVersionList[i]) - { - compatibleMin = false; - break; - } - } - - if(!compatibleMin) - return false; - } - - if(!verMax.isEmpty()) - { - QStringList verMaxList = verMax.split("."); - assert(verMaxList.size() == maxSections); - for(int i = 0; i < maxSections; i++) - { - if(verMaxList[i].toInt() > vcmiVersionList[i]) - { - return true; - } - if(verMaxList[i].toInt() < vcmiVersionList[i]) - { - return false; - } - } - } - return true; -} bool CModEntry::compareVersions(QString lesser, QString greater) { + static const int maxSections = 3; // versions consist from up to 3 sections, major.minor.patch + QStringList lesserList = lesser.split("."); QStringList greaterList = greater.split("."); @@ -140,15 +92,6 @@ bool CModEntry::isUpdateable() const return false; } -bool CModEntry::isCompatible() const -{ - if(!isInstalled()) - return false; - - auto compatibility = localData["compatibility"].toMap(); - return ::isCompatible(compatibility["min"].toString(), compatibility["max"].toString()); -} - bool CModEntry::isEssential() const { return getValue("storedLocaly").toBool(); @@ -159,11 +102,6 @@ bool CModEntry::isInstalled() const return !localData.isEmpty(); } -bool CModEntry::isValid() const -{ - return !localData.isEmpty() || !repository.isEmpty(); -} - int CModEntry::getModStatus() const { int status = 0; @@ -255,11 +193,7 @@ static QVariant getValue(QVariant input, QString path) QString remainder = "/" + path.section('/', 2, -1); entryName.remove(0, 1); - QMap keyNormalize; - for(auto & key : input.toMap().keys()) - keyNormalize[key.toLower()] = key; - - return getValue(input.toMap().value(keyNormalize[entryName]), remainder); + return getValue(input.toMap().value(entryName), remainder); } else { @@ -269,7 +203,6 @@ static QVariant getValue(QVariant input, QString path) CModEntry CModList::getMod(QString modname) const { - modname = modname.toLower(); QVariantMap repo; QVariantMap local = localModList[modname].toMap(); QVariantMap settings; @@ -313,14 +246,14 @@ CModEntry CModList::getMod(QString modname) const QVariant repoVal = getValue(entry, path); if(repoVal.isValid()) { - auto repoValMap = repoVal.toMap(); - auto compatibility = repoValMap["compatibility"].toMap(); - if(isCompatible(compatibility["min"].toString(), compatibility["max"].toString())) + if(repo.empty()) { - if(repo.empty() || CModEntry::compareVersions(repo["version"].toString(), repoValMap["version"].toString())) - { - repo = repoValMap; - } + repo = repoVal.toMap(); + } + else + { + if(CModEntry::compareVersions(repo["version"].toString(), repoVal.toMap()["version"].toString())) + repo = repoVal.toMap(); } } } @@ -364,12 +297,12 @@ QVector CModList::getModList() const { for(auto it = repo.begin(); it != repo.end(); it++) { - knownMods.insert(it.key().toLower()); + knownMods.insert(it.key()); } } for(auto it = localModList.begin(); it != localModList.end(); it++) { - knownMods.insert(it.key().toLower()); + knownMods.insert(it.key()); } for(auto entry : knownMods) diff --git a/launcher/modManager/cmodlist.h b/launcher/modManager/cmodlist.h index 2bce79c6d..a5de09622 100644 --- a/launcher/modManager/cmodlist.h +++ b/launcher/modManager/cmodlist.h @@ -51,10 +51,6 @@ public: bool isInstalled() const; // vcmi essential files bool isEssential() const; - // checks if verison is compatible with vcmi - bool isCompatible() const; - // returns if has any data - bool isValid() const; // see ModStatus enum int getModStatus() const; diff --git a/launcher/modManager/cmodlistmodel_moc.cpp b/launcher/modManager/cmodlistmodel_moc.cpp index fb2d3f1e0..2e94b8f81 100644 --- a/launcher/modManager/cmodlistmodel_moc.cpp +++ b/launcher/modManager/cmodlistmodel_moc.cpp @@ -245,7 +245,6 @@ bool CModFilterModel::filterMatchesThis(const QModelIndex & source) const { CModEntry mod = base->getMod(source.data(ModRoles::ModNameRole).toString()); return (mod.getModStatus() & filterMask) == filteredType && - mod.isValid() && QSortFilterProxyModel::filterAcceptsRow(source.row(), source.parent()); } diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp index eea55af17..3bc854bb3 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/cmodmanager.cpp @@ -169,10 +169,6 @@ bool CModManager::canEnableMod(QString modname) if(!mod.isInstalled()) return addError(modname, "Mod must be installed first"); - //check for compatibility - if(!mod.isCompatible()) - return addError(modname, "Mod is not compatible, please update VCMI and checkout latest mod revisions"); - for(auto modEntry : mod.getValue("depends").toStringList()) { if(!modList->hasMod(modEntry)) // required mod is not available diff --git a/lib/CModHandler.cpp b/lib/CModHandler.cpp index 485a8304b..135d386a4 100644 --- a/lib/CModHandler.cpp +++ b/lib/CModHandler.cpp @@ -544,14 +544,10 @@ CModInfo::Version CModInfo::Version::fromString(std::string from) { auto pointPos = from.find('.'); major = std::stoi(from.substr(0, pointPos)); - if(pointPos != std::string::npos) - { - from = from.substr(pointPos + 1); - pointPos = from.find('.'); - minor = std::stoi(from.substr(0, pointPos)); - if(pointPos != std::string::npos) - patch = std::stoi(from.substr(pointPos + 1)); - } + from = from.substr(pointPos); + pointPos = from.find('.'); + minor = std::stoi(from.substr(0, pointPos)); + patch = std::stoi(from.substr(pointPos)); } catch(const std::invalid_argument & e) { @@ -654,11 +650,8 @@ void CModInfo::loadLocalData(const JsonNode & data) } //check compatibility - bool wasEnabled = enabled; enabled &= vcmiCompatibleMin.isNull() || Version::GameVersion().compatible(vcmiCompatibleMin); enabled &= vcmiCompatibleMax.isNull() || vcmiCompatibleMax.compatible(Version::GameVersion()); - if(wasEnabled && !enabled) - logGlobal->warn("Mod %s is incompatible with current version of VCMI and cannot be enabled", name); if (enabled) validation = validated ? PASSED : PENDING; diff --git a/lib/CModHandler.h b/lib/CModHandler.h index 03028dd5f..ae225a7cb 100644 --- a/lib/CModHandler.h +++ b/lib/CModHandler.h @@ -240,6 +240,19 @@ public: static std::string getModDir(std::string name); static std::string getModFile(std::string name); + //TODO: remove as soon as backward compatilibity for versions earlier 806 is not preserved. + template void serialize(Handler &h, const int ver) + { + h & identifier; + h & description; + h & name; + h & dependencies; + h & conflicts; + h & config; + h & checksum; + h & validation; + h & enabled; + } private: void loadLocalData(const JsonNode & data); }; @@ -361,33 +374,41 @@ public: template void serialize(Handler &h, const int version) { - if(h.saving) + if(version < 806) { + h & allMods; //don't serialize mods h & activeMods; - for(auto & m : activeMods) - h & allMods[m].version; } else { - std::vector newActiveMods; - h & newActiveMods; - for(auto & m : newActiveMods) + if(h.saving) { - if(!allMods.count(m)) - throw Incompatibility(m + " unkown mod"); - - CModInfo::Version mver; - h & mver; - if(!allMods[m].version.isNull() && !mver.isNull() && !allMods[m].version.compatible(mver)) - { - std::string err = allMods[m].name + - ": version needed " + mver.toString() + - "but you have installed " + allMods[m].version.toString(); - throw Incompatibility(err); - } - allMods[m].enabled = true; + h & activeMods; + for(auto & m : activeMods) + h & allMods[m].version; + } + else + { + std::vector newActiveMods; + h & newActiveMods; + for(auto & m : newActiveMods) + { + if(!allMods.count(m)) + throw Incompatibility(m + " unkown mod"); + + CModInfo::Version mver; + h & mver; + if(!allMods[m].version.isNull() && !mver.isNull() && !allMods[m].version.compatible(mver)) + { + std::string err = allMods[m].name + + ": version needed " + mver.toString() + + "but you have installed " + allMods[m].version.toString(); + throw Incompatibility(err); + } + allMods[m].enabled = true; + } + std::swap(activeMods, newActiveMods); } - std::swap(activeMods, newActiveMods); } h & settings; diff --git a/mapeditor/Animation.cpp b/mapeditor/Animation.cpp new file mode 100644 index 000000000..f21e33b10 --- /dev/null +++ b/mapeditor/Animation.cpp @@ -0,0 +1,780 @@ +/* + * CAnimation.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "Animation.h" + +#include "BitmapHandler.h" + +#include "../lib/filesystem/Filesystem.h" +#include "../lib/filesystem/ISimpleResourceLoader.h" +#include "../lib/JsonNode.h" +#include "../lib/CRandomGenerator.h" + + +typedef std::map> source_map; +//typedef std::map image_map; +//typedef std::map group_map; + +/// Class for def loading +/// After loading will store general info (palette and frame offsets) and pointer to file itself +class DefFile +{ +private: + + struct SSpriteDef + { + ui32 size; + ui32 format; /// format in which pixel data is stored + ui32 fullWidth; /// full width and height of frame, including borders + ui32 fullHeight; + ui32 width; /// width and height of pixel data, borders excluded + ui32 height; + si32 leftMargin; + si32 topMargin; + }; + //offset[group][frame] - offset of frame data in file + std::map > offset; + + std::unique_ptr data; + std::unique_ptr> palette; + +public: + DefFile(std::string Name); + ~DefFile(); + + std::shared_ptr loadFrame(size_t frame, size_t group) const; + + const std::map getEntries() const; +}; + +class ImageLoader +{ + QImage * image; + ui8 * lineStart; + ui8 * position; + QPoint spriteSize, margins, fullSize; +public: + //load size raw pixels from data + inline void Load(size_t size, const ui8 * data); + //set size pixels to color + inline void Load(size_t size, ui8 color=0); + inline void EndLine(); + //init image with these sizes and palette + inline void init(QPoint SpriteSize, QPoint Margins, QPoint FullSize); + + ImageLoader(QImage * Img); + ~ImageLoader(); +}; + +// Extremely simple file cache. TODO: smarter, more general solution +class FileCache +{ + static const int cacheSize = 50; //Max number of cached files + struct FileData + { + ResourceID name; + size_t size; + std::unique_ptr data; + + std::unique_ptr getCopy() + { + auto ret = std::unique_ptr(new ui8[size]); + std::copy(data.get(), data.get() + size, ret.get()); + return ret; + } + FileData(ResourceID name_, size_t size_, std::unique_ptr data_): + name{std::move(name_)}, + size{size_}, + data{std::move(data_)} + {} + }; + + std::deque cache; +public: + std::unique_ptr getCachedFile(ResourceID rid) + { + for(auto & file : cache) + { + if (file.name == rid) + return file.getCopy(); + } + // Still here? Cache miss + if (cache.size() > cacheSize) + cache.pop_front(); + + auto data = CResourceHandler::get()->load(rid)->readAll(); + + cache.emplace_back(std::move(rid), data.second, std::move(data.first)); + + return cache.back().getCopy(); + } +}; + +enum class DefType : uint32_t +{ + SPELL = 0x40, + SPRITE = 0x41, + CREATURE = 0x42, + MAP = 0x43, + MAP_HERO = 0x44, + TERRAIN = 0x45, + CURSOR = 0x46, + INTERFACE = 0x47, + SPRITE_FRAME = 0x48, + BATTLE_HERO = 0x49 +}; + +static FileCache animationCache; + +/************************************************************************* + * DefFile, class used for def loading * + *************************************************************************/ + +DefFile::DefFile(std::string Name): + data(nullptr) +{ + + #if 0 + static QRgba H3_ORIG_PALETTE[8] = + { + { 0, 255, 255, SDL_ALPHA_OPAQUE}, + {255, 150, 255, SDL_ALPHA_OPAQUE}, + {255, 100, 255, SDL_ALPHA_OPAQUE}, + {255, 50, 255, SDL_ALPHA_OPAQUE}, + {255, 0, 255, SDL_ALPHA_OPAQUE}, + {255, 255, 0, SDL_ALPHA_OPAQUE}, + {180, 0, 255, SDL_ALPHA_OPAQUE}, + { 0, 255, 0, SDL_ALPHA_OPAQUE} + }; + #endif // 0 + + //First 8 colors in def palette used for transparency + static QRgb H3Palette[8] = + { + qRgba(0, 0, 0, 0), // 100% - transparency + qRgba(0, 0, 0, 32), // 75% - shadow border, + qRgba(0, 0, 0, 64), // TODO: find exact value + qRgba(0, 0, 0, 128), // TODO: for transparency + qRgba(0, 0, 0, 128), // 50% - shadow body + qRgba(0, 0, 0, 0), // 100% - selection highlight + qRgba(0, 0, 0, 128), // 50% - shadow body below selection + qRgba(0, 0, 0, 64) // 75% - shadow border below selection + }; + data = animationCache.getCachedFile(ResourceID(std::string("SPRITES/") + Name, EResType::ANIMATION)); + + palette = std::make_unique>(256); + int it = 0; + + ui32 type = read_le_u32(data.get() + it); + it+=4; + //int width = read_le_u32(data + it); it+=4;//not used + //int height = read_le_u32(data + it); it+=4; + it+=8; + ui32 totalBlocks = read_le_u32(data.get() + it); + it+=4; + + for (ui32 i= 0; i<256; i++) + { + ui8 c[3]; + c[0] = data[it++]; + c[1] = data[it++]; + c[2] = data[it++]; + (*palette)[i] = qRgba(c[0], c[1], c[2], 255); + } + + switch(static_cast(type)) + { + case DefType::SPELL: + (*palette)[0] = H3Palette[0]; + break; + case DefType::SPRITE: + case DefType::SPRITE_FRAME: + for(ui32 i= 0; i<8; i++) + (*palette)[i] = H3Palette[i]; + break; + case DefType::CREATURE: + (*palette)[0] = H3Palette[0]; + (*palette)[1] = H3Palette[1]; + (*palette)[4] = H3Palette[4]; + (*palette)[5] = H3Palette[5]; + (*palette)[6] = H3Palette[6]; + (*palette)[7] = H3Palette[7]; + break; + case DefType::MAP: + case DefType::MAP_HERO: + (*palette)[0] = H3Palette[0]; + (*palette)[1] = H3Palette[1]; + (*palette)[4] = H3Palette[4]; + //5 = owner flag, handled separately + break; + case DefType::TERRAIN: + (*palette)[0] = H3Palette[0]; + (*palette)[1] = H3Palette[1]; + (*palette)[2] = H3Palette[2]; + (*palette)[3] = H3Palette[3]; + (*palette)[4] = H3Palette[4]; + break; + case DefType::CURSOR: + (*palette)[0] = H3Palette[0]; + break; + case DefType::INTERFACE: + (*palette)[0] = H3Palette[0]; + (*palette)[1] = H3Palette[1]; + (*palette)[4] = H3Palette[4]; + //player colors handled separately + //TODO: disallow colorizing other def types + break; + case DefType::BATTLE_HERO: + (*palette)[0] = H3Palette[0]; + (*palette)[1] = H3Palette[1]; + (*palette)[4] = H3Palette[4]; + break; + default: + logAnim->error("Unknown def type %d in %s", type, Name); + break; + } + + + for (ui32 i=0; i DefFile::loadFrame(size_t frame, size_t group) const +{ + std::map >::const_iterator it; + it = offset.find(group); + assert (it != offset.end()); + + const ui8 * FDef = data.get()+it->second[frame]; + + const SSpriteDef sd = * reinterpret_cast(FDef); + SSpriteDef sprite; + + sprite.format = read_le_u32(&sd.format); + sprite.fullWidth = read_le_u32(&sd.fullWidth); + sprite.fullHeight = read_le_u32(&sd.fullHeight); + sprite.width = read_le_u32(&sd.width); + sprite.height = read_le_u32(&sd.height); + sprite.leftMargin = read_le_u32(&sd.leftMargin); + sprite.topMargin = read_le_u32(&sd.topMargin); + + ui32 currentOffset = sizeof(SSpriteDef); + + //special case for some "old" format defs (SGTWMTA.DEF and SGTWMTB.DEF) + + if(sprite.format == 1 && sprite.width > sprite.fullWidth && sprite.height > sprite.fullHeight) + { + sprite.leftMargin = 0; + sprite.topMargin = 0; + sprite.width = sprite.fullWidth; + sprite.height = sprite.fullHeight; + + currentOffset -= 16; + } + + const ui32 BaseOffset = currentOffset; + + + std::shared_ptr img = std::make_shared(sprite.fullWidth, sprite.fullHeight, QImage::Format_Indexed8); + if(!img) + throw std::runtime_error("Image memory cannot be allocated"); + + ImageLoader loader(img.get()); + loader.init(QPoint(sprite.width, sprite.height), + QPoint(sprite.leftMargin, sprite.topMargin), + QPoint(sprite.fullWidth, sprite.fullHeight)); + + switch(sprite.format) + { + case 0: + { + //pixel data is not compressed, copy data to surface + for(ui32 i=0; i(FDef+currentOffset); + currentOffset += sizeof(ui32) * sprite.height; + + for(ui32 i=0; ierror("Error: unsupported format of def file: %d", sprite.format); + break; + } + + + img->setColorTable(*palette); + return img; +} + +DefFile::~DefFile() = default; + +const std::map DefFile::getEntries() const +{ + std::map ret; + + for (auto & elem : offset) + ret[elem.first] = elem.second.size(); + return ret; +} + +/************************************************************************* + * Classes for image loaders - helpers for loading from def files * + *************************************************************************/ + +ImageLoader::ImageLoader(QImage * Img): + image(Img), + lineStart(Img->bits()), + position(Img->bits()) +{ + +} + +void ImageLoader::init(QPoint SpriteSize, QPoint Margins, QPoint FullSize) +{ + spriteSize = SpriteSize; + margins = Margins; + fullSize = FullSize; + + memset((void *)image->bits(), 0, fullSize.y() * fullSize.x()); + + lineStart = image->bits(); + lineStart += margins.y() * fullSize.x() + margins.x(); + position = lineStart; +} + +inline void ImageLoader::Load(size_t size, const ui8 * data) +{ + if(size) + { + memcpy((void *)position, data, size); + position += size; + } +} + +inline void ImageLoader::Load(size_t size, ui8 color) +{ + if (size) + { + memset((void *)position, color, size); + position += size; + } +} + +inline void ImageLoader::EndLine() +{ + lineStart += fullSize.x(); + position = lineStart; +} + +ImageLoader::~ImageLoader() +{ + //SDL_UnlockSurface(image->surf); + //SDL_SetColorKey(image->surf, SDL_TRUE, 0); + //TODO: RLE if compressed and bpp>1 +} + +/************************************************************************* + * Classes for images, support loading from file and drawing on surface * + *************************************************************************/ + +std::shared_ptr Animation::getFromExtraDef(std::string filename) +{ + size_t pos = filename.find(':'); + if (pos == -1) + return nullptr; + Animation anim(filename.substr(0, pos)); + pos++; + size_t frame = atoi(filename.c_str()+pos); + size_t group = 0; + pos = filename.find(':', pos); + if (pos != -1) + { + pos++; + group = frame; + frame = atoi(filename.c_str()+pos); + } + anim.load(frame ,group); + auto ret = anim.images[group][frame]; + anim.images.clear(); + return ret; +} + +bool Animation::loadFrame(size_t frame, size_t group) +{ + if(size(group) <= frame) + { + printError(frame, group, "LoadFrame"); + return false; + } + + auto image = getImage(frame, group, false); + if(image) + { + return true; + } + + //try to get image from def + if(source[group][frame].getType() == JsonNode::JsonType::DATA_NULL) + { + if(defFile) + { + auto frameList = defFile->getEntries(); + + if(vstd::contains(frameList, group) && frameList.at(group) > frame) // frame is present + { + images[group][frame] = defFile->loadFrame(frame, group); + return true; + } + } + return false; + // still here? image is missing + + printError(frame, group, "LoadFrame"); + images[group][frame] = std::make_shared("DEFAULT"); + } + else //load from separate file + { + auto img = getFromExtraDef(source[group][frame]["file"].String()); + //if(!img) + + //img = std::make_shared(source[group][frame]); + + images[group][frame] = img; + return true; + } + return false; +} + +bool Animation::unloadFrame(size_t frame, size_t group) +{ + auto image = getImage(frame, group, false); + if(image) + { + images[group].erase(frame); + + if(images[group].empty()) + images.erase(group); + return true; + } + return false; +} + +void Animation::init() +{ + if(defFile) + { + const std::map defEntries = defFile->getEntries(); + + for (auto & defEntry : defEntries) + source[defEntry.first].resize(defEntry.second); + } + + ResourceID resID(std::string("SPRITES/") + name, EResType::TEXT); + + //if(vstd::contains(graphics->imageLists, resID.getName())) + //initFromJson(graphics->imageLists[resID.getName()]); + + auto configList = CResourceHandler::get()->getResourcesWithName(resID); + + for(auto & loader : configList) + { + auto stream = loader->load(resID); + std::unique_ptr textData(new ui8[stream->getSize()]); + stream->read(textData.get(), stream->getSize()); + + const JsonNode config((char*)textData.get(), stream->getSize()); + + //initFromJson(config); + } +} + +void Animation::printError(size_t frame, size_t group, std::string type) const +{ + logGlobal->error("%s error: Request for frame not present in CAnimation! File name: %s, Group: %d, Frame: %d", type, name, group, frame); +} + +Animation::Animation(std::string Name): + name(Name), + preloaded(false), + defFile() +{ + size_t dotPos = name.find_last_of('.'); + if ( dotPos!=-1 ) + name.erase(dotPos); + std::transform(name.begin(), name.end(), name.begin(), toupper); + + ResourceID resource(std::string("SPRITES/") + name, EResType::ANIMATION); + + if(CResourceHandler::get()->existsResource(resource)) + defFile = std::make_shared(name); + + init(); + + if(source.empty()) + logAnim->error("Animation %s failed to load", Name); +} + +Animation::Animation(): + name(""), + preloaded(false), + defFile() +{ + init(); +} + +Animation::~Animation() = default; + +void Animation::duplicateImage(const size_t sourceGroup, const size_t sourceFrame, const size_t targetGroup) +{ + if(!source.count(sourceGroup)) + { + logAnim->error("Group %d missing in %s", sourceGroup, name); + return; + } + + if(source[sourceGroup].size() <= sourceFrame) + { + logAnim->error("Frame [%d %d] missing in %s", sourceGroup, sourceFrame, name); + return; + } + + //todo: clone actual loaded Image object + JsonNode clone(source[sourceGroup][sourceFrame]); + + if(clone.getType() == JsonNode::JsonType::DATA_NULL) + { + std::string temp = name+":"+boost::lexical_cast(sourceGroup)+":"+boost::lexical_cast(sourceFrame); + clone["file"].String() = temp; + } + + source[targetGroup].push_back(clone); + + size_t index = source[targetGroup].size() - 1; + + if(preloaded) + load(index, targetGroup); +} + +void Animation::setCustom(std::string filename, size_t frame, size_t group) +{ + if (source[group].size() <= frame) + source[group].resize(frame+1); + source[group][frame]["file"].String() = filename; + //FIXME: update image if already loaded +} + +std::shared_ptr Animation::getImage(size_t frame, size_t group, bool verbose) const +{ + auto groupIter = images.find(group); + if (groupIter != images.end()) + { + auto imageIter = groupIter->second.find(frame); + if (imageIter != groupIter->second.end()) + return imageIter->second; + } + if (verbose) + printError(frame, group, "GetImage"); + return nullptr; +} + +void Animation::load() +{ + for (auto & elem : source) + for (size_t image=0; image < elem.second.size(); image++) + loadFrame(image, elem.first); +} + +void Animation::unload() +{ + for (auto & elem : source) + for (size_t image=0; image < elem.second.size(); image++) + unloadFrame(image, elem.first); + +} + +void Animation::preload() +{ + if(!preloaded) + { + preloaded = true; + load(); + } +} + +void Animation::loadGroup(size_t group) +{ + if (vstd::contains(source, group)) + for (size_t image=0; image < source[group].size(); image++) + loadFrame(image, group); +} + +void Animation::unloadGroup(size_t group) +{ + if (vstd::contains(source, group)) + for (size_t image=0; image < source[group].size(); image++) + unloadFrame(image, group); +} + +void Animation::load(size_t frame, size_t group) +{ + loadFrame(frame, group); +} + +void Animation::unload(size_t frame, size_t group) +{ + unloadFrame(frame, group); +} + +size_t Animation::size(size_t group) const +{ + auto iter = source.find(group); + if (iter != source.end()) + return iter->second.size(); + return 0; +} + +void Animation::horizontalFlip() +{ + for(auto & group : images) + for(auto & image : group.second) + *image.second = image.second->transformed(QTransform::fromScale(-1, 1)); +} + +void Animation::verticalFlip() +{ + for(auto & group : images) + for(auto & image : group.second) + *image.second = image.second->transformed(QTransform::fromScale(1, -1)); +} + +void Animation::playerColored(PlayerColor player) +{ + //for(auto & group : images) + //for(auto & image : group.second) + //image.second->playerColored(player); +} + +void Animation::createFlippedGroup(const size_t sourceGroup, const size_t targetGroup) +{ + for(size_t frame = 0; frame < size(sourceGroup); ++frame) + { + duplicateImage(sourceGroup, frame, targetGroup); + + auto image = getImage(frame, targetGroup); + *image = image->transformed(QTransform::fromScale(1, -1)); + } +} diff --git a/mapeditor/Animation.h b/mapeditor/Animation.h new file mode 100644 index 000000000..cc5c84a3f --- /dev/null +++ b/mapeditor/Animation.h @@ -0,0 +1,87 @@ +#ifndef ANIMATION_H +#define ANIMATION_H + +#include "../lib/JsonNode.h" +#include "../lib/GameConstants.h" +#include +#include + +/* + * Base class for images, can be used for non-animation pictures as well + */ + +class DefFile; +/// Class for handling animation +class Animation +{ +private: + //source[group][position] - file with this frame, if string is empty - image located in def file + std::map> source; + + //bitmap[group][position], store objects with loaded bitmaps + std::map > > images; + + //animation file name + std::string name; + + bool preloaded; + + std::shared_ptr defFile; + + //loader, will be called by load(), require opened def file for loading from it. Returns true if image is loaded + bool loadFrame(size_t frame, size_t group); + + //unloadFrame, returns true if image has been unloaded ( either deleted or decreased refCount) + bool unloadFrame(size_t frame, size_t group); + + //initialize animation from file + //void initFromJson(const JsonNode & input); + void init(); + + //to get rid of copy-pasting error message :] + void printError(size_t frame, size_t group, std::string type) const; + + //not a very nice method to get image from another def file + //TODO: remove after implementing resource manager + std::shared_ptr getFromExtraDef(std::string filename); + +public: + Animation(std::string Name); + Animation(); + ~Animation(); + + //duplicates frame at [sourceGroup, sourceFrame] as last frame in targetGroup + //and loads it if animation is preloaded + void duplicateImage(const size_t sourceGroup, const size_t sourceFrame, const size_t targetGroup); + + // adjust the color of the animation, used in battle spell effects, e.g. Cloned objects + + //add custom surface to the selected position. + void setCustom(std::string filename, size_t frame, size_t group=0); + + std::shared_ptr getImage(size_t frame, size_t group=0, bool verbose=true) const; + + //all available frames + void load (); + void unload(); + void preload(); + + //all frames from group + void loadGroup (size_t group); + void unloadGroup(size_t group); + + //single image + void load (size_t frame, size_t group=0); + void unload(size_t frame, size_t group=0); + + //total count of frames in group (including not loaded) + size_t size(size_t group=0) const; + + void horizontalFlip(); + void verticalFlip(); + void playerColored(PlayerColor player); + + void createFlippedGroup(const size_t sourceGroup, const size_t targetGroup); +}; + +#endif // ANIMATION_H diff --git a/mapeditor/BitmapHandler.cpp b/mapeditor/BitmapHandler.cpp new file mode 100644 index 000000000..e3c1eed37 --- /dev/null +++ b/mapeditor/BitmapHandler.cpp @@ -0,0 +1,169 @@ +// +// BitmapHandler.cpp +// vcmieditor +// +// Created by nordsoft on 29.08.2022. +// +#include "StdInc.h" +#include "BitmapHandler.h" + +#include "../lib/filesystem/Filesystem.h" + +#include +#include +#include + +namespace BitmapHandler +{ + QImage loadH3PCX(ui8 * data, size_t size); + + QImage loadBitmapFromDir(std::string path, std::string fname, bool setKey=true); + + bool isPCX(const ui8 *header)//check whether file can be PCX according to header + { + ui32 fSize = read_le_u32(header + 0); + ui32 width = read_le_u32(header + 4); + ui32 height = read_le_u32(header + 8); + return fSize == width*height || fSize == width*height*3; + } + + enum Epcxformat + { + PCX8B, + PCX24B + }; + + QImage loadH3PCX(ui8 * pcx, size_t size) + { + //SDL_Surface * ret; + + Epcxformat format; + int it=0; + + ui32 fSize = read_le_u32(pcx + it); it+=4; + ui32 width = read_le_u32(pcx + it); it+=4; + ui32 height = read_le_u32(pcx + it); it+=4; + + if (fSize==width*height*3) + format=PCX24B; + else if (fSize==width*height) + format=PCX8B; + else + return QImage(); + + QSize qsize(width, height); + + if (format==PCX8B) + { + it = 0xC; + //auto bitmap = QBitmap::fromData(qsize, pcx + it); + QImage image(pcx + it, width, height, QImage::Format_Indexed8); + + //palette - last 256*3 bytes + QVector colorTable; + it = (int)size-256*3; + for (int i=0;i<256;i++) + { + char bytes[3]; + bytes[0] = pcx[it++]; + bytes[1] = pcx[it++]; + bytes[2] = pcx[it++]; + colorTable.append(qRgb(bytes[0], bytes[1], bytes[2])); + } + image.setColorTable(colorTable); + return image; + } + else + { + QImage image(pcx + it, width, height, QImage::Format_RGB32); + return image; + } + } + + QImage loadBitmapFromDir(std::string path, std::string fname, bool setKey) + { + if(!fname.size()) + { + logGlobal->warn("Call to loadBitmap with void fname!"); + return QImage(); + } + if (!CResourceHandler::get()->existsResource(ResourceID(path + fname, EResType::IMAGE))) + { + return QImage(); + } + + auto fullpath = CResourceHandler::get()->getResourceName(ResourceID(path + fname, EResType::IMAGE)); + auto readFile = CResourceHandler::get()->load(ResourceID(path + fname, EResType::IMAGE))->readAll(); + + if (isPCX(readFile.first.get())) + {//H3-style PCX + auto image = BitmapHandler::loadH3PCX(readFile.first.get(), readFile.second); + if(!image.isNull()) + { + if(image.bitPlaneCount() == 1 && setKey) + { + QVector colorTable = image.colorTable(); + colorTable[0] = qRgba(255, 255, 255, 0); + image.setColorTable(colorTable); + } + } + else + { + logGlobal->error("Failed to open %s as H3 PCX!", fname); + } + return image; + } + else + { //loading via SDL_Image + QImage image(QString::fromStdString(fullpath->make_preferred().string())); + if(!image.isNull()) + { + if(image.bitPlaneCount() == 1) + { + //set correct value for alpha\unused channel + QVector colorTable = image.colorTable(); + for(auto & c : colorTable) + c = qRgb(qRed(c), qGreen(c), qBlue(c)); + image.setColorTable(colorTable); + } + } + else + { + logGlobal->error("Failed to open %s via QImage", fname); + return image; + } + } + return QImage(); + // When modifying anything here please check two use cases: + // 1) Vampire mansion in Necropolis (not 1st color is transparent) + // 2) Battle background when fighting on grass/dirt, topmost sky part (NO transparent color) + // 3) New objects that may use 24-bit images for icons (e.g. witchking arts) + /*if (ret->format->palette) + { + CSDL_Ext::setDefaultColorKeyPresize(ret); + } + else if (ret->format->Amask) + { + SDL_SetSurfaceBlendMode(ret, SDL_BLENDMODE_BLEND); + } + else // always set + { + CSDL_Ext::setDefaultColorKey(ret); + } + return ret;*/ + } + + QImage loadBitmap(std::string fname, bool setKey) + { + QImage image = loadBitmapFromDir("DATA/", fname, setKey); + if(image.isNull()) + { + image = loadBitmapFromDir("SPRITES/", fname, setKey); + if(image.isNull()) + { + logGlobal->error("Error: Failed to find file %s", fname); + } + } + return image; + } +} diff --git a/mapeditor/BitmapHandler.h b/mapeditor/BitmapHandler.h new file mode 100644 index 000000000..bd8c307ff --- /dev/null +++ b/mapeditor/BitmapHandler.h @@ -0,0 +1,20 @@ +// +// BitmapHandler.hpp +// vcmieditor +// +// Created by nordsoft on 29.08.2022. +// + +#pragma once + +#define read_le_u16(p) (* reinterpret_cast(p)) +#define read_le_u32(p) (* reinterpret_cast(p)) + +#include + +namespace BitmapHandler +{ + //Load file from /DATA or /SPRITES + QImage loadBitmap(std::string fname, bool setKey=true); +} + diff --git a/mapeditor/CGameInfo.cpp b/mapeditor/CGameInfo.cpp new file mode 100644 index 000000000..98ac227b4 --- /dev/null +++ b/mapeditor/CGameInfo.cpp @@ -0,0 +1,108 @@ +/* + * CGameInfo.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "CGameInfo.h" + +#include "../lib/VCMI_Lib.h" + +const CGameInfo * CGI; +CClientState * CCS = nullptr; +CServerHandler * CSH; + + +CGameInfo::CGameInfo() +{ + generaltexth = nullptr; + mh = nullptr; + townh = nullptr; + globalServices = nullptr; +} + +void CGameInfo::setFromLib() +{ + globalServices = VLC; + modh = VLC->modh; + generaltexth = VLC->generaltexth; + creh = VLC->creh; + townh = VLC->townh; + heroh = VLC->heroh; + objh = VLC->objh; + spellh = VLC->spellh; + skillh = VLC->skillh; + objtypeh = VLC->objtypeh; + obstacleHandler = VLC->obstacleHandler; + battleFieldHandler = VLC->battlefieldsHandler; +} + +const ArtifactService * CGameInfo::artifacts() const +{ + return globalServices->artifacts(); +} + +const BattleFieldService * CGameInfo::battlefields() const +{ + return globalServices->battlefields(); +} + +const CreatureService * CGameInfo::creatures() const +{ + return globalServices->creatures(); +} + +const FactionService * CGameInfo::factions() const +{ + return globalServices->factions(); +} + +const HeroClassService * CGameInfo::heroClasses() const +{ + return globalServices->heroClasses(); +} + +const HeroTypeService * CGameInfo::heroTypes() const +{ + return globalServices->heroTypes(); +} + +const scripting::Service * CGameInfo::scripts() const +{ + return globalServices->scripts(); +} + +const spells::Service * CGameInfo::spells() const +{ + return globalServices->spells(); +} + +const SkillService * CGameInfo::skills() const +{ + return globalServices->skills(); +} + +const ObstacleService * CGameInfo::obstacles() const +{ + return globalServices->obstacles(); +} + + +void CGameInfo::updateEntity(Metatype metatype, int32_t index, const JsonNode & data) +{ + logGlobal->error("CGameInfo::updateEntity call is not expected."); +} + +spells::effects::Registry * CGameInfo::spellEffects() +{ + return nullptr; +} + +const spells::effects::Registry * CGameInfo::spellEffects() const +{ + return globalServices->spellEffects(); +} diff --git a/mapeditor/CGameInfo.h b/mapeditor/CGameInfo.h new file mode 100644 index 000000000..04d96d3a6 --- /dev/null +++ b/mapeditor/CGameInfo.h @@ -0,0 +1,93 @@ +/* + * CGameInfo.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include + +#include "../lib/ConstTransitivePtr.h" + +class CModHandler; +class CMapHandler; +class CHeroHandler; +class CCreatureHandler; +class CSpellHandler; +class CSkillHandler; +class CBuildingHandler; +class CObjectHandler; +class CSoundHandler; +class CMusicHandler; +class CObjectClassesHandler; +class CTownHandler; +class CGeneralTextHandler; +class CConsoleHandler; +class CCursorHandler; +class CGameState; +class IMainVideoPlayer; +class CServerHandler; +class BattleFieldHandler; +class ObstacleHandler; + +class CMap; + + +//a class for non-mechanical client GUI classes +class CClientState +{ +public: + CSoundHandler * soundh; + CMusicHandler * musich; + CConsoleHandler * consoleh; + CCursorHandler * curh; + IMainVideoPlayer * videoh; +}; +extern CClientState * CCS; + +/// CGameInfo class +/// for allowing different functions for accessing game informations +class CGameInfo : public Services +{ +public: + const ArtifactService * artifacts() const override; + const CreatureService * creatures() const override; + const FactionService * factions() const override; + const HeroClassService * heroClasses() const override; + const HeroTypeService * heroTypes() const override; + const scripting::Service * scripts() const override; + const spells::Service * spells() const override; + const SkillService * skills() const override; + const ObstacleService * obstacles() const override; + const BattleFieldService * battlefields() const override; + + void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override; + + const spells::effects::Registry * spellEffects() const override; + spells::effects::Registry * spellEffects() override; + + + ConstTransitivePtr modh; //public? + ConstTransitivePtr battleFieldHandler; + ConstTransitivePtr heroh; + ConstTransitivePtr creh; + ConstTransitivePtr spellh; + ConstTransitivePtr skillh; + ConstTransitivePtr objh; + ConstTransitivePtr objtypeh; + ConstTransitivePtr obstacleHandler; + CGeneralTextHandler * generaltexth; + CMapHandler * mh; + CTownHandler * townh; + + void setFromLib(); + + CGameInfo(); +private: + const Services * globalServices; +}; +extern const CGameInfo* CGI; diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt new file mode 100644 index 000000000..bd9cda0e5 --- /dev/null +++ b/mapeditor/CMakeLists.txt @@ -0,0 +1,139 @@ +set(editor_SRCS + StdInc.cpp + main.cpp + launcherdirs.cpp + jsonutils.cpp + mainwindow.cpp + CGameInfo.cpp + BitmapHandler.cpp + maphandler.cpp + Animation.cpp + graphics.cpp + spoiler.cpp + windownewmap.cpp + generatorprogress.cpp + mapview.cpp + radiopushbutton.cpp + objectbrowser.cpp + mapsettings.cpp + playersettings.cpp + playerparams.cpp + scenelayer.cpp + mapcontroller.cpp + validator.cpp + inspector/inspector.cpp + inspector/townbulidingswidget.cpp + inspector/armywidget.cpp + inspector/messagewidget.cpp + inspector/rewardswidget.cpp +) + +set(editor_HEADERS + StdInc.h + launcherdirs.h + jsonutils.h + mainwindow.h + CGameInfo.h + BitmapHandler.h + maphandler.h + Animation.h + graphics.h + spoiler.h + windownewmap.h + generatorprogress.h + mapview.h + radiopushbutton.h + objectbrowser.h + mapsettings.h + playersettings.h + playerparams.h + scenelayer.h + mapcontroller.h + validator.h + inspector/inspector.h + inspector/townbulidingswidget.h + inspector/armywidget.h + inspector/messagewidget.h + inspector/rewardswidget.h +) + +set(editor_FORMS + mainwindow.ui + windownewmap.ui + generatorprogress.ui + mapsettings.ui + playersettings.ui + playerparams.ui + validator.ui + inspector/townbulidingswidget.ui + inspector/armywidget.ui + inspector/messagewidget.ui + inspector/rewardswidget.ui +) + +assign_source_group(${editor_SRCS} ${editor_HEADERS} mapeditor.rc) + +# Tell CMake to run moc when necessary: +set(CMAKE_AUTOMOC ON) + +if(POLICY CMP0071) + cmake_policy(SET CMP0071 NEW) +endif() + +# As moc files are generated in the binary dir, tell CMake +# to always look for includes there: +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +if(TARGET Qt6::Core) + qt_wrap_ui(editor_UI_HEADERS ${editor_FORMS}) +else() + qt5_wrap_ui(editor_UI_HEADERS ${editor_FORMS}) +endif() + +if(WIN32) + set(editor_ICON mapeditor.rc) +endif() + +add_executable(vcmieditor WIN32 ${editor_SRCS} ${editor_HEADERS} ${editor_UI_HEADERS} ${editor_ICON}) + +if(WIN32) + set_target_properties(vcmieditor + PROPERTIES + OUTPUT_NAME "VCMI_mapeditor" + PROJECT_LABEL "VCMI_mapeditor" + ) + + # FIXME: Can't to get CMP0020 working with Vcpkg and CMake 3.8.2 + # So far I tried: + # - cmake_minimum_required set to 2.8.11 globally and in this file + # - cmake_policy in all possible places + # - used NO_POLICY_SCOPE to make sure no other parts reset policies + # Still nothing worked, warning kept appearing and WinMain didn't link automatically + target_link_libraries(vcmieditor Qt${QT_VERSION_MAJOR}::WinMain) +endif() + +if(APPLE) + # This makes Xcode project prettier by moving vcmilauncher_autogen directory into vcmiclient subfolder + set_property(GLOBAL PROPERTY AUTOGEN_TARGETS_FOLDER vcmieditor) +endif() + +target_link_libraries(vcmieditor vcmi Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network) +target_include_directories(vcmieditor + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} +) +vcmi_set_output_dir(vcmieditor "") +enable_pch(vcmieditor) + +# Copy to build directory for easier debugging +add_custom_command(TARGET vcmieditor POST_BUILD + COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/icons + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/mapeditor/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/icons +) + +install(TARGETS vcmieditor DESTINATION ${BIN_DIR}) +# copy whole directory +install(DIRECTORY icons DESTINATION ${DATA_DIR}/mapeditor) +# Install icons and desktop file on Linux +if(NOT WIN32 AND NOT APPLE) + install(FILES "vcmilauncher.desktop" DESTINATION share/applications) +endif() diff --git a/mapeditor/StdInc.cpp b/mapeditor/StdInc.cpp new file mode 100644 index 000000000..b64b59be5 --- /dev/null +++ b/mapeditor/StdInc.cpp @@ -0,0 +1 @@ +#include "StdInc.h" diff --git a/mapeditor/StdInc.h b/mapeditor/StdInc.h new file mode 100644 index 000000000..8443e7820 --- /dev/null +++ b/mapeditor/StdInc.h @@ -0,0 +1,32 @@ +#pragma once + +#include "../Global.h" + +#define VCMI_EDITOR_VERSION "0.1" +#define VCMI_EDITOR_NAME "VCMI Map Editor" + +#include +#include +#include +#include +#include +#include +#include + +inline QString pathToQString(const boost::filesystem::path & path) +{ +#ifdef VCMI_WINDOWS + return QString::fromStdWString(path.wstring()); +#else + return QString::fromStdString(path.string()); +#endif +} + +inline boost::filesystem::path qstringToPath(const QString & path) +{ +#ifdef VCMI_WINDOWS + return boost::filesystem::path(path.toStdWString()); +#else + return boost::filesystem::path(path.toUtf8().data()); +#endif +} diff --git a/mapeditor/generatorprogress.cpp b/mapeditor/generatorprogress.cpp new file mode 100644 index 000000000..e482220a3 --- /dev/null +++ b/mapeditor/generatorprogress.cpp @@ -0,0 +1,38 @@ +#include "StdInc.h" +#include "generatorprogress.h" +#include "ui_generatorprogress.h" +#include +#include + +GeneratorProgress::GeneratorProgress(Load::Progress & source, QWidget *parent) : + QDialog(parent), + ui(new Ui::GeneratorProgress), + source(source) +{ + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose); + + setWindowFlags(Qt::Window); + + show(); +} + +GeneratorProgress::~GeneratorProgress() +{ + delete ui; +} + + +void GeneratorProgress::update() +{ + while(!source.finished()) + { + int status = float(source.get()) / 2.55f; + ui->progressBar->setValue(status); + qApp->processEvents(); + } + + //delete source; + close(); +} diff --git a/mapeditor/generatorprogress.h b/mapeditor/generatorprogress.h new file mode 100644 index 000000000..b693509a9 --- /dev/null +++ b/mapeditor/generatorprogress.h @@ -0,0 +1,26 @@ +#ifndef GENERATORPROGRESS_H +#define GENERATORPROGRESS_H + +#include +#include "../lib/LoadProgress.h" + +namespace Ui { +class GeneratorProgress; +} + +class GeneratorProgress : public QDialog +{ + Q_OBJECT + +public: + explicit GeneratorProgress(Load::Progress & source, QWidget *parent = nullptr); + ~GeneratorProgress(); + + void update(); + +private: + Ui::GeneratorProgress *ui; + Load::Progress & source; +}; + +#endif // GENERATORPROGRESS_H diff --git a/mapeditor/generatorprogress.ui b/mapeditor/generatorprogress.ui new file mode 100644 index 000000000..6acb5e5d1 --- /dev/null +++ b/mapeditor/generatorprogress.ui @@ -0,0 +1,46 @@ + + + GeneratorProgress + + + Qt::ApplicationModal + + + + 0 + 0 + 400 + 60 + + + + + 400 + 60 + + + + + 400 + 64 + + + + Generating map + + + true + + + + + + 0 + + + + + + + + diff --git a/mapeditor/graphics.cpp b/mapeditor/graphics.cpp new file mode 100644 index 000000000..71e462547 --- /dev/null +++ b/mapeditor/graphics.cpp @@ -0,0 +1,378 @@ +/* + * Graphics.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "graphics.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "../lib/filesystem/Filesystem.h" +#include "../lib/filesystem/CBinaryReader.h" +#include "Animation.h" +#include "../lib/CThreadHelper.h" +#include "../lib/CModHandler.h" +#include "../lib/VCMI_Lib.h" +#include "../CCallback.h" +#include "../lib/CGeneralTextHandler.h" +#include "BitmapHandler.h" +#include "../lib/CGameState.h" +#include "../lib/JsonNode.h" +#include "../lib/CStopWatch.h" +#include "../lib/mapObjects/CObjectClassesHandler.h" +#include "../lib/mapObjects/CObjectHandler.h" +#include "../lib/CHeroHandler.h" +#include "CGameInfo.h" + +Graphics * graphics = nullptr; + +void Graphics::loadPaletteAndColors() +{ + auto textFile = CResourceHandler::get()->load(ResourceID("DATA/PLAYERS.PAL"))->readAll(); + std::string pals((char*)textFile.first.get(), textFile.second); + + playerColorPalette.resize(256); + playerColors.resize(PlayerColor::PLAYER_LIMIT_I); + int startPoint = 24; //beginning byte; used to read + for(int i=0; i<256; ++i) + { + QColor col; + col.setRed(pals[startPoint++]); + col.setGreen(pals[startPoint++]); + col.setBlue(pals[startPoint++]); + col.setAlpha(255); + startPoint++; + playerColorPalette[i] = col.rgba(); + } + + neutralColorPalette.resize(32); + + auto stream = CResourceHandler::get()->load(ResourceID("config/NEUTRAL.PAL")); + CBinaryReader reader(stream.get()); + + for(int i=0; i<32; ++i) + { + QColor col; + col.setRed(reader.readUInt8()); + col.setGreen(reader.readUInt8()); + col.setBlue(reader.readUInt8()); + col.setAlpha(255); + reader.readUInt8(); // this is "flags" entry, not alpha + neutralColorPalette[i] = col.rgba(); + } + + //colors initialization + QColor colors[] = { + {0xff,0, 0, 255}, + {0x31,0x52,0xff,255}, + {0x9c,0x73,0x52,255}, + {0x42,0x94,0x29,255}, + + {0xff,0x84,0, 255}, + {0x8c,0x29,0xa5,255}, + {0x09,0x9c,0xa5,255}, + {0xc6,0x7b,0x8c,255}}; + + for(int i=0;i<8;i++) + { + playerColors[i] = colors[i].rgba(); + } + //gray + neutralColor = qRgba(0x84, 0x84, 0x84, 0xFF); +} + +Graphics::Graphics() +{ +#if 0 + + std::vector tasks; //preparing list of graphics to load + tasks += std::bind(&Graphics::loadFonts,this); + tasks += std::bind(&Graphics::loadPaletteAndColors,this); + tasks += std::bind(&Graphics::initializeBattleGraphics,this); + tasks += std::bind(&Graphics::loadErmuToPicture,this); + tasks += std::bind(&Graphics::initializeImageLists,this); + + CThreadHelper th(&tasks,std::max((ui32)1,boost::thread::hardware_concurrency())); + th.run(); +#else + loadPaletteAndColors(); + initializeImageLists(); +#endif + + //(!) do not load any CAnimation here +} + +Graphics::~Graphics() +{ +} + +void Graphics::load() +{ + loadHeroAnimations(); + loadHeroFlagAnimations(); +} + +void Graphics::loadHeroAnimations() +{ + for(auto & elem : CGI->heroh->classes.objects) + { + for (auto templ : VLC->objtypeh->getHandlerFor(Obj::HERO, elem->getIndex())->getTemplates()) + { + if (!heroAnimations.count(templ->animationFile)) + heroAnimations[templ->animationFile] = loadHeroAnimation(templ->animationFile); + } + } + + boatAnimations[0] = loadHeroAnimation("AB01_.DEF"); + boatAnimations[1] = loadHeroAnimation("AB02_.DEF"); + boatAnimations[2] = loadHeroAnimation("AB03_.DEF"); + + + mapObjectAnimations["AB01_.DEF"] = boatAnimations[0]; + mapObjectAnimations["AB02_.DEF"] = boatAnimations[1]; + mapObjectAnimations["AB03_.DEF"] = boatAnimations[2]; +} +void Graphics::loadHeroFlagAnimations() +{ + static const std::vector HERO_FLAG_ANIMATIONS = + { + "AF00", "AF01","AF02","AF03", + "AF04", "AF05","AF06","AF07" + }; + + static const std::vector< std::vector > BOAT_FLAG_ANIMATIONS = + { + { + "ABF01L", "ABF01G", "ABF01R", "ABF01D", + "ABF01B", "ABF01P", "ABF01W", "ABF01K" + }, + { + "ABF02L", "ABF02G", "ABF02R", "ABF02D", + "ABF02B", "ABF02P", "ABF02W", "ABF02K" + }, + { + "ABF03L", "ABF03G", "ABF03R", "ABF03D", + "ABF03B", "ABF03P", "ABF03W", "ABF03K" + } + }; + + for(const auto & name : HERO_FLAG_ANIMATIONS) + heroFlagAnimations.push_back(loadHeroFlagAnimation(name)); + + for(int i = 0; i < BOAT_FLAG_ANIMATIONS.size(); i++) + for(const auto & name : BOAT_FLAG_ANIMATIONS[i]) + boatFlagAnimations[i].push_back(loadHeroFlagAnimation(name)); +} + +std::shared_ptr Graphics::loadHeroFlagAnimation(const std::string & name) +{ + //first - group number to be rotated, second - group number after rotation + static const std::vector > rotations = + { + {6,10}, {7,11}, {8,12}, {1,13}, + {2,14}, {3,15} + }; + + std::shared_ptr anim = std::make_shared(name); + anim->preload(); + + for(const auto & rotation : rotations) + { + const int sourceGroup = rotation.first; + const int targetGroup = rotation.second; + + anim->createFlippedGroup(sourceGroup, targetGroup); + } + + return anim; +} + +std::shared_ptr Graphics::loadHeroAnimation(const std::string &name) +{ + //first - group number to be rotated, second - group number after rotation + static const std::vector > rotations = + { + {6,10}, {7,11}, {8,12}, {1,13}, + {2,14}, {3,15} + }; + + std::shared_ptr anim = std::make_shared(name); + anim->preload(); + + + for(const auto & rotation : rotations) + { + const int sourceGroup = rotation.first; + const int targetGroup = rotation.second; + + anim->createFlippedGroup(sourceGroup, targetGroup); + } + + return anim; +} + +void Graphics::blueToPlayersAdv(QImage * sur, PlayerColor player) +{ + if(sur->format() == QImage::Format_Indexed8) + { + auto palette = sur->colorTable(); + if(player < PlayerColor::PLAYER_LIMIT) + { + for(int i = 0; i < 32; ++i) + palette[224 + i] = playerColorPalette[player.getNum() * 32 + i]; + } + else if(player == PlayerColor::NEUTRAL) + { + palette = neutralColorPalette; + } + else + { + logGlobal->error("Wrong player id in blueToPlayersAdv (%s)!", player.getStr()); + return; + } + //FIXME: not all player colored images have player palette at last 32 indexes + //NOTE: following code is much more correct but still not perfect (bugged with status bar) + sur->setColorTable(palette); + +#if 0 + + SDL_Color * bluePalette = playerColorPalette + 32; + + SDL_Palette * oldPalette = sur->format->palette; + + SDL_Palette * newPalette = SDL_AllocPalette(256); + + for(size_t destIndex = 0; destIndex < 256; destIndex++) + { + SDL_Color old = oldPalette->colors[destIndex]; + + bool found = false; + + for(size_t srcIndex = 0; srcIndex < 32; srcIndex++) + { + if(old.b == bluePalette[srcIndex].b && old.g == bluePalette[srcIndex].g && old.r == bluePalette[srcIndex].r) + { + found = true; + newPalette->colors[destIndex] = palette[srcIndex]; + break; + } + } + if(!found) + newPalette->colors[destIndex] = old; + } + + SDL_SetSurfacePalette(sur, newPalette); + + SDL_FreePalette(newPalette); + +#endif // 0 + } + else + { + //TODO: implement. H3 method works only for images with palettes. + // Add some kind of player-colored overlay? + // Or keep palette approach here and replace only colors of specific value(s) + // Or just wait for OpenGL support? + logGlobal->warn("Image must have palette to be player-colored!"); + } +} + +std::shared_ptr Graphics::getAnimation(const CGObjectInstance* obj) +{ + if(obj->ID == Obj::HERO) + return getHeroAnimation(obj->appearance); + return getAnimation(obj->appearance); +} + +std::shared_ptr Graphics::getHeroAnimation(const std::shared_ptr info) +{ + if(info->animationFile.empty()) + { + logGlobal->warn("Def name for hero (%d,%d) is empty!", info->id, info->subid); + return std::shared_ptr(); + } + + std::shared_ptr ret = loadHeroAnimation(info->animationFile); + + //already loaded + if(ret) + { + ret->preload(); + return ret; + } + + ret = std::make_shared(info->animationFile); + heroAnimations[info->animationFile] = ret; + + ret->preload(); + return ret; +} + +std::shared_ptr Graphics::getAnimation(const std::shared_ptr info) +{ + if(info->animationFile.empty()) + { + logGlobal->warn("Def name for obj (%d,%d) is empty!", info->id, info->subid); + return std::shared_ptr(); + } + + std::shared_ptr ret = mapObjectAnimations[info->animationFile]; + + //already loaded + if(ret) + { + ret->preload(); + return ret; + } + + ret = std::make_shared(info->animationFile); + mapObjectAnimations[info->animationFile] = ret; + + ret->preload(); + return ret; +} + +void Graphics::addImageListEntry(size_t index, const std::string & listName, const std::string & imageName) +{ + if (!imageName.empty()) + { + JsonNode entry; + entry["frame"].Integer() = index; + entry["file"].String() = imageName; + + imageLists["SPRITES/" + listName]["images"].Vector().push_back(entry); + } +} + +void Graphics::addImageListEntries(const EntityService * service) +{ + auto cb = std::bind(&Graphics::addImageListEntry, this, _1, _2, _3); + + auto loopCb = [&](const Entity * entity, bool & stop) + { + entity->registerIcons(cb); + }; + + service->forEachBase(loopCb); +} + +void Graphics::initializeImageLists() +{ + addImageListEntries(CGI->creatures()); + addImageListEntries(CGI->heroTypes()); + addImageListEntries(CGI->artifacts()); + addImageListEntries(CGI->factions()); + addImageListEntries(CGI->spells()); + addImageListEntries(CGI->skills()); +} diff --git a/mapeditor/graphics.h b/mapeditor/graphics.h new file mode 100644 index 000000000..71b150448 --- /dev/null +++ b/mapeditor/graphics.h @@ -0,0 +1,84 @@ +/* + * Graphics.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../lib/GameConstants.h" +#include + +class CGHeroInstance; +class CGTownInstance; +class CHeroClass; +struct InfoAboutHero; +struct InfoAboutTown; +class CGObjectInstance; +class ObjectTemplate; +class Animation; +class EntityService; +class JsonNode; + +/// Handles fonts, hero images, town images, various graphics +class Graphics +{ + void addImageListEntry(size_t index, const std::string & listName, const std::string & imageName); + + void addImageListEntries(const EntityService * service); + + void initializeBattleGraphics(); + void loadPaletteAndColors(); + + void loadHeroAnimations(); + //loads animation and adds required rotated frames + std::shared_ptr loadHeroAnimation(const std::string &name); + + void loadHeroFlagAnimations(); + + //loads animation and adds required rotated frames + std::shared_ptr loadHeroFlagAnimation(const std::string &name); + + void loadErmuToPicture(); + void loadFogOfWar(); + void loadFonts(); + void initializeImageLists(); + +public: + //various graphics + QVector playerColors; //array [8] + QRgb neutralColor; + QVector playerColorPalette; //palette to make interface colors good - array of size [256] + QVector neutralColorPalette; + + // [hero class def name] //added group 10: up - left, 11 - left and 12 - left down // 13 - up-left standing; 14 - left standing; 15 - left down standing + std::map< std::string, std::shared_ptr > heroAnimations; + std::vector< std::shared_ptr > heroFlagAnimations; + + // [boat type: 0 .. 2] //added group 10: up - left, 11 - left and 12 - left down // 13 - up-left standing; 14 - left standing; 15 - left down standing + std::array< std::shared_ptr, 3> boatAnimations; + + std::array< std::vector >, 3> boatFlagAnimations; + + //all other objects (not hero or boat) + std::map< std::string, std::shared_ptr > mapObjectAnimations; + + std::map imageLists; + + //functions + Graphics(); + ~Graphics(); + + void load(); + + void blueToPlayersAdv(QImage * sur, PlayerColor player); //replaces blue interface colour with a color of player + + std::shared_ptr getAnimation(const CGObjectInstance * obj); + std::shared_ptr getAnimation(const std::shared_ptr info); + std::shared_ptr getHeroAnimation(const std::shared_ptr info); +}; + +extern Graphics * graphics; diff --git a/mapeditor/icons/menu-game.png b/mapeditor/icons/menu-game.png new file mode 100644 index 0000000000000000000000000000000000000000..5f632e2b71bab48cd3c01d300a8146267c7c193a GIT binary patch literal 5482 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 literal 0 HcmV?d00001 diff --git a/mapeditor/icons/menu-settings.png b/mapeditor/icons/menu-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..4cce5d91ea81b0b051dfd7736f7beeda24380e0d GIT binary patch literal 4474 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 M07*qoM6N<$g8#!*Hvj+t literal 0 HcmV?d00001 diff --git a/mapeditor/icons/mod-delete.png b/mapeditor/icons/mod-delete.png new file mode 100644 index 0000000000000000000000000000000000000000..fa0c04d95bd286e97de68da3b40bc1cfd0dbcfb9 GIT binary patch literal 1449 zcmV;a1y=frP)zs}v|HDFrc-hzT#gh&~y7Ly3t7qk>Rx(HIkm z62M4;FC<_hFD4igee!`sF(?wTkwWh@P+IA3cei_X&wXb6XZCbygJOK}B>&DiGv9pQ zf0_BS0RQtt9<(^HbzRz^{&U1IUck=|!g#%$7aA>qfMMbLUm3(W*LLxb{C^U-($kYj z8ume<(|xJ6oiw?jGahMf1}za+>&&vipZXKZ{R4LS#vlX6kEBHLlh#X<_P+{Tf4sAe zaq@kly`!aJ`?feW5K_*8aLNGOuYhO-NGu7EOapg0+tX!FBUO}B31Hfz#op)huItSLA1jj5p_bmB5c(T@3-(57e*ejM+nj5I7#Do58fu;L*O1V9KZg zO9^<&87|hGlO8R zHb|Q}aSZ&~EX2}{u=UtU$Tl@A+j{gQMAK=tHgn>56)(-p%s{-cF~K<9CAoF^<1?~e zk47Pw%3|Rd5Iije3+mpw4U_L4gw{hxAkxqPTMi$s)T^cjy)D3<_xcn@$WtW?FUNcl z-cI6N8tY*Ys=gtNK%RW&xQg>$1RQdXY2*DN zRZpZ?+4Ui_WGOsx@(YM!isS*17Ok&`Z8(>0TBb03VHC*Wlq?7YnXL*4K?cl%iQ-xX z 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 literal 0 HcmV?d00001 diff --git a/mapeditor/icons/mod-enabled.png b/mapeditor/icons/mod-enabled.png new file mode 100644 index 0000000000000000000000000000000000000000..40204a56f4f2a67fae6c199ac8ac30be8dea745f GIT binary patch literal 1104 zcmV-W1h4yvP)0tFiqx|hd}<3erfn=qt&NE_Y22jAp4`vw+1+!_%=off)+BAv_0d24 zABLIve$4m(7~ua}r%1U1FMM=|PJOY{0I3sPygpr3``M8vkMQC?Ef*lw=u2Cw>l&VW zxTCVZx~98kX>z=)r)}4N0@!zG=LSZ|Tf3WEjewCsNBeHQqkT^eW%#CeMkxoN5I51% z(4_cv0o(Z#0AOV;p;Gd5*)MqhaNA~8)t=eW*kt6~2~gsLkYW5Va5nEaqO)v(kJ1m? zo0@zeOW?wW#^N|LJmh$u^JUM0Gb3dIx_Vl6=!&+hv96A#b4duHW7(GI`}LA1^ZD0G z&;E0O1W{Gi`@r^X{=A!l<7UA~6@I)hXiLrycE5Pu{M($?(nusgQ?b@pDJ6Pz#&|c8 zIN@DOPH*cY!BFt_+GrKYWG_QuLCh`NxRji5xHR5hskP<{X5;t19j&NH)W$ZxF?UnG z@7WLA;sCJstG!I2^y8h4n}c?K2CnCUkqAx={gUAlZywrzBE2eUtNm^3&=lpBJ@;%g zG?ijF8Qgwucx&Me}#_(mnD-x)f2VdR(x?lsrn8cb)W;Rz0%`O!CgG0i>sa_RSQLyIUSL%$^_ zb3&>Rl48kDA{21RL(L6F+t!+(u5;MVA_(zgEM-c|&W`pR=>PV5nOFt5x}o;LDRbe| z;{(GP$MJ)}hji8im$TSZ87Ap$3Lcl>o{Dq7CvDF8)Box>-QU}?Poc`emMzu(3O|r@ z7eNSxlpMMm!FVd?j84uS>+bA-;zj_r29N?!&wf8Oc-Q@#PS04@V+x~6BoLyGmxYuR zTI#u+$ncFl~X9VsORK!gxPN{LV?L~PqeDd3tlkw}C{ zDT$Pll+I^%BAuNY%za&bckt#}+pS-oci%br`IVmtA%YOXDW!r^DilTGnx+ZMvIGDQ zK#&#wvqIBYEXFwJR0u(f&;l5Q5Ls+WsbGu=(=<7NqA)McgRGOV(q0*9Q4lHZRrMEM WTe#;$n{L(s00005I1-UB5ex=H zbavvp!=0tNw7!5kT-MzQH|#`Tp^I8t&0DORVK8b1 zu7^DBVd?WY;e$_(BteJgh7_ez?q^WoIrlR9`3&C^$%r9C6-1Fsv#58{R5Sm_&14G9 z7bKw2=kCFAY;Y(MOC@7<8ESe=wePlRK!zT}r=BH21uTGo+;6D~2VRRM2f8EKapZdf z3T$`Si-SDnry4u$Pk=3D+0=?0Dm0z;<9rWkL*~b$fCv@Xy4h<%!ob-8Li{Px8J?=b zB#`kws9l((s=;3POW{NS`sFGW5aHM>CyALCRId-NQ7dz)huK6$Q*jg9QlJeR?B=<&zfvxfspCv`YKVaU(d7}&$OA$w3$jfc1k$R8lI=*?~`nKOgKIkVO{6RhxARh{z8xETr37;Q}oF6;4 zFrKt9Ay|@JUy|f4&z7&s>lF)sDHeMai+#$Km&%p#5AWY@Y$!K2K5TAo;*a?D!6$Vl z%H72SgH==4z~QvCw6qZfPau&|ltQ6U=?s>hp1!_;vB@^G?K^i{^LRl){NM{0V=rHc zPt47$t*x(bXl!lk9~>GP9eXuC^?G_nDwEI6%`ZW``xwhBA7d3_ZB3z2u0wn`;-nIuo0j;$-S-j9Nx{S_(*u zYEF6kh{Pjy?5uvn4DWPF0G&rwUJ6sz*I6X%|JXoYBRAXL?m4p zyJ5Z5E7lx_YGX#?j%)G>1Kct~xsiLr8*#BHrv{setupUi(this); + + uiCounts[0] = ui->count0; uiSlots[0] = ui->slot0; + uiCounts[1] = ui->count1; uiSlots[1] = ui->slot1; + uiCounts[2] = ui->count2; uiSlots[2] = ui->slot2; + uiCounts[3] = ui->count3; uiSlots[3] = ui->slot3; + uiCounts[4] = ui->count4; uiSlots[4] = ui->slot4; + uiCounts[5] = ui->count5; uiSlots[5] = ui->slot5; + uiCounts[6] = ui->count6; uiSlots[6] = ui->slot6; + + for(int i = 0; i < TOTAL_SLOTS; ++i) + { + uiCounts[i]->setInputMask("d0000"); + uiCounts[i]->setText("1"); + uiSlots[i]->addItem(""); + uiSlots[i]->setItemData(0, -1); + + for(int c = 0; c < VLC->creh->objects.size(); ++c) + { + auto creature = VLC->creh->objects[c]; + uiSlots[i]->insertItem(c + 1, tr(creature->getPluralName().c_str())); + uiSlots[i]->setItemData(c + 1, creature->getId().getNum()); + } + } + + ui->formationTight->setChecked(true); +} + +int ArmyWidget::searchItemIndex(int slotId, CreatureID creId) const +{ + for(int i = 0; i < uiSlots[slotId]->count(); ++i) + { + if(creId.getNum() == uiSlots[slotId]->itemData(i).toInt()) + return i; + } + return 0; +} + +void ArmyWidget::obtainData() +{ + for(int i = 0; i < TOTAL_SLOTS; ++i) + { + if(army.hasStackAtSlot(SlotID(i))) + { + auto * creature = army.getCreature(SlotID(i)); + uiSlots[i]->setCurrentIndex(searchItemIndex(i, creature->getId())); + uiCounts[i]->setText(QString::number(army.getStackCount(SlotID(i)))); + } + } + + if(army.formation) + ui->formationTight->setChecked(true); + else + ui->formationWide->setChecked(true); +} + +bool ArmyWidget::commitChanges() +{ + bool isArmed = false; + for(int i = 0; i < TOTAL_SLOTS; ++i) + { + CreatureID creId(uiSlots[i]->itemData(uiSlots[i]->currentIndex()).toInt()); + if(creId == -1) + { + if(army.hasStackAtSlot(SlotID(i))) + army.eraseStack(SlotID(i)); + } + else + { + isArmed = true; + int amount = uiCounts[i]->text().toInt(); + if(amount) + { + army.setCreature(SlotID(i), creId, amount); + } + else + { + if(army.hasStackAtSlot(SlotID(i))) + army.eraseStack(SlotID(i)); + army.putStack(SlotID(i), new CStackInstance(creId, amount, false)); + } + } + } + + army.setFormation(ui->formationTight->isChecked()); + return isArmed; +} + +ArmyWidget::~ArmyWidget() +{ + delete ui; +} + + + +ArmyDelegate::ArmyDelegate(CArmedInstance & t): army(t), QStyledItemDelegate() +{ +} + +QWidget * ArmyDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + return new ArmyWidget(army, parent); +} + +void ArmyDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + if(auto * ed = qobject_cast(editor)) + { + ed->obtainData(); + } + else + { + QStyledItemDelegate::setEditorData(editor, index); + } +} + +void ArmyDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + if(auto * ed = qobject_cast(editor)) + { + auto isArmed = ed->commitChanges(); + model->setData(index, "dummy"); + if(isArmed) + model->setData(index, "HAS ARMY"); + else + model->setData(index, ""); + } + else + { + QStyledItemDelegate::setModelData(editor, model, index); + } +} diff --git a/mapeditor/inspector/armywidget.h b/mapeditor/inspector/armywidget.h new file mode 100644 index 000000000..367182adc --- /dev/null +++ b/mapeditor/inspector/armywidget.h @@ -0,0 +1,49 @@ +#ifndef ARMYWIDGET_H +#define ARMYWIDGET_H + +#include +#include "../lib/mapObjects/CArmedInstance.h" + +const int TOTAL_SLOTS = 7; + +namespace Ui { +class ArmyWidget; +} + +class ArmyWidget : public QDialog +{ + Q_OBJECT + +public: + explicit ArmyWidget(CArmedInstance &, QWidget *parent = nullptr); + ~ArmyWidget(); + + void obtainData(); + bool commitChanges(); + +private: + int searchItemIndex(int slotId, CreatureID creId) const; + + Ui::ArmyWidget *ui; + CArmedInstance & army; + std::array uiCounts; + std::array uiSlots; +}; + +class ArmyDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + using QStyledItemDelegate::QStyledItemDelegate; + + ArmyDelegate(CArmedInstance &); + + QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + +private: + CArmedInstance & army; +}; + +#endif // ARMYWIDGET_H diff --git a/mapeditor/inspector/armywidget.ui b/mapeditor/inspector/armywidget.ui new file mode 100644 index 000000000..a808f0c3a --- /dev/null +++ b/mapeditor/inspector/armywidget.ui @@ -0,0 +1,298 @@ + + + ArmyWidget + + + + 0 + 0 + 318 + 314 + + + + + 318 + 314 + + + + Army settings + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 30 + 0 + + + + + 50 + 16777215 + + + + Qt::ImhDigitsOnly + + + + + + + + 0 + 0 + + + + + 30 + 0 + + + + + 50 + 16777215 + + + + Qt::ImhDigitsOnly + + + + + + + + 0 + 0 + + + + + 30 + 0 + + + + + 50 + 16777215 + + + + Qt::ImhDigitsOnly + + + + + + + + 0 + 0 + + + + + 30 + 0 + + + + + 50 + 16777215 + + + + Qt::ImhDigitsOnly + + + + + + + + 0 + 0 + + + + Wide formation + + + + + + + + 0 + 0 + + + + + 30 + 0 + + + + + 50 + 16777215 + + + + Qt::ImhDigitsOnly + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 30 + 0 + + + + + 50 + 16777215 + + + + Qt::ImhDigitsOnly + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 30 + 0 + + + + + 50 + 16777215 + + + + Qt::ImhDigitsOnly + + + + + + + + 0 + 0 + + + + Tight formation + + + + + + + + diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp new file mode 100644 index 000000000..236abeb8b --- /dev/null +++ b/mapeditor/inspector/inspector.cpp @@ -0,0 +1,735 @@ +#include "StdInc.h" +#include "inspector.h" +#include "../lib/CArtHandler.h" +#include "../lib/spells/CSpellHandler.h" +#include "../lib/CHeroHandler.h" +#include "../lib/CRandomGenerator.h" +#include "../lib/mapObjects/CObjectClassesHandler.h" +#include "../lib/mapping/CMap.h" + +#include "townbulidingswidget.h" +#include "armywidget.h" +#include "messagewidget.h" +#include "rewardswidget.h" + +//===============IMPLEMENT OBJECT INITIALIZATION FUNCTIONS================ +Initializer::Initializer(CGObjectInstance * o, const PlayerColor & pl) : defaultPlayer(pl) +{ + logGlobal->info("New object instance initialized"); +///IMPORTANT! initialize order should be from base objects to derived objects + INIT_OBJ_TYPE(CGResource); + INIT_OBJ_TYPE(CGArtifact); + INIT_OBJ_TYPE(CArmedInstance); + INIT_OBJ_TYPE(CGShipyard); + INIT_OBJ_TYPE(CGGarrison); + INIT_OBJ_TYPE(CGMine); + INIT_OBJ_TYPE(CGDwelling); + INIT_OBJ_TYPE(CGTownInstance); + INIT_OBJ_TYPE(CGCreature); + INIT_OBJ_TYPE(CGHeroInstance); + INIT_OBJ_TYPE(CGSignBottle); + INIT_OBJ_TYPE(CGLighthouse); + //INIT_OBJ_TYPE(CGPandoraBox); +} + +bool stringToBool(const QString & s) +{ + if(s == "TRUE") + return true; + //if(s == "FALSE") + return false; +} + +void Initializer::initialize(CArmedInstance * o) +{ + if(!o) return; +} + +void Initializer::initialize(CGSignBottle * o) +{ + if(!o) return; +} + +void Initializer::initialize(CGCreature * o) +{ + if(!o) return; + + o->character = CGCreature::Character::HOSTILE; + o->putStack(SlotID(0), new CStackInstance(CreatureID(o->subID), 0, false)); +} + +void Initializer::initialize(CGDwelling * o) +{ + if(!o) return; + + o->tempOwner = defaultPlayer; + + switch(o->ID) + { + case Obj::RANDOM_DWELLING: + case Obj::RANDOM_DWELLING_LVL: + case Obj::RANDOM_DWELLING_FACTION: + o->initRandomObjectInfo(); + } +} + +void Initializer::initialize(CGGarrison * o) +{ + if(!o) return; + + o->tempOwner = defaultPlayer; + o->removableUnits = true; +} + +void Initializer::initialize(CGShipyard * o) +{ + if(!o) return; + + o->tempOwner = defaultPlayer; +} + +void Initializer::initialize(CGLighthouse * o) +{ + if(!o) return; + + o->tempOwner = defaultPlayer; +} + +void Initializer::initialize(CGHeroInstance * o) +{ + if(!o) return; + + o->tempOwner = defaultPlayer; + if(o->ID == Obj::HERO) + { + for(auto t : VLC->heroh->objects) + { + if(t->heroClass == VLC->heroh->classes.objects[o->subID].get()) + { + o->type = VLC->heroh->objects[o->subID]; + break; + } + } + } + + if(!o->type) + o->type = VLC->heroh->objects.at(o->subID); + + o->name = o->type->getName(); + o->sex = o->type->sex; + o->biography = o->type->biography; + o->portrait = o->type->imageIndex; + o->randomizeArmy(o->type->heroClass->faction); +} + +void Initializer::initialize(CGTownInstance * o) +{ + if(!o) return; + + const std::vector castleLevels{"village", "fort", "citadel", "castle", "capitol"}; + int lvl = vstd::find_pos(castleLevels, o->appearance->stringID); + o->builtBuildings.insert(BuildingID::DEFAULT); + if(lvl > -1) o->builtBuildings.insert(BuildingID::TAVERN); + if(lvl > 0) o->builtBuildings.insert(BuildingID::FORT); + if(lvl > 1) o->builtBuildings.insert(BuildingID::CITADEL); + if(lvl > 2) o->builtBuildings.insert(BuildingID::CASTLE); + if(lvl > 3) o->builtBuildings.insert(BuildingID::CAPITOL); + + for(auto spell : VLC->spellh->objects) //add all regular spells to town + { + if(!spell->isSpecial() && !spell->isCreatureAbility()) + o->possibleSpells.push_back(spell->id); + } +} + +void Initializer::initialize(CGArtifact * o) +{ + if(!o) return; + + if(o->ID == Obj::SPELL_SCROLL) + { + std::vector out; + for(auto spell : VLC->spellh->objects) //spellh size appears to be greater (?) + { + //if(map->isAllowedSpell(spell->id)) + { + out.push_back(spell->id); + } + } + auto a = CArtifactInstance::createScroll(*RandomGeneratorUtil::nextItem(out, CRandomGenerator::getDefault())); + o->storedArtifact = a; + } +} + +void Initializer::initialize(CGMine * o) +{ + if(!o) return; + + o->tempOwner = defaultPlayer; + o->producedResource = Res::ERes(o->subID); + o->producedQuantity = o->defaultResProduction(); +} + +void Initializer::initialize(CGResource * o) +{ + if(!o) return; + + o->amount = CGResource::RANDOM_AMOUNT; +} + +//===============IMPLEMENT PROPERTIES SETUP=============================== +void Inspector::updateProperties(CArmedInstance * o) +{ + if(!o) return; + + auto * delegate = new ArmyDelegate(*o); + addProperty("Army", PropertyEditorPlaceholder(), delegate, false); +} + +void Inspector::updateProperties(CGDwelling * o) +{ + if(!o) return; + + addProperty("Owner", o->tempOwner, false); +} + +void Inspector::updateProperties(CGLighthouse * o) +{ + if(!o) return; + + addProperty("Owner", o->tempOwner, false); +} + +void Inspector::updateProperties(CGGarrison * o) +{ + if(!o) return; + + addProperty("Owner", o->tempOwner, false); + addProperty("Removable units", o->removableUnits, InspectorDelegate::boolDelegate(), false); +} + +void Inspector::updateProperties(CGShipyard * o) +{ + if(!o) return; + + addProperty("Owner", o->tempOwner, false); +} + +void Inspector::updateProperties(CGHeroInstance * o) +{ + if(!o) return; + + addProperty("Owner", o->tempOwner, o->ID == Obj::PRISON); //field is not editable for prison + addProperty("Experience", o->exp, false); + addProperty("Hero class", o->type->heroClass->getName(), true); + + { + auto * delegate = new InspectorDelegate; + delegate->options << "MALE" << "FEMALE"; + addProperty("Sex", (o->sex ? "FEMALE" : "MALE"), delegate , false); + } + addProperty("Name", o->name, false); + addProperty("Biography", o->biography, new MessageDelegate, false); + addProperty("Portrait", o->portrait, false); + + { + auto * delegate = new InspectorDelegate; + for(int i = 0; i < VLC->heroh->objects.size(); ++i) + { + if(map->allowedHeroes.at(i)) + { + if(o->ID == Obj::PRISON || (o->type && VLC->heroh->objects[i]->heroClass->getIndex() == o->type->heroClass->getIndex())) + delegate->options << QObject::tr(VLC->heroh->objects[i]->getName().c_str()); + } + } + addProperty("Hero type", o->type->getName(), delegate, false); + } +} + +void Inspector::updateProperties(CGTownInstance * o) +{ + if(!o) return; + + addProperty("Town name", o->name, false); + + auto * delegate = new TownBuildingsDelegate(*o); + addProperty("Buildings", PropertyEditorPlaceholder(), delegate, false); +} + +void Inspector::updateProperties(CGArtifact * o) +{ + if(!o) return; + + addProperty("Message", o->message, false); + + CArtifactInstance * instance = o->storedArtifact; + if(instance) + { + SpellID spellId = instance->getGivenSpellID(); + if(spellId != -1) + { + auto * delegate = new InspectorDelegate; + for(auto spell : VLC->spellh->objects) + { + //if(map->isAllowedSpell(spell->id)) + delegate->options << QObject::tr(spell->name.c_str()); + } + addProperty("Spell", VLC->spellh->objects[spellId]->name, delegate, false); + } + } +} + +void Inspector::updateProperties(CGMine * o) +{ + if(!o) return; + + addProperty("Owner", o->tempOwner, false); + addProperty("Resource", o->producedResource); + addProperty("Productivity", o->producedQuantity, false); +} + +void Inspector::updateProperties(CGResource * o) +{ + if(!o) return; + + addProperty("Amount", o->amount, false); + addProperty("Message", o->message, false); +} + +void Inspector::updateProperties(CGSignBottle * o) +{ + if(!o) return; + + addProperty("Message", o->message, new MessageDelegate, false); +} + +void Inspector::updateProperties(CGCreature * o) +{ + if(!o) return; + + addProperty("Message", o->message, false); + { + auto * delegate = new InspectorDelegate; + delegate->options << "COMPLIANT" << "FRIENDLY" << "AGRESSIVE" << "HOSTILE" << "SAVAGE"; + addProperty("Character", (CGCreature::Character)o->character, delegate, false); + } + addProperty("Never flees", o->neverFlees, InspectorDelegate::boolDelegate(), false); + addProperty("Not growing", o->notGrowingTeam, InspectorDelegate::boolDelegate(), false); + addProperty("Artifact reward", o->gainedArtifact); //TODO: implement in setProperty + addProperty("Army", PropertyEditorPlaceholder(), true); + addProperty("Amount", o->stacks[SlotID(0)]->count, false); + //addProperty("Resources reward", o->resources); //TODO: implement in setProperty +} + +void Inspector::updateProperties(CGPandoraBox * o) +{ + if(!o) return; + + auto * delegate = new RewardsPandoraDelegate(*map, *o); + addProperty("Reward", PropertyEditorPlaceholder(), delegate, false); +} + +void Inspector::updateProperties(CGEvent * o) +{ + if(!o) return; + + addProperty("Remove after", o->removeAfterVisit, InspectorDelegate::boolDelegate(), false); + addProperty("Human trigger", o->humanActivate, InspectorDelegate::boolDelegate(), false); + addProperty("Cpu trigger", o->computerActivate, InspectorDelegate::boolDelegate(), false); + //ui8 availableFor; //players whom this event is available for +} + + +void Inspector::updateProperties() +{ + if(!obj) + return; + table->setRowCount(0); //cleanup table + + addProperty("Indentifier", obj); + addProperty("ID", obj->ID.getNum()); + addProperty("SubID", obj->subID); + addProperty("InstanceName", obj->instanceName); + addProperty("TypeName", obj->typeName); + addProperty("SubTypeName", obj->subTypeName); + + if(!dynamic_cast(obj)) + { + auto factory = VLC->objtypeh->getHandlerFor(obj->ID, obj->subID); + addProperty("IsStatic", factory->isStaticObject()); + } + + auto * delegate = new InspectorDelegate(); + delegate->options << "NEUTRAL"; + for(int p = 0; p < map->players.size(); ++p) + if(map->players[p].canAnyonePlay()) + delegate->options << QString("PLAYER %1").arg(p); + addProperty("Owner", obj->tempOwner, delegate, true); + + UPDATE_OBJ_PROPERTIES(CArmedInstance); + UPDATE_OBJ_PROPERTIES(CGResource); + UPDATE_OBJ_PROPERTIES(CGArtifact); + UPDATE_OBJ_PROPERTIES(CGMine); + UPDATE_OBJ_PROPERTIES(CGGarrison); + UPDATE_OBJ_PROPERTIES(CGShipyard); + UPDATE_OBJ_PROPERTIES(CGDwelling); + UPDATE_OBJ_PROPERTIES(CGTownInstance); + UPDATE_OBJ_PROPERTIES(CGCreature); + UPDATE_OBJ_PROPERTIES(CGHeroInstance); + UPDATE_OBJ_PROPERTIES(CGSignBottle); + UPDATE_OBJ_PROPERTIES(CGLighthouse); + UPDATE_OBJ_PROPERTIES(CGPandoraBox); + UPDATE_OBJ_PROPERTIES(CGEvent); + + table->show(); +} + +//===============IMPLEMENT PROPERTY UPDATE================================ +void Inspector::setProperty(const QString & key, const QVariant & value) +{ + if(!obj) + return; + + if(key == "Owner") + { + PlayerColor owner(value.toString().mid(6).toInt()); //receiving PLAYER N, N has index 6 + if(value == "NEUTRAL") + owner = PlayerColor::NEUTRAL; + if(value == "UNFLAGGABLE") + owner = PlayerColor::UNFLAGGABLE; + obj->tempOwner = owner; + } + + SET_PROPERTIES(CArmedInstance); + SET_PROPERTIES(CGTownInstance); + SET_PROPERTIES(CGArtifact); + SET_PROPERTIES(CGMine); + SET_PROPERTIES(CGResource); + SET_PROPERTIES(CGDwelling); + SET_PROPERTIES(CGGarrison); + SET_PROPERTIES(CGCreature); + SET_PROPERTIES(CGHeroInstance); + SET_PROPERTIES(CGShipyard); + SET_PROPERTIES(CGSignBottle); + SET_PROPERTIES(CGLighthouse); + SET_PROPERTIES(CGPandoraBox); + SET_PROPERTIES(CGEvent); +} + +void Inspector::setProperty(CArmedInstance * o, const QString & key, const QVariant & value) +{ + if(!o) return; +} + +void Inspector::setProperty(CGLighthouse * o, const QString & key, const QVariant & value) +{ + if(!o) return; +} + +void Inspector::setProperty(CGPandoraBox * o, const QString & key, const QVariant & value) +{ + if(!o) return; +} + +void Inspector::setProperty(CGEvent * o, const QString & key, const QVariant & value) +{ + if(!o) return; + + if("Remove after") + o->removeAfterVisit = stringToBool(value.toString()); + + if("Human trigger") + o->humanActivate = stringToBool(value.toString()); + + if("Cpu trigger") + o->computerActivate = stringToBool(value.toString()); +} + +void Inspector::setProperty(CGTownInstance * o, const QString & key, const QVariant & value) +{ + if(!o) return; + + if(key == "Town name") + o->name = value.toString().toStdString(); +} + +void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVariant & value) +{ + if(!o) return; + + if(key == "Message") + o->message = value.toString().toStdString(); +} + +void Inspector::setProperty(CGMine * o, const QString & key, const QVariant & value) +{ + if(!o) return; + + if(key == "Productivity") + o->producedQuantity = value.toString().toInt(); +} + +void Inspector::setProperty(CGArtifact * o, const QString & key, const QVariant & value) +{ + if(!o) return; + + if(key == "Message") + o->message = value.toString().toStdString(); + + if(o->storedArtifact && key == "Spell") + { + for(auto spell : VLC->spellh->objects) + { + if(spell->name == value.toString().toStdString()) + { + o->storedArtifact = CArtifactInstance::createScroll(spell->getId()); + break; + } + } + } +} + +void Inspector::setProperty(CGDwelling * o, const QString & key, const QVariant & value) +{ + if(!o) return; +} + +void Inspector::setProperty(CGGarrison * o, const QString & key, const QVariant & value) +{ + if(!o) return; + + if(key == "Removable units") + o->removableUnits = stringToBool(value.toString()); +} + +void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVariant & value) +{ + if(!o) return; + + if(key == "Sex") + o->sex = value.toString() == "MALE" ? 0 : 1; + + if(key == "Name") + o->name = value.toString().toStdString(); + + if(key == "Hero type") + { + for(auto t : VLC->heroh->objects) + { + if(t->getName() == value.toString().toStdString()) + o->type = t.get(); + } + o->name = o->type->getName(); + o->sex = o->type->sex; + o->biography = o->type->biography; + o->portrait = o->type->imageIndex; + o->randomizeArmy(o->type->heroClass->faction); + updateProperties(); //updating other properties after change + } +} + +void Inspector::setProperty(CGShipyard * o, const QString & key, const QVariant & value) +{ + if(!o) return; +} + +void Inspector::setProperty(CGResource * o, const QString & key, const QVariant & value) +{ + if(!o) return; + + if(key == "Amount") + o->amount = value.toString().toInt(); +} + +void Inspector::setProperty(CGCreature * o, const QString & key, const QVariant & value) +{ + if(!o) return; + + if(key == "Message") + o->message = value.toString().toStdString(); + if(key == "Character") + { + //COMPLIANT = 0, FRIENDLY = 1, AGRESSIVE = 2, HOSTILE = 3, SAVAGE = 4 + if(value == "COMPLIANT") + o->character = CGCreature::Character::COMPLIANT; + if(value == "FRIENDLY") + o->character = CGCreature::Character::FRIENDLY; + if(value == "AGRESSIVE") + o->character = CGCreature::Character::AGRESSIVE; + if(value == "HOSTILE") + o->character = CGCreature::Character::HOSTILE; + if(value == "SAVAGE") + o->character = CGCreature::Character::SAVAGE; + } + if(key == "Never flees") + o->neverFlees = stringToBool(value.toString()); + if(key == "Not growing") + o->notGrowingTeam = stringToBool(value.toString()); + if(key == "Amount") + o->stacks[SlotID(0)]->count = value.toString().toInt(); +} + + +//===============IMPLEMENT PROPERTY VALUE TYPE============================ +QTableWidgetItem * Inspector::addProperty(CGObjectInstance * value) +{ + using NumericPointer = unsigned long long; + static_assert(sizeof(CGObjectInstance *) == sizeof(NumericPointer), + "Compilied for 64 bit arcitecture. Use NumericPointer = unsigned int"); + return new QTableWidgetItem(QString::number(reinterpret_cast(value))); +} + +QTableWidgetItem * Inspector::addProperty(Inspector::PropertyEditorPlaceholder value) +{ + auto item = new QTableWidgetItem(""); + item->setData(Qt::UserRole, QString("PropertyEditor")); + return item; +} + +QTableWidgetItem * Inspector::addProperty(unsigned int value) +{ + return new QTableWidgetItem(QString::number(value)); +} + +QTableWidgetItem * Inspector::addProperty(int value) +{ + return new QTableWidgetItem(QString::number(value)); +} + +QTableWidgetItem * Inspector::addProperty(bool value) +{ + return new QTableWidgetItem(value ? "TRUE" : "FALSE"); +} + +QTableWidgetItem * Inspector::addProperty(const std::string & value) +{ + return addProperty(QString::fromStdString(value)); +} + +QTableWidgetItem * Inspector::addProperty(const QString & value) +{ + return new QTableWidgetItem(value); +} + +QTableWidgetItem * Inspector::addProperty(const int3 & value) +{ + return new QTableWidgetItem(QString("(%1, %2, %3)").arg(value.x, value.y, value.z)); +} + +QTableWidgetItem * Inspector::addProperty(const PlayerColor & value) +{ + auto str = QString("PLAYER %1").arg(value.getNum()); + if(value == PlayerColor::NEUTRAL) + str = "NEUTRAL"; + if(value == PlayerColor::UNFLAGGABLE) + str = "UNFLAGGABLE"; + return new QTableWidgetItem(str); +} + +QTableWidgetItem * Inspector::addProperty(const Res::ERes & value) +{ + QString str; + switch (value) { + case Res::ERes::WOOD: + str = "WOOD"; + break; + case Res::ERes::ORE: + str = "ORE"; + break; + case Res::ERes::SULFUR: + str = "SULFUR"; + break; + case Res::ERes::GEMS: + str = "GEMS"; + break; + case Res::ERes::MERCURY: + str = "MERCURY"; + break; + case Res::ERes::CRYSTAL: + str = "CRYSTAL"; + break; + case Res::ERes::GOLD: + str = "GOLD"; + break; + default: + break; + } + return new QTableWidgetItem(str); +} + +QTableWidgetItem * Inspector::addProperty(CGCreature::Character value) +{ + QString str; + switch (value) { + case CGCreature::Character::COMPLIANT: + str = "COMPLIANT"; + break; + case CGCreature::Character::FRIENDLY: + str = "FRIENDLY"; + break; + case CGCreature::Character::AGRESSIVE: + str = "AGRESSIVE"; + break; + case CGCreature::Character::HOSTILE: + str = "HOSTILE"; + break; + case CGCreature::Character::SAVAGE: + str = "SAVAGE"; + break; + default: + break; + } + return new QTableWidgetItem(str); +} + +//======================================================================== + +Inspector::Inspector(CMap * m, CGObjectInstance * o, QTableWidget * t): obj(o), table(t), map(m) +{ +} + +/* + * Delegates + */ + +InspectorDelegate * InspectorDelegate::boolDelegate() +{ + auto * d = new InspectorDelegate; + d->options << "TRUE" << "FALSE"; + return d; +} + +QWidget * InspectorDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + return new QComboBox(parent); +} + +void InspectorDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + if(QComboBox *ed = qobject_cast(editor)) + { + ed->addItems(options); + } + else + { + QStyledItemDelegate::setEditorData(editor, index); + } +} + +void InspectorDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + if(QComboBox *ed = qobject_cast(editor)) + { + if(!options.isEmpty()) + { + QMap data; + data[0] = options[ed->currentIndex()]; + model->setItemData(index, data); + } + } + else + { + QStyledItemDelegate::setModelData(editor, model, index); + } +} + diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h new file mode 100644 index 000000000..f4d866433 --- /dev/null +++ b/mapeditor/inspector/inspector.h @@ -0,0 +1,154 @@ +#ifndef INSPECTOR_H +#define INSPECTOR_H + +#include +#include +#include +#include "../lib/int3.h" +#include "../lib/GameConstants.h" +#include "../lib/mapObjects/MapObjects.h" +#include "../lib/ResourceSet.h" + +#define DECLARE_OBJ_TYPE(x) void initialize(x*); +#define DECLARE_OBJ_PROPERTY_METHODS(x) \ +void updateProperties(x*); \ +void setProperty(x*, const QString &, const QVariant &); + +#define INIT_OBJ_TYPE(x) initialize(dynamic_cast(o)) +#define UPDATE_OBJ_PROPERTIES(x) updateProperties(dynamic_cast(obj)) +#define SET_PROPERTIES(x) setProperty(dynamic_cast(obj), key, value) + + +class Initializer +{ +public: + //===============DECLARE MAP OBJECTS====================================== + DECLARE_OBJ_TYPE(CArmedInstance); + DECLARE_OBJ_TYPE(CGShipyard); + DECLARE_OBJ_TYPE(CGTownInstance); + DECLARE_OBJ_TYPE(CGArtifact); + DECLARE_OBJ_TYPE(CGMine); + DECLARE_OBJ_TYPE(CGResource); + DECLARE_OBJ_TYPE(CGDwelling); + DECLARE_OBJ_TYPE(CGGarrison); + DECLARE_OBJ_TYPE(CGHeroInstance); + DECLARE_OBJ_TYPE(CGCreature); + DECLARE_OBJ_TYPE(CGSignBottle); + DECLARE_OBJ_TYPE(CGLighthouse); + //DECLARE_OBJ_TYPE(CGEvent); + //DECLARE_OBJ_TYPE(CGPandoraBox); + + + Initializer(CGObjectInstance *, const PlayerColor &); + +private: + PlayerColor defaultPlayer; +}; + +class Inspector +{ +protected: + struct PropertyEditorPlaceholder {}; + +//===============DECLARE PROPERTIES SETUP================================= + DECLARE_OBJ_PROPERTY_METHODS(CArmedInstance); + DECLARE_OBJ_PROPERTY_METHODS(CGTownInstance); + DECLARE_OBJ_PROPERTY_METHODS(CGShipyard); + DECLARE_OBJ_PROPERTY_METHODS(CGArtifact); + DECLARE_OBJ_PROPERTY_METHODS(CGMine); + DECLARE_OBJ_PROPERTY_METHODS(CGResource); + DECLARE_OBJ_PROPERTY_METHODS(CGDwelling); + DECLARE_OBJ_PROPERTY_METHODS(CGGarrison); + DECLARE_OBJ_PROPERTY_METHODS(CGHeroInstance); + DECLARE_OBJ_PROPERTY_METHODS(CGCreature); + DECLARE_OBJ_PROPERTY_METHODS(CGSignBottle); + DECLARE_OBJ_PROPERTY_METHODS(CGLighthouse); + DECLARE_OBJ_PROPERTY_METHODS(CGPandoraBox); + DECLARE_OBJ_PROPERTY_METHODS(CGEvent); + +//===============DECLARE PROPERTY VALUE TYPE============================== + QTableWidgetItem * addProperty(unsigned int value); + QTableWidgetItem * addProperty(int value); + QTableWidgetItem * addProperty(const std::string & value); + QTableWidgetItem * addProperty(const QString & value); + QTableWidgetItem * addProperty(const int3 & value); + QTableWidgetItem * addProperty(const PlayerColor & value); + QTableWidgetItem * addProperty(const Res::ERes & value); + QTableWidgetItem * addProperty(bool value); + QTableWidgetItem * addProperty(CGObjectInstance * value); + QTableWidgetItem * addProperty(CGCreature::Character value); + QTableWidgetItem * addProperty(PropertyEditorPlaceholder value); + +//===============END OF DECLARATION======================================= + +public: + Inspector(CMap *, CGObjectInstance *, QTableWidget *); + + void setProperty(const QString & key, const QVariant & value); + + void updateProperties(); + +protected: + + template + void addProperty(const QString & key, const T & value, QAbstractItemDelegate * delegate, bool restricted) + { + auto * itemValue = addProperty(value); + if(restricted) + itemValue->setFlags(Qt::NoItemFlags); + + QTableWidgetItem * itemKey = nullptr; + if(keyItems.contains(key)) + { + itemKey = keyItems[key]; + table->setItem(table->row(itemKey), 1, itemValue); + if(delegate) + table->setItemDelegateForRow(table->row(itemKey), delegate); + } + else + { + itemKey = new QTableWidgetItem(key); + itemKey->setFlags(Qt::NoItemFlags); + keyItems[key] = itemKey; + + table->setRowCount(row + 1); + table->setItem(row, 0, itemKey); + table->setItem(row, 1, itemValue); + table->setItemDelegateForRow(row, delegate); + ++row; + } + } + + template + void addProperty(const QString & key, const T & value, bool restricted = true) + { + addProperty(key, value, nullptr, restricted); + } + +protected: + int row = 0; + QTableWidget * table; + CGObjectInstance * obj; + QMap keyItems; + CMap * map; +}; + + + + +class InspectorDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + static InspectorDelegate * boolDelegate(); + + using QStyledItemDelegate::QStyledItemDelegate; + + QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + + QStringList options; +}; + +#endif // INSPECTOR_H diff --git a/mapeditor/inspector/messagewidget.cpp b/mapeditor/inspector/messagewidget.cpp new file mode 100644 index 000000000..a514f531b --- /dev/null +++ b/mapeditor/inspector/messagewidget.cpp @@ -0,0 +1,53 @@ +#include "messagewidget.h" +#include "ui_messagewidget.h" + +MessageWidget::MessageWidget(QWidget *parent) : + QDialog(parent), + ui(new Ui::MessageWidget) +{ + ui->setupUi(this); +} + +MessageWidget::~MessageWidget() +{ + delete ui; +} + +void MessageWidget::setMessage(const QString & m) +{ + ui->messageEdit->setPlainText(m); +} + +QString MessageWidget::getMessage() const +{ + return ui->messageEdit->toPlainText(); +} + +QWidget * MessageDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + return new MessageWidget(parent); +} + +void MessageDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + if(auto *ed = qobject_cast(editor)) + { + ed->setMessage(index.data().toString()); + } + else + { + QStyledItemDelegate::setEditorData(editor, index); + } +} + +void MessageDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + if(auto *ed = qobject_cast(editor)) + { + model->setData(index, ed->getMessage()); + } + else + { + QStyledItemDelegate::setModelData(editor, model, index); + } +} diff --git a/mapeditor/inspector/messagewidget.h b/mapeditor/inspector/messagewidget.h new file mode 100644 index 000000000..b8fce8e00 --- /dev/null +++ b/mapeditor/inspector/messagewidget.h @@ -0,0 +1,37 @@ +#ifndef MESSAGEWIDGET_H +#define MESSAGEWIDGET_H + +#include + +namespace Ui { +class MessageWidget; +} + +class MessageWidget : public QDialog +{ + Q_OBJECT + +public: + explicit MessageWidget(QWidget *parent = nullptr); + ~MessageWidget(); + + void setMessage(const QString &); + QString getMessage() const; + +private: + Ui::MessageWidget *ui; +}; + + +class MessageDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + using QStyledItemDelegate::QStyledItemDelegate; + + QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; +}; + +#endif // MESSAGEWIDGET_H diff --git a/mapeditor/inspector/messagewidget.ui b/mapeditor/inspector/messagewidget.ui new file mode 100644 index 000000000..925d8073e --- /dev/null +++ b/mapeditor/inspector/messagewidget.ui @@ -0,0 +1,33 @@ + + + MessageWidget + + + + 0 + 0 + 306 + 201 + + + + + 306 + 201 + + + + Message + + + true + + + + + + + + + + diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp new file mode 100644 index 000000000..cde3ceb64 --- /dev/null +++ b/mapeditor/inspector/rewardswidget.cpp @@ -0,0 +1,329 @@ +#include "rewardswidget.h" +#include "ui_rewardswidget.h" +#include "../lib/VCMI_Lib.h" +#include "../lib/CSkillHandler.h" +#include "../lib/spells/CSpellHandler.h" +#include "../lib/CArtHandler.h" +#include "../lib/CCreatureHandler.h" +#include "../lib/StringConstants.h" + +RewardsWidget::RewardsWidget(const CMap & m, CGPandoraBox & p, QWidget *parent) : + QDialog(parent), + map(m), + pandora(&p), + ui(new Ui::RewardsWidget) +{ + ui->setupUi(this); + + for(auto & type : rewardTypes) + ui->rewardType->addItem(QString::fromStdString(type)); +} + +RewardsWidget::~RewardsWidget() +{ + delete ui; +} + +QList RewardsWidget::getListForType(int typeId) +{ + assert(typeId < rewardTypes.size()); + QList result; + + switch (typeId) { + case 4: //resources + //to convert string to index WOOD = 0, MERCURY, ORE, SULFUR, CRYSTAL, GEMS, GOLD, MITHRIL, + result.append("Wood"); + result.append("Mercury"); + result.append("Ore"); + result.append("Sulfur"); + result.append("Crystals"); + result.append("Gems"); + result.append("Gold"); + break; + + case 5: + for(auto s : PrimarySkill::names) + result.append(QString::fromStdString(s)); + break; + + case 6: + //abilities + for(int i = 0; i < map.allowedAbilities.size(); ++i) + { + if(map.allowedAbilities[i]) + result.append(QString::fromStdString(VLC->skillh->objects.at(i)->getName())); + } + break; + + case 7: + //arts + for(int i = 0; i < map.allowedArtifact.size(); ++i) + { + if(map.allowedArtifact[i]) + result.append(QString::fromStdString(VLC->arth->objects.at(i)->getName())); + } + break; + + case 8: + //spells + for(int i = 0; i < map.allowedSpell.size(); ++i) + { + if(map.allowedSpell[i]) + result.append(QString::fromStdString(VLC->spellh->objects.at(i)->getName())); + } + break; + + case 9: + //creatures + for(auto creature : VLC->creh->objects) + { + result.append(QString::fromStdString(creature->getName())); + } + break; + } + return result; +} + +void RewardsWidget::on_rewardType_activated(int index) +{ + ui->rewardList->clear(); + ui->rewardList->setEnabled(true); + assert(index < rewardTypes.size()); + + auto l = getListForType(index); + if(l.empty()) + ui->rewardList->setEnabled(false); + + for(auto & s : l) + ui->rewardList->addItem(s); +} + +void RewardsWidget::obtainData() +{ + if(pandora) + { + if(pandora->gainedExp > 0) + addReward(0, 0, pandora->gainedExp); + if(pandora->manaDiff) + addReward(1, 0, pandora->manaDiff); + if(pandora->moraleDiff) + addReward(2, 0, pandora->moraleDiff); + if(pandora->luckDiff) + addReward(3, 0, pandora->luckDiff); + if(pandora->resources.nonZero()) + { + for(Res::ResourceSet::nziterator resiter(pandora->resources); resiter.valid(); ++resiter) + addReward(4, resiter->resType, resiter->resVal); + } + for(int idx = 0; idx < pandora->primskills.size(); ++idx) + { + if(pandora->primskills[idx]) + addReward(5, idx, pandora->primskills[idx]); + } + assert(pandora->abilities.size() == pandora->abilityLevels.size()); + for(int idx = 0; idx < pandora->abilities.size(); ++idx) + { + addReward(6, pandora->abilities[idx].getNum(), pandora->abilityLevels[idx]); + } + for(auto art : pandora->artifacts) + { + addReward(7, art.getNum(), 1); + } + for(auto spell : pandora->spells) + { + addReward(8, spell.getNum(), 1); + } + for(int i = 0; i < pandora->creatures.Slots().size(); ++i) + { + if(auto c = pandora->creatures.getCreature(SlotID(i))) + addReward(9, c->getId(), pandora->creatures.getStackCount(SlotID(i))); + } + } +} + +bool RewardsWidget::commitChanges() +{ + bool haveRewards = false; + if(pandora) + { + pandora->abilities.clear(); + pandora->abilityLevels.clear(); + pandora->primskills.resize(GameConstants::PRIMARY_SKILLS, 0); + pandora->resources = Res::ResourceSet(); + pandora->artifacts.clear(); + pandora->spells.clear(); + pandora->creatures.clear(); + + for(int row = 0; row < rewards; ++row) + { + haveRewards = true; + int typeId = ui->rewardsTable->item(row, 0)->data(Qt::UserRole).toInt(); + int listId = ui->rewardsTable->item(row, 1) ? ui->rewardsTable->item(row, 1)->data(Qt::UserRole).toInt() : 0; + int amount = ui->rewardsTable->item(row, 2)->data(Qt::UserRole).toInt(); + switch(typeId) + { + case 0: + pandora->gainedExp = amount; + break; + + case 1: + pandora->manaDiff = amount; + break; + + case 2: + pandora->moraleDiff = amount; + break; + + case 3: + pandora->luckDiff = amount; + break; + + case 4: + pandora->resources.at(listId) = amount; + break; + + case 5: + pandora->primskills[listId] = amount; + break; + + case 6: + pandora->abilities.push_back(SecondarySkill(listId)); + pandora->abilityLevels.push_back(amount); + break; + + case 7: + pandora->artifacts.push_back(ArtifactID(listId)); + break; + + case 8: + pandora->spells.push_back(SpellID(listId)); + break; + + case 9: + auto slot = pandora->creatures.getFreeSlot(); + if(slot != SlotID() && amount > 0) + pandora->creatures.addToSlot(slot, CreatureID(listId), amount); + break; + } + } + } + return haveRewards; +} + +void RewardsWidget::on_rewardList_activated(int index) +{ + ui->rewardAmount->setText(QString::number(1)); +} + +void RewardsWidget::addReward(int typeId, int listId, int amount) +{ + ui->rewardsTable->setRowCount(++rewards); + + auto itemType = new QTableWidgetItem(QString::fromStdString(rewardTypes[typeId])); + itemType->setData(Qt::UserRole, typeId); + ui->rewardsTable->setItem(rewards - 1, 0, itemType); + + auto l = getListForType(typeId); + if(!l.empty()) + { + auto itemCurr = new QTableWidgetItem(getListForType(typeId)[listId]); + itemCurr->setData(Qt::UserRole, listId); + ui->rewardsTable->setItem(rewards - 1, 1, itemCurr); + } + + QString am = QString::number(amount); + switch(ui->rewardType->currentIndex()) + { + case 6: + if(amount <= 1) + am = "Basic"; + if(amount == 2) + am = "Advanced"; + if(amount >= 3) + am = "Expert"; + break; + + case 7: + case 8: + am = ""; + amount = 1; + break; + } + auto itemCount = new QTableWidgetItem(am); + itemCount->setData(Qt::UserRole, amount); + ui->rewardsTable->setItem(rewards - 1, 2, itemCount); +} + + +void RewardsWidget::on_buttonAdd_clicked() +{ + addReward(ui->rewardType->currentIndex(), ui->rewardList->currentIndex(), ui->rewardAmount->text().toInt()); +} + + +void RewardsWidget::on_buttonRemove_clicked() +{ + ui->rewardsTable->removeRow(ui->rewardsTable->currentRow()); + --rewards; +} + + +void RewardsWidget::on_buttonClear_clicked() +{ + ui->rewardsTable->clear(); + rewards = 0; +} + + +void RewardsWidget::on_rewardsTable_itemSelectionChanged() +{ + /*auto type = ui->rewardsTable->item(ui->rewardsTable->currentRow(), 0); + ui->rewardType->setCurrentIndex(type->data(Qt::UserRole).toInt()); + ui->rewardType->activated(ui->rewardType->currentIndex()); + + type = ui->rewardsTable->item(ui->rewardsTable->currentRow(), 1); + ui->rewardList->setCurrentIndex(type->data(Qt::UserRole).toInt()); + ui->rewardList->activated(ui->rewardList->currentIndex()); + + type = ui->rewardsTable->item(ui->rewardsTable->currentRow(), 2); + ui->rewardAmount->setText(QString::number(type->data(Qt::UserRole).toInt()));*/ +} + + +RewardsPandoraDelegate::RewardsPandoraDelegate(const CMap & m, CGPandoraBox & t): map(m), pandora(t), QStyledItemDelegate() +{ +} + +QWidget * RewardsPandoraDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + return new RewardsWidget(map, pandora, parent); +} + +void RewardsPandoraDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + if(auto * ed = qobject_cast(editor)) + { + ed->obtainData(); + } + else + { + QStyledItemDelegate::setEditorData(editor, index); + } +} + +void RewardsPandoraDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + if(auto * ed = qobject_cast(editor)) + { + auto isArmed = ed->commitChanges(); + model->setData(index, "dummy"); + if(isArmed) + model->setData(index, "HAS REWARD"); + else + model->setData(index, ""); + } + else + { + QStyledItemDelegate::setModelData(editor, model, index); + } +} diff --git a/mapeditor/inspector/rewardswidget.h b/mapeditor/inspector/rewardswidget.h new file mode 100644 index 000000000..3da2fbcb5 --- /dev/null +++ b/mapeditor/inspector/rewardswidget.h @@ -0,0 +1,79 @@ +#ifndef REWARDSWIDGET_H +#define REWARDSWIDGET_H + +#include +#include "../lib/mapObjects/CGPandoraBox.h" +#include "../lib/mapping/CMap.h" + +namespace Ui { +class RewardsWidget; +} + +/* + ui32 gainedExp; + si32 manaDiff; //amount of gained / lost mana + si32 moraleDiff; //morale modifier + si32 luckDiff; //luck modifier + TResources resources;//gained / lost resources + std::vector primskills;//gained / lost prim skills + std::vector abilities; //gained abilities + std::vector abilityLevels; //levels of gained abilities + std::vector artifacts; //gained artifacts + std::vector spells; //gained spells + CCreatureSet creatures; //gained creatures + */ + +const std::array rewardTypes{"Experience", "Mana", "Morale", "Luck", "Resource", "Primary skill", "Secondary skill", "Artifact", "Spell", "Creature"}; + +class RewardsWidget : public QDialog +{ + Q_OBJECT + +public: + explicit RewardsWidget(const CMap &, CGPandoraBox &, QWidget *parent = nullptr); + ~RewardsWidget(); + + void obtainData(); + bool commitChanges(); + +private slots: + void on_rewardType_activated(int index); + + void on_rewardList_activated(int index); + + void on_buttonAdd_clicked(); + + void on_buttonRemove_clicked(); + + void on_buttonClear_clicked(); + + void on_rewardsTable_itemSelectionChanged(); + +private: + void addReward(int typeId, int listId, int amount); + QList getListForType(int typeId); + + Ui::RewardsWidget *ui; + CGPandoraBox * pandora; + const CMap & map; + int rewards = 0; +}; + +class RewardsPandoraDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + using QStyledItemDelegate::QStyledItemDelegate; + + RewardsPandoraDelegate(const CMap &, CGPandoraBox &); + + QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + +private: + CGPandoraBox & pandora; + const CMap & map; +}; + +#endif // REWARDSWIDGET_H diff --git a/mapeditor/inspector/rewardswidget.ui b/mapeditor/inspector/rewardswidget.ui new file mode 100644 index 000000000..32dbcfe7d --- /dev/null +++ b/mapeditor/inspector/rewardswidget.ui @@ -0,0 +1,83 @@ + + + RewardsWidget + + + + 0 + 0 + 645 + 335 + + + + Rewards + + + + + + Remove selected + + + + + + + + 80 + 16777215 + + + + Qt::ImhDigitsOnly + + + + + + + Delete all + + + + + + + Add or change + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + 3 + + + false + + + + + + + + + + + + + + + + + diff --git a/mapeditor/inspector/townbulidingswidget.cpp b/mapeditor/inspector/townbulidingswidget.cpp new file mode 100644 index 000000000..992966633 --- /dev/null +++ b/mapeditor/inspector/townbulidingswidget.cpp @@ -0,0 +1,243 @@ +#include "townbulidingswidget.h" +#include "ui_townbulidingswidget.h" +#include "../lib/CModHandler.h" +#include "../lib/CGeneralTextHandler.h" + +std::string defaultBuildingIdConversion(BuildingID bId) +{ + switch(bId) + { + case BuildingID::DEFAULT: return "DEFAULT"; + case BuildingID::MAGES_GUILD_1: return "MAGES_GUILD_1"; + case BuildingID::MAGES_GUILD_2: return "MAGES_GUILD_2"; + case BuildingID::MAGES_GUILD_3: return "MAGES_GUILD_3"; + case BuildingID::MAGES_GUILD_4: return "MAGES_GUILD_4"; + case BuildingID::MAGES_GUILD_5: return "MAGES_GUILD_5"; + case BuildingID::TAVERN: return "TAVERN"; + case BuildingID::SHIPYARD: return "SHIPYARD"; + case BuildingID::FORT: return "FORT"; + case BuildingID::CITADEL: return "CITADEL"; + case BuildingID::CASTLE: return "CASTLE"; + case BuildingID::VILLAGE_HALL: return "VILLAGE_HALL"; + case BuildingID::TOWN_HALL: return "TOWN_HALL"; + case BuildingID::CITY_HALL: return "CITY_HALL"; + case BuildingID::CAPITOL: return "CAPITOL"; + case BuildingID::MARKETPLACE: return "MARKETPLACE"; + case BuildingID::RESOURCE_SILO: return "RESOURCE_SILO"; + case BuildingID::BLACKSMITH: return "BLACKSMITH"; + case BuildingID::SPECIAL_1: return "SPECIAL_1"; + case BuildingID::SPECIAL_2: return "SPECIAL_2"; + case BuildingID::SPECIAL_3: return "SPECIAL_3"; + case BuildingID::SPECIAL_4: return "SPECIAL_4"; + case BuildingID::HORDE_1: return "HORDE_1"; + case BuildingID::HORDE_1_UPGR: return "HORDE_1_UPGR"; + case BuildingID::HORDE_2: return "HORDE_2"; + case BuildingID::HORDE_2_UPGR: return "HORDE_2_UPGR"; + case BuildingID::SHIP: return "SHIP"; + case BuildingID::GRAIL: return "GRAIL"; + case BuildingID::EXTRA_TOWN_HALL: return "EXTRA_TOWN_HALL"; + case BuildingID::EXTRA_CITY_HALL: return "EXTRA_CITY_HALL"; + case BuildingID::EXTRA_CAPITOL: return "EXTRA_CAPITOL"; + case BuildingID::DWELL_LVL_1: return "DWELL_LVL_1"; + case BuildingID::DWELL_LVL_2: return "DWELL_LVL_2"; + case BuildingID::DWELL_LVL_3: return "DWELL_LVL_3"; + case BuildingID::DWELL_LVL_4: return "DWELL_LVL_4"; + case BuildingID::DWELL_LVL_5: return "DWELL_LVL_5"; + case BuildingID::DWELL_LVL_6: return "DWELL_LVL_6"; + case BuildingID::DWELL_LVL_7: return "DWELL_LVL_7"; + case BuildingID::DWELL_LVL_1_UP: return "DWELL_LVL_1_UP"; + case BuildingID::DWELL_LVL_2_UP: return "DWELL_LVL_2_UP"; + case BuildingID::DWELL_LVL_3_UP: return "DWELL_LVL_3_UP"; + case BuildingID::DWELL_LVL_4_UP: return "DWELL_LVL_4_UP"; + case BuildingID::DWELL_LVL_5_UP: return "DWELL_LVL_5_UP"; + case BuildingID::DWELL_LVL_6_UP: return "DWELL_LVL_6_UP"; + case BuildingID::DWELL_LVL_7_UP: return "DWELL_LVL_7_UP"; + default: + return "UNKNOWN"; + } +} + +TownBulidingsWidget::TownBulidingsWidget(CGTownInstance & t, QWidget *parent) : + town(t), + QDialog(parent), + ui(new Ui::TownBulidingsWidget) +{ + ui->setupUi(this); + ui->treeView->setModel(&model); + //ui->treeView->setColumnCount(3); + model.setHorizontalHeaderLabels(QStringList() << QStringLiteral("Type") << QStringLiteral("Enabled") << QStringLiteral("Built")); + + //setAttribute(Qt::WA_DeleteOnClose); +} + +TownBulidingsWidget::~TownBulidingsWidget() +{ + delete ui; +} + +QStandardItem * TownBulidingsWidget::addBuilding(const CTown & ctown, int bId, std::set & remaining) +{ + BuildingID buildingId(bId); + const CBuilding * building = ctown.buildings.at(buildingId); + if(!building) + { + remaining.erase(bId); + return nullptr; + } + + QString name = tr(building->Name().c_str()); + + if(name.isEmpty()) + name = QString::fromStdString(defaultBuildingIdConversion(buildingId)); + + QList checks; + + checks << new QStandardItem(name); + checks.back()->setData(bId, Qt::UserRole); + + checks << new QStandardItem; + checks.back()->setCheckable(true); + checks.back()->setCheckState(town.forbiddenBuildings.count(buildingId) ? Qt::Unchecked : Qt::Checked); + checks.back()->setData(bId, Qt::UserRole); + + checks << new QStandardItem; + checks.back()->setCheckable(true); + checks.back()->setCheckState(town.builtBuildings.count(buildingId) ? Qt::Checked : Qt::Unchecked); + checks.back()->setData(bId, Qt::UserRole); + + if(building->getBase() == buildingId) + { + model.appendRow(checks); + } + else + { + QStandardItem * parent = nullptr; + std::vector stack; + stack.push_back(QModelIndex()); + while(!parent && !stack.empty()) + { + auto pindex = stack.back(); + stack.pop_back(); + for(int i = 0; i < model.rowCount(pindex); ++i) + { + QModelIndex index = model.index(i, 0, pindex); + if(building->upgrade == model.itemFromIndex(index)->data(Qt::UserRole).toInt()) + { + parent = model.itemFromIndex(index); + break; + } + if(model.hasChildren(index)) + stack.push_back(index); + } + } + + if(!parent) + parent = addBuilding(ctown, building->upgrade.getNum(), remaining); + + if(!parent) + { + remaining.erase(bId); + return nullptr; + } + + parent->appendRow(checks); + } + + remaining.erase(bId); + return checks.front(); +} + +void TownBulidingsWidget::addBuildings(const CTown & ctown) +{ + auto buildings = ctown.getAllBuildings(); + while(!buildings.empty()) + { + addBuilding(ctown, *buildings.begin(), buildings); + } + ui->treeView->resizeColumnToContents(0); + ui->treeView->resizeColumnToContents(1); + ui->treeView->resizeColumnToContents(2); +} + +std::set TownBulidingsWidget::getForbiddenBuildings() +{ + std::set result; + for(int i = 0; i < model.rowCount(); ++i) + { + if(auto * item = model.item(i, 1)) + if(item->checkState() == Qt::Unchecked) + result.emplace(item->data(Qt::UserRole).toInt()); + } + + return result; +} + +std::set TownBulidingsWidget::getBuiltBuildings() +{ + std::set result; + for(int i = 0; i < model.rowCount(); ++i) + { + if(auto * item = model.item(i, 2)) + if(item->checkState() == Qt::Checked) + result.emplace(item->data(Qt::UserRole).toInt()); + } + + return result; +} + +void TownBulidingsWidget::on_treeView_expanded(const QModelIndex &index) +{ + ui->treeView->resizeColumnToContents(0); +} + +void TownBulidingsWidget::on_treeView_collapsed(const QModelIndex &index) +{ + ui->treeView->resizeColumnToContents(0); +} + + +TownBuildingsDelegate::TownBuildingsDelegate(CGTownInstance & t): town(t), QStyledItemDelegate() +{ +} + +QWidget * TownBuildingsDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + return new TownBulidingsWidget(town, parent); +} + +void TownBuildingsDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + if(auto * ed = qobject_cast(editor)) + { + auto * ctown = town.town; + if(!ctown) + ctown = VLC->townh->randomTown; + if(!ctown) + throw std::runtime_error("No Town defined for type selected"); + + ed->addBuildings(*ctown); + } + else + { + QStyledItemDelegate::setEditorData(editor, index); + } +} + +void TownBuildingsDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + if(auto * ed = qobject_cast(editor)) + { + town.forbiddenBuildings = ed->getForbiddenBuildings(); + town.builtBuildings = ed->getBuiltBuildings(); + + auto data = model->itemData(index); + model->setData(index, "dummy"); + model->setItemData(index, data); //dummy change to trigger signal + } + else + { + QStyledItemDelegate::setModelData(editor, model, index); + } +} + + diff --git a/mapeditor/inspector/townbulidingswidget.h b/mapeditor/inspector/townbulidingswidget.h new file mode 100644 index 000000000..64d253b0b --- /dev/null +++ b/mapeditor/inspector/townbulidingswidget.h @@ -0,0 +1,53 @@ +#ifndef TOWNBULIDINGSWIDGET_H +#define TOWNBULIDINGSWIDGET_H + +#include +#include "../lib/mapObjects/CGTownInstance.h" + +namespace Ui { +class TownBulidingsWidget; +} + +class TownBulidingsWidget : public QDialog +{ + Q_OBJECT + + QStandardItem * addBuilding(const CTown & ctown, int bId, std::set & remaining); + +public: + explicit TownBulidingsWidget(CGTownInstance &, QWidget *parent = nullptr); + ~TownBulidingsWidget(); + + void addBuildings(const CTown & ctown); + std::set getForbiddenBuildings(); + std::set getBuiltBuildings(); + +private slots: + void on_treeView_expanded(const QModelIndex &index); + + void on_treeView_collapsed(const QModelIndex &index); + +private: + Ui::TownBulidingsWidget *ui; + CGTownInstance & town; + mutable QStandardItemModel model; +}; + +class TownBuildingsDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + using QStyledItemDelegate::QStyledItemDelegate; + + TownBuildingsDelegate(CGTownInstance &); + + QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + +private: + CGTownInstance & town; + //std::set +}; + +#endif // TOWNBULIDINGSWIDGET_H diff --git a/mapeditor/inspector/townbulidingswidget.ui b/mapeditor/inspector/townbulidingswidget.ui new file mode 100644 index 000000000..942c5d5f5 --- /dev/null +++ b/mapeditor/inspector/townbulidingswidget.ui @@ -0,0 +1,49 @@ + + + TownBulidingsWidget + + + + 0 + 0 + 480 + 280 + + + + + 0 + 0 + + + + + 480 + 280 + + + + Buildings + + + true + + + + + + QAbstractItemView::NoEditTriggers + + + true + + + true + + + + + + + + diff --git a/mapeditor/jsonutils.cpp b/mapeditor/jsonutils.cpp new file mode 100644 index 000000000..fb2c66834 --- /dev/null +++ b/mapeditor/jsonutils.cpp @@ -0,0 +1,125 @@ +/* + * jsonutils.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "jsonutils.h" +#include "../lib/filesystem/FileStream.h" + +static QVariantMap JsonToMap(const JsonMap & json) +{ + QVariantMap map; + for(auto & entry : json) + { + map.insert(QString::fromUtf8(entry.first.c_str()), JsonUtils::toVariant(entry.second)); + } + return map; +} + +static QVariantList JsonToList(const JsonVector & json) +{ + QVariantList list; + for(auto & entry : json) + { + list.push_back(JsonUtils::toVariant(entry)); + } + return list; +} + +static JsonVector VariantToList(QVariantList variant) +{ + JsonVector vector; + for(auto & entry : variant) + { + vector.push_back(JsonUtils::toJson(entry)); + } + return vector; +} + +static JsonMap VariantToMap(QVariantMap variant) +{ + JsonMap map; + for(auto & entry : variant.toStdMap()) + { + map[entry.first.toUtf8().data()] = JsonUtils::toJson(entry.second); + } + return map; +} + +namespace JsonUtils +{ + +QVariant toVariant(const JsonNode & node) +{ + switch(node.getType()) + { + break; + case JsonNode::JsonType::DATA_NULL: + return QVariant(); + break; + case JsonNode::JsonType::DATA_BOOL: + return QVariant(node.Bool()); + break; + case JsonNode::JsonType::DATA_FLOAT: + return QVariant(node.Float()); + break; + case JsonNode::JsonType::DATA_STRING: + return QVariant(QString::fromUtf8(node.String().c_str())); + break; + case JsonNode::JsonType::DATA_VECTOR: + return JsonToList(node.Vector()); + break; + case JsonNode::JsonType::DATA_STRUCT: + return JsonToMap(node.Struct()); + } + return QVariant(); +} + +QVariant JsonFromFile(QString filename) +{ + QFile file(filename); + file.open(QFile::ReadOnly); + auto data = file.readAll(); + + if(data.size() == 0) + { + logGlobal->error("Failed to open file %s", filename.toUtf8().data()); + return QVariant(); + } + else + { + JsonNode node(data.data(), data.size()); + return toVariant(node); + } +} + +JsonNode toJson(QVariant object) +{ + JsonNode ret; + + if(object.canConvert()) + ret.Struct() = VariantToMap(object.toMap()); + else if(object.canConvert()) + ret.Vector() = VariantToList(object.toList()); + else if(object.userType() == QMetaType::QString) + ret.String() = object.toString().toUtf8().data(); + else if(object.userType() == QMetaType::Bool) + ret.Bool() = object.toBool(); + else if(object.canConvert()) + ret.Float() = object.toFloat(); + + return ret; +} + +void JsonToFile(QString filename, QVariant object) +{ + FileStream file(qstringToPath(filename), std::ios::out | std::ios_base::binary); + file << toJson(object).toJson(); +} + +} diff --git a/mapeditor/jsonutils.h b/mapeditor/jsonutils.h new file mode 100644 index 000000000..09425b48f --- /dev/null +++ b/mapeditor/jsonutils.h @@ -0,0 +1,22 @@ +/* + * jsonutils.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include +#include "../lib/JsonNode.h" + +namespace JsonUtils +{ +QVariant toVariant(const JsonNode & node); +QVariant JsonFromFile(QString filename); + +JsonNode toJson(QVariant object); +void JsonToFile(QString filename, QVariant object); +} diff --git a/mapeditor/launcherdirs.cpp b/mapeditor/launcherdirs.cpp new file mode 100644 index 000000000..97d456bb5 --- /dev/null +++ b/mapeditor/launcherdirs.cpp @@ -0,0 +1,36 @@ +/* + * launcherdirs.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "launcherdirs.h" + +#include "../lib/VCMIDirs.h" + +static CLauncherDirs launcherDirsGlobal; + +CLauncherDirs::CLauncherDirs() +{ + QDir().mkdir(downloadsPath()); + QDir().mkdir(modsPath()); +} + +CLauncherDirs & CLauncherDirs::get() +{ + return launcherDirsGlobal; +} + +QString CLauncherDirs::downloadsPath() +{ + return pathToQString(VCMIDirs::get().userCachePath() / "downloads"); +} + +QString CLauncherDirs::modsPath() +{ + return pathToQString(VCMIDirs::get().userDataPath() / "Mods"); +} diff --git a/mapeditor/launcherdirs.h b/mapeditor/launcherdirs.h new file mode 100644 index 000000000..9117bd9fb --- /dev/null +++ b/mapeditor/launcherdirs.h @@ -0,0 +1,22 @@ +/* + * launcherdirs.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +/// similar to lib/VCMIDirs, controls where all launcher-related data will be stored +class CLauncherDirs +{ +public: + CLauncherDirs(); + + static CLauncherDirs & get(); + + QString downloadsPath(); + QString modsPath(); +}; diff --git a/mapeditor/main.cpp b/mapeditor/main.cpp new file mode 100644 index 000000000..766cd9af9 --- /dev/null +++ b/mapeditor/main.cpp @@ -0,0 +1,19 @@ +/* + * main.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include +#include "StdInc.h" +#include "mainwindow.h" + +int main(int argc, char * argv[]) +{ + QApplication vcmieditor(argc, argv); + MainWindow mainWindow; + return vcmieditor.exec(); +} diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp new file mode 100644 index 000000000..c41c8130b --- /dev/null +++ b/mapeditor/mainwindow.cpp @@ -0,0 +1,1118 @@ +#include "StdInc.h" +#include "mainwindow.h" +#include "ui_mainwindow.h" + +#include +#include +#include +#include + +#include "../lib/VCMIDirs.h" +#include "../lib/VCMI_Lib.h" +#include "../lib/logging/CBasicLogConfigurator.h" +#include "../lib/CConfigHandler.h" +#include "../lib/filesystem/Filesystem.h" +#include "../lib/GameConstants.h" +#include "../lib/mapping/CMapService.h" +#include "../lib/mapping/CMap.h" +#include "../lib/mapping/CMapEditManager.h" +#include "../lib/Terrain.h" +#include "../lib/mapObjects/CObjectClassesHandler.h" +#include "../lib/filesystem/CFilesystemLoader.h" + + +#include "CGameInfo.h" +#include "maphandler.h" +#include "graphics.h" +#include "windownewmap.h" +#include "objectbrowser.h" +#include "inspector/inspector.h" +#include "mapsettings.h" +#include "playersettings.h" +#include "validator.h" + +static CBasicLogConfigurator * logConfig; + +QJsonValue jsonFromPixmap(const QPixmap &p) +{ + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + p.save(&buffer, "PNG"); + auto const encoded = buffer.data().toBase64(); + return {QLatin1String(encoded)}; +} + +QPixmap pixmapFromJson(const QJsonValue &val) +{ + auto const encoded = val.toString().toLatin1(); + QPixmap p; + p.loadFromData(QByteArray::fromBase64(encoded), "PNG"); + return p; +} + +void init() +{ + + loadDLLClasses(); + const_cast(CGI)->setFromLib(); + logGlobal->info("Initializing VCMI_Lib"); +} + +void MainWindow::loadUserSettings() +{ + //load window settings + QSettings s(Ui::teamName, Ui::appName); + + auto size = s.value(mainWindowSizeSetting).toSize(); + if (size.isValid()) + { + resize(size); + } + auto position = s.value(mainWindowPositionSetting).toPoint(); + if (!position.isNull()) + { + move(position); + } +} + +void MainWindow::saveUserSettings() +{ + QSettings s(Ui::teamName, Ui::appName); + s.setValue(mainWindowSizeSetting, size()); + s.setValue(mainWindowPositionSetting, pos()); +} + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow), + controller(this) +{ + ui->setupUi(this); + loadUserSettings(); //For example window size + setTitle(); + + // Set current working dir to executable folder. + // This is important on Mac for relative paths to work inside DMG. + QDir::setCurrent(QApplication::applicationDirPath()); + + //configure logging + const boost::filesystem::path logPath = VCMIDirs::get().userCachePath() / "VCMI_Editor_log.txt"; + console = new CConsoleHandler(); + logConfig = new CBasicLogConfigurator(logPath, console); + logConfig->configureDefault(); + logGlobal->info("The log file will be saved to %s", logPath); + + //init + preinitDLL(::console); + settings.init(); + + // Initialize logging based on settings + logConfig->configure(); + logGlobal->debug("settings = %s", settings.toJsonNode().toJson()); + + // Some basic data validation to produce better error messages in cases of incorrect install + auto testFile = [](std::string filename, std::string message) -> bool + { + if (CResourceHandler::get()->existsResource(ResourceID(filename))) + return true; + + logGlobal->error("Error: %s was not found!", message); + return false; + }; + + if(!testFile("DATA/HELP.TXT", "Heroes III data") || + !testFile("MODS/VCMI/MOD.JSON", "VCMI data")) + { + QApplication::quit(); + } + + conf.init(); + logGlobal->info("Loading settings"); + + CGI = new CGameInfo(); //contains all global informations about game (texts, lodHandlers, map handler etc.) + init(); + + graphics = new Graphics(); // should be before curh->init() + graphics->load();//must be after Content loading but should be in main thread + + + if(!testFile("DATA/new-menu/Background.png", "Cannot find file")) + { + QApplication::quit(); + } + + //now let's try to draw + //auto resPath = *CResourceHandler::get()->getResourceName(ResourceID("DATA/new-menu/Background.png")); + + ui->mapView->setScene(controller.scene(0)); + ui->mapView->setController(&controller); + ui->mapView->setOptimizationFlags(QGraphicsView::DontSavePainterState | QGraphicsView::DontAdjustForAntialiasing); + connect(ui->mapView, &MapView::openObjectProperties, this, &MainWindow::loadInspector); + + ui->minimapView->setScene(controller.miniScene(0)); + ui->minimapView->setController(&controller); + connect(ui->minimapView, &MinimapView::cameraPositionChanged, ui->mapView, &MapView::cameraChanged); + + scenePreview = new QGraphicsScene(this); + ui->objectPreview->setScene(scenePreview); + + //scenes[0]->addPixmap(QPixmap(QString::fromStdString(resPath.native()))); + + //loading objects + loadObjectsTree(); + + ui->tabWidget->setCurrentIndex(0); + + for(int i = 0; i < 8; ++i) + { + connect(getActionPlayer(PlayerColor(i)), &QAction::toggled, this, [&, i](){switchDefaultPlayer(PlayerColor(i));}); + } + connect(getActionPlayer(PlayerColor::NEUTRAL), &QAction::toggled, this, [&](){switchDefaultPlayer(PlayerColor::NEUTRAL);}); + onPlayersChanged(); + + show(); + + //Load map from command line + if(qApp->arguments().size() == 2) + openMap(qApp->arguments().at(1)); +} + +MainWindow::~MainWindow() +{ + saveUserSettings(); //save window size etc. + delete ui; +} + +bool MainWindow::getAnswerAboutUnsavedChanges() +{ + if(unsaved) + { + auto sure = QMessageBox::question(this, "Confirmation", "Unsaved changes will be lost, are you sure?"); + if(sure == QMessageBox::No) + { + return false; + } + } + return true; +} + +void MainWindow::closeEvent(QCloseEvent *event) +{ + if(getAnswerAboutUnsavedChanges()) + QMainWindow::closeEvent(event); + else + event->ignore(); +} + +void MainWindow::setStatusMessage(const QString & status) +{ + statusBar()->showMessage(status); +} + +void MainWindow::setTitle() +{ + QString title = QString("%1%2 - %3 (v%4)").arg(filename, unsaved ? "*" : "", VCMI_EDITOR_NAME, VCMI_EDITOR_VERSION); + setWindowTitle(title); +} + +void MainWindow::mapChanged() +{ + unsaved = true; + setTitle(); +} + +void MainWindow::initializeMap(bool isNew) +{ + unsaved = isNew; + if(isNew) + filename.clear(); + setTitle(); + + mapLevel = 0; + ui->mapView->setScene(controller.scene(mapLevel)); + ui->minimapView->setScene(controller.miniScene(mapLevel)); + ui->minimapView->dimensions(); + + setStatusMessage(QString("Scene objects: %1").arg(ui->mapView->scene()->items().size())); + + //enable settings + ui->actionMapSettings->setEnabled(true); + ui->actionPlayers_settings->setEnabled(true); + + onPlayersChanged(); +} + +bool MainWindow::openMap(const QString & filenameSelect) +{ + QFileInfo fi(filenameSelect); + std::string fname = fi.fileName().toStdString(); + std::string fdir = fi.dir().path().toStdString(); + + ResourceID resId("MAPEDITOR/" + fname, EResType::MAP); + + //addFilesystem takes care about memory deallocation if case of failure, no memory leak here + auto * mapEditorFilesystem = new CFilesystemLoader("MAPEDITOR/", fdir, 0); + CResourceHandler::removeFilesystem("local", "mapEditor"); + CResourceHandler::addFilesystem("local", "mapEditor", mapEditorFilesystem); + + if(!CResourceHandler::get("mapEditor")->existsResource(resId)) + { + QMessageBox::warning(this, "Failed to open map", "Cannot open map from this folder"); + return false; + } + + CMapService mapService; + try + { + controller.setMap(mapService.loadMap(resId)); + } + catch(const std::exception & e) + { + QMessageBox::critical(this, "Failed to open map", e.what()); + return false; + } + + filename = filenameSelect; + initializeMap(controller.map()->version != EMapFormat::VCMI); + return true; +} + +void MainWindow::on_actionOpen_triggered() +{ + if(!getAnswerAboutUnsavedChanges()) + return; + + auto filenameSelect = QFileDialog::getOpenFileName(this, tr("Open Image"), QString::fromStdString(VCMIDirs::get().userCachePath().make_preferred().string()), tr("Homm3 Files (*.vmap *.h3m)")); + + if(filenameSelect.isNull()) + return; + + openMap(filenameSelect); +} + +void MainWindow::saveMap() +{ + if(!controller.map()) + return; + + if(!unsaved) + return; + + //validate map + auto issues = Validator::validate(controller.map()); + bool critical = false; + for(auto & issue : issues) + critical |= issue.critical; + + if(!issues.empty()) + { + if(critical) + QMessageBox::warning(this, "Map validation", "Map has critical problems and most probably will not be playable. Open Validator from the Map menu to see issues found"); + else + QMessageBox::information(this, "Map validation", "Map has some errors. Open Validator from the Map menu to see issues found"); + } + + CMapService mapService; + try + { + mapService.saveMap(controller.getMapUniquePtr(), filename.toStdString()); + } + catch(const std::exception & e) + { + QMessageBox::critical(this, "Failed to save map", e.what()); + return; + } + + unsaved = false; + setTitle(); +} + +void MainWindow::on_actionSave_as_triggered() +{ + if(!controller.map()) + return; + + auto filenameSelect = QFileDialog::getSaveFileName(this, tr("Save map"), "", tr("VCMI maps (*.vmap)")); + + if(filenameSelect.isNull()) + return; + + if(filenameSelect == filename) + return; + + filename = filenameSelect; + + saveMap(); +} + + +void MainWindow::on_actionNew_triggered() +{ + if(getAnswerAboutUnsavedChanges()) + new WindowNewMap(this); +} + +void MainWindow::on_actionSave_triggered() +{ + if(!controller.map()) + return; + + if(filename.isNull()) + { + auto filenameSelect = QFileDialog::getSaveFileName(this, tr("Save map"), "", tr("VCMI maps (*.vmap)")); + + if(filenameSelect.isNull()) + return; + + filename = filenameSelect; + } + + saveMap(); +} + +void MainWindow::terrainButtonClicked(Terrain terrain) +{ + controller.commitTerrainChange(mapLevel, terrain); +} + +void MainWindow::roadOrRiverButtonClicked(std::string type, bool isRoad) +{ + controller.commitRoadOrRiverChange(mapLevel, type, isRoad); +} + +void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool staticOnly) +{ + auto knownObjects = VLC->objtypeh->knownObjects(); + for(auto ID : knownObjects) + { + if(catalog.count(ID)) + continue; + + addGroupIntoCatalog(groupName, true, staticOnly, ID); + } +} + +void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool useCustomName, bool staticOnly, int ID) +{ + QStandardItem * itemGroup = nullptr; + auto itms = objectsModel.findItems(QString::fromStdString(groupName)); + if(itms.empty()) + { + itemGroup = new QStandardItem(QString::fromStdString(groupName)); + objectsModel.appendRow(itemGroup); + } + else + { + itemGroup = itms.front(); + } + + auto knownSubObjects = VLC->objtypeh->knownSubObjects(ID); + for(auto secondaryID : knownSubObjects) + { + auto factory = VLC->objtypeh->getHandlerFor(ID, secondaryID); + auto templates = factory->getTemplates(); + bool singleTemplate = templates.size() == 1; + if(staticOnly && !factory->isStaticObject()) + continue; + + auto subGroupName = QString::fromStdString(factory->subTypeName); + auto customName = factory->getCustomName(); + if(customName) + subGroupName = tr(customName->c_str()); + + auto * itemType = new QStandardItem(subGroupName); + for(int templateId = 0; templateId < templates.size(); ++templateId) + { + auto templ = templates[templateId]; + + //selecting file + const std::string & afile = templ->editorAnimationFile.empty() ? templ->animationFile : templ->editorAnimationFile; + + //creating picture + QPixmap preview(128, 128); + preview.fill(QColor(255, 255, 255)); + QPainter painter(&preview); + Animation animation(afile); + animation.preload(); + auto picture = animation.getImage(0); + if(picture && picture->width() && picture->height()) + { + qreal xscale = qreal(128) / qreal(picture->width()), yscale = qreal(128) / qreal(picture->height()); + qreal scale = std::min(xscale, yscale); + painter.scale(scale, scale); + painter.drawImage(QPoint(0, 0), *picture); + } + + //add parameters + QJsonObject data{{"id", QJsonValue(ID)}, + {"subid", QJsonValue(secondaryID)}, + {"template", QJsonValue(templateId)}, + {"animationEditor", QString::fromStdString(templ->editorAnimationFile)}, + {"animation", QString::fromStdString(templ->animationFile)}, + {"preview", jsonFromPixmap(preview)}}; + + //create object to extract name + std::unique_ptr temporaryObj(factory->create(templ)); + QString translated = useCustomName ? tr(temporaryObj->getObjectName().c_str()) : subGroupName; + + //do not have extra level + if(singleTemplate) + { + if(useCustomName) + itemType->setText(translated); + itemType->setIcon(QIcon(preview)); + itemType->setData(data); + } + else + { + if(useCustomName) + itemType->setText(translated); + auto * item = new QStandardItem(QIcon(preview), QString::fromStdString(templ->stringID)); + item->setData(data); + itemType->appendRow(item); + } + } + itemGroup->appendRow(itemType); + catalog.insert(ID); + } +} + +void MainWindow::loadObjectsTree() +{ + try + { + ui->terrainFilterCombo->addItem(""); + //adding terrains + for(auto & terrain : Terrain::Manager::terrains()) + { + QPushButton *b = new QPushButton(QString::fromStdString(terrain)); + ui->terrainLayout->addWidget(b); + connect(b, &QPushButton::clicked, this, [this, terrain]{ terrainButtonClicked(terrain); }); + + //filter + ui->terrainFilterCombo->addItem(QString::fromStdString(terrain)); + } + //add spacer to keep terrain button on the top + ui->terrainLayout->addItem(new QSpacerItem(20, 20, QSizePolicy::Minimum, QSizePolicy::Expanding)); + //adding roads + for(auto & road : ROAD_NAMES) + { + QPushButton *b = new QPushButton(QString::fromStdString(road)); + ui->roadLayout->addWidget(b); + connect(b, &QPushButton::clicked, this, [this, road]{ roadOrRiverButtonClicked(road, true); }); + } + //add spacer to keep terrain button on the top + ui->roadLayout->addItem(new QSpacerItem(20, 20, QSizePolicy::Minimum, QSizePolicy::Expanding)); + //adding rivers + for(auto & river : RIVER_NAMES) + { + QPushButton *b = new QPushButton(QString::fromStdString(river)); + ui->riverLayout->addWidget(b); + connect(b, &QPushButton::clicked, this, [this, river]{ roadOrRiverButtonClicked(river, false); }); + } + //add spacer to keep terrain button on the top + ui->riverLayout->addItem(new QSpacerItem(20, 20, QSizePolicy::Minimum, QSizePolicy::Expanding)); + + if(objectBrowser) + throw std::runtime_error("object browser exists"); + + //model + objectsModel.setHorizontalHeaderLabels(QStringList() << QStringLiteral("Type")); + objectBrowser = new ObjectBrowser(this); + objectBrowser->setSourceModel(&objectsModel); + objectBrowser->setDynamicSortFilter(false); + objectBrowser->setRecursiveFilteringEnabled(true); + ui->treeView->setModel(objectBrowser); + ui->treeView->setSelectionBehavior(QAbstractItemView::SelectItems); + ui->treeView->setSelectionMode(QAbstractItemView::SingleSelection); + connect(ui->treeView->selectionModel(), SIGNAL(currentChanged(const QModelIndex &, const QModelIndex &)), this, SLOT(treeViewSelected(const QModelIndex &, const QModelIndex &))); + + + //adding objects + addGroupIntoCatalog("TOWNS", false, false, Obj::TOWN); + addGroupIntoCatalog("TOWNS", false, false, Obj::RANDOM_TOWN); + addGroupIntoCatalog("TOWNS", true, false, Obj::SHIPYARD); + addGroupIntoCatalog("TOWNS", true, false, Obj::GARRISON); + addGroupIntoCatalog("TOWNS", true, false, Obj::GARRISON2); + addGroupIntoCatalog("OBJECTS", true, false, Obj::ALTAR_OF_SACRIFICE); + addGroupIntoCatalog("OBJECTS", true, false, Obj::ARENA); + addGroupIntoCatalog("OBJECTS", true, false, Obj::BLACK_MARKET); + addGroupIntoCatalog("OBJECTS", true, false, Obj::BUOY); + addGroupIntoCatalog("OBJECTS", true, false, Obj::CARTOGRAPHER); + addGroupIntoCatalog("OBJECTS", true, false, Obj::SWAN_POND); + addGroupIntoCatalog("OBJECTS", true, false, Obj::COVER_OF_DARKNESS); + addGroupIntoCatalog("OBJECTS", true, false, Obj::CORPSE); + addGroupIntoCatalog("OBJECTS", true, false, Obj::FAERIE_RING); + addGroupIntoCatalog("OBJECTS", true, false, Obj::FOUNTAIN_OF_FORTUNE); + addGroupIntoCatalog("OBJECTS", true, false, Obj::FOUNTAIN_OF_YOUTH); + addGroupIntoCatalog("OBJECTS", true, false, Obj::GARDEN_OF_REVELATION); + addGroupIntoCatalog("OBJECTS", true, false, Obj::HILL_FORT); + addGroupIntoCatalog("OBJECTS", true, false, Obj::IDOL_OF_FORTUNE); + addGroupIntoCatalog("OBJECTS", true, false, Obj::LIBRARY_OF_ENLIGHTENMENT); + addGroupIntoCatalog("OBJECTS", true, false, Obj::LIGHTHOUSE); + addGroupIntoCatalog("OBJECTS", true, false, Obj::SCHOOL_OF_MAGIC); + addGroupIntoCatalog("OBJECTS", true, false, Obj::MAGIC_SPRING); + addGroupIntoCatalog("OBJECTS", true, false, Obj::MAGIC_WELL); + addGroupIntoCatalog("OBJECTS", true, false, Obj::MERCENARY_CAMP); + addGroupIntoCatalog("OBJECTS", true, false, Obj::MERMAID); + addGroupIntoCatalog("OBJECTS", true, false, Obj::MYSTICAL_GARDEN); + addGroupIntoCatalog("OBJECTS", true, false, Obj::OASIS); + addGroupIntoCatalog("OBJECTS", true, false, Obj::LEAN_TO); + addGroupIntoCatalog("OBJECTS", true, false, Obj::OBELISK); + addGroupIntoCatalog("OBJECTS", true, false, Obj::REDWOOD_OBSERVATORY); + addGroupIntoCatalog("OBJECTS", true, false, Obj::PILLAR_OF_FIRE); + addGroupIntoCatalog("OBJECTS", true, false, Obj::STAR_AXIS); + addGroupIntoCatalog("OBJECTS", true, false, Obj::RALLY_FLAG); + addGroupIntoCatalog("OBJECTS", true, false, Obj::WATERING_HOLE); + addGroupIntoCatalog("OBJECTS", true, false, Obj::SCHOLAR); + addGroupIntoCatalog("OBJECTS", true, false, Obj::SHRINE_OF_MAGIC_INCANTATION); + addGroupIntoCatalog("OBJECTS", true, false, Obj::SHRINE_OF_MAGIC_GESTURE); + addGroupIntoCatalog("OBJECTS", true, false, Obj::SHRINE_OF_MAGIC_THOUGHT); + addGroupIntoCatalog("OBJECTS", true, false, Obj::SIRENS); + addGroupIntoCatalog("OBJECTS", true, false, Obj::STABLES); + addGroupIntoCatalog("OBJECTS", true, false, Obj::TAVERN); + addGroupIntoCatalog("OBJECTS", true, false, Obj::TEMPLE); + addGroupIntoCatalog("OBJECTS", true, false, Obj::DEN_OF_THIEVES); + addGroupIntoCatalog("OBJECTS", true, false, Obj::TRADING_POST); + addGroupIntoCatalog("OBJECTS", true, false, Obj::TRADING_POST_SNOW); + addGroupIntoCatalog("OBJECTS", true, false, Obj::LEARNING_STONE); + addGroupIntoCatalog("OBJECTS", true, false, Obj::TREE_OF_KNOWLEDGE); + addGroupIntoCatalog("OBJECTS", true, false, Obj::UNIVERSITY); + addGroupIntoCatalog("OBJECTS", true, false, Obj::WAGON); + addGroupIntoCatalog("OBJECTS", true, false, Obj::SCHOOL_OF_WAR); + addGroupIntoCatalog("OBJECTS", true, false, Obj::WAR_MACHINE_FACTORY); + addGroupIntoCatalog("OBJECTS", true, false, Obj::WARRIORS_TOMB); + addGroupIntoCatalog("OBJECTS", true, false, Obj::WITCH_HUT); + addGroupIntoCatalog("OBJECTS", true, false, Obj::FREELANCERS_GUILD); + addGroupIntoCatalog("OBJECTS", true, false, Obj::SANCTUARY); + addGroupIntoCatalog("OBJECTS", true, false, Obj::MARLETTO_TOWER); + addGroupIntoCatalog("HEROES", true, false, Obj::PRISON); + addGroupIntoCatalog("HEROES", false, false, Obj::HERO); + addGroupIntoCatalog("HEROES", false, false, Obj::RANDOM_HERO); + addGroupIntoCatalog("HEROES", false, false, Obj::HERO_PLACEHOLDER); + addGroupIntoCatalog("HEROES", false, false, Obj::BOAT); + addGroupIntoCatalog("ARTIFACTS", true, false, Obj::ARTIFACT); + addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_ART); + addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_TREASURE_ART); + addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_MINOR_ART); + addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_MAJOR_ART); + addGroupIntoCatalog("ARTIFACTS", false, false, Obj::RANDOM_RELIC_ART); + addGroupIntoCatalog("ARTIFACTS", true, false, Obj::SPELL_SCROLL); + addGroupIntoCatalog("ARTIFACTS", true, false, Obj::PANDORAS_BOX); + addGroupIntoCatalog("RESOURCES", true, false, Obj::RANDOM_RESOURCE); + addGroupIntoCatalog("RESOURCES", false, false, Obj::RESOURCE); + addGroupIntoCatalog("RESOURCES", true, false, Obj::SEA_CHEST); + addGroupIntoCatalog("RESOURCES", true, false, Obj::TREASURE_CHEST); + addGroupIntoCatalog("RESOURCES", true, false, Obj::CAMPFIRE); + addGroupIntoCatalog("RESOURCES", true, false, Obj::SHIPWRECK_SURVIVOR); + addGroupIntoCatalog("RESOURCES", true, false, Obj::FLOTSAM); + addGroupIntoCatalog("BANKS", true, false, Obj::CREATURE_BANK); + addGroupIntoCatalog("BANKS", true, false, Obj::DRAGON_UTOPIA); + addGroupIntoCatalog("BANKS", true, false, Obj::CRYPT); + addGroupIntoCatalog("BANKS", true, false, Obj::DERELICT_SHIP); + addGroupIntoCatalog("BANKS", true, false, Obj::PYRAMID); + addGroupIntoCatalog("BANKS", true, false, Obj::SHIPWRECK); + addGroupIntoCatalog("DWELLINGS", true, false, Obj::CREATURE_GENERATOR1); + addGroupIntoCatalog("DWELLINGS", true, false, Obj::CREATURE_GENERATOR2); + addGroupIntoCatalog("DWELLINGS", true, false, Obj::CREATURE_GENERATOR3); + addGroupIntoCatalog("DWELLINGS", true, false, Obj::CREATURE_GENERATOR4); + addGroupIntoCatalog("DWELLINGS", true, false, Obj::REFUGEE_CAMP); + addGroupIntoCatalog("DWELLINGS", false, false, Obj::RANDOM_DWELLING); + addGroupIntoCatalog("DWELLINGS", false, false, Obj::RANDOM_DWELLING_LVL); + addGroupIntoCatalog("DWELLINGS", false, false, Obj::RANDOM_DWELLING_FACTION); + addGroupIntoCatalog("GROUNDS", true, false, Obj::CURSED_GROUND1); + addGroupIntoCatalog("GROUNDS", true, false, Obj::MAGIC_PLAINS1); + addGroupIntoCatalog("GROUNDS", true, false, Obj::CLOVER_FIELD); + addGroupIntoCatalog("GROUNDS", true, false, Obj::CURSED_GROUND2); + addGroupIntoCatalog("GROUNDS", true, false, Obj::EVIL_FOG); + addGroupIntoCatalog("GROUNDS", true, false, Obj::FAVORABLE_WINDS); + addGroupIntoCatalog("GROUNDS", true, false, Obj::FIERY_FIELDS); + addGroupIntoCatalog("GROUNDS", true, false, Obj::HOLY_GROUNDS); + addGroupIntoCatalog("GROUNDS", true, false, Obj::LUCID_POOLS); + addGroupIntoCatalog("GROUNDS", true, false, Obj::MAGIC_CLOUDS); + addGroupIntoCatalog("GROUNDS", true, false, Obj::MAGIC_PLAINS2); + addGroupIntoCatalog("GROUNDS", true, false, Obj::ROCKLANDS); + addGroupIntoCatalog("GROUNDS", true, false, Obj::HOLE); + addGroupIntoCatalog("TELEPORTS", true, false, Obj::MONOLITH_ONE_WAY_ENTRANCE); + addGroupIntoCatalog("TELEPORTS", true, false, Obj::MONOLITH_ONE_WAY_EXIT); + addGroupIntoCatalog("TELEPORTS", true, false, Obj::MONOLITH_TWO_WAY); + addGroupIntoCatalog("TELEPORTS", true, false, Obj::SUBTERRANEAN_GATE); + addGroupIntoCatalog("TELEPORTS", true, false, Obj::WHIRLPOOL); + addGroupIntoCatalog("MINES", true, false, Obj::MINE); + addGroupIntoCatalog("MINES", false, false, Obj::ABANDONED_MINE); + addGroupIntoCatalog("MINES", true, false, Obj::WINDMILL); + addGroupIntoCatalog("MINES", true, false, Obj::WATER_WHEEL); + addGroupIntoCatalog("TRIGGERS", true, false, Obj::EVENT); + addGroupIntoCatalog("TRIGGERS", true, false, Obj::GRAIL); + addGroupIntoCatalog("TRIGGERS", true, false, Obj::SIGN); + addGroupIntoCatalog("TRIGGERS", true, false, Obj::OCEAN_BOTTLE); + addGroupIntoCatalog("MONSTERS", false, false, Obj::MONSTER); + addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER); + addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L1); + addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L2); + addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L3); + addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L4); + addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L5); + addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L6); + addGroupIntoCatalog("MONSTERS", true, false, Obj::RANDOM_MONSTER_L7); + addGroupIntoCatalog("QUESTS", true, false, Obj::SEER_HUT); + addGroupIntoCatalog("QUESTS", true, false, Obj::BORDER_GATE); + addGroupIntoCatalog("QUESTS", true, false, Obj::QUEST_GUARD); + addGroupIntoCatalog("QUESTS", true, false, Obj::HUT_OF_MAGI); + addGroupIntoCatalog("QUESTS", true, false, Obj::EYE_OF_MAGI); + addGroupIntoCatalog("QUESTS", true, false, Obj::BORDERGUARD); + addGroupIntoCatalog("QUESTS", true, false, Obj::KEYMASTER); + addGroupIntoCatalog("wog object", true, false, Obj::WOG_OBJECT); + addGroupIntoCatalog("OBSTACLES", true); + addGroupIntoCatalog("OTHER", false); + } + catch(const std::exception & e) + { + QMessageBox::critical(this, "Mods loading problem", "Critical error during Mods loading. Disable invalid mods and restart."); + } +} + +void MainWindow::on_actionLevel_triggered() +{ + if(controller.map() && controller.map()->twoLevel) + { + mapLevel = mapLevel ? 0 : 1; + ui->mapView->setScene(controller.scene(mapLevel)); + ui->minimapView->setScene(controller.miniScene(mapLevel)); + if (mapLevel == 0) + { + ui->actionLevel->setToolTip(tr("View underground")); + } + else + { + ui->actionLevel->setToolTip(tr("View surface")); + } + } +} + +void MainWindow::on_actionUndo_triggered() +{ + QString str("Undo clicked"); + statusBar()->showMessage(str, 1000); + + if (controller.map()) + { + controller.undo(); + } +} + +void MainWindow::on_actionRedo_triggered() +{ + QString str("Redo clicked"); + displayStatus(str); + + if (controller.map()) + { + controller.redo(); + } +} + +void MainWindow::on_actionPass_triggered(bool checked) +{ + QString str("Passability clicked"); + displayStatus(str); + + if(controller.map()) + { + controller.scene(0)->passabilityView.show(checked); + controller.scene(1)->passabilityView.show(checked); + } +} + + +void MainWindow::on_actionGrid_triggered(bool checked) +{ + QString str("Grid clicked"); + displayStatus(str); + + if(controller.map()) + { + controller.scene(0)->gridView.show(checked); + controller.scene(0)->gridView.show(checked); + } +} + +void MainWindow::changeBrushState(int idx) +{ + +} + +void MainWindow::on_toolBrush_clicked(bool checked) +{ + //ui->toolBrush->setChecked(false); + ui->toolBrush2->setChecked(false); + ui->toolBrush4->setChecked(false); + ui->toolArea->setChecked(false); + ui->toolLasso->setChecked(false); + + if(checked) + ui->mapView->selectionTool = MapView::SelectionTool::Brush; + else + ui->mapView->selectionTool = MapView::SelectionTool::None; + + ui->tabWidget->setCurrentIndex(0); +} + +void MainWindow::on_toolBrush2_clicked(bool checked) +{ + ui->toolBrush->setChecked(false); + //ui->toolBrush2->setChecked(false); + ui->toolBrush4->setChecked(false); + ui->toolArea->setChecked(false); + ui->toolLasso->setChecked(false); + + if(checked) + ui->mapView->selectionTool = MapView::SelectionTool::Brush2; + else + ui->mapView->selectionTool = MapView::SelectionTool::None; + + ui->tabWidget->setCurrentIndex(0); +} + + +void MainWindow::on_toolBrush4_clicked(bool checked) +{ + ui->toolBrush->setChecked(false); + ui->toolBrush2->setChecked(false); + //ui->toolBrush4->setChecked(false); + ui->toolArea->setChecked(false); + ui->toolLasso->setChecked(false); + + if(checked) + ui->mapView->selectionTool = MapView::SelectionTool::Brush4; + else + ui->mapView->selectionTool = MapView::SelectionTool::None; + + ui->tabWidget->setCurrentIndex(0); +} + +void MainWindow::on_toolArea_clicked(bool checked) +{ + ui->toolBrush->setChecked(false); + ui->toolBrush2->setChecked(false); + ui->toolBrush4->setChecked(false); + //ui->toolArea->setChecked(false); + ui->toolLasso->setChecked(false); + + if(checked) + ui->mapView->selectionTool = MapView::SelectionTool::Area; + else + ui->mapView->selectionTool = MapView::SelectionTool::None; + + ui->tabWidget->setCurrentIndex(0); +} + +void MainWindow::on_actionErase_triggered() +{ + on_toolErase_clicked(); +} + +void MainWindow::on_toolErase_clicked() +{ + if(controller.map()) + { + controller.commitObjectErase(mapLevel); + } + ui->tabWidget->setCurrentIndex(0); +} + +void MainWindow::preparePreview(const QModelIndex &index, bool createNew) +{ + scenePreview->clear(); + + auto data = objectsModel.itemFromIndex(objectBrowser->mapToSource(index))->data().toJsonObject(); + + if(!data.empty()) + { + auto preview = data["preview"]; + if(preview != QJsonValue::Undefined) + { + QPixmap objPreview = pixmapFromJson(preview); + scenePreview->addPixmap(objPreview); + + auto objId = data["id"].toInt(); + auto objSubId = data["subid"].toInt(); + auto templateId = data["template"].toInt(); + + if(controller.discardObject(mapLevel) || createNew) + { + auto factory = VLC->objtypeh->getHandlerFor(objId, objSubId); + auto templ = factory->getTemplates()[templateId]; + controller.createObject(mapLevel, factory->create(templ)); + } + } + } +} + + +void MainWindow::treeViewSelected(const QModelIndex & index, const QModelIndex & deselected) +{ + preparePreview(index, false); +} + + +void MainWindow::on_treeView_activated(const QModelIndex &index) +{ + ui->toolBrush->setChecked(false); + ui->toolBrush2->setChecked(false); + ui->toolBrush4->setChecked(false); + ui->toolArea->setChecked(false); + ui->toolLasso->setChecked(false); + ui->mapView->selectionTool = MapView::SelectionTool::None; + + preparePreview(index, true); +} + + +void MainWindow::on_terrainFilterCombo_currentTextChanged(const QString &arg1) +{ + if(!objectBrowser) + return; + + objectBrowser->terrain = arg1.isEmpty() ? Terrain::ANY : Terrain(arg1.toStdString()); + objectBrowser->invalidate(); + objectBrowser->sort(0); +} + + +void MainWindow::on_filter_textChanged(const QString &arg1) +{ + if(!objectBrowser) + return; + + objectBrowser->filter = arg1; + objectBrowser->invalidate(); + objectBrowser->sort(0); +} + + +void MainWindow::on_actionFill_triggered() +{ + QString str("Fill clicked"); + displayStatus(str); + + if(!controller.map()) + return; + + controller.commitObstacleFill(mapLevel); +} + +void MainWindow::loadInspector(CGObjectInstance * obj, bool switchTab) +{ + if(switchTab) + ui->tabWidget->setCurrentIndex(1); + Inspector inspector(controller.map(), obj, ui->inspectorWidget); + inspector.updateProperties(); +} + +void MainWindow::on_inspectorWidget_itemChanged(QTableWidgetItem *item) +{ + if(!item->isSelected()) + return; + + int r = item->row(); + int c = item->column(); + if(c < 1) + return; + + auto * tableWidget = item->tableWidget(); + + //get identifier + auto identifier = tableWidget->item(0, 1)->text(); + static_assert(sizeof(CGObjectInstance *) == sizeof(decltype(identifier.toLongLong())), + "Compilied for 64 bit arcitecture. Use .toInt() method"); + + CGObjectInstance * obj = reinterpret_cast(identifier.toLongLong()); + + //get parameter name + auto param = tableWidget->item(r, c - 1)->text(); + + //set parameter + Inspector inspector(controller.map(), obj, tableWidget); + inspector.setProperty(param, item->text()); + controller.commitObjectChange(mapLevel); +} + +void MainWindow::on_actionMapSettings_triggered() +{ + auto settingsDialog = new MapSettings(controller, this); + settingsDialog->setWindowModality(Qt::WindowModal); + settingsDialog->setModal(true); +} + + +void MainWindow::on_actionPlayers_settings_triggered() +{ + auto settingsDialog = new PlayerSettings(controller, this); + settingsDialog->setWindowModality(Qt::WindowModal); + settingsDialog->setModal(true); + connect(settingsDialog, &QDialog::finished, this, &MainWindow::onPlayersChanged); +} + +QAction * MainWindow::getActionPlayer(const PlayerColor & player) +{ + if(player.getNum() == 0) return ui->actionPlayer_1; + if(player.getNum() == 1) return ui->actionPlayer_2; + if(player.getNum() == 2) return ui->actionPlayer_3; + if(player.getNum() == 3) return ui->actionPlayer_4; + if(player.getNum() == 4) return ui->actionPlayer_5; + if(player.getNum() == 5) return ui->actionPlayer_6; + if(player.getNum() == 6) return ui->actionPlayer_7; + if(player.getNum() == 7) return ui->actionPlayer_8; + return ui->actionNeutral; +} + +void MainWindow::switchDefaultPlayer(const PlayerColor & player) +{ + if(controller.defaultPlayer == player) + return; + + ui->actionNeutral->blockSignals(true); + ui->actionNeutral->setChecked(PlayerColor::NEUTRAL == player); + ui->actionNeutral->blockSignals(false); + for(int i = 0; i < 8; ++i) + { + getActionPlayer(PlayerColor(i))->blockSignals(true); + getActionPlayer(PlayerColor(i))->setChecked(PlayerColor(i) == player); + getActionPlayer(PlayerColor(i))->blockSignals(false); + } + controller.defaultPlayer = player; +} + +void MainWindow::onPlayersChanged() +{ + if(controller.map()) + { + getActionPlayer(PlayerColor::NEUTRAL)->setEnabled(true); + for(int i = 0; i < controller.map()->players.size(); ++i) + getActionPlayer(PlayerColor(i))->setEnabled(controller.map()->players.at(i).canAnyonePlay()); + if(!getActionPlayer(controller.defaultPlayer)->isEnabled() || controller.defaultPlayer == PlayerColor::NEUTRAL) + switchDefaultPlayer(PlayerColor::NEUTRAL); + } + else + { + for(int i = 0; i < PlayerColor::PLAYER_LIMIT.getNum(); ++i) + getActionPlayer(PlayerColor(i))->setEnabled(false); + getActionPlayer(PlayerColor::NEUTRAL)->setEnabled(false); + } + +} + + + +void MainWindow::enableUndo(bool enable) +{ + ui->actionUndo->setEnabled(enable); +} + +void MainWindow::enableRedo(bool enable) +{ + ui->actionRedo->setEnabled(enable); +} + +void MainWindow::onSelectionMade(int level, bool anythingSelected) +{ + if (level == mapLevel) + { + auto info = QString::asprintf("Selection on layer %d: %b", level, anythingSelected ? "true" : "false"); + setStatusMessage(info); + + ui->actionErase->setEnabled(anythingSelected); + ui->toolErase->setEnabled(anythingSelected); + } +} +void MainWindow::displayStatus(const QString& message, int timeout /* = 2000 */) +{ + statusBar()->showMessage(message, timeout); +} + +void MainWindow::on_actionValidate_triggered() +{ + new Validator(controller.map(), this); +} + + +void MainWindow::on_actionUpdate_appearance_triggered() +{ + if(!controller.map()) + return; + + if(controller.scene(mapLevel)->selectionObjectsView.getSelection().empty()) + { + QMessageBox::information(this, "Update appearance", "No objects selected"); + return; + } + + if(QMessageBox::Yes != QMessageBox::question(this, "Update appearance", "This operation is irreversible. Do you want to continue?")) + return; + + controller.scene(mapLevel)->selectionTerrainView.clear(); + + int errors = 0; + std::set staticObjects; + for(auto * obj : controller.scene(mapLevel)->selectionObjectsView.getSelection()) + { + auto handler = VLC->objtypeh->getHandlerFor(obj->ID, obj->subID); + if(!controller.map()->isInTheMap(obj->visitablePos())) + { + ++errors; + continue; + } + + auto terrain = controller.map()->getTile(obj->visitablePos()).terType; + + if(handler->isStaticObject()) + { + staticObjects.insert(obj); + if(obj->appearance->canBePlacedAt(terrain)) + { + controller.scene(mapLevel)->selectionObjectsView.deselectObject(obj); + continue; + } + + for(auto & offset : obj->appearance->getBlockedOffsets()) + controller.scene(mapLevel)->selectionTerrainView.select(obj->pos + offset); + } + else + { + auto app = handler->getOverride(terrain, obj); + if(!app) + { + if(obj->appearance->canBePlacedAt(terrain)) + continue; + + auto templates = handler->getTemplates(terrain); + if(templates.empty()) + { + ++errors; + continue; + } + app = templates.front(); + } + auto tiles = controller.mapHandler()->getTilesUnderObject(obj); + obj->appearance = app; + controller.mapHandler()->invalidate(tiles); + controller.mapHandler()->invalidate(obj); + controller.scene(mapLevel)->selectionObjectsView.deselectObject(obj); + } + } + controller.commitObjectChange(mapLevel); + controller.commitObjectErase(mapLevel); + controller.commitObstacleFill(mapLevel); + + + if(errors) + QMessageBox::warning(this, "Update appearance", QString("Errors occured. %1 objects were not updated").arg(errors)); +} + + +void MainWindow::on_actionRecreate_obstacles_triggered() +{ + +} + diff --git a/mapeditor/mainwindow.h b/mapeditor/mainwindow.h new file mode 100644 index 000000000..143c91555 --- /dev/null +++ b/mapeditor/mainwindow.h @@ -0,0 +1,148 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include "mapcontroller.h" +#include "../lib/Terrain.h" + + +class CMap; +class ObjectBrowser; +class CGObjectInstance; + +namespace Ui +{ + class MainWindow; + const QString teamName = "VCMI Team"; + const QString appName = "VCMI Map Editor"; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + + const QString mainWindowSizeSetting = "MainWindow/Size"; + const QString mainWindowPositionSetting = "MainWindow/Position"; + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + + void initializeMap(bool isNew); + + void saveMap(); + bool openMap(const QString &); + + MapView * mapView(); + + void loadObjectsTree(); + + void setStatusMessage(const QString & status); + + int getMapLevel() const {return mapLevel;} + + MapController controller; + +private slots: + void on_actionOpen_triggered(); + + void on_actionSave_as_triggered(); + + void on_actionNew_triggered(); + + void on_actionLevel_triggered(); + + void on_actionSave_triggered(); + + void on_actionErase_triggered(); + + void on_actionUndo_triggered(); + + void on_actionRedo_triggered(); + + void on_actionPass_triggered(bool checked); + + void on_actionGrid_triggered(bool checked); + + void on_toolBrush_clicked(bool checked); + + void on_toolArea_clicked(bool checked); + + void terrainButtonClicked(Terrain terrain); + void roadOrRiverButtonClicked(std::string type, bool isRoad); + + void on_toolErase_clicked(); + + void on_treeView_activated(const QModelIndex &index); + + void on_terrainFilterCombo_currentTextChanged(const QString &arg1); + + void on_filter_textChanged(const QString &arg1); + + void on_actionFill_triggered(); + + void on_toolBrush2_clicked(bool checked); + + void on_toolBrush4_clicked(bool checked); + + void on_inspectorWidget_itemChanged(QTableWidgetItem *item); + + void on_actionMapSettings_triggered(); + + void on_actionPlayers_settings_triggered(); + + void on_actionValidate_triggered(); + + void on_actionUpdate_appearance_triggered(); + + void on_actionRecreate_obstacles_triggered(); + + void switchDefaultPlayer(const PlayerColor &); + +public slots: + + void treeViewSelected(const QModelIndex &selected, const QModelIndex &deselected); + void loadInspector(CGObjectInstance * obj, bool switchTab); + void mapChanged(); + void enableUndo(bool enable); + void enableRedo(bool enable); + void onSelectionMade(int level, bool anythingSelected); + void onPlayersChanged(); + + void displayStatus(const QString& message, int timeout = 2000); + +private: + void preparePreview(const QModelIndex &index, bool createNew); + void addGroupIntoCatalog(const std::string & groupName, bool staticOnly); + void addGroupIntoCatalog(const std::string & groupName, bool useCustomName, bool staticOnly, int ID); + + QAction * getActionPlayer(const PlayerColor &); + + void changeBrushState(int idx); + void setTitle(); + + void closeEvent(QCloseEvent *event) override; + + bool getAnswerAboutUnsavedChanges(); + + void loadUserSettings(); + void saveUserSettings(); + +private: + Ui::MainWindow *ui; + ObjectBrowser * objectBrowser = nullptr; + QGraphicsScene * scenePreview; + + QString filename; + bool unsaved = false; + + QStandardItemModel objectsModel; + + int mapLevel = 0; + + std::set catalog; +}; + +#endif // MAINWINDOW_H diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui new file mode 100644 index 000000000..4764dcd16 --- /dev/null +++ b/mapeditor/mainwindow.ui @@ -0,0 +1,1120 @@ + + + MainWindow + + + + 0 + 0 + 1024 + 768 + + + + VCMI Map Editor + + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + 0 + 0 + + + + true + + + QAbstractScrollArea::AdjustToContents + + + + + + + + + 0 + 0 + 1024 + 22 + + + + + File + + + + + + + + + Map + + + + + + + + + + Edit + + + + + + + + View + + + + + + + + Player + + + + + + + + + + + + + + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + 192 + 214 + + + + + 192 + 214 + + + + 2 + + + + + 524287 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 192 + 192 + + + + + 192 + 192 + + + + Qt::ScrollBarAlwaysOff + + + Qt::ScrollBarAlwaysOff + + + + + + + + + + 0 + 0 + + + + + 268 + 196 + + + + + 524287 + 524287 + + + + 2 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + 0 + + + + + 0 + 0 + + + + Browser + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + false + + + Qt::ClickFocus + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoDragDrop + + + QAbstractItemView::SelectItems + + + + 32 + 32 + + + + 12 + + + true + + + true + + + + + + + + Inspector + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 10 + + + + QAbstractItemView::AnyKeyPressed|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + QAbstractItemView::SingleSelection + + + 2 + + + false + + + 20 + + + + Property + + + + + Value + + + + + + + + + + + + + + + + 0 + 0 + + + + + 128 + 496 + + + + 1 + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Brush + + + + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + 1 + + + true + + + false + + + + + + + true + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + 2 + + + true + + + false + + + + + + + true + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + 4 + + + true + + + false + + + + + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + [] + + + true + + + false + + + + + + + false + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + O + + + true + + + false + + + + + + + false + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + E + + + false + + + false + + + + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + 0 + + + + + 0 + 0 + 128 + 271 + + + + + 0 + 0 + + + + Terrains + + + + 1 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 1 + + + + + + + + + 0 + 0 + 128 + 271 + + + + + 0 + 0 + + + + Roads + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + 0 + 0 + 128 + 271 + + + + + 0 + 0 + + + + Rivers + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + 128 + 128 + + + + + 128 + 128 + + + + + + + + + + Open + + + Ctrl+O + + + + + Save + + + Ctrl+S + + + + + New + + + Ctrl+N + + + + + Save as + + + Ctrl+Shift+S + + + + + U/G + + + View underground + + + U + + + + + true + + + Pass + + + P + + + + + Cut + + + Ctrl+X + + + + + Copy + + + Ctrl+C + + + + + Paste + + + Ctrl+V + + + + + Fill + + + Fills the selection with obstacles + + + F + + + + + true + + + Grid + + + G + + + + + false + + + General + + + Map title and description + + + + + Players settings + + + + + false + + + Undo + + + Undo + + + Ctrl+Z + + + true + + + + + false + + + Redo + + + Ctrl+Y + + + true + + + + + false + + + Erase + + + Backspace, Del + + + + + true + + + Neutral + + + Ctrl+0 + + + + + Validate + + + + + false + + + Update appearance + + + + + false + + + Recreate obstacles + + + + + true + + + Player 1 + + + Ctrl+1 + + + + + true + + + Player 2 + + + Ctrl+2 + + + + + true + + + Player 3 + + + Ctrl+3 + + + + + true + + + Player 4 + + + Ctrl+4 + + + + + true + + + Player 5 + + + Ctrl+5 + + + + + true + + + Player 6 + + + Ctrl+6 + + + + + true + + + Player 7 + + + Ctrl+7 + + + + + true + + + Player 8 + + + Ctrl+8 + + + + + + MapView + QGraphicsView +
mapview.h
+
+ + MinimapView + QGraphicsView +
mapview.h
+
+
+ + + + enableUndo(bool) + enableRedo(bool) + +
diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp new file mode 100644 index 000000000..f5c8c004d --- /dev/null +++ b/mapeditor/mapcontroller.cpp @@ -0,0 +1,466 @@ +#include "mapcontroller.h" + +#include "../lib/GameConstants.h" +#include "../lib/mapping/CMapService.h" +#include "../lib/mapping/CMap.h" +#include "../lib/mapping/CMapEditManager.h" +#include "../lib/Terrain.h" +#include "../lib/mapObjects/CObjectClassesHandler.h" +#include "../lib/rmg/ObstaclePlacer.h" +#include "../lib/CSkillHandler.h" +#include "../lib/spells/CSpellHandler.h" +#include "../lib/CHeroHandler.h" +#include "mapview.h" +#include "scenelayer.h" +#include "maphandler.h" +#include "mainwindow.h" +#include "inspector/inspector.h" + + +MapController::MapController(MainWindow * m): main(m) +{ + _scenes[0].reset(new MapScene(0)); + _scenes[1].reset(new MapScene(1)); + _miniscenes[0].reset(new MinimapScene(0)); + _miniscenes[1].reset(new MinimapScene(1)); + connectScenes(); +} + +void MapController::connectScenes() +{ + for (int level = 0; level <= 1; level++) + { + //selections for both layers will be handled separately + QObject::connect(_scenes[level].get(), &MapScene::selected, [this, level](bool anythingSelected) + { + main->onSelectionMade(level, anythingSelected); + }); + } +} + +MapController::~MapController() +{ +} + +const std::unique_ptr & MapController::getMapUniquePtr() const +{ + return _map; +} + +CMap * MapController::map() +{ + return _map.get(); +} + +MapHandler * MapController::mapHandler() +{ + return _mapHandler.get(); +} + +MapScene * MapController::scene(int level) +{ + return _scenes[level].get(); +} + +MinimapScene * MapController::miniScene(int level) +{ + return _miniscenes[level].get(); +} + +void MapController::repairMap() +{ + //fix owners for objects + for(auto obj : _map->objects) + { + if(obj->getOwner() == PlayerColor::UNFLAGGABLE) + { + if(dynamic_cast(obj.get()) || + dynamic_cast(obj.get()) || + dynamic_cast(obj.get()) || + dynamic_cast(obj.get()) || + dynamic_cast(obj.get()) || + dynamic_cast(obj.get())) + obj->tempOwner = PlayerColor::NEUTRAL; + } + //fix hero instance + if(auto * nih = dynamic_cast(obj.get())) + { + auto type = VLC->heroh->objects[nih->subID]; + + if(nih->ID == Obj::HERO) + nih->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); + //fix spells + if(nih->spellbookContainsSpell(SpellID::PRESET)) + { + nih->removeSpellFromSpellbook(SpellID::PRESET); + } + else + { + for(auto spellID : type->spells) + nih->addSpellToSpellbook(spellID); + } + //fix portrait + if(nih->portrait < 0 || nih->portrait == 255) + nih->portrait = type->imageIndex; + } + //fix town instance + if(auto * tnh = dynamic_cast(obj.get())) + { + if(tnh->getTown()) + { + vstd::erase_if(tnh->builtBuildings, [tnh](BuildingID bid) + { + return !tnh->getTown()->buildings.count(bid); + }); + vstd::erase_if(tnh->forbiddenBuildings, [tnh](BuildingID bid) + { + return !tnh->getTown()->buildings.count(bid); + }); + } + } + + } + + //there might be extra skills, arts and spells not imported from map + if(VLC->skillh->getDefaultAllowed().size() > map()->allowedAbilities.size()) + { + for(int i = map()->allowedAbilities.size(); i < VLC->skillh->getDefaultAllowed().size(); ++i) + map()->allowedAbilities.push_back(false); + } + if(VLC->arth->getDefaultAllowed().size() > map()->allowedArtifact.size()) + { + for(int i = map()->allowedArtifact.size(); i < VLC->arth->getDefaultAllowed().size(); ++i) + map()->allowedArtifact.push_back(false); + } + if(VLC->spellh->getDefaultAllowed().size() > map()->allowedSpell.size()) + { + for(int i = map()->allowedSpell.size(); i < VLC->spellh->getDefaultAllowed().size(); ++i) + map()->allowedSpell.push_back(false); + } +} + +void MapController::setMap(std::unique_ptr cmap) +{ + _map = std::move(cmap); + + repairMap(); + + _scenes[0].reset(new MapScene(0)); + _scenes[1].reset(new MapScene(1)); + _miniscenes[0].reset(new MinimapScene(0)); + _miniscenes[1].reset(new MinimapScene(1)); + resetMapHandler(); + sceneForceUpdate(); + + connectScenes(); + + _map->getEditManager()->getUndoManager().setUndoCallback([this](bool allowUndo, bool allowRedo) + { + main->enableUndo(allowUndo); + main->enableRedo(allowRedo); + } + ); +} + +void MapController::sceneForceUpdate() +{ + _scenes[0]->updateViews(); + _miniscenes[0]->updateViews(); + if(_map->twoLevel) + { + _scenes[1]->updateViews(); + _miniscenes[1]->updateViews(); + } +} + +void MapController::sceneForceUpdate(int level) +{ + _scenes[level]->updateViews(); + _miniscenes[level]->updateViews(); +} + +void MapController::resetMapHandler() +{ + if(!_mapHandler) + _mapHandler.reset(new MapHandler()); + _mapHandler->reset(map()); + _scenes[0]->initialize(*this); + _scenes[1]->initialize(*this); + _miniscenes[0]->initialize(*this); + _miniscenes[1]->initialize(*this); +} + +void MapController::commitTerrainChange(int level, const Terrain & terrain) +{ + std::vector v(_scenes[level]->selectionTerrainView.selection().begin(), + _scenes[level]->selectionTerrainView.selection().end()); + if(v.empty()) + return; + + _scenes[level]->selectionTerrainView.clear(); + _scenes[level]->selectionTerrainView.draw(); + + _map->getEditManager()->getTerrainSelection().setSelection(v); + _map->getEditManager()->drawTerrain(terrain, &CRandomGenerator::getDefault()); + + for(auto & t : v) + _scenes[level]->terrainView.setDirty(t); + _scenes[level]->terrainView.draw(); + + _miniscenes[level]->updateViews(); + main->mapChanged(); +} + +void MapController::commitRoadOrRiverChange(int level, const std::string & type, bool isRoad) +{ + std::vector v(_scenes[level]->selectionTerrainView.selection().begin(), + _scenes[level]->selectionTerrainView.selection().end()); + if(v.empty()) + return; + + _scenes[level]->selectionTerrainView.clear(); + _scenes[level]->selectionTerrainView.draw(); + + _map->getEditManager()->getTerrainSelection().setSelection(v); + if(isRoad) + _map->getEditManager()->drawRoad(type, &CRandomGenerator::getDefault()); + else + _map->getEditManager()->drawRiver(type, &CRandomGenerator::getDefault()); + + for(auto & t : v) + _scenes[level]->terrainView.setDirty(t); + _scenes[level]->terrainView.draw(); + + _miniscenes[level]->updateViews(); + main->mapChanged(); +} + +void MapController::commitObjectErase(int level) +{ + auto selectedObjects = _scenes[level]->selectionObjectsView.getSelection(); + if (selectedObjects.size() > 1) + { + //mass erase => undo in one operation + _map->getEditManager()->removeObjects(selectedObjects); + } + else if (selectedObjects.size() == 1) + { + _map->getEditManager()->removeObject(*selectedObjects.begin()); + } + else //nothing to erase - shouldn't be here + { + return; + } + + for (auto obj : selectedObjects) + { + //invalidate tiles under objects + _mapHandler->invalidate(_mapHandler->getTilesUnderObject(obj)); + } + + _scenes[level]->selectionObjectsView.clear(); + _scenes[level]->objectsView.draw(); + _scenes[level]->selectionObjectsView.draw(); + _scenes[level]->passabilityView.update(); + + _miniscenes[level]->updateViews(); + main->mapChanged(); +} + +bool MapController::discardObject(int level) const +{ + _scenes[level]->selectionObjectsView.clear(); + if(_scenes[level]->selectionObjectsView.newObject) + { + delete _scenes[level]->selectionObjectsView.newObject; + _scenes[level]->selectionObjectsView.newObject = nullptr; + _scenes[level]->selectionObjectsView.shift = QPoint(0, 0); + _scenes[level]->selectionObjectsView.selectionMode = 0; + _scenes[level]->selectionObjectsView.draw(); + return true; + } + return false; +} + +void MapController::createObject(int level, CGObjectInstance * obj) const +{ + _scenes[level]->selectionObjectsView.newObject = obj; + _scenes[level]->selectionObjectsView.selectionMode = 2; + _scenes[level]->selectionObjectsView.draw(); +} + +void MapController::commitObstacleFill(int level) +{ + auto selection = _scenes[level]->selectionTerrainView.selection(); + if(selection.empty()) + return; + + //split by zones + std::map terrainSelected; + for(auto & t : selection) + { + auto tl = _map->getTile(t); + if(tl.blocked || tl.visitable) + continue; + + terrainSelected[tl.terType].blockedArea.add(t); + } + + for(auto & sel : terrainSelected) + { + sel.second.collectPossibleObstacles(sel.first); + sel.second.placeObstacles(_map.get(), CRandomGenerator::getDefault()); + } + + _mapHandler->invalidateObjects(); + + _scenes[level]->selectionTerrainView.clear(); + _scenes[level]->selectionTerrainView.draw(); + _scenes[level]->objectsView.draw(); + _scenes[level]->passabilityView.update(); + + _miniscenes[level]->updateViews(); + main->mapChanged(); +} + +void MapController::commitObjectChange(int level) +{ + //for( auto * o : _scenes[level]->selectionObjectsView.getSelection()) + //_mapHandler->invalidate(o); + + _scenes[level]->objectsView.draw(); + _scenes[level]->selectionObjectsView.draw(); + _scenes[level]->passabilityView.update(); + + _miniscenes[level]->updateViews(); + main->mapChanged(); +} + + +void MapController::commitChangeWithoutRedraw() +{ + //DO NOT REDRAW + main->mapChanged(); +} + +void MapController::commitObjectShift(int level) +{ + auto shift = _scenes[level]->selectionObjectsView.shift; + bool makeShift = !shift.isNull(); + if(makeShift) + { + for(auto * obj : _scenes[level]->selectionObjectsView.getSelection()) + { + int3 pos = obj->pos; + pos.z = level; + pos.x += shift.x(); pos.y += shift.y(); + + auto prevPositions = _mapHandler->getTilesUnderObject(obj); + _map->getEditManager()->moveObject(obj, pos); + _mapHandler->invalidate(prevPositions); + _mapHandler->invalidate(obj); + } + } + + _scenes[level]->selectionObjectsView.newObject = nullptr; + _scenes[level]->selectionObjectsView.shift = QPoint(0, 0); + _scenes[level]->selectionObjectsView.selectionMode = 0; + + if(makeShift) + { + _scenes[level]->objectsView.draw(); + _scenes[level]->selectionObjectsView.draw(); + _scenes[level]->passabilityView.update(); + + _miniscenes[level]->updateViews(); + main->mapChanged(); + } +} + +void MapController::commitObjectCreate(int level) +{ + auto * newObj = _scenes[level]->selectionObjectsView.newObject; + if(!newObj) + return; + + auto shift = _scenes[level]->selectionObjectsView.shift; + + int3 pos = newObj->pos; + pos.z = level; + pos.x += shift.x(); pos.y += shift.y(); + + newObj->pos = pos; + + Initializer init(newObj, defaultPlayer); + + _map->getEditManager()->insertObject(newObj); + _mapHandler->invalidate(newObj); + + _scenes[level]->selectionObjectsView.newObject = nullptr; + _scenes[level]->selectionObjectsView.shift = QPoint(0, 0); + _scenes[level]->selectionObjectsView.selectionMode = 0; + _scenes[level]->objectsView.draw(); + _scenes[level]->selectionObjectsView.draw(); + _scenes[level]->passabilityView.update(); + + _miniscenes[level]->updateViews(); + main->mapChanged(); +} + +bool MapController::canPlaceObject(int level, CGObjectInstance * newObj, QString & error) const +{ + //need this because of possible limits + auto rmgInfo = VLC->objtypeh->getHandlerFor(newObj->ID, newObj->subID)->getRMGInfo(); + + //find all objects of such type + int objCounter = 0; + for(auto o : _map->objects) + { + if(o->ID == newObj->ID && o->subID == newObj->subID) + { + ++objCounter; + } + } + + if((rmgInfo.mapLimit && objCounter >= rmgInfo.mapLimit) + || (newObj->ID == Obj::GRAIL && objCounter >= 1)) //special case for grail + { + auto typeName = QString::fromStdString(newObj->typeName); + auto subTypeName = QString::fromStdString(newObj->subTypeName); + error = QString("Reached map limit for object %1 - %2").arg(typeName, subTypeName); + return false; //maplimit reached + } + if(defaultPlayer == PlayerColor::NEUTRAL && (newObj->ID == Obj::HERO || newObj->ID == Obj::RANDOM_HERO)) + { + error = "Hero cannot be created as NEUTRAL"; + return false; + } + if(defaultPlayer != PlayerColor::NEUTRAL && newObj->ID == Obj::PRISON) + { + error = "Prison must be a NEUTRAL"; + return false; + } + + if(newObj->ID == Obj::ARTIFACT && !_map->allowedArtifact.at(newObj->subID)) + { + error = "Artifact is not allowed. Check map settings."; + return false; + } + return true; +} + +void MapController::undo() +{ + _map->getEditManager()->getUndoManager().undo(); + resetMapHandler(); + sceneForceUpdate(); + main->mapChanged(); +} + +void MapController::redo() +{ + _map->getEditManager()->getUndoManager().redo(); + resetMapHandler(); + sceneForceUpdate(); + main->mapChanged(); +} diff --git a/mapeditor/mapcontroller.h b/mapeditor/mapcontroller.h new file mode 100644 index 000000000..09fbc5d6c --- /dev/null +++ b/mapeditor/mapcontroller.h @@ -0,0 +1,62 @@ +#ifndef MAPCONTROLLER_H +#define MAPCONTROLLER_H + +#include "maphandler.h" +#include "mapview.h" +#include "../lib/mapping/CMap.h" +#include "../lib/Terrain.h" + +class MainWindow; +class MapController +{ +public: + MapController(MainWindow *); + MapController(const MapController &) = delete; + MapController(const MapController &&) = delete; + ~MapController(); + + void setMap(std::unique_ptr); + + void repairMap(); + + const std::unique_ptr & getMapUniquePtr() const; //to be used for map saving + CMap * map(); + MapHandler * mapHandler(); + MapScene * scene(int level); + MinimapScene * miniScene(int level); + + void resetMapHandler(); + + void sceneForceUpdate(); + void sceneForceUpdate(int level); + + void commitTerrainChange(int level, const Terrain & terrain); + void commitRoadOrRiverChange(int level, const std::string & type, bool isRoad); + void commitObjectErase(const CGObjectInstance* obj); + void commitObjectErase(int level); + void commitObstacleFill(int level); + void commitChangeWithoutRedraw(); + void commitObjectShift(int level); + void commitObjectCreate(int level); + void commitObjectChange(int level); + + bool discardObject(int level) const; + void createObject(int level, CGObjectInstance * obj) const; + bool canPlaceObject(int level, CGObjectInstance * obj, QString & error) const; + + void undo(); + void redo(); + + PlayerColor defaultPlayer; + +private: + std::unique_ptr _map; + std::unique_ptr _mapHandler; + MainWindow * main; + mutable std::array, 2> _scenes; + mutable std::array, 2> _miniscenes; + + void connectScenes(); +}; + +#endif // MAPCONTROLLER_H diff --git a/mapeditor/mapeditor.ico b/mapeditor/mapeditor.ico new file mode 100644 index 0000000000000000000000000000000000000000..544c420fa7cb14c76849354a80810bfd89ca043f GIT binary patch literal 81276 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 literal 0 HcmV?d00001 diff --git a/mapeditor/mapeditor.rc b/mapeditor/mapeditor.rc new file mode 100644 index 000000000..ac6469878 --- /dev/null +++ b/mapeditor/mapeditor.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON "mapeditor.ico" diff --git a/mapeditor/maphandler.cpp b/mapeditor/maphandler.cpp new file mode 100644 index 000000000..0e01a0864 --- /dev/null +++ b/mapeditor/maphandler.cpp @@ -0,0 +1,538 @@ +#include "StdInc.h" +#include "maphandler.h" +#include "graphics.h" +#include "../lib/mapping/CMap.h" +#include "../lib/mapObjects/CGHeroInstance.h" +#include "../lib/mapObjects/CObjectClassesHandler.h" +#include "../lib/CHeroHandler.h" +#include "../lib/CTownHandler.h" +#include "../lib/CModHandler.h" +#include "../lib/mapping/CMap.h" +#include "../lib/GameConstants.h" +#include "../lib/JsonDetail.h" + +const int tileSize = 32; + +static bool objectBlitOrderSorter(const TileObject & a, const TileObject & b) +{ + return MapHandler::compareObjectBlitOrder(a.obj, b.obj); +} + +int MapHandler::index(int x, int y, int z) const +{ + return z * (sizes.x * sizes.y) + y * sizes.x + x; +} + +int MapHandler::index(const int3 & p) const +{ + return index(p.x, p.y, p.z); +} + +MapHandler::MapHandler() +{ + initTerrainGraphics(); + logGlobal->info("\tPreparing terrain, roads, rivers, borders"); +} + +void MapHandler::reset(const CMap * Map) +{ + ttiles.clear(); + map = Map; + + //sizes of terrain + sizes.x = map->width; + sizes.y = map->height; + sizes.z = map->twoLevel ? 2 : 1; + + initObjectRects(); + logGlobal->info("\tMaking object rects"); +} + +void MapHandler::initTerrainGraphics() +{ + static const std::map ROAD_FILES = + { + {ROAD_NAMES[1], "dirtrd"}, + {ROAD_NAMES[2], "gravrd"}, + {ROAD_NAMES[3], "cobbrd"} + }; + + static const std::map RIVER_FILES = + { + {RIVER_NAMES[1], "clrrvr"}, + {RIVER_NAMES[2], "icyrvr"}, + {RIVER_NAMES[3], "mudrvr"}, + {RIVER_NAMES[4], "lavrvr"} + }; + + + auto loadFlipped = [](TFlippedAnimations & animation, TFlippedCache & cache, const std::map & files) + { + for(auto & type : files) + { + animation[type.first] = make_unique(type.second); + animation[type.first]->preload(); + const size_t views = animation[type.first]->size(0); + cache[type.first].resize(views); + + for(int j = 0; j < views; j++) + cache[type.first][j] = animation[type.first]->getImage(j); + } + }; + + std::map terrainFiles; + for(auto & terrain : Terrain::Manager::terrains()) + { + terrainFiles[terrain] = Terrain::Manager::getInfo(terrain).tilesFilename; + } + + loadFlipped(terrainAnimations, terrainImages, terrainFiles); + loadFlipped(roadAnimations, roadImages, ROAD_FILES); + loadFlipped(riverAnimations, riverImages, RIVER_FILES); +} + +void MapHandler::drawTerrainTile(QPainter & painter, int x, int y, int z) +{ + auto & tinfo = map->getTile(int3(x, y, z)); + ui8 rotation = tinfo.extTileFlags % 4; + + if(terrainImages.at(tinfo.terType).size() <= tinfo.terView) + return; + + bool hflip = (rotation == 1 || rotation == 3), vflip = (rotation == 2 || rotation == 3); + painter.drawImage(x * tileSize, y * tileSize, terrainImages.at(tinfo.terType)[tinfo.terView]->mirrored(hflip, vflip)); +} + +void MapHandler::drawRoad(QPainter & painter, int x, int y, int z) +{ + auto & tinfo = map->getTile(int3(x, y, z)); + auto * tinfoUpper = map->isInTheMap(int3(x, y - 1, z)) ? &map->getTile(int3(x, y - 1, z)) : nullptr; + + if (tinfoUpper && tinfoUpper->roadType != ROAD_NAMES[0]) + { + QRect source(0, tileSize / 2, tileSize, tileSize / 2); + ui8 rotation = (tinfoUpper->extTileFlags >> 4) % 4; + bool hflip = (rotation == 1 || rotation == 3), vflip = (rotation == 2 || rotation == 3); + if(roadImages.at(tinfoUpper->roadType).size() > tinfoUpper->roadDir) + { + painter.drawImage(QPoint(x * tileSize, y * tileSize), roadImages.at(tinfoUpper->roadType)[tinfoUpper->roadDir]->mirrored(hflip, vflip), source); + } + } + + if(tinfo.roadType != ROAD_NAMES[0]) //print road from this tile + { + QRect source(0, 0, tileSize, tileSize / 2); + ui8 rotation = (tinfo.extTileFlags >> 4) % 4; + bool hflip = (rotation == 1 || rotation == 3), vflip = (rotation == 2 || rotation == 3); + if(roadImages.at(tinfo.roadType).size() > tinfo.roadDir) + { + painter.drawImage(QPoint(x * tileSize, y * tileSize + tileSize / 2), roadImages.at(tinfo.roadType)[tinfo.roadDir]->mirrored(hflip, vflip), source); + } + } +} + +void MapHandler::drawRiver(QPainter & painter, int x, int y, int z) +{ + auto & tinfo = map->getTile(int3(x, y, z)); + + if(tinfo.riverType == RIVER_NAMES[0]) + return; + + if(riverImages.at(tinfo.riverType).size() <= tinfo.riverDir) + return; + + ui8 rotation = (tinfo.extTileFlags >> 2) % 4; + bool hflip = (rotation == 1 || rotation == 3), vflip = (rotation == 2 || rotation == 3); + + painter.drawImage(x * tileSize, y * tileSize, riverImages.at(tinfo.riverType)[tinfo.riverDir]->mirrored(hflip, vflip)); +} + +void setPlayerColor(QImage * sur, PlayerColor player) +{ + if(player == PlayerColor::UNFLAGGABLE) + return; + if(sur->format() == QImage::Format_Indexed8) + { + QRgb color = graphics->neutralColor; + if(player != PlayerColor::NEUTRAL && player < PlayerColor::PLAYER_LIMIT) + color = graphics->playerColors.at(player.getNum()); + + sur->setColor(5, color); + } + else + logGlobal->warn("Warning, setPlayerColor called on not 8bpp surface!"); +} + +void MapHandler::initObjectRects() +{ + ttiles.resize(sizes.x * sizes.y * sizes.z); + + //initializing objects / rects + for(const CGObjectInstance * elem : map->objects) + { + CGObjectInstance *obj = const_cast(elem); + if( !obj + || (obj->ID==Obj::HERO && static_cast(obj)->inTownGarrison) //garrisoned hero + || (obj->ID==Obj::BOAT && static_cast(obj)->hero)) //boat with hero (hero graphics is used) + { + continue; + } + + std::shared_ptr animation = graphics->getAnimation(obj); + + //no animation at all + if(!animation) + continue; + + //empty animation + if(animation->size(0) == 0) + continue; + + auto image = animation->getImage(0, obj->ID == Obj::HERO ? 2 : 0); + if(!image) + { + //workaound for prisons + image = animation->getImage(0, 0); + if(!image) + continue; + } + + + for(int fx=0; fx < obj->getWidth(); ++fx) + { + for(int fy=0; fy < obj->getHeight(); ++fy) + { + int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); + QRect cr(image->width() - fx * tileSize - tileSize, + image->height() - fy * tileSize - tileSize, + image->width(), + image->height()); + + TileObject toAdd(obj, cr); + + if( map->isInTheMap(currTile) && // within map + cr.x() + cr.width() > 0 && // image has data on this tile + cr.y() + cr.height() > 0 && + obj->coveringAt(currTile.x, currTile.y) // object is visible here + ) + { + ttiles[index(currTile)].push_back(toAdd); + } + } + } + } + + for(auto & tt : ttiles) + { + stable_sort(tt.begin(), tt.end(), objectBlitOrderSorter); + } +} + +bool MapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b) +{ + if (!a) + return true; + if (!b) + return false; + if (a->appearance->printPriority != b->appearance->printPriority) + return a->appearance->printPriority > b->appearance->printPriority; + + if(a->pos.y != b->pos.y) + return a->pos.y < b->pos.y; + + if(b->ID==Obj::HERO && a->ID!=Obj::HERO) + return true; + if(b->ID!=Obj::HERO && a->ID==Obj::HERO) + return false; + + if(!a->isVisitable() && b->isVisitable()) + return true; + if(!b->isVisitable() && a->isVisitable()) + return false; + if(a->pos.x < b->pos.x) + return true; + return false; +} + +TileObject::TileObject(CGObjectInstance * obj_, QRect rect_) +: obj(obj_), +rect(rect_) +{ +} + +TileObject::~TileObject() +{ +} + +std::shared_ptr MapHandler::findFlagBitmap(const CGHeroInstance * hero, int anim, const PlayerColor color, int group) const +{ + if(!hero || hero->boat) + return std::shared_ptr(); + + return findFlagBitmapInternal(graphics->heroFlagAnimations.at(color.getNum()), anim, group, hero->moveDir, !hero->isStanding); +} + +std::shared_ptr MapHandler::findFlagBitmapInternal(std::shared_ptr animation, int anim, int group, ui8 dir, bool moving) const +{ + size_t groupSize = animation->size(group); + if(groupSize == 0) + return nullptr; + + if(moving) + return animation->getImage(anim % groupSize, group); + else + return animation->getImage((anim / 4) % groupSize, group); +} + +MapHandler::AnimBitmapHolder MapHandler::findObjectBitmap(const CGObjectInstance * obj, int anim, int group) const +{ + if(!obj) + return MapHandler::AnimBitmapHolder(); + + // normal object + std::shared_ptr animation = graphics->getAnimation(obj); + size_t groupSize = animation->size(group); + if(groupSize == 0) + return MapHandler::AnimBitmapHolder(); + + animation->playerColored(obj->tempOwner); + auto bitmap = animation->getImage(anim % groupSize, group); + + if(!bitmap) + return MapHandler::AnimBitmapHolder(); + + setPlayerColor(bitmap.get(), obj->tempOwner); + + return MapHandler::AnimBitmapHolder(bitmap); +} + +std::vector & MapHandler::getObjects(int x, int y, int z) +{ + return ttiles[index(x, y, z)]; +} + +void MapHandler::drawObjects(QPainter & painter, int x, int y, int z) +{ + for(auto & object : getObjects(x, y, z)) + { + const CGObjectInstance * obj = object.obj; + if(!obj) + { + logGlobal->error("Stray map object that isn't fading"); + return; + } + + uint8_t animationFrame = 0; + + auto objData = findObjectBitmap(obj, animationFrame, obj->ID == Obj::HERO ? 2 : 0); + if(obj->ID == Obj::HERO && obj->tempOwner.isValidPlayer()) + objData.flagBitmap = findFlagBitmap(dynamic_cast(obj), 0, obj->tempOwner, 4); + + if(objData.objBitmap) + { + auto pos = obj->getPosition(); + + painter.drawImage(QPoint(x * tileSize, y * tileSize), *objData.objBitmap, object.rect); + + if(objData.flagBitmap) + { + if(x == pos.x && y == pos.y) + painter.drawImage(QPoint((x - 2) * tileSize, (y - 1) * tileSize), *objData.flagBitmap); + } + } + } +} + +void MapHandler::drawObject(QPainter & painter, const TileObject & object) +{ + const CGObjectInstance * obj = object.obj; + if (!obj) + { + logGlobal->error("Stray map object that isn't fading"); + return; + } + + uint8_t animationFrame = 0; + + auto objData = findObjectBitmap(obj, animationFrame, obj->ID == Obj::HERO ? 2 : 0); + if(obj->ID == Obj::HERO && obj->tempOwner.isValidPlayer()) + objData.flagBitmap = findFlagBitmap(dynamic_cast(obj), 0, obj->tempOwner, 0); + + if (objData.objBitmap) + { + auto pos = obj->getPosition(); + + painter.drawImage(pos.x * tileSize - object.rect.x(), pos.y * tileSize - object.rect.y(), *objData.objBitmap); + + if (objData.flagBitmap) + { + if(object.rect.x() == pos.x && object.rect.y() == pos.y) + painter.drawImage(pos.x * tileSize - object.rect.x(), pos.y * tileSize - object.rect.y(), *objData.flagBitmap); + } + } +} + + +void MapHandler::drawObjectAt(QPainter & painter, const CGObjectInstance * obj, int x, int y) +{ + if (!obj) + { + logGlobal->error("Stray map object that isn't fading"); + return; + } + + uint8_t animationFrame = 0; + + auto objData = findObjectBitmap(obj, animationFrame, obj->ID == Obj::HERO ? 2 : 0); + std::vector> debugFlagImages; + if(obj->ID == Obj::HERO && obj->tempOwner.isValidPlayer()) + objData.flagBitmap = findFlagBitmap(dynamic_cast(obj), 0, obj->tempOwner, 4); + + if (objData.objBitmap) + { + painter.drawImage(QPoint((x + 1) * 32 - objData.objBitmap->width(), (y + 1) * 32 - objData.objBitmap->height()), *objData.objBitmap); + + if (objData.flagBitmap) + painter.drawImage(QPoint((x + 1) * 32 - objData.objBitmap->width(), (y + 1) * 32 - objData.objBitmap->height()), *objData.flagBitmap); + } +} + +QRgb MapHandler::getTileColor(int x, int y, int z) +{ + // if object at tile is owned - it will be colored as its owner + for(auto & object : getObjects(x, y, z)) + { + if(!object.obj->getBlockedPos().count(int3(x, y, z))) + continue; + + PlayerColor player = object.obj->getOwner(); + if(player == PlayerColor::NEUTRAL) + return graphics->neutralColor; + else + if (player < PlayerColor::PLAYER_LIMIT) + return graphics->playerColors[player.getNum()]; + } + + // else - use terrain color (blocked version or normal) + auto & tile = map->getTile(int3(x, y, z)); + auto color = Terrain::Manager::getInfo(tile.terType).minimapUnblocked; + if (tile.blocked && (!tile.visitable)) + color = Terrain::Manager::getInfo(tile.terType).minimapBlocked; + + return qRgb(color[0], color[1], color[2]); +} + +void MapHandler::drawMinimapTile(QPainter & painter, int x, int y, int z) +{ + painter.setPen(getTileColor(x, y, z)); + painter.drawPoint(x, y); +} + +void MapHandler::invalidate(int x, int y, int z) +{ + auto & objects = getObjects(x, y, z); + + for(auto obj = objects.begin(); obj != objects.end();) + { + //object was removed + if(std::find(map->objects.begin(), map->objects.end(), obj->obj) == map->objects.end()) + { + obj = objects.erase(obj); + continue; + } + + //object was moved + auto & pos = obj->obj->pos; + if(pos.z != z || pos.x < x || pos.y < y || pos.x - obj->obj->getWidth() >= x || pos.y - obj->obj->getHeight() >= y) + { + obj = objects.erase(obj); + continue; + } + + ++obj; + } + + stable_sort(objects.begin(), objects.end(), objectBlitOrderSorter); +} + +void MapHandler::invalidate(CGObjectInstance * obj) +{ + std::shared_ptr animation = graphics->getAnimation(obj); + + //no animation at all or empty animation + if(!animation || animation->size(0) == 0) + return; + + auto image = animation->getImage(0, obj->ID == Obj::HERO ? 2 : 0); + if(!image) + return; + + for(int fx=0; fx < obj->getWidth(); ++fx) + { + for(int fy=0; fy < obj->getHeight(); ++fy) + { + //object presented on the tile + int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); + QRect cr(image->width() - fx * tileSize - tileSize, image->height() - fy * tileSize - tileSize, image->width(), image->height()); + + if( map->isInTheMap(currTile) && // within map + cr.x() + cr.width() > 0 && // image has data on this tile + cr.y() + cr.height() > 0 && + obj->coveringAt(currTile.x, currTile.y) // object is visible here + ) + { + auto & objects = ttiles[index(currTile)]; + bool found = false; + for(auto & o : objects) + { + if(o.obj == obj) + { + o.rect = cr; + found = true; + break; + } + } + if(!found) + objects.emplace_back(obj, cr); + + stable_sort(objects.begin(), objects.end(), objectBlitOrderSorter); + } + } + } +} + +std::vector MapHandler::getTilesUnderObject(CGObjectInstance * obj) const +{ + std::vector result; + for(int fx=0; fx < obj->getWidth(); ++fx) + { + for(int fy=0; fy < obj->getHeight(); ++fy) + { + //object presented on the tile + int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); + if(map->isInTheMap(currTile) && // within map + obj->coveringAt(currTile.x, currTile.y) // object is visible here + ) + { + result.push_back(currTile); + } + } + } + return result; +} + +void MapHandler::invalidateObjects() +{ + for(auto obj : map->objects) + { + invalidate(obj); + } +} + +void MapHandler::invalidate(const std::vector & tiles) +{ + for(auto & currTile : tiles) + { + invalidate(currTile.x, currTile.y, currTile.z); + } +} diff --git a/mapeditor/maphandler.h b/mapeditor/maphandler.h new file mode 100644 index 000000000..39423df02 --- /dev/null +++ b/mapeditor/maphandler.h @@ -0,0 +1,107 @@ +#ifndef MAPHANDLER_H +#define MAPHANDLER_H + +#include "StdInc.h" +#include "../lib/mapping/CMap.h" +#include "Animation.h" + +#include +#include +#include + +class CGObjectInstance; +class CGBoat; +class PlayerColor; + +struct TileObject +{ + CGObjectInstance *obj; + QRect rect; + + TileObject(CGObjectInstance *obj_, QRect rect_); + ~TileObject(); +}; + +using TileObjects = std::vector; //pointers to objects being on this tile with rects to be easier to blit this tile on screen + +class MapHandler +{ +public: + struct AnimBitmapHolder + { + std::shared_ptr objBitmap; // main object bitmap + std::shared_ptr flagBitmap; // flag bitmap for the object (probably only for heroes and boats with heroes) + + AnimBitmapHolder(std::shared_ptr objBitmap_ = nullptr, std::shared_ptr flagBitmap_ = nullptr) + : objBitmap(objBitmap_), + flagBitmap(flagBitmap_) + {} + }; + +private: + + int index(int x, int y, int z) const; + int index(const int3 &) const; + + std::shared_ptr findFlagBitmapInternal(std::shared_ptr animation, int anim, int group, ui8 dir, bool moving) const; + std::shared_ptr findFlagBitmap(const CGHeroInstance * obj, int anim, const PlayerColor color, int group) const; + AnimBitmapHolder findObjectBitmap(const CGObjectInstance * obj, int anim, int group = 0) const; + + //FIXME: unique_ptr should be enough, but fails to compile in MSVS 2013 + typedef std::map> TFlippedAnimations; //[type, rotation] + typedef std::map>> TFlippedCache;//[type, view type, rotation] + + TFlippedAnimations terrainAnimations;//[terrain type, rotation] + TFlippedCache terrainImages;//[terrain type, view type, rotation] + + TFlippedAnimations roadAnimations;//[road type, rotation] + TFlippedCache roadImages;//[road type, view type, rotation] + + TFlippedAnimations riverAnimations;//[river type, rotation] + TFlippedCache riverImages;//[river type, view type, rotation] + + std::vector ttiles; //informations about map tiles + int3 sizes; //map size (x = width, y = height, z = number of levels) + const CMap * map; + + enum class EMapCacheType : char + { + TERRAIN, OBJECTS, ROADS, RIVERS, FOW, HEROES, HERO_FLAGS, FRAME, AFTER_LAST + }; + + void initObjectRects(); + void initTerrainGraphics(); + QRgb getTileColor(int x, int y, int z); + +public: + MapHandler(); + ~MapHandler() = default; + + void reset(const CMap * Map); + + void updateWater(); + + void drawTerrainTile(QPainter & painter, int x, int y, int z); + /// draws a river segment on current tile + void drawRiver(QPainter & painter, int x, int y, int z); + /// draws a road segment on current tile + void drawRoad(QPainter & painter, int x, int y, int z); + + void invalidate(int x, int y, int z); //invalidates all objects in particular tile + void invalidate(CGObjectInstance *); //invalidates object rects + void invalidate(const std::vector &); //invalidates all tiles + void invalidateObjects(); //invalidates all objects on the map + std::vector getTilesUnderObject(CGObjectInstance *) const; + + /// draws all objects on current tile (higher-level logic, unlike other draw*** methods) + void drawObjects(QPainter & painter, int x, int y, int z); + void drawObject(QPainter & painter, const TileObject & object); + void drawObjectAt(QPainter & painter, const CGObjectInstance * object, int x, int y); + std::vector & getObjects(int x, int y, int z); + + void drawMinimapTile(QPainter & painter, int x, int y, int z); + + static bool compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b); +}; + +#endif // MAPHANDLER_H diff --git a/mapeditor/mapsettings.cpp b/mapeditor/mapsettings.cpp new file mode 100644 index 000000000..55b505ddd --- /dev/null +++ b/mapeditor/mapsettings.cpp @@ -0,0 +1,102 @@ +#include "mapsettings.h" +#include "ui_mapsettings.h" +#include "mainwindow.h" + +#include "../lib/CSkillHandler.h" +#include "../lib/spells/CSpellHandler.h" +#include "../lib/CArtHandler.h" +#include "../lib/CHeroHandler.h" + +MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : + QDialog(parent), + ui(new Ui::MapSettings), + controller(ctrl) +{ + ui->setupUi(this); + + assert(controller.map()); + + ui->mapNameEdit->setText(tr(controller.map()->name.c_str())); + ui->mapDescriptionEdit->setPlainText(tr(controller.map()->description.c_str())); + + show(); + + + for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->skillh->objects[i]->getName())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(controller.map()->allowedAbilities[i] ? Qt::Checked : Qt::Unchecked); + ui->listAbilities->addItem(item); + } + for(int i = 0; i < controller.map()->allowedSpell.size(); ++i) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->spellh->objects[i]->getName())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(controller.map()->allowedSpell[i] ? Qt::Checked : Qt::Unchecked); + ui->listSpells->addItem(item); + } + for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->arth->objects[i]->getName())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(controller.map()->allowedArtifact[i] ? Qt::Checked : Qt::Unchecked); + ui->listArts->addItem(item); + } + for(int i = 0; i < controller.map()->allowedHeroes.size(); ++i) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->heroh->objects[i]->getName())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(controller.map()->allowedHeroes[i] ? Qt::Checked : Qt::Unchecked); + ui->listHeroes->addItem(item); + } + + //ui8 difficulty; + //ui8 levelLimit; + + //std::string victoryMessage; + //std::string defeatMessage; + //ui16 victoryIconIndex; + //ui16 defeatIconIndex; + + //std::vector players; /// The default size of the vector is PlayerColor::PLAYER_LIMIT. +} + +MapSettings::~MapSettings() +{ + delete ui; +} + +void MapSettings::on_pushButton_clicked() +{ + controller.map()->name = ui->mapNameEdit->text().toStdString(); + controller.map()->description = ui->mapDescriptionEdit->toPlainText().toStdString(); + controller.commitChangeWithoutRedraw(); + + for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i) + { + auto * item = ui->listAbilities->item(i); + controller.map()->allowedAbilities[i] = item->checkState() == Qt::Checked; + } + for(int i = 0; i < controller.map()->allowedSpell.size(); ++i) + { + auto * item = ui->listSpells->item(i); + controller.map()->allowedSpell[i] = item->checkState() == Qt::Checked; + } + for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) + { + auto * item = ui->listArts->item(i); + controller.map()->allowedArtifact[i] = item->checkState() == Qt::Checked; + } + for(int i = 0; i < controller.map()->allowedHeroes.size(); ++i) + { + auto * item = ui->listHeroes->item(i); + controller.map()->allowedHeroes[i] = item->checkState() == Qt::Checked; + } + + close(); +} diff --git a/mapeditor/mapsettings.h b/mapeditor/mapsettings.h new file mode 100644 index 000000000..07e1119ac --- /dev/null +++ b/mapeditor/mapsettings.h @@ -0,0 +1,27 @@ +#ifndef MAPSETTINGS_H +#define MAPSETTINGS_H + +#include +#include "mapcontroller.h" + +namespace Ui { +class MapSettings; +} + +class MapSettings : public QDialog +{ + Q_OBJECT + +public: + explicit MapSettings(MapController & controller, QWidget *parent = nullptr); + ~MapSettings(); + +private slots: + void on_pushButton_clicked(); + +private: + Ui::MapSettings *ui; + MapController & controller; +}; + +#endif // MAPSETTINGS_H diff --git a/mapeditor/mapsettings.ui b/mapeditor/mapsettings.ui new file mode 100644 index 000000000..8a428d77a --- /dev/null +++ b/mapeditor/mapsettings.ui @@ -0,0 +1,175 @@ + + + MapSettings + + + Qt::ApplicationModal + + + + 0 + 0 + 454 + 480 + + + + + 0 + 0 + + + + Map settings + + + + + + 4 + + + + General + + + + + + Map name + + + + + + + + + + Map description + + + + + + + + + + + Abilities + + + + + + QAbstractItemView::ScrollPerItem + + + true + + + QListView::Adjust + + + QListView::Batched + + + 30 + + + + + + + + Spells + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::ScrollPerItem + + + true + + + QListView::Batched + + + 30 + + + + + + + + Artifacts + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::ScrollPerItem + + + true + + + QListView::Batched + + + 30 + + + + + + + + Heroes + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::ScrollPerItem + + + true + + + QListView::Batched + + + 30 + + + + + + + + + + + Ok + + + + + + + + diff --git a/mapeditor/mapview.cpp b/mapeditor/mapview.cpp new file mode 100644 index 000000000..ef5c4e9b9 --- /dev/null +++ b/mapeditor/mapview.cpp @@ -0,0 +1,448 @@ +#include "StdInc.h" +#include "mapview.h" +#include "mainwindow.h" +#include +#include "mapcontroller.h" + +MinimapView::MinimapView(QWidget * parent): + QGraphicsView(parent) +{ + // Disable scrollbars + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); +} + +void MinimapView::dimensions() +{ + fitInView(0, 0, controller->map()->width, controller->map()->height, Qt::KeepAspectRatio); +} + +void MinimapView::setController(MapController * ctrl) +{ + controller = ctrl; +} + +void MinimapView::mouseMoveEvent(QMouseEvent *mouseEvent) +{ + this->update(); + + auto * sc = static_cast(scene()); + if(!sc) + return; + + int w = sc->viewport.viewportWidth(); + int h = sc->viewport.viewportHeight(); + auto pos = mapToScene(mouseEvent->pos()); + pos.setX(pos.x() - w / 2); + pos.setY(pos.y() - h / 2); + + QPointF point = pos * 32; + + emit cameraPositionChanged(point); +} + +void MinimapView::mousePressEvent(QMouseEvent* event) +{ + mouseMoveEvent(event); +} + +MapView::MapView(QWidget * parent): + QGraphicsView(parent), + selectionTool(MapView::SelectionTool::None) +{ +} + +void MapView::cameraChanged(const QPointF & pos) +{ + //ui->mapView->translate(pos.x(), pos.y()); + horizontalScrollBar()->setValue(pos.x()); + verticalScrollBar()->setValue(pos.y()); +} + + +void MapView::setController(MapController * ctrl) +{ + controller = ctrl; +} + +void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) +{ + this->update(); + + auto * sc = static_cast(scene()); + if(!sc || !controller->map()) + return; + + auto pos = mapToScene(mouseEvent->pos()); //TODO: do we need to check size? + int3 tile(pos.x() / 32, pos.y() / 32, sc->level); + + if(tile == tilePrev) //do not redraw + return; + + tilePrev = tile; + + //main->setStatusMessage(QString("x: %1 y: %2").arg(QString::number(pos.x()), QString::number(pos.y()))); + + switch(selectionTool) + { + case MapView::SelectionTool::Brush: + if(mouseEvent->buttons() & Qt::RightButton) + sc->selectionTerrainView.erase(tile); + else if(mouseEvent->buttons() == Qt::LeftButton) + sc->selectionTerrainView.select(tile); + sc->selectionTerrainView.draw(); + break; + + case MapView::SelectionTool::Brush2: + { + std::array extra{ int3{0, 0, 0}, int3{1, 0, 0}, int3{0, 1, 0}, int3{1, 1, 0} }; + for(auto & e : extra) + { + if(mouseEvent->buttons() & Qt::RightButton) + sc->selectionTerrainView.erase(tile + e); + else if(mouseEvent->buttons() == Qt::LeftButton) + sc->selectionTerrainView.select(tile + e); + } + } + sc->selectionTerrainView.draw(); + break; + + case MapView::SelectionTool::Brush4: + { + std::array extra{ + int3{-1, -1, 0}, int3{0, -1, 0}, int3{1, -1, 0}, int3{2, -1, 0}, + int3{-1, 0, 0}, int3{0, 0, 0}, int3{1, 0, 0}, int3{2, 0, 0}, + int3{-1, 1, 0}, int3{0, 1, 0}, int3{1, 1, 0}, int3{2, 1, 0}, + int3{-1, 2, 0}, int3{0, 2, 0}, int3{1, 2, 0}, int3{2, 2, 0} + }; + for(auto & e : extra) + { + if(mouseEvent->buttons() & Qt::RightButton) + sc->selectionTerrainView.erase(tile + e); + else if(mouseEvent->buttons() == Qt::LeftButton) + sc->selectionTerrainView.select(tile + e); + } + } + sc->selectionTerrainView.draw(); + break; + + case MapView::SelectionTool::Area: + if(mouseEvent->buttons() & Qt::RightButton || !mouseEvent->buttons() & Qt::LeftButton) + break; + + sc->selectionTerrainView.clear(); + for(int j = std::min(tile.y, tileStart.y); j < std::max(tile.y, tileStart.y); ++j) + { + for(int i = std::min(tile.x, tileStart.x); i < std::max(tile.x, tileStart.x); ++i) + { + sc->selectionTerrainView.select(int3(i, j, sc->level)); + } + } + sc->selectionTerrainView.draw(); + break; + + case MapView::SelectionTool::None: + if(mouseEvent->buttons() & Qt::RightButton) + break; + + auto sh = tile - tileStart; + sc->selectionObjectsView.shift = QPoint(sh.x, sh.y); + + if(sh.x || sh.y) + { + if(sc->selectionObjectsView.newObject) + { + sc->selectionObjectsView.shift = QPoint(tile.x, tile.y); + sc->selectionObjectsView.selectObject(sc->selectionObjectsView.newObject); + sc->selectionObjectsView.selectionMode = 2; + } + else if(mouseEvent->buttons() & Qt::LeftButton) + { + if(sc->selectionObjectsView.selectionMode == 1) + { + sc->selectionObjectsView.clear(); + sc->selectionObjectsView.selectObjects(tileStart.x, tileStart.y, tile.x, tile.y); + } + } + } + + sc->selectionObjectsView.draw(); + break; + } +} + +void MapView::mousePressEvent(QMouseEvent *event) +{ + this->update(); + + auto * sc = static_cast(scene()); + if(!sc || !controller->map()) + return; + + mouseStart = mapToScene(event->pos()); + tileStart = tilePrev = int3(mouseStart.x() / 32, mouseStart.y() / 32, sc->level); + + if(sc->selectionTerrainView.selection().count(tileStart)) + pressedOnSelected = true; + else + pressedOnSelected = false; + + switch(selectionTool) + { + case MapView::SelectionTool::Brush: + sc->selectionObjectsView.clear(); + sc->selectionObjectsView.draw(); + + if(event->button() == Qt::RightButton) + sc->selectionTerrainView.erase(tileStart); + else if(event->button() == Qt::LeftButton) + sc->selectionTerrainView.select(tileStart); + sc->selectionTerrainView.draw(); + break; + + case MapView::SelectionTool::Brush2: + sc->selectionObjectsView.clear(); + sc->selectionObjectsView.draw(); + { + std::array extra{ int3{0, 0, 0}, int3{1, 0, 0}, int3{0, 1, 0}, int3{1, 1, 0} }; + for(auto & e : extra) + { + if(event->button() == Qt::RightButton) + sc->selectionTerrainView.erase(tileStart + e); + else if(event->button() == Qt::LeftButton) + sc->selectionTerrainView.select(tileStart + e); + } + } + sc->selectionTerrainView.draw(); + break; + + case MapView::SelectionTool::Brush4: + sc->selectionObjectsView.clear(); + sc->selectionObjectsView.draw(); + { + std::array extra{ + int3{-1, -1, 0}, int3{0, -1, 0}, int3{1, -1, 0}, int3{2, -1, 0}, + int3{-1, 0, 0}, int3{0, 0, 0}, int3{1, 0, 0}, int3{2, 0, 0}, + int3{-1, 1, 0}, int3{0, 1, 0}, int3{1, 1, 0}, int3{2, 1, 0}, + int3{-1, 2, 0}, int3{0, 2, 0}, int3{1, 2, 0}, int3{2, 2, 0} + }; + for(auto & e : extra) + { + if(event->button() == Qt::RightButton) + sc->selectionTerrainView.erase(tileStart + e); + else if(event->button() == Qt::LeftButton) + sc->selectionTerrainView.select(tileStart + e); + } + } + sc->selectionTerrainView.draw(); + break; + + case MapView::SelectionTool::Area: + if(event->button() == Qt::RightButton) + break; + + sc->selectionTerrainView.clear(); + sc->selectionTerrainView.draw(); + sc->selectionObjectsView.clear(); + sc->selectionObjectsView.draw(); + break; + + case MapView::SelectionTool::None: + sc->selectionTerrainView.clear(); + sc->selectionTerrainView.draw(); + + if(sc->selectionObjectsView.newObject && sc->selectionObjectsView.isSelected(sc->selectionObjectsView.newObject)) + { + if(event->button() == Qt::RightButton) + controller->discardObject(sc->level); + } + else + { + if(event->button() == Qt::LeftButton) + { + auto * obj = sc->selectionObjectsView.selectObjectAt(tileStart.x, tileStart.y); + if(obj) + { + if(sc->selectionObjectsView.isSelected(obj)) + { + if(qApp->keyboardModifiers() & Qt::ControlModifier) + { + sc->selectionObjectsView.deselectObject(obj); + sc->selectionObjectsView.selectionMode = 1; + } + else + sc->selectionObjectsView.selectionMode = 2; + } + else + { + if(!(qApp->keyboardModifiers() & Qt::ControlModifier)) + sc->selectionObjectsView.clear(); + sc->selectionObjectsView.selectionMode = 2; + sc->selectionObjectsView.selectObject(obj); + } + } + else + { + sc->selectionObjectsView.clear(); + sc->selectionObjectsView.selectionMode = 1; + } + } + sc->selectionObjectsView.shift = QPoint(0, 0); + sc->selectionObjectsView.draw(); + } + break; + } + + //main->setStatusMessage(QString("x: %1 y: %2").arg(QString::number(event->pos().x()), QString::number(event->pos().y()))); +} + +void MapView::mouseReleaseEvent(QMouseEvent *event) +{ + this->update(); + + auto * sc = static_cast(scene()); + if(!sc || !controller->map()) + return; + + switch(selectionTool) + { + case MapView::SelectionTool::None: + if(event->button() == Qt::RightButton) + break; + //switch position + bool tab = false; + if(sc->selectionObjectsView.selectionMode == 2) + { + if(sc->selectionObjectsView.newObject) + { + QString errorMsg; + if(controller->canPlaceObject(sc->level, sc->selectionObjectsView.newObject, errorMsg)) + controller->commitObjectCreate(sc->level); + else + { + QMessageBox::information(this, "Can't place object", errorMsg); + break; + } + } + else + controller->commitObjectShift(sc->level); + } + else + { + sc->selectionObjectsView.selectionMode = 0; + sc->selectionObjectsView.shift = QPoint(0, 0); + sc->selectionObjectsView.draw(); + tab = true; + //check if we have only one object + } + auto selection = sc->selectionObjectsView.getSelection(); + if(selection.size() == 1) + { + emit openObjectProperties(*selection.begin(), tab); + } + break; + } +} + +bool MapView::viewportEvent(QEvent *event) +{ + if(auto * sc = static_cast(scene())) + { + //auto rect = sceneRect(); + auto rect = mapToScene(viewport()->geometry()).boundingRect(); + controller->miniScene(sc->level)->viewport.setViewport(rect.x() / 32, rect.y() / 32, rect.width() / 32, rect.height() / 32); + } + return QGraphicsView::viewportEvent(event); +} + +MapSceneBase::MapSceneBase(int lvl): + QGraphicsScene(nullptr), + level(lvl) +{ +} + +void MapSceneBase::initialize(MapController & controller) +{ + for(auto * layer : getAbstractLayers()) + layer->initialize(controller); +} + +void MapSceneBase::updateViews() +{ + for(auto * layer : getAbstractLayers()) + layer->update(); +} + +MapScene::MapScene(int lvl): + MapSceneBase(lvl), + gridView(this), + passabilityView(this), + selectionTerrainView(this), + terrainView(this), + objectsView(this), + selectionObjectsView(this), + isTerrainSelected(false), + isObjectSelected(false) +{ + connect(&selectionTerrainView, &SelectionTerrainLayer::selectionMade, this, &MapScene::terrainSelected); + connect(&selectionObjectsView, &SelectionObjectsLayer::selectionMade, this, &MapScene::objectSelected); +} + +std::list MapScene::getAbstractLayers() +{ + //sequence is important because it defines rendering order + return { + &terrainView, + &objectsView, + &gridView, + &passabilityView, + &selectionTerrainView, + &selectionObjectsView + }; +} + +void MapScene::updateViews() +{ + MapSceneBase::updateViews(); + + terrainView.show(true); + objectsView.show(true); + selectionTerrainView.show(true); + selectionObjectsView.show(true); +} + +void MapScene::terrainSelected(bool anythingSelected) +{ + isTerrainSelected = anythingSelected; + emit selected(isTerrainSelected || isObjectSelected); +} + +void MapScene::objectSelected(bool anythingSelected) +{ + isObjectSelected = anythingSelected; + emit selected(isTerrainSelected || isObjectSelected); +} + +MinimapScene::MinimapScene(int lvl): + MapSceneBase(lvl), + minimapView(this), + viewport(this) +{ +} + +std::list MinimapScene::getAbstractLayers() +{ + //sequence is important because it defines rendering order + return { + &minimapView, + &viewport + }; +} + +void MinimapScene::updateViews() +{ + MapSceneBase::updateViews(); + + minimapView.show(true); + viewport.show(true); +} diff --git a/mapeditor/mapview.h b/mapeditor/mapview.h new file mode 100644 index 000000000..3262f0f42 --- /dev/null +++ b/mapeditor/mapview.h @@ -0,0 +1,133 @@ +#ifndef MAPVIEW_H +#define MAPVIEW_H + +#include +#include +#include "scenelayer.h" +#include "../lib/int3.h" + + +class CGObjectInstance; +class MainWindow; +class MapController; + +class MapSceneBase : public QGraphicsScene +{ + Q_OBJECT; +public: + MapSceneBase(int lvl); + + const int level; + + virtual void updateViews(); + virtual void initialize(MapController &); + +protected: + virtual std::list getAbstractLayers() = 0; +}; + +class MinimapScene : public MapSceneBase +{ +public: + MinimapScene(int lvl); + + void updateViews() override; + + MinimapLayer minimapView; + MinimapViewLayer viewport; + +protected: + std::list getAbstractLayers() override; +}; + +class MapScene : public MapSceneBase +{ + Q_OBJECT +public: + MapScene(int lvl); + + void updateViews() override; + + GridLayer gridView; + PassabilityLayer passabilityView; + SelectionTerrainLayer selectionTerrainView; + TerrainLayer terrainView; + ObjectsLayer objectsView; + SelectionObjectsLayer selectionObjectsView; + +signals: + void selected(bool anything); + +public slots: + void terrainSelected(bool anything); + void objectSelected(bool anything); + +protected: + std::list getAbstractLayers() override; + + bool isTerrainSelected; + bool isObjectSelected; + +}; + +class MapView : public QGraphicsView +{ + Q_OBJECT +public: + enum class SelectionTool + { + None, Brush, Brush2, Brush4, Area, Lasso + }; + +public: + MapView(QWidget * parent); + void setController(MapController *); + + SelectionTool selectionTool; + +public slots: + void mouseMoveEvent(QMouseEvent * mouseEvent) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + + void cameraChanged(const QPointF & pos); + +signals: + void openObjectProperties(CGObjectInstance *, bool switchTab); + //void viewportChanged(const QRectF & rect); + +protected: + bool viewportEvent(QEvent *event) override; + +private: + MapController * controller = nullptr; + QPointF mouseStart; + int3 tileStart; + int3 tilePrev; + bool pressedOnSelected; +}; + +class MinimapView : public QGraphicsView +{ + Q_OBJECT +public: + MinimapView(QWidget * parent); + void setController(MapController *); + + void dimensions(); + +public slots: + void mouseMoveEvent(QMouseEvent * mouseEvent) override; + void mousePressEvent(QMouseEvent* event) override; + +signals: + void cameraPositionChanged(const QPointF & newPosition); + +private: + MapController * controller = nullptr; + + int displayWidth = 192; + int displayHeight = 192; +}; + +#endif // MAPVIEW_H diff --git a/mapeditor/objectbrowser.cpp b/mapeditor/objectbrowser.cpp new file mode 100644 index 000000000..770564480 --- /dev/null +++ b/mapeditor/objectbrowser.cpp @@ -0,0 +1,68 @@ +#include "objectbrowser.h" +#include "../lib/mapObjects/CObjectClassesHandler.h" + +ObjectBrowser::ObjectBrowser(QObject *parent) + : QSortFilterProxyModel{parent}, terrain(Terrain::ANY) +{ +} + +bool ObjectBrowser::filterAcceptsRow(int source_row, const QModelIndex & source_parent) const +{ + bool result = QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); + + QModelIndex currentIndex = sourceModel()->index(source_row, 0, source_parent); + int childCount = currentIndex.model()->rowCount(currentIndex); + if(childCount) + return false; + + auto item = dynamic_cast(sourceModel())->itemFromIndex(currentIndex); + if(!item) + return result; + + if(!filterAcceptsRowText(source_row, source_parent)) + return false; + + if(terrain == Terrain::ANY) + return result; + + auto data = item->data().toJsonObject(); + if(data.empty()) + return result; + + auto objIdJson = data["id"]; + if(objIdJson == QJsonValue::Undefined) + return result; + + auto objId = data["id"].toInt(); + auto objSubId = data["subid"].toInt(); + auto templateId = data["template"].toInt(); + + auto factory = VLC->objtypeh->getHandlerFor(objId, objSubId); + auto templ = factory->getTemplates()[templateId]; + + result = result & templ->canBePlacedAt(terrain); + + //text filter + + return result; +} + +bool ObjectBrowser::filterAcceptsRowText(int source_row, const QModelIndex &source_parent) const +{ + if(source_parent.isValid()) + { + if(filterAcceptsRowText(source_parent.row(), source_parent.parent())) + return true; + } + + QModelIndex index = sourceModel()->index(source_row, 0 ,source_parent); + if(!index.isValid()) + return false; + + auto item = dynamic_cast(sourceModel())->itemFromIndex(index); + if(!item) + return false; + + return (filter.isNull() || filter.isEmpty() || item->text().contains(filter, Qt::CaseInsensitive)); +} + diff --git a/mapeditor/objectbrowser.h b/mapeditor/objectbrowser.h new file mode 100644 index 000000000..87ac744b8 --- /dev/null +++ b/mapeditor/objectbrowser.h @@ -0,0 +1,20 @@ +#ifndef OBJECTBROWSER_H +#define OBJECTBROWSER_H + +#include +#include "../lib/Terrain.h" + +class ObjectBrowser : public QSortFilterProxyModel +{ +public: + explicit ObjectBrowser(QObject *parent = nullptr); + + Terrain terrain; + QString filter; + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override; + bool filterAcceptsRowText(int source_row, const QModelIndex &source_parent) const; +}; + +#endif // OBJECTBROWSER_H diff --git a/mapeditor/playerparams.cpp b/mapeditor/playerparams.cpp new file mode 100644 index 000000000..9b23d0ec3 --- /dev/null +++ b/mapeditor/playerparams.cpp @@ -0,0 +1,139 @@ +#include "StdInc.h" +#include "playerparams.h" +#include "ui_playerparams.h" +#include "../lib/CTownHandler.h" + +PlayerParams::PlayerParams(MapController & ctrl, int playerId, QWidget *parent) : + QWidget(parent), + ui(new Ui::PlayerParams), + controller(ctrl) +{ + ui->setupUi(this); + + playerColor = playerId; + assert(controller.map()->players.size() > playerColor); + playerInfo = controller.map()->players[playerColor]; + + //load factions + for(auto idx : VLC->townh->getAllowedFactions()) + { + CFaction * faction = VLC->townh->objects.at(idx); + auto * item = new QListWidgetItem(QString::fromStdString(faction->name)); + item->setData(Qt::UserRole, QVariant::fromValue(idx)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + ui->allowedFactions->addItem(item); + if(playerInfo.allowedFactions.count(idx)) + item->setCheckState(Qt::Checked); + else + item->setCheckState(Qt::Unchecked); + } + QObject::connect(ui->allowedFactions, SIGNAL(itemChanged(QListWidgetItem*)), + this, SLOT(allowedFactionsCheck(QListWidgetItem*))); + + //load checks + bool canHumanPlay = playerInfo.canHumanPlay; //need variable to restore after signal received + playerInfo.canComputerPlay = true; //computer always can play + ui->radioCpu->setChecked(true); + if(canHumanPlay) + ui->radioHuman->setChecked(true); + if(playerInfo.isFactionRandom) + ui->randomFaction->setChecked(true); + if(playerInfo.generateHeroAtMainTown) + ui->generateHero->setChecked(true); + + //load towns + int foundMainTown = -1; + for(int i = 0, townIndex = 0; i < controller.map()->objects.size(); ++i) + { + if(auto town = dynamic_cast(controller.map()->objects[i].get())) + { + auto * ctown = town->town; + if(!ctown) + ctown = VLC->townh->randomTown; + if(ctown && town->getOwner().getNum() == playerColor) + { + if(playerInfo.hasMainTown && playerInfo.posOfMainTown == town->pos) + foundMainTown = townIndex; + auto name = town->name + ", (random)"; + if(ctown->faction) + name = town->getObjectName(); + ui->mainTown->addItem(tr(name.c_str()), QVariant::fromValue(i)); + ++townIndex; + } + } + } + + if(foundMainTown > -1) + { + ui->mainTown->setCurrentIndex(foundMainTown + 1); + } + else + { + ui->generateHero->setEnabled(false); + playerInfo.hasMainTown = false; + playerInfo.generateHeroAtMainTown = false; + playerInfo.posOfMainTown = int3(-1, -1, -1); + } + + ui->playerColor->setTitle(QString("PlayerID: %1").arg(playerId)); + show(); +} + +PlayerParams::~PlayerParams() +{ + delete ui; +} + +void PlayerParams::on_radioHuman_toggled(bool checked) +{ + if(checked) + playerInfo.canHumanPlay = true; +} + + +void PlayerParams::on_radioCpu_toggled(bool checked) +{ + if(checked) + playerInfo.canHumanPlay = false; +} + + +void PlayerParams::on_generateHero_stateChanged(int arg1) +{ + playerInfo.generateHeroAtMainTown = ui->generateHero->isChecked(); +} + + +void PlayerParams::on_randomFaction_stateChanged(int arg1) +{ + playerInfo.isFactionRandom = ui->randomFaction->isChecked(); +} + + +void PlayerParams::allowedFactionsCheck(QListWidgetItem * item) +{ + if(item->checkState() == Qt::Checked) + playerInfo.allowedFactions.insert(item->data(Qt::UserRole).toInt()); + else + playerInfo.allowedFactions.erase(item->data(Qt::UserRole).toInt()); +} + + +void PlayerParams::on_mainTown_activated(int index) +{ + if(index == 0) //default + { + ui->generateHero->setEnabled(false); + ui->generateHero->setChecked(false); + playerInfo.hasMainTown = false; + playerInfo.posOfMainTown = int3(-1, -1, -1); + } + else + { + ui->generateHero->setEnabled(true); + auto town = controller.map()->objects.at(ui->mainTown->currentData().toInt()); + playerInfo.hasMainTown = true; + playerInfo.posOfMainTown = town->pos; + } +} + diff --git a/mapeditor/playerparams.h b/mapeditor/playerparams.h new file mode 100644 index 000000000..4219bdafb --- /dev/null +++ b/mapeditor/playerparams.h @@ -0,0 +1,42 @@ +#ifndef PLAYERPARAMS_H +#define PLAYERPARAMS_H + +#include +#include "../lib/mapping/CMap.h" +#include "mapcontroller.h" + +namespace Ui { +class PlayerParams; +} + +class PlayerParams : public QWidget +{ + Q_OBJECT + +public: + explicit PlayerParams(MapController & controller, int playerId, QWidget *parent = nullptr); + ~PlayerParams(); + + PlayerInfo playerInfo; + int playerColor; + +private slots: + void on_radioHuman_toggled(bool checked); + + void on_radioCpu_toggled(bool checked); + + void on_mainTown_activated(int index); + + void on_generateHero_stateChanged(int arg1); + + void on_randomFaction_stateChanged(int arg1); + + void allowedFactionsCheck(QListWidgetItem *); + +private: + Ui::PlayerParams *ui; + + MapController & controller; +}; + +#endif // PLAYERPARAMS_H diff --git a/mapeditor/playerparams.ui b/mapeditor/playerparams.ui new file mode 100644 index 000000000..bd2444b8f --- /dev/null +++ b/mapeditor/playerparams.ui @@ -0,0 +1,142 @@ + + + PlayerParams + + + + 0 + 0 + 505 + 160 + + + + + 0 + 0 + + + + + 16777215 + 160 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 16777215 + 160 + + + + GroupBox + + + + + + + No team + + + + + + + + Human/CPU + + + + + + + CPU only + + + + + + + Team + + + + + + + Main town + + + + + + + Random faction + + + + + + + Generate hero at main + + + + + + + + (default) + + + + + + + + true + + + + 0 + 0 + + + + Qt::ClickFocus + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + + + + + + + + + diff --git a/mapeditor/playersettings.cpp b/mapeditor/playersettings.cpp new file mode 100644 index 000000000..85272acfb --- /dev/null +++ b/mapeditor/playersettings.cpp @@ -0,0 +1,77 @@ +#include "StdInc.h" +#include "playersettings.h" +#include "ui_playersettings.h" +#include "playerparams.h" +#include "mainwindow.h" + +PlayerSettings::PlayerSettings(MapController & ctrl, QWidget *parent) : + QDialog(parent), + ui(new Ui::PlayerSettings), + controller(ctrl) +{ + ui->setupUi(this); + show(); + + int players = 0; + for(auto & p : controller.map()->players) + { + if(p.canAnyonePlay()) + { + paramWidgets.push_back(new PlayerParams(controller, players)); + ui->playersLayout->addWidget(paramWidgets.back()); + ++players; + } + } + + if(players < 2) + ui->playersCount->setCurrentText(""); + else + ui->playersCount->setCurrentIndex(players - 2); + + setAttribute(Qt::WA_DeleteOnClose); +} + +PlayerSettings::~PlayerSettings() +{ + delete ui; +} + +void PlayerSettings::on_playersCount_currentIndexChanged(int index) +{ + assert(index + 2 <= controller.map()->players.size()); + + for(int i = 0; i < index + 2; ++i) + { + if(i < paramWidgets.size()) + continue; + + auto & p = controller.map()->players[i]; + p.canComputerPlay = true; + paramWidgets.push_back(new PlayerParams(controller, i)); + ui->playersLayout->addWidget(paramWidgets.back()); + } + + assert(!paramWidgets.empty()); + for(int i = paramWidgets.size() - 1; i >= index + 2; --i) + { + auto & p = controller.map()->players[i]; + p.canComputerPlay = false; + p.canHumanPlay = false; + ui->playersLayout->removeWidget(paramWidgets[i]); + delete paramWidgets[i]; + paramWidgets.pop_back(); + } +} + + +void PlayerSettings::on_pushButton_clicked() +{ + for(auto * w : paramWidgets) + { + controller.map()->players[w->playerColor] = w->playerInfo; + } + + controller.commitChangeWithoutRedraw(); + close(); +} + diff --git a/mapeditor/playersettings.h b/mapeditor/playersettings.h new file mode 100644 index 000000000..d6fab335f --- /dev/null +++ b/mapeditor/playersettings.h @@ -0,0 +1,34 @@ +#ifndef PLAYERSETTINGS_H +#define PLAYERSETTINGS_H + +#include "StdInc.h" +#include +#include "playerparams.h" + +namespace Ui { +class PlayerSettings; +} + +class PlayerSettings : public QDialog +{ + Q_OBJECT + +public: + explicit PlayerSettings(MapController & controller, QWidget *parent = nullptr); + ~PlayerSettings(); + +private slots: + + void on_playersCount_currentIndexChanged(int index); + + void on_pushButton_clicked(); + +private: + Ui::PlayerSettings *ui; + + std::vector paramWidgets; + + MapController & controller; +}; + +#endif // PLAYERSETTINGS_H diff --git a/mapeditor/playersettings.ui b/mapeditor/playersettings.ui new file mode 100644 index 000000000..5d111ee04 --- /dev/null +++ b/mapeditor/playersettings.ui @@ -0,0 +1,117 @@ + + + PlayerSettings + + + Qt::WindowModal + + + + 0 + 0 + 654 + 283 + + + + Qt::NoFocus + + + Player settings + + + true + + + + + + true + + + + + 0 + 0 + 628 + 187 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + Players + + + + + + + + 2 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 7 + + + + + 8 + + + + + + + + Ok + + + + + + + + diff --git a/mapeditor/radiopushbutton.cpp b/mapeditor/radiopushbutton.cpp new file mode 100644 index 000000000..1f615a559 --- /dev/null +++ b/mapeditor/radiopushbutton.cpp @@ -0,0 +1,7 @@ +#include "StdInc.h" +#include "radiopushbutton.h" + +RadioPushButton::RadioPushButton() +{ + +} diff --git a/mapeditor/radiopushbutton.h b/mapeditor/radiopushbutton.h new file mode 100644 index 000000000..3ff1fbbb2 --- /dev/null +++ b/mapeditor/radiopushbutton.h @@ -0,0 +1,12 @@ +#ifndef RADIOPUSHBUTTON_H +#define RADIOPUSHBUTTON_H + +#include + +class RadioPushButton : public QPushButton +{ +public: + RadioPushButton(); +}; + +#endif // RADIOPUSHBUTTON_H diff --git a/mapeditor/scenelayer.cpp b/mapeditor/scenelayer.cpp new file mode 100644 index 000000000..93a964866 --- /dev/null +++ b/mapeditor/scenelayer.cpp @@ -0,0 +1,564 @@ +#include "StdInc.h" +#include "scenelayer.h" +#include "mainwindow.h" +#include "../lib/mapping/CMapEditManager.h" +#include "inspector/inspector.h" +#include "mapview.h" +#include "mapcontroller.h" + +AbstractLayer::AbstractLayer(MapSceneBase * s): scene(s) +{ +} + +void AbstractLayer::initialize(MapController & controller) +{ + map = controller.map(); + handler = controller.mapHandler(); +} + +void AbstractLayer::show(bool show) +{ + if(isShown == show) + return; + + isShown = show; + + redraw(); +} + +void AbstractLayer::redraw() +{ + if(item) + { + if(pixmap && isShown) + item->setPixmap(*pixmap); + else + item->setPixmap(emptyPixmap); + } + else + { + if(pixmap && isShown) + item.reset(scene->addPixmap(*pixmap)); + else + item.reset(scene->addPixmap(emptyPixmap)); + } +} + +GridLayer::GridLayer(MapSceneBase * s): AbstractLayer(s) +{ +} + +void GridLayer::update() +{ + if(!map) + return; + + pixmap.reset(new QPixmap(map->width * 32, map->height * 32)); + pixmap->fill(QColor(0, 0, 0, 0)); + QPainter painter(pixmap.get()); + painter.setPen(QColor(0, 0, 0, 190)); + + for(int j = 0; j < map->height; ++j) + { + painter.drawLine(0, j * 32, map->width * 32 - 1, j * 32); + } + for(int i = 0; i < map->width; ++i) + { + painter.drawLine(i * 32, 0, i * 32, map->height * 32 - 1); + } + + redraw(); +} + +PassabilityLayer::PassabilityLayer(MapSceneBase * s): AbstractLayer(s) +{ +} + +void PassabilityLayer::update() +{ + if(!map) + return; + + pixmap.reset(new QPixmap(map->width * 32, map->height * 32)); + pixmap->fill(QColor(0, 0, 0, 0)); + + if(scene->level == 0 || map->twoLevel) + { + QPainter painter(pixmap.get()); + for(int j = 0; j < map->height; ++j) + { + for(int i = 0; i < map->width; ++i) + { + auto tl = map->getTile(int3(i, j, scene->level)); + if(tl.blocked || tl.visitable) + { + painter.fillRect(i * 32, j * 32, 31, 31, tl.visitable ? QColor(200, 200, 0, 64) : QColor(255, 0, 0, 64)); + } + } + } + } + + redraw(); +} + +SelectionTerrainLayer::SelectionTerrainLayer(MapSceneBase * s): AbstractLayer(s) +{ +} + +void SelectionTerrainLayer::update() +{ + if(!map) + return; + + area.clear(); + areaAdd.clear(); + areaErase.clear(); + onSelection(); + + pixmap.reset(new QPixmap(map->width * 32, map->height * 32)); + pixmap->fill(QColor(0, 0, 0, 0)); + + redraw(); +} + +void SelectionTerrainLayer::draw() +{ + if(!pixmap) + return; + + QPainter painter(pixmap.get()); + painter.setCompositionMode(QPainter::CompositionMode_Source); + for(auto & t : areaAdd) + { + painter.fillRect(t.x * 32, t.y * 32, 31, 31, QColor(128, 128, 128, 96)); + } + for(auto & t : areaErase) + { + painter.fillRect(t.x * 32, t.y * 32, 31, 31, QColor(0, 0, 0, 0)); + } + + areaAdd.clear(); + areaErase.clear(); + + redraw(); +} + +void SelectionTerrainLayer::select(const int3 & tile) +{ + if(!map || !map->isInTheMap(tile)) + return; + + if(!area.count(tile)) + { + area.insert(tile); + areaAdd.insert(tile); + areaErase.erase(tile); + } + onSelection(); +} + +void SelectionTerrainLayer::erase(const int3 & tile) +{ + if(!map || !map->isInTheMap(tile)) + return; + + if(area.count(tile)) + { + area.erase(tile); + areaErase.insert(tile); + areaAdd.erase(tile); + } + onSelection(); +} + +void SelectionTerrainLayer::clear() +{ + areaErase = area; + areaAdd.clear(); + area.clear(); + onSelection(); +} + +const std::set & SelectionTerrainLayer::selection() const +{ + return area; +} + +void SelectionTerrainLayer::onSelection() +{ + emit selectionMade(!area.empty()); +} + + +TerrainLayer::TerrainLayer(MapSceneBase * s): AbstractLayer(s) +{ +} + +void TerrainLayer::update() +{ + if(!map) + return; + + pixmap.reset(new QPixmap(map->width * 32, map->height * 32)); + draw(false); +} + +void TerrainLayer::setDirty(const int3 & tile) +{ + dirty.insert(tile); +} + +void TerrainLayer::draw(bool onlyDirty) +{ + if(!pixmap) + return; + + if(!map) + return; + + QPainter painter(pixmap.get()); + //painter.setCompositionMode(QPainter::CompositionMode_Source); + + if(onlyDirty) + { + std::set forRedrawing(dirty), neighbours; + for(auto & t : dirty) + { + for(auto & tt : int3::getDirs()) + { + if(map->isInTheMap(t + tt)) + neighbours.insert(t + tt); + } + } + for(auto & t : neighbours) + { + for(auto & tt : int3::getDirs()) + { + forRedrawing.insert(t); + if(map->isInTheMap(t + tt)) + forRedrawing.insert(t + tt); + } + } + for(auto & t : forRedrawing) + { + handler->drawTerrainTile(painter, t.x, t.y, scene->level); + handler->drawRiver(painter, t.x, t.y, scene->level); + handler->drawRoad(painter, t.x, t.y, scene->level); + } + } + else + { + for(int j = 0; j < map->height; ++j) + { + for(int i = 0; i < map->width; ++i) + { + handler->drawTerrainTile(painter, i, j, scene->level); + handler->drawRiver(painter, i, j, scene->level); + handler->drawRoad(painter, i, j, scene->level); + } + } + } + + dirty.clear(); + redraw(); +} + +ObjectsLayer::ObjectsLayer(MapSceneBase * s): AbstractLayer(s) +{ +} + +void ObjectsLayer::update() +{ + if(!map) + return; + + pixmap.reset(new QPixmap(map->width * 32, map->height * 32)); + pixmap->fill(QColor(0, 0, 0, 0)); + draw(false); +} + +void ObjectsLayer::draw(bool onlyDirty) +{ + if(!pixmap) + return; + + if(!map) + return; + + pixmap->fill(QColor(0, 0, 0, 0)); + QPainter painter(pixmap.get()); + std::set drawen; + + + for(int j = 0; j < map->height; ++j) + { + for(int i = 0; i < map->width; ++i) + { + handler->drawObjects(painter, i, j, scene->level); + /*auto & objects = main->getMapHandler()->getObjects(i, j, scene->level); + for(auto & object : objects) + { + if(!object.obj || drawen.count(object.obj)) + continue; + + if(!onlyDirty || dirty.count(object.obj)) + { + main->getMapHandler()->drawObject(painter, object); + drawen.insert(object.obj); + } + }*/ + } + } + + dirty.clear(); + redraw(); +} + +void ObjectsLayer::setDirty(int x, int y) +{ + /*auto & objects = main->getMapHandler()->getObjects(x, y, scene->level); + for(auto & object : objects) + { + if(object.obj) + dirty.insert(object.obj); + }*/ +} + +void ObjectsLayer::setDirty(const CGObjectInstance * object) +{ +} + +SelectionObjectsLayer::SelectionObjectsLayer(MapSceneBase * s): AbstractLayer(s), newObject(nullptr) +{ +} + +void SelectionObjectsLayer::update() +{ + if(!map) + return; + + selectedObjects.clear(); + onSelection(); + shift = QPoint(); + if(newObject) + delete newObject; + newObject = nullptr; + + pixmap.reset(new QPixmap(map->width * 32, map->height * 32)); + //pixmap->fill(QColor(0, 0, 0, 0)); + + draw(); +} + +void SelectionObjectsLayer::draw() +{ + if(!pixmap) + return; + + pixmap->fill(QColor(0, 0, 0, 0)); + + QPainter painter(pixmap.get()); + painter.setCompositionMode(QPainter::CompositionMode_Source); + painter.setPen(QColor(255, 255, 255)); + + for(auto * obj : selectedObjects) + { + if(obj != newObject) + { + QRect bbox(obj->getPosition().x, obj->getPosition().y, 1, 1); + for(auto & t : obj->getBlockedPos()) + { + QPoint topLeft(std::min(t.x, bbox.topLeft().x()), std::min(t.y, bbox.topLeft().y())); + bbox.setTopLeft(topLeft); + QPoint bottomRight(std::max(t.x, bbox.bottomRight().x()), std::max(t.y, bbox.bottomRight().y())); + bbox.setBottomRight(bottomRight); + } + + painter.setOpacity(1.0); + painter.drawRect(bbox.x() * 32, bbox.y() * 32, bbox.width() * 32, bbox.height() * 32); + } + + //show translation + if(selectionMode == 2 && (shift.x() || shift.y())) + { + painter.setOpacity(0.5); + auto newPos = QPoint(obj->getPosition().x, obj->getPosition().y) + shift; + handler->drawObjectAt(painter, obj, newPos.x(), newPos.y()); + } + } + + redraw(); +} + +CGObjectInstance * SelectionObjectsLayer::selectObjectAt(int x, int y) const +{ + if(!map || !map->isInTheMap(int3(x, y, scene->level))) + return nullptr; + + auto & objects = handler->getObjects(x, y, scene->level); + + //visitable is most important + for(auto & object : objects) + { + if(!object.obj) + continue; + + if(object.obj->visitableAt(x, y)) + { + return object.obj; + } + } + + //if not visitable tile - try to get blocked + for(auto & object : objects) + { + if(!object.obj) + continue; + + if(object.obj->blockingAt(x, y)) + { + return object.obj; + } + } + + //finally, we can take any object + for(auto & object : objects) + { + if(!object.obj) + continue; + + if(object.obj->coveringAt(x, y)) + { + return object.obj; + } + } + + return nullptr; +} + +void SelectionObjectsLayer::selectObjects(int x1, int y1, int x2, int y2) +{ + if(!map) + return; + + if(x1 > x2) + std::swap(x1, x2); + + if(y1 > y2) + std::swap(y1, y2); + + for(int j = y1; j < y2; ++j) + { + for(int i = x1; i < x2; ++i) + { + for(auto & o : handler->getObjects(i, j, scene->level)) + selectObject(o.obj, false); //do not inform about each object added + } + } + onSelection(); +} + +void SelectionObjectsLayer::selectObject(CGObjectInstance * obj, bool inform /* = true */) +{ + selectedObjects.insert(obj); + if (inform) + { + onSelection(); + } +} + +void SelectionObjectsLayer::deselectObject(CGObjectInstance * obj) +{ + selectedObjects.erase(obj); +} + +bool SelectionObjectsLayer::isSelected(const CGObjectInstance * obj) const +{ + return selectedObjects.count(const_cast(obj)); +} + +std::set SelectionObjectsLayer::getSelection() const +{ + return selectedObjects; +} + +void SelectionObjectsLayer::clear() +{ + selectedObjects.clear(); + onSelection(); + shift.setX(0); + shift.setY(0); +} + +void SelectionObjectsLayer::onSelection() +{ + emit selectionMade(!selectedObjects.empty()); +} + +MinimapLayer::MinimapLayer(MapSceneBase * s): AbstractLayer(s) +{ + +} + +void MinimapLayer::update() +{ + if(!map) + return; + + pixmap.reset(new QPixmap(map->width, map->height)); + + QPainter painter(pixmap.get()); + //coordinate transfomation + for(int j = 0; j < map->height; ++j) + { + for(int i = 0; i < map->width; ++i) + { + handler->drawMinimapTile(painter, i, j, scene->level); + } + } + + redraw(); +} + +MinimapViewLayer::MinimapViewLayer(MapSceneBase * s): AbstractLayer(s) +{ +} + +void MinimapViewLayer::update() +{ + if(!map) + return; + + pixmap.reset(new QPixmap(map->width, map->height)); + pixmap->fill(QColor(0, 0, 0, 0)); + + QPainter painter(pixmap.get()); + painter.setPen(QColor(255, 255, 255)); + painter.drawRect(x, y, w, h); + + redraw(); +} + +void MinimapViewLayer::draw() +{ + if(!map) + return; + + pixmap->fill(QColor(0, 0, 0, 0)); + + //maybe not optimal but ok + QPainter painter(pixmap.get()); + painter.setPen(QColor(255, 255, 255)); + painter.drawRect(x, y, w, h); + + redraw(); +} + +void MinimapViewLayer::setViewport(int _x, int _y, int _w, int _h) +{ + x = _x; + y = _y; + w = _w; + h = _h; + draw(); +} diff --git a/mapeditor/scenelayer.h b/mapeditor/scenelayer.h new file mode 100644 index 000000000..fc1b236ea --- /dev/null +++ b/mapeditor/scenelayer.h @@ -0,0 +1,178 @@ +#ifndef SCENELAYER_H +#define SCENELAYER_H + +#include "../lib/int3.h" + +class MapSceneBase; +class MapScene; +class CGObjectInstance; +class MapController; +class CMap; +class MapHandler; + +class AbstractLayer : public QObject +{ + Q_OBJECT +public: + AbstractLayer(MapSceneBase * s); + + virtual void update() = 0; + + void show(bool show); + void redraw(); + void initialize(MapController & controller); + +protected: + MapSceneBase * scene; + CMap * map = nullptr; + MapHandler * handler = nullptr; + bool isShown = false; + + std::unique_ptr pixmap; + QPixmap emptyPixmap; + +private: + std::unique_ptr item; +}; + + +class GridLayer: public AbstractLayer +{ + Q_OBJECT +public: + GridLayer(MapSceneBase * s); + + void update() override; +}; + +class PassabilityLayer: public AbstractLayer +{ + Q_OBJECT +public: + PassabilityLayer(MapSceneBase * s); + + void update() override; +}; + +class SelectionTerrainLayer: public AbstractLayer +{ + Q_OBJECT +public: + SelectionTerrainLayer(MapSceneBase* s); + + void update() override; + + void draw(); + void select(const int3 & tile); + void erase(const int3 & tile); + void clear(); + + const std::set & selection() const; + +signals: + void selectionMade(bool anythingSlected); + +private: + std::set area, areaAdd, areaErase; + + void onSelection(); +}; + + +class TerrainLayer: public AbstractLayer +{ + Q_OBJECT +public: + TerrainLayer(MapSceneBase * s); + + void update() override; + + void draw(bool onlyDirty = true); + void setDirty(const int3 & tile); + +private: + std::set dirty; +}; + + +class ObjectsLayer: public AbstractLayer +{ + Q_OBJECT +public: + ObjectsLayer(MapSceneBase * s); + + void update() override; + + void draw(bool onlyDirty = true); //TODO: implement dirty + + void setDirty(int x, int y); + void setDirty(const CGObjectInstance * object); + +private: + std::set objDirty; + std::set dirty; +}; + + +class SelectionObjectsLayer: public AbstractLayer +{ + Q_OBJECT +public: + SelectionObjectsLayer(MapSceneBase* s); + + void update() override; + + void draw(); + + CGObjectInstance * selectObjectAt(int x, int y) const; + void selectObjects(int x1, int y1, int x2, int y2); + void selectObject(CGObjectInstance *, bool inform = true); + void deselectObject(CGObjectInstance *); + bool isSelected(const CGObjectInstance *) const; + std::set getSelection() const; + void moveSelection(int x, int y); + void clear(); + + QPoint shift; + CGObjectInstance * newObject; + //FIXME: magic number + int selectionMode = 0; //0 - nothing, 1 - selection, 2 - movement + +signals: + void selectionMade(bool anythingSlected); + +private: + std::set selectedObjects; + + void onSelection(); +}; + +class MinimapLayer: public AbstractLayer +{ +public: + MinimapLayer(MapSceneBase * s); + + void update() override; +}; + +class MinimapViewLayer: public AbstractLayer +{ +public: + MinimapViewLayer(MapSceneBase * s); + + void setViewport(int x, int y, int w, int h); + + void draw(); + void update() override; + + int viewportX() const {return x;} + int viewportY() const {return y;} + int viewportWidth() const {return w;} + int viewportHeight() const {return h;} + +private: + int x = 0, y = 0, w = 1, h = 1; + +}; + +#endif // SCENELAYER_H diff --git a/mapeditor/spoiler.cpp b/mapeditor/spoiler.cpp new file mode 100644 index 000000000..5d8535694 --- /dev/null +++ b/mapeditor/spoiler.cpp @@ -0,0 +1,59 @@ +#include "StdInc.h" +#include + +#include "spoiler.h" + +Spoiler::Spoiler(const QString & title, const int animationDuration, QWidget *parent) : QWidget(parent), animationDuration(animationDuration) +{ + toggleButton.setStyleSheet("QToolButton { border: none; }"); + toggleButton.setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + toggleButton.setArrowType(Qt::ArrowType::RightArrow); + toggleButton.setText(title); + toggleButton.setCheckable(true); + toggleButton.setChecked(false); + + headerLine.setFrameShape(QFrame::HLine); + headerLine.setFrameShadow(QFrame::Sunken); + headerLine.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); + + contentArea.setStyleSheet("QScrollArea { background-color: white; border: none; }"); + contentArea.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + // start out collapsed + contentArea.setMaximumHeight(0); + contentArea.setMinimumHeight(0); + // let the entire widget grow and shrink with its content + toggleAnimation.addAnimation(new QPropertyAnimation(this, "minimumHeight")); + toggleAnimation.addAnimation(new QPropertyAnimation(this, "maximumHeight")); + toggleAnimation.addAnimation(new QPropertyAnimation(&contentArea, "maximumHeight")); + // don't waste space + mainLayout.setVerticalSpacing(0); + mainLayout.setContentsMargins(0, 0, 0, 0); + int row = 0; + mainLayout.addWidget(&toggleButton, row, 0, 1, 1, Qt::AlignLeft); + mainLayout.addWidget(&headerLine, row++, 2, 1, 1); + mainLayout.addWidget(&contentArea, row, 0, 1, 3); + setLayout(&mainLayout); + QObject::connect(&toggleButton, &QToolButton::clicked, [this](const bool checked) { + toggleButton.setArrowType(checked ? Qt::ArrowType::DownArrow : Qt::ArrowType::RightArrow); + toggleAnimation.setDirection(checked ? QAbstractAnimation::Forward : QAbstractAnimation::Backward); + toggleAnimation.start(); + }); +} + +void Spoiler::setContentLayout(QLayout & contentLayout) +{ + delete contentArea.layout(); + contentArea.setLayout(&contentLayout); + const auto collapsedHeight = sizeHint().height() - contentArea.maximumHeight(); + auto contentHeight = contentLayout.sizeHint().height(); + for (int i = 0; i < toggleAnimation.animationCount() - 1; ++i) { + QPropertyAnimation * spoilerAnimation = static_cast(toggleAnimation.animationAt(i)); + spoilerAnimation->setDuration(animationDuration); + spoilerAnimation->setStartValue(collapsedHeight); + spoilerAnimation->setEndValue(collapsedHeight + contentHeight); + } + QPropertyAnimation * contentAnimation = static_cast(toggleAnimation.animationAt(toggleAnimation.animationCount() - 1)); + contentAnimation->setDuration(animationDuration); + contentAnimation->setStartValue(0); + contentAnimation->setEndValue(contentHeight); +} diff --git a/mapeditor/spoiler.h b/mapeditor/spoiler.h new file mode 100644 index 000000000..86ef6b10d --- /dev/null +++ b/mapeditor/spoiler.h @@ -0,0 +1,27 @@ +#ifndef SPOILER_H +#define SPOILER_H + +#include +#include +#include +#include +#include +#include + +class Spoiler : public QWidget { + Q_OBJECT + +private: + QGridLayout mainLayout; + QToolButton toggleButton; + QFrame headerLine; + QParallelAnimationGroup toggleAnimation; + QScrollArea contentArea; + int animationDuration{300}; + +public: + explicit Spoiler(const QString & title = "", const int animationDuration = 300, QWidget *parent = 0); + void setContentLayout(QLayout & contentLayout); +}; + +#endif // SPOILER_H diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp new file mode 100644 index 000000000..766912723 --- /dev/null +++ b/mapeditor/validator.cpp @@ -0,0 +1,159 @@ +#include "StdInc.h" +#include "validator.h" +#include "ui_validator.h" +#include "../lib/mapObjects/MapObjects.h" +#include "../lib/CHeroHandler.h" + +Validator::Validator(const CMap * map, QWidget *parent) : + QDialog(parent), + ui(new Ui::Validator) +{ + ui->setupUi(this); + + show(); + + setAttribute(Qt::WA_DeleteOnClose); + + std::array icons{"mapeditor/icons/mod-update.png", "mapeditor/icons/mod-delete.png"}; + + for(auto & issue : Validator::validate(map)) + { + auto * item = new QListWidgetItem(QIcon(icons[issue.critical ? 1 : 0]), issue.message); + ui->listWidget->addItem(item); + } +} + +Validator::~Validator() +{ + delete ui; +} + +std::list Validator::validate(const CMap * map) +{ + std::list issues; + + if(!map) + { + issues.emplace_back("Map is not loaded", true); + return issues; + } + + try + { + //check player settings + int hplayers = 0; + int cplayers = 0; + std::map amountOfCastles; + for(int i = 0; i < map->players.size(); ++i) + { + auto & p = map->players[i]; + if(p.canAnyonePlay()) + amountOfCastles[i] = 0; + if(p.canComputerPlay) + ++cplayers; + if(p.canHumanPlay) + ++hplayers; + if(p.allowedFactions.empty()) + issues.emplace_back(QString("No factions allowed for player %1").arg(i), true); + } + if(hplayers + cplayers == 0) + issues.emplace_back("No players allowed to play this map", true); + if(hplayers + cplayers == 1) + issues.emplace_back("Map is allowed for one player and cannot be started", true); + if(!hplayers) + issues.emplace_back("No human players allowed to play this map", true); + + //checking all objects in the map + for(auto o : map->objects) + { + //owners for objects + if(o->getOwner() == PlayerColor::UNFLAGGABLE) + { + if(dynamic_cast(o.get()) || + dynamic_cast(o.get()) || + dynamic_cast(o.get()) || + dynamic_cast(o.get()) || + dynamic_cast(o.get())) + { + issues.emplace_back(QString("Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner").arg(o->instanceName.c_str()), true); + } + } + //checking towns + if(auto * ins = dynamic_cast(o.get())) + { + bool has = amountOfCastles.count(ins->getOwner().getNum()); + if(!has && ins->getOwner() != PlayerColor::NEUTRAL) + issues.emplace_back(QString("Town %1 has undefined owner %s").arg(ins->instanceName.c_str(), ins->getOwner().getStr().c_str()), true); + if(has) + ++amountOfCastles[ins->getOwner().getNum()]; + } + //checking heroes and prisons + if(auto * ins = dynamic_cast(o.get())) + { + if(ins->ID == Obj::PRISON) + { + if(ins->getOwner() != PlayerColor::NEUTRAL) + issues.emplace_back(QString("Prison %1 must be a NEUTRAL").arg(ins->instanceName.c_str()), true); + } + else + { + bool has = amountOfCastles.count(ins->getOwner().getNum()); + if(!has) + issues.emplace_back(QString("Hero %1 must have an owner").arg(ins->instanceName.c_str()), true); + else + issues.emplace_back(QString("Hero %1: heroes on map are not supported in current version").arg(ins->instanceName.c_str()), false); + } + if(ins->type) + { + if(!map->allowedHeroes[ins->type->getId().getNum()]) + issues.emplace_back(QString("Hero %1 is prohibited by map settings").arg(ins->instanceName.c_str()), false); + } + else + issues.emplace_back(QString("Hero %1 has an empty type and must be removed").arg(ins->instanceName.c_str()), true); + } + + //checking for arts + if(auto * ins = dynamic_cast(o.get())) + { + if(ins->ID == Obj::SPELL_SCROLL) + { + if(ins->storedArtifact) + { + if(!map->allowedSpell[ins->storedArtifact->id.getNum()]) + issues.emplace_back(QString("Spell scroll %1 is prohibited by map settings").arg(ins->instanceName.c_str()), false); + } + else + issues.emplace_back(QString("Spell scroll %1 doesn't have instance assigned and must be removed").arg(ins->instanceName.c_str()), true); + } + else + { + if(ins->ID == Obj::ARTIFACT && !map->allowedArtifact[ins->subID]) + { + issues.emplace_back(QString("Artifact %1 is prohibited by map settings").arg(ins->instanceName.c_str()), false); + } + } + } + } + + //verification of starting towns + for(auto & mp : amountOfCastles) + if(mp.second == 0) + issues.emplace_back(QString("Player %1 doesn't have any starting town").arg(mp.first), false); + + //verification of map name and description + if(map->name.empty()) + issues.emplace_back("Map name is not specified", false); + if(map->description.empty()) + issues.emplace_back("Map description is not specified", false); + } + catch(const std::exception & e) + { + issues.emplace_back(QString("Exception occurs during validation: %1").arg(e.what()), true); + } + catch(...) + { + issues.emplace_back("Unknown exception occurs during validation", true); + } + + return issues; +} diff --git a/mapeditor/validator.h b/mapeditor/validator.h new file mode 100644 index 000000000..26955c1b2 --- /dev/null +++ b/mapeditor/validator.h @@ -0,0 +1,33 @@ +#ifndef VALIDATOR_H +#define VALIDATOR_H + +#include +#include "../lib/mapping/CMap.h" + +namespace Ui { +class Validator; +} + +class Validator : public QDialog +{ + Q_OBJECT +public: + struct Issue + { + QString message; + bool critical; + + Issue(const QString & m, bool c): message(m), critical(c) {} + }; + +public: + explicit Validator(const CMap * map, QWidget *parent = nullptr); + ~Validator(); + + static std::list validate(const CMap * map); + +private: + Ui::Validator *ui; +}; + +#endif // VALIDATOR_H diff --git a/mapeditor/validator.ui b/mapeditor/validator.ui new file mode 100644 index 000000000..ac3dc607c --- /dev/null +++ b/mapeditor/validator.ui @@ -0,0 +1,72 @@ + + + Validator + + + Qt::NonModal + + + + 0 + 0 + 482 + 178 + + + + Map validation results + + + true + + + + + + + 18 + + + + QFrame::Sunken + + + 1 + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + + 32 + 32 + + + + QListView::Adjust + + + + 0 + 32 + + + + QListView::ListMode + + + false + + + true + + + + + + + + diff --git a/mapeditor/windownewmap.cpp b/mapeditor/windownewmap.cpp new file mode 100644 index 000000000..b02a45e65 --- /dev/null +++ b/mapeditor/windownewmap.cpp @@ -0,0 +1,405 @@ +#include "StdInc.h" +#include "../lib/mapping/CMap.h" +#include "../lib/rmg/CRmgTemplateStorage.h" +#include "../lib/rmg/CRmgTemplate.h" +#include "../lib/rmg/CMapGenerator.h" +#include "../lib/VCMI_Lib.h" +#include "../lib/mapping/CMapEditManager.h" +#include "../lib/CGeneralTextHandler.h" + +#include "windownewmap.h" +#include "ui_windownewmap.h" +#include "mainwindow.h" +#include "generatorprogress.h" + +WindowNewMap::WindowNewMap(QWidget *parent) : + QDialog(parent), + ui(new Ui::WindowNewMap) +{ + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose); + + setWindowModality(Qt::ApplicationModal); + + loadUserSettings(); + + ui->widthTxt->setInputMask("d00"); + ui->heightTxt->setInputMask("d00"); + + //setup initial parameters - can depend on loaded settings + mapGenOptions.setWidth(ui->widthTxt->text().toInt()); + mapGenOptions.setHeight(ui->heightTxt->text().toInt()); + bool twoLevel = ui->twoLevelCheck->isChecked(); + mapGenOptions.setHasTwoLevels(twoLevel); + updateTemplateList(); + + loadLastTemplate(); + + show(); +} + +WindowNewMap::~WindowNewMap() +{ + saveUserSettings(); + delete ui; +} + +void WindowNewMap::loadUserSettings() +{ + //load last saved settings + QSettings s(Ui::teamName, Ui::appName); + + auto width = s.value(newMapWidth); + if (width.isValid()) + { + ui->widthTxt->setText(width.toString()); + } + auto height = s.value(newMapHeight); + if (height.isValid()) + { + ui->heightTxt->setText(height.toString()); + } + auto twoLevel = s.value(newMapTwoLevel); + if (twoLevel.isValid()) + { + ui->twoLevelCheck->setChecked(twoLevel.toBool()); + } + auto generateRandom = s.value(newMapGenerateRandom); + if (generateRandom.isValid()) + { + ui->randomMapCheck->setChecked(generateRandom.toBool()); + } + auto players = s.value(newMapPlayers); + if (players.isValid()) + { + ui->humanCombo->setCurrentIndex(players.toInt()); + } + auto cpuPlayers = s.value(newMapCpuPlayers); + if (cpuPlayers.isValid()) + { + ui->cpuCombo->setCurrentIndex(cpuPlayers.toInt()); + } + //TODO: teams when implemented + + auto waterContent = s.value(newMapWaterContent); + if (waterContent.isValid()) + { + switch (waterContent.toInt()) + { + case EWaterContent::RANDOM: + ui->waterOpt1->setChecked(true); break; + case EWaterContent::NONE: + ui->waterOpt2->setChecked(true); break; + case EWaterContent::NORMAL: + ui->waterOpt3->setChecked(true); break; + case EWaterContent::ISLANDS: + ui->waterOpt4->setChecked(true); break; + } + + } + auto monsterStrength = s.value(newMapMonsterStrength); + if (monsterStrength.isValid()) + { + switch (monsterStrength.toInt()) + { + case EMonsterStrength::RANDOM: + ui->monsterOpt1->setChecked(true); break; + case EMonsterStrength::GLOBAL_WEAK: + ui->monsterOpt2->setChecked(true); break; + case EMonsterStrength::GLOBAL_NORMAL: + ui->monsterOpt3->setChecked(true); break; + case EMonsterStrength::GLOBAL_STRONG: + ui->monsterOpt4->setChecked(true); break; + } + } +} + +void WindowNewMap::loadLastTemplate() +{ + //this requires already loaded template list + + QSettings s(Ui::teamName, Ui::appName); + auto templateName = s.value(newMapTemplate); + if (templateName.isValid()) + { + auto qstr = templateName.toString(); + + //Template might have been removed, then silently comboBox will be set to empty string + auto index = ui->templateCombo->findText(qstr); + ui->templateCombo->setCurrentIndex(index); + on_templateCombo_activated(index); + } +} + +void WindowNewMap::saveUserSettings() +{ + QSettings s(Ui::teamName, Ui::appName); + s.setValue(newMapWidth, ui->widthTxt->text().toInt()); + s.setValue(newMapHeight, ui->heightTxt->text().toInt()); + s.setValue(newMapTwoLevel, ui->twoLevelCheck->isChecked()); + s.setValue(newMapGenerateRandom, ui->randomMapCheck->isChecked()); + + s.setValue(newMapPlayers,ui->humanCombo->currentIndex()); + s.setValue(newMapCpuPlayers,ui->cpuCombo->currentIndex()); + //TODO: teams when implemented + + EWaterContent::EWaterContent water = EWaterContent::RANDOM; + if(ui->waterOpt1->isChecked()) + water = EWaterContent::RANDOM; + else if(ui->waterOpt2->isChecked()) + water = EWaterContent::NONE; + else if(ui->waterOpt3->isChecked()) + water = EWaterContent::NORMAL; + else if(ui->waterOpt4->isChecked()) + water = EWaterContent::ISLANDS; + s.setValue(newMapWaterContent, static_cast(water)); + + EMonsterStrength::EMonsterStrength monster = EMonsterStrength::RANDOM; + if(ui->monsterOpt1->isChecked()) + monster = EMonsterStrength::RANDOM; + else if(ui->monsterOpt2->isChecked()) + monster = EMonsterStrength::GLOBAL_WEAK; + else if(ui->monsterOpt3->isChecked()) + monster = EMonsterStrength::GLOBAL_NORMAL; + else if(ui->monsterOpt4->isChecked()) + monster = EMonsterStrength::GLOBAL_STRONG; + s.setValue(newMapMonsterStrength, static_cast(monster)); + + auto templateName = ui->templateCombo->currentText(); + if (templateName.size() && templateName != defaultTemplate) + { + s.setValue(newMapTemplate, templateName); + } + else + { + s.setValue(newMapTemplate, ""); + } +} + +void WindowNewMap::on_cancelButton_clicked() +{ + close(); +} + +void generateRandomMap(CMapGenerator & gen, MainWindow * window) +{ + window->controller.setMap(gen.generate()); +} + +std::unique_ptr generateEmptyMap(CMapGenOptions & options) +{ + std::unique_ptr map(new CMap); + map->version = EMapFormat::VCMI; + map->width = options.getWidth(); + map->height = options.getHeight(); + map->twoLevel = options.getHasTwoLevels(); + + map->initTerrain(); + map->getEditManager()->clearTerrain(&CRandomGenerator::getDefault()); + + return std::move(map); +} + +void WindowNewMap::on_okButtong_clicked() +{ + EWaterContent::EWaterContent water = EWaterContent::RANDOM; + EMonsterStrength::EMonsterStrength monster = EMonsterStrength::RANDOM; + if(ui->waterOpt1->isChecked()) + water = EWaterContent::RANDOM; + if(ui->waterOpt2->isChecked()) + water = EWaterContent::NONE; + if(ui->waterOpt3->isChecked()) + water = EWaterContent::NORMAL; + if(ui->waterOpt4->isChecked()) + water = EWaterContent::ISLANDS; + if(ui->monsterOpt1->isChecked()) + monster = EMonsterStrength::RANDOM; + if(ui->monsterOpt2->isChecked()) + monster = EMonsterStrength::GLOBAL_WEAK; + if(ui->monsterOpt3->isChecked()) + monster = EMonsterStrength::GLOBAL_NORMAL; + if(ui->monsterOpt4->isChecked()) + monster = EMonsterStrength::GLOBAL_STRONG; + + mapGenOptions.setWaterContent(water); + mapGenOptions.setMonsterStrength(monster); + + std::unique_ptr nmap; + if(ui->randomMapCheck->isChecked()) + { + //verify map template + if(mapGenOptions.getPossibleTemplates().empty()) + { + QMessageBox::warning(this, "No template", "No template for parameters scecified. Random map cannot be generated."); + return; + } + + CMapGenerator generator(mapGenOptions); + auto progressBarWnd = new GeneratorProgress(generator, this); + progressBarWnd->show(); + + try + { + auto f = std::async(std::launch::async, &CMapGenerator::generate, &generator); + progressBarWnd->update(); + nmap = f.get(); + } + catch(const std::exception & e) + { + QMessageBox::critical(this, "RMG failure", e.what()); + } + } + else + { + auto f = std::async(std::launch::async, &::generateEmptyMap, std::ref(mapGenOptions)); + nmap = f.get(); + } + + + static_cast(parent())->controller.setMap(std::move(nmap)); + static_cast(parent())->initializeMap(true); + close(); +} + +void WindowNewMap::on_sizeCombo_activated(int index) +{ + std::map> sizes + { + {0, {36, 36}}, + {1, {72, 72}}, + {2, {108, 108}}, + {3, {144, 144}}, + }; + + ui->widthTxt->setText(QString::number(sizes[index].first)); + ui->heightTxt->setText(QString::number(sizes[index].second)); +} + + +void WindowNewMap::on_twoLevelCheck_stateChanged(int arg1) +{ + bool twoLevel = ui->twoLevelCheck->isChecked(); + mapGenOptions.setHasTwoLevels(twoLevel); + updateTemplateList(); +} + + +void WindowNewMap::on_humanCombo_activated(int index) +{ + int humans = players.at(index); + if(humans > playerLimit) + { + humans = playerLimit; + ui->humanCombo->setCurrentIndex(humans); + return; + } + + mapGenOptions.setPlayerCount(humans); + + int teams = mapGenOptions.getTeamCount(); + if(teams > humans - 1) + { + teams = humans - 1; + //TBD + } + + int cpu = mapGenOptions.getCompOnlyPlayerCount(); + if(cpu > playerLimit - humans) + { + cpu = playerLimit - humans; + ui->cpuCombo->setCurrentIndex(cpu + 1); + } + + int cpuTeams = mapGenOptions.getCompOnlyTeamCount(); //comp only players - 1 + if(cpuTeams > cpu - 1) + { + cpuTeams = cpu - 1; + //TBD + } + + //void setMapTemplate(const CRmgTemplate * value); + updateTemplateList(); +} + + +void WindowNewMap::on_cpuCombo_activated(int index) +{ + int humans = mapGenOptions.getPlayerCount(); + int cpu = cpuPlayers.at(index); + if(cpu > playerLimit - humans) + { + cpu = playerLimit - humans; + ui->cpuCombo->setCurrentIndex(cpu + 1); + return; + } + + mapGenOptions.setCompOnlyPlayerCount(cpu); + updateTemplateList(); +} + + +void WindowNewMap::on_randomMapCheck_stateChanged(int arg1) +{ + randomMap = ui->randomMapCheck->isChecked(); + ui->templateCombo->setEnabled(randomMap); + updateTemplateList(); +} + + +void WindowNewMap::on_templateCombo_activated(int index) +{ + if(index == 0) + { + mapGenOptions.setMapTemplate(nullptr); + return; + } + + auto * templ = VLC->tplh->getTemplateByName(ui->templateCombo->currentText().toStdString()); + mapGenOptions.setMapTemplate(templ); +} + + +void WindowNewMap::on_widthTxt_textChanged(const QString &arg1) +{ + int sz = arg1.toInt(); + if(sz > 1) + { + mapGenOptions.setWidth(arg1.toInt()); + updateTemplateList(); + } +} + + +void WindowNewMap::on_heightTxt_textChanged(const QString &arg1) +{ + int sz = arg1.toInt(); + if(sz > 1) + { + mapGenOptions.setHeight(arg1.toInt()); + updateTemplateList(); + } +} + +void WindowNewMap::updateTemplateList() +{ + ui->templateCombo->clear(); + ui->templateCombo->setCurrentIndex(-1); + + if(!randomMap) + return; + + mapGenOptions.setMapTemplate(nullptr); + auto templates = mapGenOptions.getPossibleTemplates(); + if(templates.empty()) + return; + + ui->templateCombo->addItem(defaultTemplate); + + for(auto * templ : templates) + { + ui->templateCombo->addItem(QString::fromStdString(templ->getName())); + } + + ui->templateCombo->setCurrentIndex(0); +} diff --git a/mapeditor/windownewmap.h b/mapeditor/windownewmap.h new file mode 100644 index 000000000..3f9950131 --- /dev/null +++ b/mapeditor/windownewmap.h @@ -0,0 +1,96 @@ +#ifndef WINDOWNEWMAP_H +#define WINDOWNEWMAP_H + +#include +#include "../lib/rmg/CMapGenOptions.h" + +namespace Ui +{ + class WindowNewMap; +} + +class WindowNewMap : public QDialog +{ + Q_OBJECT + + const QString newMapWidth = "NewMapWindow/Width"; + const QString newMapHeight = "NewMapWindow/Height"; + const QString newMapTwoLevel = "NewMapWindow/TwoLevel"; + const QString newMapGenerateRandom = "NewMapWindow/GenerateRandom"; + const QString newMapPlayers = "NewMapWindow/Players"; //map index + const QString newMapCpuPlayers = "NewMapWindow/CpuPlayers"; //map index + const QString newMapWaterContent = "NewMapWindow/WaterContent"; + const QString newMapMonsterStrength = "NewMapWindow/MonsterStrength"; + const QString newMapTemplate = "NewMapWindow/Template"; + + const QString defaultTemplate = "[default]"; + + const int playerLimit = 8; + + const std::map players + { + {0, CMapGenOptions::RANDOM_SIZE}, + {1, 1}, + {2, 2}, + {3, 3}, + {4, 4}, + {5, 5}, + {6, 6}, + {7, 7}, + {8, 8} + }; + + const std::map cpuPlayers + { + {0, CMapGenOptions::RANDOM_SIZE}, + {1, 0}, + {2, 1}, + {3, 2}, + {4, 3}, + {5, 4}, + {6, 5}, + {7, 6}, + {8, 7} + }; + +public: + explicit WindowNewMap(QWidget *parent = nullptr); + ~WindowNewMap(); + +private slots: + void on_cancelButton_clicked(); + + void on_okButtong_clicked(); + + void on_sizeCombo_activated(int index); + + void on_twoLevelCheck_stateChanged(int arg1); + + void on_humanCombo_activated(int index); + + void on_cpuCombo_activated(int index); + + void on_randomMapCheck_stateChanged(int arg1); + + void on_templateCombo_activated(int index); + + void on_widthTxt_textChanged(const QString &arg1); + + void on_heightTxt_textChanged(const QString &arg1); + +private: + + void updateTemplateList(); + + void loadUserSettings(); + void loadLastTemplate(); + void saveUserSettings(); + +private: + Ui::WindowNewMap *ui; + + CMapGenOptions mapGenOptions; + bool randomMap = false; +}; + +#endif // WINDOWNEWMAP_H diff --git a/mapeditor/windownewmap.ui b/mapeditor/windownewmap.ui new file mode 100644 index 000000000..5723c626b --- /dev/null +++ b/mapeditor/windownewmap.ui @@ -0,0 +1,784 @@ + + + WindowNewMap + + + + 0 + 0 + 448 + 379 + + + + + 0 + 0 + + + + + 390 + 351 + + + + + 448 + 379 + + + + Create new map + + + false + + + + + 10 + 20 + 291 + 81 + + + + Map size + + + + + 0 + 20 + 261 + 68 + + + + + + + Two level map + + + + + + + Qt::ImhDigitsOnly + + + 36 + + + 3 + + + + + + + Height + + + + + + + Qt::ImhDigitsOnly + + + 36 + + + 3 + + + + + + + + 48 + 0 + + + + + 96 + 16777215 + + + + Width + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 8 + 20 + + + + + + + + + 96 + 0 + + + + + 120 + 16777215 + + + + + S (36x36) + + + + + M (72x72) + + + + + L (108x108) + + + + + XL (144x144) + + + + + + + + + + + + + 10 + 120 + 431 + 251 + + + + + 0 + 0 + + + + Random map + + + + + 10 + 20 + 411 + 91 + + + + Players + + + + + 10 + 20 + 391 + 68 + + + + + + + + 48 + 0 + + + + + 64 + 16777215 + + + + + 0 + + + + + + + + + 0 + + + + + + + + + 96 + 0 + + + + + 120 + 16777215 + + + + Human/Computer + + + + + + + + 96 + 0 + + + + + 120 + 16777215 + + + + + Random + + + + + 1 + + + + + 2 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 7 + + + + + 8 + + + + + + + + Computer only + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + Random + + + + + 0 + + + + + 1 + + + + + 2 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 7 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 10 + 170 + 411 + 41 + + + + Monster strength + + + + + 0 + 20 + 411 + 26 + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Random + + + true + + + + + + + + 0 + 0 + + + + + 120 + 16777215 + + + + Weak + + + + + + + + 0 + 0 + + + + + 120 + 16777215 + + + + Normal + + + + + + + + 0 + 0 + + + + + 120 + 16777215 + + + + Strong + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 10 + 120 + 411 + 41 + + + + + 0 + 0 + + + + + 480 + 96 + + + + Water content + + + + + 0 + 20 + 411 + 26 + + + + + + + + 0 + 0 + + + + + 144 + 96 + + + + Random + + + true + + + + + + + + 0 + 0 + + + + + 144 + 96 + + + + None + + + + + + + + 0 + 0 + + + + + 144 + 96 + + + + Normal + + + + + + + + 0 + 0 + + + + + 144 + 96 + + + + Islands + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 10 + 220 + 411 + 32 + + + + + + + + 0 + 0 + + + + + 120 + 16777215 + + + + Template + + + + + + + false + + + -1 + + + + + + + + + + 10 + 100 + 291 + 20 + + + + Generate random map + + + + + + 310 + 20 + 111 + 101 + + + + + + + + 0 + 0 + + + + + 0 + 36 + + + + + 16777215 + 36 + + + + Ok + + + + + + + + 0 + 0 + + + + + 16777215 + 36 + + + + Cancel + + + + + + + + +