diff --git a/src/fheroes2/editor/editor_interface.cpp b/src/fheroes2/editor/editor_interface.cpp index 5f7840a1d..5272d870c 100644 --- a/src/fheroes2/editor/editor_interface.cpp +++ b/src/fheroes2/editor/editor_interface.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -62,7 +63,6 @@ #include "interface_radar.h" #include "localevent.h" #include "map_format_helper.h" -#include "map_object_info.h" #include "maps.h" #include "maps_fileinfo.h" #include "maps_tiles.h" @@ -866,6 +866,167 @@ namespace return allowedMonsters; } + bool isObjectMovable( const Maps::ObjectGroup group ) + { + switch ( group ) { + case Maps::ObjectGroup::ADVENTURE_ARTIFACTS: + case Maps::ObjectGroup::ADVENTURE_DWELLINGS: + case Maps::ObjectGroup::ADVENTURE_MINES: + case Maps::ObjectGroup::ADVENTURE_MISCELLANEOUS: + case Maps::ObjectGroup::ADVENTURE_POWER_UPS: + case Maps::ObjectGroup::ADVENTURE_TREASURES: + case Maps::ObjectGroup::ADVENTURE_WATER: + case Maps::ObjectGroup::KINGDOM_HEROES: + case Maps::ObjectGroup::KINGDOM_TOWNS: + case Maps::ObjectGroup::LANDSCAPE_MISCELLANEOUS: + case Maps::ObjectGroup::LANDSCAPE_MOUNTAINS: + case Maps::ObjectGroup::LANDSCAPE_ROCKS: + case Maps::ObjectGroup::LANDSCAPE_TREES: + case Maps::ObjectGroup::LANDSCAPE_WATER: + case Maps::ObjectGroup::MONSTERS: + return true; + default: + return false; + } + } + + bool getMovableObjectInfo( const Maps::Map_Format::MapFormat & mapFormat, const int32_t tileId, int32_t & type, Maps::ObjectGroup & group, uint32_t & objectUID ) + { + // Check which object we really need to move. + // The logic should be aligned with placeObjectOnTile() function from maps_tiles_helper.cpp file. + const auto & tile = world.getTile( tileId ); + const MP2::MapObjectType objectType{ tile.getMainObjectType() }; + if ( objectType == MP2::OBJ_NONE ) { + // No object is on the tile. + return false; + } + + // Many objects are located on more than one tile. + // In this case we cannot rely just on MapFormat. + // + // First, find all possible objects in the area around the tile. + const fheroes2::Point tilePos{ tileId % mapFormat.width, tileId / mapFormat.width }; + + const int32_t minX = std::max( 0, tilePos.x - Maps::maxObjectDimensions.width ); + const int32_t maxX = std::min( mapFormat.width, tilePos.x + Maps::maxObjectDimensions.width + 1 ); + const int32_t minY = std::max( 0, tilePos.y - Maps::maxObjectDimensions.height ); + const int32_t maxY = std::min( mapFormat.width, tilePos.y + Maps::maxObjectDimensions.height + 1 ); + + std::map potentialObjects; + for ( int32_t y = minY; y < maxY; ++y ) { + const int32_t tileOffsetY{ y * mapFormat.width }; + for ( int32_t x = minX; x < maxX; ++x ) { + for ( const auto & object : mapFormat.tiles[x + tileOffsetY].objects ) { + if ( isObjectMovable( object.group ) ) { + potentialObjects.emplace( object.id, &object ); + } + } + } + } + + if ( potentialObjects.empty() ) { + // No movable objects exist. + return false; + } + + auto foundObjectIter = potentialObjects.end(); + + auto checkMainObjectPart = [&tile, &potentialObjects, objectType]() { + const auto & mainObjectPart = tile.getMainObjectPart(); + if ( mainObjectPart.isPassabilityTransparent() ) { + return potentialObjects.end(); + } + + // This is an action object. Only the main object part and then ground object parts should be searched. + if ( Maps::getObjectTypeByIcn( mainObjectPart.icnType, mainObjectPart.icnIndex ) == objectType ) { + return potentialObjects.find( mainObjectPart._uid ); + } + + return potentialObjects.end(); + }; + + auto searchGroundObjectParts = [&tile, &potentialObjects, objectType]() { + // We search from end to start of the container. + const auto & groundObjectParts = tile.getGroundObjectParts(); + for ( auto objectPartIter = groundObjectParts.crbegin(); objectPartIter != groundObjectParts.crend(); ++objectPartIter ) { + if ( objectPartIter->isPassabilityTransparent() ) { + continue; + } + + if ( Maps::getObjectTypeByIcn( objectPartIter->icnType, objectPartIter->icnIndex ) != objectType ) { + continue; + } + + auto potentialObjectIter = potentialObjects.find( objectPartIter->_uid ); + if ( potentialObjectIter != potentialObjects.end() ) { + return potentialObjectIter; + } + } + + return potentialObjects.end(); + }; + + auto searchTopObjectParts = [&tile, &potentialObjects, objectType]() { + // We search from end to start of the container. + const auto & topObjectParts = tile.getTopObjectParts(); + for ( auto objectPartIter = topObjectParts.crbegin(); objectPartIter != topObjectParts.crend(); ++objectPartIter ) { + if ( Maps::getObjectTypeByIcn( objectPartIter->icnType, objectPartIter->icnIndex ) != objectType ) { + continue; + } + + auto potentialObjectIter = potentialObjects.find( objectPartIter->_uid ); + if ( potentialObjectIter != potentialObjects.end() ) { + return potentialObjectIter; + } + } + + return potentialObjects.end(); + }; + + if ( MP2::isOffGameActionObject( objectType ) ) { + // This is an action object. Top level objects parts aren't considered for this case. + foundObjectIter = checkMainObjectPart(); + + if ( foundObjectIter == potentialObjects.end() ) { + foundObjectIter = searchGroundObjectParts(); + } + } + else { + foundObjectIter = searchTopObjectParts(); + + if ( foundObjectIter == potentialObjects.end() ) { + foundObjectIter = checkMainObjectPart(); + } + + if ( foundObjectIter == potentialObjects.end() ) { + foundObjectIter = searchGroundObjectParts(); + } + } + + if ( foundObjectIter == potentialObjects.end() ) { + return false; + } + + // The object has been found. + assert( foundObjectIter->second != nullptr ); + const Maps::Map_Format::TileObjectInfo & object = *( foundObjectIter->second ); + + type = static_cast( object.index ); + if ( object.group == Maps::ObjectGroup::KINGDOM_TOWNS ) { + // Castles and towns store their colors inside flags. + // The chosen tile might not be the main tile of the castle. + // We need to get flag information from the main tile. + const int32_t mainTileIndex = Maps::Tile::getIndexOfMainTile( tile ); + + const int color = Maps::getTownColorIndex( mapFormat, ( mainTileIndex >= 0 ) ? mainTileIndex : tileId, object.id ); + type = Interface::EditorPanel::generateTownObjectProperties( type, color ); + } + + group = object.group; + objectUID = object.id; + return true; + } + #if defined( WITH_DEBUG ) int32_t getObjectIndex( const Maps::Map_Format::MapFormat & mapFormat, const uint32_t uid, const Maps::ObjectGroup group ) { @@ -1436,7 +1597,6 @@ namespace Interface _brushTiles.clear(); } } - else if ( _editorPanel.isStreamDraw() ) { if ( le.isMouseLeftButtonPressed() ) { if ( _brushTiles.count( _tileUnderCursor ) == 0 ) { @@ -1455,6 +1615,36 @@ namespace Interface _brushTiles.clear(); } } + else if ( _editorPanel.isDetailEdit() ) { + if ( le.isMouseLeftButtonPressed() ) { + if ( _brushTiles.count( _tileUnderCursor ) == 0 ) { + if ( _brushTiles.empty() ) { + _movableObjectInfo = {}; + _movableObjectInfo.tileIndex = _tileUnderCursor; + } + + if ( _brushTiles.size() == 1 ) { + assert( _movableObjectInfo.tileIndex >= 0 ); + + if ( getMovableObjectInfo( _mapFormat, _movableObjectInfo.tileIndex, _movableObjectInfo.objectType, _movableObjectInfo.groupType, + _movableObjectInfo.objectUID ) ) { + _editorPanel.setObjectBasedCursor( _movableObjectInfo.objectType, _movableObjectInfo.groupType ); + updateCursor( _tileUnderCursor ); + } + } + + _brushTiles.emplace( _tileUnderCursor ); + } + } + else { + if ( le.isMouseLeftButtonReleased() ) { + _tryToMoveObject( _movableObjectInfo, _tileUnderCursor ); + } + + // Make sure to clear the action related information. + _resetMovableObjectInfo(); + } + } } else if ( _areaSelectionStartTileId != -1 ) { assert( _editorPanel.showAreaSelectRect() && isBrushEmpty ); @@ -1728,6 +1918,9 @@ namespace Interface // Trigger an action only when metadata has been changed to avoid expensive computations and bloated list of actions. // Comparing a metadata structure is much faster than restoring the whole map. + // Since this is an edit mode, clear object movement mode information. + _resetMovableObjectInfo(); + for ( const auto & object : _mapFormat.tiles[tileIndex].objects ) { const auto & objectGroupInfo = Maps::getObjectsByGroup( object.group ); assert( object.index <= objectGroupInfo.size() ); @@ -2183,20 +2376,30 @@ namespace Interface return; } - const fheroes2::Point tilePos = tile.GetCenter(); - const Maps::ObjectGroup groupType = _editorPanel.getSelectedObjectGroup(); - if ( _moveExistingObject( tile.GetIndex(), groupType, objectType ) ) { + if ( _tryToMoveObjectOnTop( tile.GetIndex(), groupType, objectType ) ) { return; } + std::unique_ptr action; + if ( _tryToPlaceObject( tile, objectType, groupType, true, action ) ) { + assert( action.get() != nullptr ); + action->commit(); + } + } + + bool EditorInterface::_tryToPlaceObject( Maps::Tile & tile, const int32_t objectType, const Maps::ObjectGroup groupType, const bool isNewObject, + std::unique_ptr & action ) + { + const fheroes2::Point tilePos = tile.GetCenter(); + std::string errorMessage; if ( groupType == Maps::ObjectGroup::KINGDOM_HEROES ) { if ( !verifyObjectPlacement( tilePos, groupType, objectType, errorMessage ) ) { _warningMessage.reset( std::move( errorMessage ) ); - return; + return false; } const auto & objectInfo = Maps::getObjectInfo( groupType, objectType ); @@ -2230,18 +2433,22 @@ namespace Interface std::string warning( _( "A maximum of %{count} heroes including jailed heroes can be placed on the map." ) ); StringReplace( warning, "%{count}", AllHeroes::getMaximumAllowedHeroes() ); _warningMessage.reset( std::move( warning ) ); - return; + return false; } if ( kingdomHeroCount >= GameStatic::GetKingdomMaxHeroes() ) { std::string warning( _( "A maximum of %{count} heroes of the same color can be placed on the map." ) ); StringReplace( warning, "%{count}", GameStatic::GetKingdomMaxHeroes() ); _warningMessage.reset( std::move( warning ) ); - return; + return false; } - if ( !_setObjectOnTileAsAction( tile, groupType, objectType ) ) { - return; + if ( !action ) { + action = std::make_unique( _historyManager, _mapFormat ); + } + + if ( !_setObjectOnTile( tile, groupType, objectType ) ) { + return false; } Heroes * hero = world.GetHeroForHire( Race::IndexToRace( static_cast( objectInfo.metadata[1] ) ) ); @@ -2261,7 +2468,7 @@ namespace Interface else if ( groupType == Maps::ObjectGroup::ADVENTURE_ARTIFACTS ) { if ( !verifyObjectPlacement( tilePos, groupType, objectType, errorMessage ) ) { _warningMessage.reset( std::move( errorMessage ) ); - return; + return false; } const auto & objectInfo = Maps::getObjectInfo( groupType, objectType ); @@ -2272,7 +2479,7 @@ namespace Interface for ( const auto & object : tileInfo.objects ) { if ( groupType == object.group && static_cast( objectType ) == object.index ) { _warningMessage.reset( _( "Only one Random Ultimate Artifact can be placed on the map." ) ); - return; + return false; } } } @@ -2280,15 +2487,22 @@ namespace Interface // For each Spell Scroll artifact we select a spell. if ( objectInfo.objectType == MP2::OBJ_ARTIFACT && objectInfo.metadata[0] == Artifact::SPELL_SCROLL ) { - const int spellId = Dialog::selectSpell( Spell::RANDOM, true ).GetID(); + int spellId = Spell::NONE; + if ( isNewObject ) { + spellId = Dialog::selectSpell( Spell::RANDOM, true ).GetID(); - if ( spellId == Spell::NONE ) { - // We do not place the Spell Scroll artifact if the spell for it was not selected. - return; + if ( spellId == Spell::NONE ) { + // We do not place the Spell Scroll artifact if the spell for it was not selected. + return false; + } } - if ( !_setObjectOnTileAsAction( tile, groupType, objectType ) ) { - return; + if ( !action ) { + action = std::make_unique( _historyManager, _mapFormat ); + } + + if ( !_setObjectOnTile( tile, groupType, objectType ) ) { + return false; } assert( !_mapFormat.tiles[tile.GetIndex()].objects.empty() ); @@ -2304,32 +2518,38 @@ namespace Interface Maps::setSpellOnTile( tile, spellId ); } else { - _setObjectOnTileAsAction( tile, groupType, objectType ); + if ( !action ) { + action = std::make_unique( _historyManager, _mapFormat ); + } + + if ( !_setObjectOnTile( tile, groupType, objectType ) ) { + return false; + } } } else if ( groupType == Maps::ObjectGroup::KINGDOM_TOWNS ) { int32_t type = -1; int32_t color = -1; - _editorPanel.getTownObjectProperties( type, color ); + EditorPanel::getTownObjectProperties( objectType, type, color ); if ( type < 0 || color < 0 ) { // Check your logic! assert( 0 ); - return; + return false; } if ( !verifyObjectPlacement( tilePos, groupType, type, errorMessage ) ) { _warningMessage.reset( std::move( errorMessage ) ); - return; + return false; } - fheroes2::ActionCreator action( _historyManager, _mapFormat ); + if ( !action ) { + action = std::make_unique( _historyManager, _mapFormat ); + } if ( !_placeCastle( tilePos.x, tilePos.y, Color::IndexToColor( color ), type ) ) { - return; + return false; } - action.commit(); - if ( !Maps::updateMapPlayers( _mapFormat ) ) { _warningMessage.reset( _( "Failed to update player information." ) ); } @@ -2338,40 +2558,40 @@ namespace Interface if ( objectType < 0 ) { // Check your logic! assert( 0 ); - return; + return false; } if ( !verifyObjectPlacement( tilePos, groupType, objectType, errorMessage ) ) { _warningMessage.reset( std::move( errorMessage ) ); - return; + return false; } - fheroes2::ActionCreator action( _historyManager, _mapFormat ); + if ( !action ) { + action = std::make_unique( _historyManager, _mapFormat ); + } if ( !_setObjectOnTile( tile, groupType, objectType ) ) { - return; + return false; } - - action.commit(); } else if ( groupType == Maps::ObjectGroup::LANDSCAPE_MISCELLANEOUS ) { if ( !verifyObjectPlacement( tilePos, groupType, objectType, errorMessage ) ) { _warningMessage.reset( std::move( errorMessage ) ); - return; + return false; } - fheroes2::ActionCreator action( _historyManager, _mapFormat ); + if ( !action ) { + action = std::make_unique( _historyManager, _mapFormat ); + } if ( !_setObjectOnTile( tile, groupType, objectType ) ) { - return; + return false; } // For River Deltas we update the nearby Streams to properly connect to them. if ( const int riverDeltaDirection = Maps::getRiverDeltaDirectionByIndex( groupType, objectType ); riverDeltaDirection != Direction::UNKNOWN ) { Maps::updateStreamsToDeltaConnection( _mapFormat, tile.GetIndex(), riverDeltaDirection ); } - - action.commit(); } else if ( groupType == Maps::ObjectGroup::ADVENTURE_MISCELLANEOUS ) { const auto & objectInfo = Maps::getObjectInfo( groupType, objectType ); @@ -2382,7 +2602,7 @@ namespace Interface std::string warning( _( "A maximum of %{count} obelisks can be placed on the map." ) ); StringReplace( warning, "%{count}", numOfPuzzleTiles ); _warningMessage.reset( std::move( warning ) ); - return; + return false; } } else if ( objectInfo.objectType == MP2::OBJ_JAIL ) { @@ -2401,25 +2621,35 @@ namespace Interface std::string warning( _( "A maximum of %{count} heroes including jailed heroes can be placed on the map." ) ); StringReplace( warning, "%{count}", AllHeroes::getMaximumAllowedHeroes() ); _warningMessage.reset( std::move( warning ) ); - return; + return false; } } if ( !verifyObjectPlacement( tilePos, groupType, objectType, errorMessage ) ) { _warningMessage.reset( std::move( errorMessage ) ); - return; + return false; } - _setObjectOnTileAsAction( tile, groupType, objectType ); + if ( !action ) { + action = std::make_unique( _historyManager, _mapFormat ); + } + + if ( !_setObjectOnTile( tile, groupType, objectType ) ) { + return false; + } } else if ( groupType == Maps::ObjectGroup::ADVENTURE_TREASURES ) { if ( !verifyObjectPlacement( tilePos, groupType, objectType, errorMessage ) ) { _warningMessage.reset( std::move( errorMessage ) ); - return; + return false; } - if ( !_setObjectOnTileAsAction( tile, groupType, objectType ) ) { - return; + if ( !action ) { + action = std::make_unique( _historyManager, _mapFormat ); + } + + if ( !_setObjectOnTile( tile, groupType, objectType ) ) { + return false; } assert( !_mapFormat.tiles[tile.GetIndex()].objects.empty() ); @@ -2437,11 +2667,82 @@ namespace Interface else { if ( !verifyObjectPlacement( tilePos, groupType, objectType, errorMessage ) ) { _warningMessage.reset( std::move( errorMessage ) ); - return; + return false; } - _setObjectOnTileAsAction( tile, groupType, objectType ); + if ( !action ) { + action = std::make_unique( _historyManager, _mapFormat ); + } + + if ( !_setObjectOnTile( tile, groupType, objectType ) ) { + return false; + } } + + return true; + } + + void EditorInterface::_tryToMoveObject( const MovableObjectInfo & movableObjectInfo, const int32_t destinationTile ) + { + assert( movableObjectInfo.tileIndex >= 0 ); + assert( destinationTile >= 0 ); + + if ( movableObjectInfo.groupType == Maps::ObjectGroup::NONE ) { + // No object to move. + return; + } + + if ( movableObjectInfo.tileIndex == destinationTile ) { + _tryToMoveObjectOnTop( movableObjectInfo.tileIndex, movableObjectInfo.groupType, movableObjectInfo.objectType ); + return; + } + + Maps::Tile & tile = world.getTile( destinationTile ); + + // Since we want to preserve the object UID (and not to break translations) + // we need to temporary set the last object UID here and then reset it back to what it should be before the changes. + const uint32_t originalLastObjectUID = Maps::getLastObjectUID(); + + // Some objects have metadata. We need to save it before removing objects. + // Ideally, we should save only the object's metadata but to make things easier, let's save everything for now. + // TODO: save only the current object metadata. + std::map castleMetadata = _mapFormat.castleMetadata; + std::map heroMetadata = _mapFormat.heroMetadata; + std::map sphinxMetadata = _mapFormat.sphinxMetadata; + std::map signMetadata = _mapFormat.signMetadata; + std::map adventureMapEventMetadata = _mapFormat.adventureMapEventMetadata; + std::map selectionObjectMetadata = _mapFormat.selectionObjectMetadata; + std::map capturableObjectsMetadata = _mapFormat.capturableObjectsMetadata; + std::map monsterMetadata = _mapFormat.monsterMetadata; + std::map artifactMetadata = _mapFormat.artifactMetadata; + std::map resourceMetadata = _mapFormat.resourceMetadata; + + auto action = std::make_unique( _historyManager, _mapFormat ); + removeObjects( _mapFormat, { movableObjectInfo.objectUID }, { movableObjectInfo.groupType } ); + + Maps::setLastObjectUID( movableObjectInfo.objectUID - 1 ); + + if ( _tryToPlaceObject( tile, movableObjectInfo.objectType, movableObjectInfo.groupType, false, action ) ) { + assert( action.get() != nullptr ); + + // If this assertion blows up then the code is invalid. + assert( Maps::getLastObjectUID() == movableObjectInfo.objectUID ); + + _mapFormat.castleMetadata = std::move( castleMetadata ); + _mapFormat.heroMetadata = std::move( heroMetadata ); + _mapFormat.sphinxMetadata = std::move( sphinxMetadata ); + _mapFormat.signMetadata = std::move( signMetadata ); + _mapFormat.adventureMapEventMetadata = std::move( adventureMapEventMetadata ); + _mapFormat.selectionObjectMetadata = std::move( selectionObjectMetadata ); + _mapFormat.capturableObjectsMetadata = std::move( capturableObjectsMetadata ); + _mapFormat.monsterMetadata = std::move( monsterMetadata ); + _mapFormat.artifactMetadata = std::move( artifactMetadata ); + _mapFormat.resourceMetadata = std::move( resourceMetadata ); + + action->commit(); + } + + Maps::setLastObjectUID( originalLastObjectUID ); } void EditorInterface::mouseCursorAreaPressRight( const int32_t tileIndex ) const @@ -2479,18 +2780,6 @@ namespace Interface return true; } - bool EditorInterface::_setObjectOnTileAsAction( Maps::Tile & tile, const Maps::ObjectGroup groupType, const int32_t objectIndex ) - { - fheroes2::ActionCreator action( _historyManager, _mapFormat ); - - if ( _setObjectOnTile( tile, groupType, objectIndex ) ) { - action.commit(); - return true; - } - - return false; - } - bool EditorInterface::updateRandomMapConfiguration( const int32_t mapWidth ) { Maps::Random_Generator::Configuration temp{ _randomMapConfig }; @@ -2766,14 +3055,14 @@ namespace Interface } } - bool EditorInterface::_moveExistingObject( const int32_t tileIndex, const Maps::ObjectGroup groupType, int32_t objectIndex ) + bool EditorInterface::_tryToMoveObjectOnTop( const int32_t tileIndex, const Maps::ObjectGroup groupType, int32_t objectIndex ) { assert( tileIndex >= 0 && static_cast( tileIndex ) < _mapFormat.tiles.size() ); if ( groupType == Maps::ObjectGroup::KINGDOM_TOWNS ) { int32_t type = -1; int32_t color = -1; - _editorPanel.getTownObjectProperties( type, color ); + EditorPanel::getTownObjectProperties( objectIndex, type, color ); if ( type < 0 || color < 0 ) { // Check your logic! assert( 0 ); @@ -2782,17 +3071,6 @@ namespace Interface objectIndex = type; } - else if ( groupType == Maps::ObjectGroup::ADVENTURE_MINES ) { - const int32_t type = _editorPanel.getSelectedObjectType(); - - if ( type < 0 ) { - // Check your logic! - assert( 0 ); - return false; - } - - objectIndex = type; - } for ( const auto & object : _mapFormat.tiles[tileIndex].objects ) { if ( object.group == groupType && object.index == static_cast( objectIndex ) ) { @@ -2899,4 +3177,18 @@ namespace Interface } } } + + void EditorInterface::_resetMovableObjectInfo() + { + if ( _movableObjectInfo.tileIndex < 0 ) { + return; + } + + _brushTiles.clear(); + _movableObjectInfo = {}; + + Cursor::Get().SetThemes( Cursor::POINTER ); + setCursorUpdater( {} ); + updateCursor( _tileUnderCursor ); + } } diff --git a/src/fheroes2/editor/editor_interface.h b/src/fheroes2/editor/editor_interface.h index ccd1d5d11..dfd9c29b4 100644 --- a/src/fheroes2/editor/editor_interface.h +++ b/src/fheroes2/editor/editor_interface.h @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -31,6 +32,7 @@ #include "history_manager.h" #include "interface_base.h" #include "map_format_info.h" +#include "map_object_info.h" #include "map_random_generator.h" #include "timing.h" @@ -39,8 +41,6 @@ enum class PlayerColor : uint8_t; namespace Maps { class Tile; - - enum class ObjectGroup : uint8_t; } namespace Interface @@ -147,6 +147,14 @@ namespace Interface fheroes2::Time _timer; }; + struct MovableObjectInfo final + { + int32_t tileIndex{ -1 }; + int32_t objectType{ -1 }; + uint32_t objectUID{ 0 }; + Maps::ObjectGroup groupType{ Maps::ObjectGroup::NONE }; + }; + EditorInterface() : BaseInterface( true ) , _editorPanel( *this ) @@ -159,14 +167,19 @@ namespace Interface bool _setObjectOnTile( Maps::Tile & tile, const Maps::ObjectGroup groupType, const int32_t objectIndex ); - bool _setObjectOnTileAsAction( Maps::Tile & tile, const Maps::ObjectGroup groupType, const int32_t objectIndex ); - void _handleObjectMouseLeftClick( Maps::Tile & tile ); + // Returns true if the placement was successful. + // 'action' input argument will be populated in case of success. + bool _tryToPlaceObject( Maps::Tile & tile, const int32_t objectType, const Maps::ObjectGroup groupType, const bool isNewObject, + std::unique_ptr & action ); + + void _tryToMoveObject( const MovableObjectInfo & movableObjectInfo, const int32_t destinationTile ); + void _validateObjectsOnTerrainUpdate(); - // Returns true if an existing object was moved. - bool _moveExistingObject( const int32_t tileIndex, const Maps::ObjectGroup groupType, int32_t objectIndex ); + // Returns true if an existing object was moved on top. + bool _tryToMoveObjectOnTop( const int32_t tileIndex, const Maps::ObjectGroup groupType, int32_t objectIndex ); void _updateObjectMetadata( const Maps::Map_Format::TileObjectInfo & object, const uint32_t newObjectUID ); @@ -174,6 +187,8 @@ namespace Interface bool _placeCastle( const int32_t posX, const int32_t posY, const PlayerColor color, const int32_t type ); + void _resetMovableObjectInfo(); + EditorPanel _editorPanel; int32_t _areaSelectionStartTileId{ -1 }; @@ -181,6 +196,8 @@ namespace Interface std::set _brushTiles; + MovableObjectInfo _movableObjectInfo; + Maps::Random_Generator::Configuration _randomMapConfig; std::function _cursorUpdater; diff --git a/src/fheroes2/editor/editor_interface_panel.cpp b/src/fheroes2/editor/editor_interface_panel.cpp index 408394bfa..24d813a06 100644 --- a/src/fheroes2/editor/editor_interface_panel.cpp +++ b/src/fheroes2/editor/editor_interface_panel.cpp @@ -553,7 +553,7 @@ namespace Interface } } else if ( _selectedInstrument == Instrument::DETAIL ) { - const fheroes2::Text instrumentName( _( "Cell\nDetails" ), fheroes2::FontType::normalWhite() ); + const fheroes2::Text instrumentName( _( "Click to edit\n\nor\n\nclick and hold to move" ), fheroes2::FontType::normalWhite() ); instrumentName.draw( _rectInstrumentPanel.x + 5, _rectInstrumentPanel.y + 8, _rectInstrumentPanel.width - 10, display ); } else if ( _selectedInstrument == Instrument::ROAD ) { @@ -620,16 +620,25 @@ namespace Interface if ( _selectedLandscapeObject < 0 ) { return -1; } + + assert( static_cast( _selectedLandscapeObject ) < _selectedLandscapeObjectType.size() ); + return _selectedLandscapeObjectType[_selectedLandscapeObject]; case Instrument::ADVENTURE_OBJECTS: if ( _selectedAdventureObject < 0 ) { return -1; } + + assert( static_cast( _selectedAdventureObject ) < _selectedAdventureObjectType.size() ); + return _selectedAdventureObjectType[_selectedAdventureObject]; case Instrument::KINGDOM_OBJECTS: if ( _selectedKingdomObject < 0 ) { return -1; } + + assert( static_cast( _selectedKingdomObject ) < _selectedKingdomObjectType.size() ); + return _selectedKingdomObjectType[_selectedKingdomObject]; default: // Why are you trying to get type for the non-object instrument. Check your logic! @@ -797,78 +806,24 @@ namespace Interface { switch ( _selectedInstrument ) { case Instrument::MONSTERS: - _interface.setCursorUpdater( - [type = getSelectedObjectType(), group = getSelectedObjectGroup()]( const int32_t /*tileIndex*/ ) { setCustomCursor( group, type ); } ); + setObjectBasedCursor( getSelectedObjectType(), getSelectedObjectGroup() ); return; case Instrument::LANDSCAPE_OBJECTS: - switch ( _selectedLandscapeObject ) { - case LandscapeObjectBrush::LANDSCAPE_MISC: - case LandscapeObjectBrush::MOUNTAINS: - case LandscapeObjectBrush::ROCKS: - case LandscapeObjectBrush::TREES: - case LandscapeObjectBrush::WATER_OBJECTS: - _interface.setCursorUpdater( - [type = getSelectedObjectType(), group = getSelectedObjectGroup()]( const int32_t /*tileIndex*/ ) { setCustomCursor( group, type ); } ); + if ( _selectedLandscapeObject > -1 ) { + setObjectBasedCursor( getSelectedObjectType(), getSelectedObjectGroup() ); return; - default: - break; } break; case Instrument::ADVENTURE_OBJECTS: - switch ( _selectedAdventureObject ) { - case AdventureObjectBrush::ADVENTURE_MISC: - case AdventureObjectBrush::ARTIFACTS: - case AdventureObjectBrush::DWELLINGS: - case AdventureObjectBrush::MINES: - case AdventureObjectBrush::POWER_UPS: - case AdventureObjectBrush::TREASURES: - case AdventureObjectBrush::WATER_ADVENTURE: - _interface.setCursorUpdater( - [type = getSelectedObjectType(), group = getSelectedObjectGroup()]( const int32_t /*tileIndex*/ ) { setCustomCursor( group, type ); } ); + if ( _selectedAdventureObject > -1 ) { + setObjectBasedCursor( getSelectedObjectType(), getSelectedObjectGroup() ); return; - default: - break; } break; case Instrument::KINGDOM_OBJECTS: - switch ( _selectedKingdomObject ) { - case KingdomObjectBrush::HEROES: - _interface.setCursorUpdater( [type = getSelectedObjectType()]( const int32_t /*tileIndex*/ ) { - if ( type == -1 ) { - // The object type is not set. We show the POINTER cursor for this case. - Cursor::Get().SetThemes( Cursor::POINTER ); - return; - } - - // TODO: render ICN::MINIHERO from the existing hero images. - const fheroes2::Sprite & image = fheroes2::AGG::GetICN( ICN::MINIHERO, type ); - - // Mini-hero images contain a pole with a flag. - // This causes a situation that a selected tile does not properly correspond to current position of the cursor. - // We need to add a hardcoded correction. - const int32_t heroCorrectionY{ 12 }; - Cursor::Get().setCustomImage( image, { -image.width() / 2, -image.height() / 2 - heroCorrectionY } ); - } ); + if ( _selectedKingdomObject > -1 ) { + setObjectBasedCursor( getSelectedObjectType(), getSelectedObjectGroup() ); return; - case KingdomObjectBrush::TOWNS: - _interface.setCursorUpdater( [this]( const int32_t tileIndex ) { - int32_t type = -1; - int32_t color = -1; - getTownObjectProperties( type, color ); - - if ( type == -1 || color == -1 ) { - // The object type is not set. We show the POINTER cursor for this case. - Cursor::Get().SetThemes( Cursor::POINTER ); - return; - } - - const fheroes2::Sprite & image = fheroes2::generateTownObjectImage( type, color, world.getTile( tileIndex ).GetGround() ); - - Cursor::Get().setCustomImage( image, { image.x(), image.y() } ); - } ); - return; - default: - break; } break; default: @@ -878,6 +833,68 @@ namespace Interface _interface.setCursorUpdater( {} ); } + void EditorPanel::setObjectBasedCursor( const int32_t type, const Maps::ObjectGroup group ) + { + if ( type < 0 ) { + // The object type is not set. We show the POINTER cursor for this case. + Cursor::Get().SetThemes( Cursor::POINTER ); + _interface.setCursorUpdater( {} ); + return; + } + + switch ( group ) { + case Maps::ObjectGroup::ADVENTURE_ARTIFACTS: + case Maps::ObjectGroup::ADVENTURE_DWELLINGS: + case Maps::ObjectGroup::ADVENTURE_MINES: + case Maps::ObjectGroup::ADVENTURE_MISCELLANEOUS: + case Maps::ObjectGroup::ADVENTURE_POWER_UPS: + case Maps::ObjectGroup::ADVENTURE_TREASURES: + case Maps::ObjectGroup::ADVENTURE_WATER: + case Maps::ObjectGroup::LANDSCAPE_MISCELLANEOUS: + case Maps::ObjectGroup::LANDSCAPE_MOUNTAINS: + case Maps::ObjectGroup::LANDSCAPE_ROCKS: + case Maps::ObjectGroup::LANDSCAPE_TREES: + case Maps::ObjectGroup::LANDSCAPE_WATER: + case Maps::ObjectGroup::MONSTERS: + _interface.setCursorUpdater( [group, type]( const int32_t /*tileIndex*/ ) { setCustomCursor( group, type ); } ); + return; + case Maps::ObjectGroup::KINGDOM_HEROES: + _interface.setCursorUpdater( [type]( const int32_t /*tileIndex*/ ) { + // TODO: render ICN::MINIHERO from the existing hero images. + const fheroes2::Sprite & image = fheroes2::AGG::GetICN( ICN::MINIHERO, type ); + + // Mini-hero images contain a pole with a flag. + // This causes a situation that a selected tile does not properly correspond to current position of the cursor. + // We need to add a hardcoded correction. + constexpr int32_t heroCorrectionY{ 12 }; + Cursor::Get().setCustomImage( image, { -image.width() / 2, -image.height() / 2 - heroCorrectionY } ); + } ); + return; + case Maps::ObjectGroup::KINGDOM_TOWNS: { + int32_t townType = -1; + int32_t color = -1; + getTownObjectProperties( type, townType, color ); + + _interface.setCursorUpdater( [townType, color]( const int32_t tileIndex ) { + if ( townType == -1 || color == -1 ) { + // The object type is not set. We show the POINTER cursor for this case. + Cursor::Get().SetThemes( Cursor::POINTER ); + return; + } + + const fheroes2::Sprite & image = fheroes2::generateTownObjectImage( townType, color, world.getTile( tileIndex ).GetGround() ); + + Cursor::Get().setCustomImage( image, { image.x(), image.y() } ); + } ); + return; + } + default: + break; + } + + _interface.setCursorUpdater( {} ); + } + void EditorPanel::updateUndoRedoButtonsStates( const bool isUndoAvailable, const bool isRedoAvailable ) { auto updateButtonState = []( fheroes2::ButtonBase & button, const bool isAvailable ) { @@ -1140,10 +1157,10 @@ namespace Interface int32_t type = -1; int32_t color = -1; - getTownObjectProperties( type, color ); + getTownObjectProperties( getSelectedObjectType(), type, color ); Dialog::selectTownType( type, color ); - return _generateTownObjectProperties( type, color ); + return generateTownObjectProperties( type, color ); } ); return res; } @@ -1213,7 +1230,7 @@ namespace Interface Dialog::ZERO ); } else if ( le.isMouseRightButtonPressedInArea( _instrumentButtonsRect[Instrument::DETAIL] ) ) { - fheroes2::showStandardTextMessage( _( "Detail Mode" ), _( "Used for special editing of action objects." ), Dialog::ZERO ); + fheroes2::showStandardTextMessage( _( "Detail Mode" ), _( "Used for moving or customizing objects." ), Dialog::ZERO ); } else if ( le.isMouseRightButtonPressedInArea( _instrumentButtonsRect[Instrument::ADVENTURE_OBJECTS] ) ) { fheroes2::showStandardTextMessage( _( "Adventure Objects Mode" ), @@ -1285,7 +1302,7 @@ namespace Interface _interface.updateCursor( _interface.getGameArea().GetValidTileIdFromPoint( LocalEvent::Get().getMouseCursorPos() ) ); } - void EditorPanel::getTownObjectProperties( int32_t & type, int32_t & color ) const + void EditorPanel::getTownObjectProperties( const int32_t packedType, int32_t & type, int32_t & color ) { const auto & townObjects = Maps::getObjectsByGroup( Maps::ObjectGroup::KINGDOM_TOWNS ); if ( townObjects.empty() ) { @@ -1296,11 +1313,11 @@ namespace Interface return; } - type = _selectedKingdomObjectType[KingdomObjectBrush::TOWNS] % static_cast( townObjects.size() ); - color = _selectedKingdomObjectType[KingdomObjectBrush::TOWNS] / static_cast( townObjects.size() ); + type = packedType % static_cast( townObjects.size() ); + color = packedType / static_cast( townObjects.size() ); } - int32_t EditorPanel::_generateTownObjectProperties( const int32_t type, const int32_t color ) + int32_t EditorPanel::generateTownObjectProperties( const int32_t type, const int32_t color ) { const auto & townObjects = Maps::getObjectsByGroup( Maps::ObjectGroup::KINGDOM_TOWNS ); if ( townObjects.empty() ) { diff --git a/src/fheroes2/editor/editor_interface_panel.h b/src/fheroes2/editor/editor_interface_panel.h index 59d3679b7..c6a3c5f0f 100644 --- a/src/fheroes2/editor/editor_interface_panel.h +++ b/src/fheroes2/editor/editor_interface_panel.h @@ -96,8 +96,17 @@ namespace Interface bool useMouseDragMovement() const { - return ( _selectedInstrument != Instrument::TERRAIN && _selectedInstrument != Instrument::ERASE && _selectedInstrument != Instrument::ROAD - && _selectedInstrument != Instrument::STREAM ); + switch ( _selectedInstrument ) { + case Instrument::ADVENTURE_OBJECTS: + case Instrument::KINGDOM_OBJECTS: + case Instrument::LANDSCAPE_OBJECTS: + case Instrument::MONSTERS: + return true; + default: + break; + } + + return false; } // Set Editor panel positions on screen. @@ -116,12 +125,16 @@ namespace Interface Maps::ObjectGroup getSelectedObjectGroup() const; - void getTownObjectProperties( int32_t & type, int32_t & color ) const; - static const char * getObjectGroupName( const Maps::ObjectGroup groupName ); void updateUndoRedoButtonsStates( const bool isUndoAvailable, const bool isRedoAvailable ); + void setObjectBasedCursor( const int32_t type, const Maps::ObjectGroup group ); + + static void getTownObjectProperties( const int32_t packedType, int32_t & type, int32_t & color ); + + static int32_t generateTownObjectProperties( const int32_t type, const int32_t color ); + private: static int _getGroundId( const uint8_t brushId ); @@ -135,8 +148,6 @@ namespace Interface static const char * _getKingdomObjectTypeName( const uint8_t brushId ); static const char * _getEraseObjectTypeName( const uint32_t eraseObjectType ); - static int32_t _generateTownObjectProperties( const int32_t type, const int32_t color ); - void _setCursor(); void handleObjectMouseClick( const std::function & typeSelection ); diff --git a/src/fheroes2/heroes/heroes.cpp b/src/fheroes2/heroes/heroes.cpp index 58016f556..1e73c6c72 100644 --- a/src/fheroes2/heroes/heroes.cpp +++ b/src/fheroes2/heroes/heroes.cpp @@ -1300,7 +1300,7 @@ void Heroes::SetVisited( const int32_t tileIndex, const Visit::Type type /* = Vi // An object could be bigger than 1 tile so we need to check all its tiles. constexpr int32_t searchDist = []() constexpr { - constexpr int32_t max = std::max( Maps::maxActionObjectDimensions.width, Maps::maxActionObjectDimensions.height ); + constexpr int32_t max = std::max( Maps::maxActionGroundObjectDimensions.width, Maps::maxActionGroundObjectDimensions.height ); static_assert( max > 0 ); return max - 1; @@ -1342,7 +1342,7 @@ void Heroes::setVisitedForAllies( const int32_t tileIndex ) const // An object could be bigger than 1 tile so we need to check all its tiles. constexpr int32_t searchDist = []() constexpr { - constexpr int32_t max = std::max( Maps::maxActionObjectDimensions.width, Maps::maxActionObjectDimensions.height ); + constexpr int32_t max = std::max( Maps::maxActionGroundObjectDimensions.width, Maps::maxActionGroundObjectDimensions.height ); static_assert( max > 0 ); return max - 1; diff --git a/src/fheroes2/maps/map_object_info.cpp b/src/fheroes2/maps/map_object_info.cpp index c557b4303..db75b0450 100644 --- a/src/fheroes2/maps/map_object_info.cpp +++ b/src/fheroes2/maps/map_object_info.cpp @@ -6156,24 +6156,45 @@ namespace } // Calculate the maximum possible dimensions of the area occupied by the parts of multi-tile action objects and verify them. + fheroes2::Size maxActionObjDim{ 1, 1 }; fheroes2::Size maxObjDim{ 1, 1 }; for ( const auto & objects : objectData ) { for ( const auto & objectInfo : objects ) { - if ( !MP2::isOffGameActionObject( objectInfo.objectType ) ) { - continue; + if ( MP2::isOffGameActionObject( objectInfo.objectType ) ) { + fheroes2::Point minOffset; + fheroes2::Point maxOffset; + + for ( const auto & info : objectInfo.groundLevelParts ) { + if ( info.objectType == objectInfo.objectType ) { + minOffset.x = std::min( minOffset.x, info.tileOffset.x ); + minOffset.y = std::min( minOffset.y, info.tileOffset.y ); + maxOffset.x = std::max( maxOffset.x, info.tileOffset.x ); + maxOffset.y = std::max( maxOffset.y, info.tileOffset.y ); + } + } + + const fheroes2::Size objDim{ maxOffset.x - minOffset.x + 1, maxOffset.y - minOffset.y + 1 }; + + maxActionObjDim.width = std::max( maxActionObjDim.width, objDim.width ); + maxActionObjDim.height = std::max( maxActionObjDim.height, objDim.height ); } fheroes2::Point minOffset; fheroes2::Point maxOffset; for ( const auto & info : objectInfo.groundLevelParts ) { - if ( info.objectType == objectInfo.objectType ) { - minOffset.x = std::min( minOffset.x, info.tileOffset.x ); - minOffset.y = std::min( minOffset.y, info.tileOffset.y ); - maxOffset.x = std::max( maxOffset.x, info.tileOffset.x ); - maxOffset.y = std::max( maxOffset.y, info.tileOffset.y ); - } + minOffset.x = std::min( minOffset.x, info.tileOffset.x ); + minOffset.y = std::min( minOffset.y, info.tileOffset.y ); + maxOffset.x = std::max( maxOffset.x, info.tileOffset.x ); + maxOffset.y = std::max( maxOffset.y, info.tileOffset.y ); + } + + for ( const auto & info : objectInfo.topLevelParts ) { + minOffset.x = std::min( minOffset.x, info.tileOffset.x ); + minOffset.y = std::min( minOffset.y, info.tileOffset.y ); + maxOffset.x = std::max( maxOffset.x, info.tileOffset.x ); + maxOffset.y = std::max( maxOffset.y, info.tileOffset.y ); } const fheroes2::Size objDim{ maxOffset.x - minOffset.x + 1, maxOffset.y - minOffset.y + 1 }; @@ -6185,7 +6206,8 @@ namespace // If this assertion blows up then some in-game logic might break. If a new object of a non-standard // size has been added, consider updating the maximum action object dimensions. - assert( maxObjDim == Maps::maxActionObjectDimensions ); + assert( maxActionObjDim == Maps::maxActionGroundObjectDimensions ); + assert( maxObjDim == Maps::maxObjectDimensions ); #endif isPopulated = true; diff --git a/src/fheroes2/maps/map_object_info.h b/src/fheroes2/maps/map_object_info.h index 73986a4bb..b181be8bb 100644 --- a/src/fheroes2/maps/map_object_info.h +++ b/src/fheroes2/maps/map_object_info.h @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2023 - 2025 * + * Copyright (C) 2023 - 2026 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * @@ -30,8 +30,11 @@ namespace Maps { - // The maximum possible dimensions (width and height) for an action object on the map. - constexpr fheroes2::Size maxActionObjectDimensions{ 4, 2 }; + // The maximum possible dimensions (width and height) for an action ground object on the map. + constexpr fheroes2::Size maxActionGroundObjectDimensions{ 4, 2 }; + + // The maximum object dimensions possible, including top and bottom parts. + constexpr fheroes2::Size maxObjectDimensions{ 8, 5 }; // An object usually contains of multiple parts / tiles. Each part has its own features like object layer type or image index. // An object always contains a main object part.