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 000000000..5f632e2b7 Binary files /dev/null and b/mapeditor/icons/menu-game.png differ diff --git a/mapeditor/icons/menu-mods.png b/mapeditor/icons/menu-mods.png new file mode 100644 index 000000000..92e5deaad Binary files /dev/null and b/mapeditor/icons/menu-mods.png differ diff --git a/mapeditor/icons/menu-settings.png b/mapeditor/icons/menu-settings.png new file mode 100644 index 000000000..4cce5d91e Binary files /dev/null and b/mapeditor/icons/menu-settings.png differ diff --git a/mapeditor/icons/mod-delete.png b/mapeditor/icons/mod-delete.png new file mode 100644 index 000000000..fa0c04d95 Binary files /dev/null and b/mapeditor/icons/mod-delete.png differ diff --git a/mapeditor/icons/mod-disabled.png b/mapeditor/icons/mod-disabled.png new file mode 100644 index 000000000..14b11cdf4 Binary files /dev/null and b/mapeditor/icons/mod-disabled.png differ diff --git a/mapeditor/icons/mod-download.png b/mapeditor/icons/mod-download.png new file mode 100644 index 000000000..15644a404 Binary files /dev/null and b/mapeditor/icons/mod-download.png differ diff --git a/mapeditor/icons/mod-enabled.png b/mapeditor/icons/mod-enabled.png new file mode 100644 index 000000000..40204a56f Binary files /dev/null and b/mapeditor/icons/mod-enabled.png differ diff --git a/mapeditor/icons/mod-update.png b/mapeditor/icons/mod-update.png new file mode 100644 index 000000000..b8bdbc6a2 Binary files /dev/null and b/mapeditor/icons/mod-update.png differ diff --git a/mapeditor/inspector/armywidget.cpp b/mapeditor/inspector/armywidget.cpp new file mode 100644 index 000000000..e171a009d --- /dev/null +++ b/mapeditor/inspector/armywidget.cpp @@ -0,0 +1,142 @@ +#include "armywidget.h" +#include "ui_armywidget.h" +#include "CCreatureHandler.h" + + +ArmyWidget::ArmyWidget(CArmedInstance & a, QWidget *parent) : + QDialog(parent), + army(a), + ui(new Ui::ArmyWidget) +{ + ui->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 000000000..544c420fa Binary files /dev/null and b/mapeditor/mapeditor.ico differ 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 + + + + + + + + +