浏览代码

Merge pull request #2030 from Nordsoft91/boats

Nordsoft91 2 年之前
父节点
当前提交
a553a4aa66

+ 1 - 1
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -864,7 +864,7 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
 
 		if(actor->hero->tempOwner != ai->playerID)
 		{
-			bool onLand = !actor->hero->boat;
+			bool onLand = !actor->hero->boat || actor->hero->boat->layer != EPathfindingLayer::SAIL;
 			actor->initialMovement = actor->hero->maxMovePoints(onLand);
 		}
 

+ 1 - 1
AI/Nullkiller/Pathfinding/Actors.cpp

@@ -42,7 +42,7 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t
 	baseActor(this), carrierParent(nullptr), otherParent(nullptr), actorExchangeCount(1), armyCost(), actorAction()
 {
 	initialPosition = hero->visitablePos();
-	layer = hero->boat ? EPathfindingLayer::SAIL : EPathfindingLayer::LAND;
+	layer = hero->boat ? hero->boat->layer : EPathfindingLayer::LAND;
 	initialMovement = hero->movement;
 	initialTurn = 0;
 	armyValue = hero->getArmyStrength();

+ 7 - 26
client/mapView/MapRenderer.cpp

@@ -26,6 +26,7 @@
 #include "../../lib/RoadHandler.h"
 #include "../../lib/TerrainHandler.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/mapObjects/MiscObjects.h"
 #include "../../lib/mapping/CMap.h"
 
 struct NeighborTilesInfo
@@ -394,15 +395,11 @@ std::shared_ptr<CAnimation> MapRendererObjects::getBaseAnimation(const CGObjectI
 
 	bool generateMovementGroups = (info->id == Obj::BOAT) || (info->id == Obj::HERO);
 
-	//TODO: relocate to config file?
 	// Boat appearance files only contain single, unanimated image
 	// proper boat animations are actually in different file
-	static const std::vector<std::string> boatAnimations = {
-		"AB01_.def", "AB02_.def", "AB03_.def"
-	};
-
 	if (info->id == Obj::BOAT)
-		return getAnimation(boatAnimations[info->subid], generateMovementGroups);
+		if(auto boat = dynamic_cast<const CGBoat*>(obj); boat && !boat->actualAnimation.empty())
+			return getAnimation(boat->actualAnimation, generateMovementGroups);
 
 	return getAnimation(info->animationFile, generateMovementGroups);
 }
@@ -438,13 +435,6 @@ std::shared_ptr<CAnimation> MapRendererObjects::getFlagAnimation(const CGObjectI
 		"AF00", "AF01", "AF02", "AF03", "AF04", "AF05", "AF06", "AF07"
 	};
 
-	//TODO: relocate to config file?
-	static const std::vector<std::vector<std::string>> boatFlags = {
-		{"ABF01L", "ABF01G", "ABF01R", "ABF01D", "ABF01B", "ABF01P", "ABF01W", "ABF01K"},
-		{"ABF02L", "ABF02G", "ABF02R", "ABF02D", "ABF02B", "ABF02P", "ABF02W", "ABF02K"},
-		{"ABF03L", "ABF03G", "ABF03R", "ABF03D", "ABF03B", "ABF03P", "ABF03W", "ABF03K"}
-	};
-
 	if(obj->ID == Obj::HERO)
 	{
 		assert(dynamic_cast<const CGHeroInstance *>(obj) != nullptr);
@@ -455,12 +445,8 @@ std::shared_ptr<CAnimation> MapRendererObjects::getFlagAnimation(const CGObjectI
 	if(obj->ID == Obj::BOAT)
 	{
 		const auto * boat = dynamic_cast<const CGBoat *>(obj);
-		assert(boat->subID < boatFlags.size());
-		if (boat->hero)
-		{
-			assert(boat->hero->tempOwner.isValidPlayer());
-			return getAnimation(boatFlags[boat->subID][boat->hero->tempOwner.getNum()], true);
-		}
+		if(boat && boat->hero && !boat->flagAnimations[boat->hero->tempOwner.getNum()].empty())
+			return getAnimation(boat->flagAnimations[boat->hero->tempOwner.getNum()], true);
 	}
 
 	return nullptr;
@@ -470,15 +456,10 @@ std::shared_ptr<CAnimation> MapRendererObjects::getOverlayAnimation(const CGObje
 {
 	if(obj->ID == Obj::BOAT)
 	{
-		//TODO: relocate to config file?
 		// Boats have additional animation with waves around boat
-		static const std::vector<std::string> boatAnimations = {
-			"ABM01_.def", "ABM02_.def", "ABM03_.def"
-		};
-
 		const auto * boat = dynamic_cast<const CGBoat *>(obj);
-		if (boat->hero)
-			return getAnimation(boatAnimations[obj->subID], true);
+		if(boat && boat->hero && !boat->overlayAnimation.empty())
+			return getAnimation(boat->overlayAnimation, true);
 	}
 	return nullptr;
 }

+ 2 - 2
client/windows/GUIClasses.cpp

@@ -1079,7 +1079,7 @@ void CExchangeWindow::updateWidgets()
 	}
 }
 
-CShipyardWindow::CShipyardWindow(const TResources & cost, int state, int boatType, const std::function<void()> & onBuy)
+CShipyardWindow::CShipyardWindow(const TResources & cost, int state, BoatId boatType, const std::function<void()> & onBuy)
 	: CStatusbarWindow(PLAYER_COLORED, "TPSHIP")
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
@@ -1089,7 +1089,7 @@ CShipyardWindow::CShipyardWindow(const TResources & cost, int state, int boatTyp
 	std::string boatFilenames[3] = {"AB01_", "AB02_", "AB03_"};
 
 	Point waterCenter = Point(bgWater->pos.x+bgWater->pos.w/2, bgWater->pos.y+bgWater->pos.h/2);
-	bgShip = std::make_shared<CAnimImage>(boatFilenames[boatType], 0, 7, 120, 96, 0);
+	bgShip = std::make_shared<CAnimImage>(boatFilenames[boatType.getNum()], 0, 7, 120, 96, 0);
 	bgShip->center(waterCenter);
 
 	// Create resource icons and costs.

+ 1 - 1
client/windows/GUIClasses.h

@@ -358,7 +358,7 @@ class CShipyardWindow : public CStatusbarWindow
 	std::shared_ptr<CButton> quit;
 
 public:
-	CShipyardWindow(const TResources & cost, int state, int boatType, const std::function<void()> & onBuy);
+	CShipyardWindow(const TResources & cost, int state, BoatId boatType, const std::function<void()> & onBuy);
 };
 
 /// Creature transformer window

+ 29 - 3
config/objects/moddables.json

@@ -115,9 +115,35 @@
 			}
 		},
 		"types" : {
-			"evil" : { "index" : 0,	"rmg" : { "mapLimit" : 64 } },
-			"good" : { "index" : 1, "rmg" : { "mapLimit" : 64 } },
-			"neutral" : { "index" : 2, "rmg" : { "mapLimit" : 64 } },
+			"evil" : 
+			{ 
+				"index" : 0,	
+				"layer" : "sail",
+				"actualAnimation" : "AB01_.def",
+				"overlayAnimation" : "ABM01_.def",
+				"onboardAssaultAllowed" : true,
+				"onboardVisitAllowed" : true,
+				"flagAnimations" : ["ABF01L", "ABF01G", "ABF01R", "ABF01D", "ABF01B", "ABF01P", "ABF01W", "ABF01K"]
+			},
+			"good" : 
+			{ 
+				"index" : 1, 
+				"layer" : "sail",
+				"actualAnimation" : "AB02_.def",
+				"overlayAnimation" : "ABM02_.def",
+				"onboardAssaultAllowed" : true,
+				"onboardVisitAllowed" : true,
+				"flagAnimations" : ["ABF02L", "ABF02G", "ABF02R", "ABF02D", "ABF02B", "ABF02P", "ABF02W", "ABF02K"]
+			},
+			"neutral" : { 
+				"index" : 2, 
+				"layer" : "sail",
+				"actualAnimation" : "AB03_.def",
+				"overlayAnimation" : "ABM03_.def",
+				"onboardAssaultAllowed" : true,
+				"onboardVisitAllowed" : true,
+				"flagAnimations" : ["ABF03L", "ABF03G", "ABF03R", "ABF03D", "ABF03B", "ABF03P", "ABF03W", "ABF03K"]
+			},
 		}
 	},
 

+ 12 - 4
lib/CPathfinder.cpp

@@ -151,7 +151,7 @@ void NodeStorage::resetTile(const int3 & tile, const EPathfindingLayer & layer,
 
 std::vector<CGPathNode *> NodeStorage::getInitialNodes()
 {
-	auto * initialNode = getNode(out.hpos, out.hero->boat ? EPathfindingLayer::SAIL : EPathfindingLayer::LAND);
+	auto * initialNode = getNode(out.hpos, out.hero->boat ? out.hero->boat->layer : EPathfindingLayer::LAND);
 
 	initialNode->turns = 0;
 	initialNode->moveRemains = out.hero->movement;
@@ -1019,12 +1019,18 @@ bool TurnInfo::isLayerAvailable(const EPathfindingLayer & layer) const
 	switch(layer)
 	{
 	case EPathfindingLayer::AIR:
+		if(hero && hero->boat && hero->boat->layer == EPathfindingLayer::AIR)
+			break;
+			
 		if(!hasBonusOfType(Bonus::FLYING_MOVEMENT))
 			return false;
 
 		break;
 
 	case EPathfindingLayer::WATER:
+		if(hero && hero->boat && hero->boat->layer == EPathfindingLayer::WATER)
+			break;
+			
 		if(!hasBonusOfType(Bonus::WATER_WALKING))
 			return false;
 
@@ -1236,15 +1242,17 @@ int CPathfinderHelper::getMovementCost(
 
 	bool isSailLayer;
 	if(indeterminate(isDstSailLayer))
-		isSailLayer = hero->boat != nullptr && dt->terType->isWater();
+		isSailLayer = hero->boat && hero->boat->layer == EPathfindingLayer::SAIL && dt->terType->isWater();
 	else
 		isSailLayer = static_cast<bool>(isDstSailLayer);
 
 	bool isWaterLayer;
 	if(indeterminate(isDstWaterLayer))
-		isWaterLayer = dt->terType->isWater();
+		isWaterLayer = ((hero->boat && hero->boat->layer == EPathfindingLayer::WATER) || ti->hasBonusOfType(Bonus::WATER_WALKING)) && dt->terType->isWater();
 	else
 		isWaterLayer = static_cast<bool>(isDstWaterLayer);
+	
+	bool isAirLayer = (hero->boat && hero->boat->layer == EPathfindingLayer::AIR) || ti->hasBonusOfType(Bonus::FLYING_MOVEMENT);
 
 	int ret = hero->getTileCost(*dt, *ct, ti);
 	if(isSailLayer)
@@ -1252,7 +1260,7 @@ int CPathfinderHelper::getMovementCost(
 		if(ct->hasFavorableWinds())
 			ret = static_cast<int>(ret * 2.0 / 3);
 	}
-	else if(ti->hasBonusOfType(Bonus::FLYING_MOVEMENT))
+	else if(isAirLayer)
 		vstd::amin(ret, GameConstants::BASE_MOVEMENT_COST + ti->valOfBonuses(Bonus::FLYING_MOVEMENT));
 	else if(isWaterLayer && ti->hasBonusOfType(Bonus::WATER_WALKING))
 		ret = static_cast<int>(ret * (100.0 + ti->valOfBonuses(Bonus::WATER_WALKING)) / 100.0);

+ 10 - 0
lib/GameConstants.h

@@ -1241,6 +1241,16 @@ class BattleField : public BaseForID<BattleField, si32>
 	DLL_LINKAGE static BattleField fromString(const std::string & identifier);
 };
 
+enum class EBoatId
+{
+	NONE = -1,
+	BOAT_EVIL = 0,
+	BOAT_GOOD,
+	BOAT_NEUTRAL
+};
+
+using BoatId = Identifier<EBoatId>;
+
 enum class ETerrainId {
 	NATIVE_TERRAIN = -4,
 	ANY_TERRAIN = -3,

+ 3 - 0
lib/NetPacksLib.cpp

@@ -1176,6 +1176,7 @@ void RemoveObject::applyGs(CGameState *gs)
 		//If hero on Boat is removed, the Boat disappears
 		if(beatenHero->boat)
 		{
+			beatenHero->detachFrom(const_cast<CGBoat&>(*beatenHero->boat));
 			gs->map->instanceNames.erase(beatenHero->boat->instanceName);
 			gs->map->objects[beatenHero->boat->id.getNum()].dellNull();
 			beatenHero->boat = nullptr;
@@ -1291,6 +1292,7 @@ void TryMoveHero::applyGs(CGameState *gs)
 
 		gs->map->removeBlockVisTiles(boat); //hero blockvis mask will be used, we don't need to duplicate it with boat
 		h->boat = boat;
+		h->attachTo(*boat);
 		boat->hero = h;
 	}
 	else if(result == DISEMBARK) //hero leaves boat to destination tile
@@ -1300,6 +1302,7 @@ void TryMoveHero::applyGs(CGameState *gs)
 		b->pos = start;
 		b->hero = nullptr;
 		gs->map->addBlockVisTiles(b);
+		h->detachFrom(*b);
 		h->boat = nullptr;
 	}
 

+ 8 - 0
lib/StringConstants.h

@@ -114,4 +114,12 @@ namespace NMetaclass
     };
 }
 
+namespace NPathfindingLayer
+{
+	const std::string names[EPathfindingLayer::NUM_LAYERS] =
+	{
+		"land", "sail", "water", "air"
+	};
+}
+
 VCMI_LIB_NAMESPACE_END

+ 9 - 11
lib/mapObjects/CGHeroInstance.cpp

@@ -935,18 +935,14 @@ si32 CGHeroInstance::getManaNewTurn() const
 // 	ai->putAt(this, ai->firstAvailableSlot(this));
 // }
 
-int CGHeroInstance::getBoatType() const
+BoatId CGHeroInstance::getBoatType() const
 {
 	switch(type->heroClass->getAlignment())
 	{
-	case EAlignment::GOOD:
-		return 1;
-	case EAlignment::EVIL:
-		return 0;
-	case EAlignment::NEUTRAL:
-		return 2;
-	default:
-		throw std::runtime_error("Wrong alignment!");
+		case EAlignment::EVIL : return EBoatId::BOAT_EVIL;
+		case EAlignment::GOOD : return EBoatId::BOAT_GOOD;
+		case EAlignment::NEUTRAL : return EBoatId::BOAT_NEUTRAL;
+		default: return EBoatId::NONE;
 	}
 }
 
@@ -1124,9 +1120,11 @@ int CGHeroInstance::movementPointsAfterEmbark(int MPsBefore, int basicCost, bool
 
 	if(!ti->hasBonusOfType(Bonus::FREE_SHIP_BOARDING))
 		return 0; // take all MPs by default
+	
+	auto boatLayer = boat ? boat->layer : EPathfindingLayer::SAIL;
 
-	int mp1 = ti->getMaxMovePoints(disembark ? EPathfindingLayer::LAND : EPathfindingLayer::SAIL);
-	int mp2 = ti->getMaxMovePoints(disembark ? EPathfindingLayer::SAIL : EPathfindingLayer::LAND);
+	int mp1 = ti->getMaxMovePoints(disembark ? EPathfindingLayer::LAND : boatLayer);
+	int mp2 = ti->getMaxMovePoints(disembark ? boatLayer : EPathfindingLayer::LAND);
 	int ret = static_cast<int>((MPsBefore - basicCost) * static_cast<double>(mp1) / mp2);
 	return ret;
 }

+ 1 - 1
lib/mapObjects/CGHeroInstance.h

@@ -133,7 +133,7 @@ public:
 	int getSightRadius() const override; //sight distance (should be used if player-owned structure)
 	//////////////////////////////////////////////////////////////////////////
 
-	int getBoatType() const override; //0 - evil (if a ship can be evil...?), 1 - good, 2 - neutral
+	BoatId getBoatType() const override; //0 - evil (if a ship can be evil...?), 1 - good, 2 - neutral
 	void getOutOffsets(std::vector<int3> &offsets) const override; //offsets to obj pos when we boat can be placed
 
 	//////////////////////////////////////////////////////////////////////////

+ 5 - 6
lib/mapObjects/CGTownInstance.cpp

@@ -1085,16 +1085,15 @@ void CGTownInstance::clearArmy() const
 	}
 }
 
-int CGTownInstance::getBoatType() const
+BoatId CGTownInstance::getBoatType() const
 {
 	switch (town->faction->alignment)
 	{
-	case EAlignment::EVIL : return 0;
-	case EAlignment::GOOD : return 1;
-	case EAlignment::NEUTRAL : return 2;
+		case EAlignment::EVIL : return EBoatId::BOAT_EVIL;
+		case EAlignment::GOOD : return EBoatId::BOAT_GOOD;
+		case EAlignment::NEUTRAL : return EBoatId::BOAT_NEUTRAL;
+		default: return EBoatId::NONE;
 	}
-	assert(0);
-	return -1;
 }
 
 int CGTownInstance::getMarketEfficiency() const

+ 1 - 1
lib/mapObjects/CGTownInstance.h

@@ -293,7 +293,7 @@ public:
 	bool passableFor(PlayerColor color) const override;
 	//int3 getSightCenter() const override; //"center" tile from which the sight distance is calculated
 	int getSightRadius() const override; //returns sight distance
-	int getBoatType() const override; //0 - evil (if a ship can be evil...?), 1 - good, 2 - neutral
+	BoatId getBoatType() const override; //0 - evil (if a ship can be evil...?), 1 - good, 2 - neutral
 	void getOutOffsets(std::vector<int3> &offsets) const override; //offsets to obj pos when we boat can be placed. Parameter will be cleared
 	int getMarketEfficiency() const override; //=market count
 	bool allowsTrade(EMarketMode::EMarketMode mode) const override;

+ 1 - 1
lib/mapObjects/CObjectClassesHandler.cpp

@@ -39,6 +39,7 @@ CObjectClassesHandler::CObjectClassesHandler()
 	SET_HANDLER_CLASS("hero", CHeroInstanceConstructor);
 	SET_HANDLER_CLASS("town", CTownInstanceConstructor);
 	SET_HANDLER_CLASS("bank", CBankInstanceConstructor);
+	SET_HANDLER_CLASS("boat", BoatInstanceConstructor);
 
 	SET_HANDLER_CLASS("static", CObstacleConstructor);
 	SET_HANDLER_CLASS("", CObstacleConstructor);
@@ -54,7 +55,6 @@ CObjectClassesHandler::CObjectClassesHandler()
 	SET_HANDLER("cartographer", CCartographer);
 	SET_HANDLER("artifact", CGArtifact);
 	SET_HANDLER("blackMarket", CGBlackMarket);
-	SET_HANDLER("boat", CGBoat);
 	SET_HANDLER("borderGate", CGBorderGate);
 	SET_HANDLER("borderGuard", CGBorderGuard);
 	SET_HANDLER("monster", CGCreature);

+ 2 - 2
lib/mapObjects/CObjectHandler.cpp

@@ -463,10 +463,10 @@ IBoatGenerator::EGeneratorState IBoatGenerator::shipyardStatus() const
 		return TILE_BLOCKED; //blocked
 }
 
-int IBoatGenerator::getBoatType() const
+BoatId IBoatGenerator::getBoatType() const
 {
 	//We make good ships by default
-	return 1;
+	return EBoatId::BOAT_GOOD;
 }
 
 

+ 1 - 1
lib/mapObjects/CObjectHandler.h

@@ -86,7 +86,7 @@ public:
 	IBoatGenerator(const CGObjectInstance *O);
 	virtual ~IBoatGenerator() = default;
 
-	virtual int getBoatType() const; //0 - evil (if a ship can be evil...?), 1 - good, 2 - neutral
+	virtual BoatId getBoatType() const; //0 - evil (if a ship can be evil...?), 1 - good, 2 - neutral
 	virtual void getOutOffsets(std::vector<int3> &offsets) const =0; //offsets to obj pos when we boat can be placed
 	int3 bestLocation() const; //returns location when the boat should be placed
 

+ 2 - 1
lib/mapObjects/CRewardableObject.cpp

@@ -19,6 +19,7 @@
 #include "../CPlayerState.h"
 #include "../spells/CSpellHandler.h"
 #include "../spells/ISpellMechanics.h"
+#include "../mapObjects/MiscObjects.h"
 
 #include "CObjectClassesHandler.h"
 
@@ -316,7 +317,7 @@ void CRewardableObject::grantRewardAfterLevelup(const CRewardVisitInfo & info, c
 		smp.val = hero->movement;
 
 		if (info.reward.movePercentage >= 0) // percent from max
-			smp.val = hero->maxMovePoints(hero->boat != nullptr) * info.reward.movePercentage / 100;
+			smp.val = hero->maxMovePoints(hero->boat && hero->boat->layer == EPathfindingLayer::SAIL) * info.reward.movePercentage / 100;
 		smp.val = std::max<si32>(0, smp.val + info.reward.movePoints);
 
 		cb->setMovePoints(&smp);

+ 34 - 0
lib/mapObjects/CommonConstructors.cpp

@@ -21,6 +21,7 @@
 #include "JsonRandom.h"
 #include "../CModHandler.h"
 #include "../IGameCallback.h"
+#include "../StringConstants.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -257,6 +258,39 @@ std::vector<const CCreature *> CDwellingInstanceConstructor::getProducedCreature
 	return creatures;
 }
 
+void BoatInstanceConstructor::initTypeData(const JsonNode & input)
+{
+	layer = EPathfindingLayer::SAIL;
+	int pos = vstd::find_pos(NPathfindingLayer::names, input["layer"].String());
+	if(pos != -1)
+		layer = EPathfindingLayer(pos);
+	onboardAssaultAllowed = input["onboardAssaultAllowed"].Bool();
+	onboardVisitAllowed = input["onboardVisitAllowed"].Bool();
+	actualAnimation = input["actualAnimation"].String();
+	overlayAnimation = input["overlayAnimation"].String();
+	for(int i = 0; i < flagAnimations.size() && i < input["flagAnimations"].Vector().size(); ++i)
+		flagAnimations[i] = input["flagAnimations"].Vector()[i].String();
+	bonuses = JsonRandom::loadBonuses(input["bonuses"]);
+}
+
+CGObjectInstance * BoatInstanceConstructor::create(std::shared_ptr<const ObjectTemplate> tmpl) const
+{
+	CGBoat * boat = createTyped(tmpl);
+	boat->layer = layer;
+	boat->actualAnimation = actualAnimation;
+	boat->overlayAnimation = overlayAnimation;
+	boat->flagAnimations = flagAnimations;
+	for(auto & b : bonuses)
+		boat->addNewBonus(std::make_shared<Bonus>(b));
+	
+	return boat;
+}
+
+void BoatInstanceConstructor::configureObject(CGObjectInstance * object, CRandomGenerator & rng) const
+{
+
+}
+
 bool CBankInstanceConstructor::hasNameTextID() const
 {
 	return true;

+ 31 - 0
lib/mapObjects/CommonConstructors.h

@@ -140,6 +140,37 @@ public:
 	}
 };
 
+class BoatInstanceConstructor : public CDefaultObjectTypeHandler<CGBoat>
+{
+protected:
+	void initTypeData(const JsonNode & config) override;
+	
+	std::vector<Bonus> bonuses;
+	EPathfindingLayer layer;
+	bool onboardAssaultAllowed; //if true, hero can attack units from transport
+	bool onboardVisitAllowed; //if true, hero can visit objects from transport
+	
+	std::string actualAnimation; //for OH3 boats those have actual animations
+	std::string overlayAnimation; //waves animations
+	std::array<std::string, PlayerColor::PLAYER_LIMIT_I> flagAnimations;
+	
+public:
+	CGObjectInstance * create(std::shared_ptr<const ObjectTemplate> tmpl = nullptr) const override;
+	void configureObject(CGObjectInstance * object, CRandomGenerator & rng) const override;
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & static_cast<CDefaultObjectTypeHandler<CGBoat>&>(*this);
+		h & layer;
+		h & onboardAssaultAllowed;
+		h & onboardVisitAllowed;
+		h & bonuses;
+		h & actualAnimation;
+		h & overlayAnimation;
+		h & flagAnimations;
+	}
+};
+
 struct BankConfig
 {
 	ui32 value = 0; //overall value of given things

+ 2 - 2
lib/mapObjects/JsonRandom.cpp

@@ -312,8 +312,8 @@ namespace JsonRandom
 		std::vector<Bonus> ret;
 		for (const JsonNode & entry : value.Vector())
 		{
-			auto bonus = JsonUtils::parseBonus(entry);
-			ret.push_back(*bonus);
+			if(auto bonus = JsonUtils::parseBonus(entry))
+				ret.push_back(*bonus);
 		}
 		return ret;
 	}

+ 17 - 1
lib/mapObjects/MiscObjects.h

@@ -414,11 +414,19 @@ public:
 	}
 };
 
-class DLL_LINKAGE CGBoat : public CGObjectInstance
+class DLL_LINKAGE CGBoat : public CGObjectInstance, public CBonusSystemNode
 {
 public:
 	ui8 direction;
 	const CGHeroInstance *hero;  //hero on board
+	bool onboardAssaultAllowed; //if true, hero can attack units from transport
+	bool onboardVisitAllowed; //if true, hero can visit objects from transport
+	EPathfindingLayer::EEPathfindingLayer layer;
+	
+	//animation filenames. If empty - animations won't be used
+	std::string actualAnimation; //for OH3 boats those have actual animations
+	std::string overlayAnimation; //waves animations
+	std::array<std::string, PlayerColor::PLAYER_LIMIT_I> flagAnimations;
 
 	void initObj(CRandomGenerator & rand) override;
 
@@ -426,12 +434,20 @@ public:
 	{
 		hero = nullptr;
 		direction = 4;
+		layer = EPathfindingLayer::EEPathfindingLayer::SAIL;
 	}
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & static_cast<CGObjectInstance&>(*this);
+		h & static_cast<CBonusSystemNode&>(*this);
 		h & direction;
 		h & hero;
+		h & layer;
+		h & onboardAssaultAllowed;
+		h & onboardVisitAllowed;
+		h & actualAnimation;
+		h & overlayAnimation;
+		h & flagAnimations;
 	}
 };
 

+ 1 - 0
lib/registerTypes/RegisterTypes.h

@@ -88,6 +88,7 @@ void registerTypesMapObjectTypes(Serializer &s)
 	s.template registerType<AObjectTypeHandler, CTownInstanceConstructor>();
 	s.template registerType<AObjectTypeHandler, CDwellingInstanceConstructor>();
 	s.template registerType<AObjectTypeHandler, CBankInstanceConstructor>();
+	s.template registerType<AObjectTypeHandler, BoatInstanceConstructor>();
 	s.template registerType<AObjectTypeHandler, CObstacleConstructor>();
 
 #define REGISTER_GENERIC_HANDLER(TYPENAME) s.template registerType<AObjectTypeHandler, CDefaultObjectTypeHandler<TYPENAME> >()

+ 2 - 2
lib/spells/AdventureSpellMechanics.cpp

@@ -185,7 +185,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment
 		if(obj && obj->ID == Obj::BOAT)
 		{
 			const auto * b = dynamic_cast<const CGBoat *>(obj);
-			if(b->hero)
+			if(b->hero || b->layer != EPathfindingLayer::SAIL)
 				continue; //we're looking for unoccupied boat
 
 			double nDist = b->pos.dist2d(parameters.caster->getHeroCaster()->visitablePos());
@@ -215,7 +215,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment
 	{
 		NewObject no;
 		no.ID = Obj::BOAT;
-		no.subID = parameters.caster->getHeroCaster()->getBoatType();
+		no.subID = parameters.caster->getHeroCaster()->getBoatType().getNum();
 		no.pos = summonPos + int3(1,0,0);
 		env->apply(&no);
 	}

+ 21 - 8
server/CGameHandler.cpp

@@ -2264,7 +2264,10 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
 	const int3 guardPos = gs->guardingCreaturePosition(hmpos);
 
 	const bool embarking = !h->boat && !t.visitableObjects.empty() && t.visitableObjects.back()->ID == Obj::BOAT;
-	const bool disembarking = h->boat && t.terType->isLand() && !t.blocked;
+	const bool disembarking = h->boat
+		&& t.terType->isLand()
+		&& (dst == h->pos
+			|| (h->boat->layer == EPathfindingLayer::SAIL && !t.blocked));
 
 	//result structure for start - movement failed, no move points used
 	TryMoveHero tmh;
@@ -2278,8 +2281,8 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
 	auto pathfinderHelper = std::make_unique<CPathfinderHelper>(gs, h, PathfinderOptions());
 	auto ti = pathfinderHelper->getTurnInfo();
 
-	const bool canFly = pathfinderHelper->hasBonusOfType(Bonus::FLYING_MOVEMENT);
-	const bool canWalkOnSea = pathfinderHelper->hasBonusOfType(Bonus::WATER_WALKING);
+	const bool canFly = pathfinderHelper->hasBonusOfType(Bonus::FLYING_MOVEMENT) || (h->boat && h->boat->layer == EPathfindingLayer::AIR);
+	const bool canWalkOnSea = pathfinderHelper->hasBonusOfType(Bonus::WATER_WALKING) || (h->boat && h->boat->layer == EPathfindingLayer::WATER);
 	const int cost = pathfinderHelper->getMovementCost(h->visitablePos(), hmpos, nullptr, nullptr, h->movement);
 
 	//it's a rock or blocked and not visitable tile
@@ -2288,7 +2291,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
 			&& complain("Cannot move hero, destination tile is blocked!"))
 		|| ((!h->boat && !canWalkOnSea && !canFly && t.terType->isWater() && (t.visitableObjects.size() < 1 ||  (t.visitableObjects.back()->ID != Obj::BOAT && t.visitableObjects.back()->ID != Obj::HERO)))  //hero is not on boat/water walking and dst water tile doesn't contain boat/hero (objs visitable from land) -> we test back cause boat may be on top of another object (#276)
 			&& complain("Cannot move hero, destination tile is on water!"))
-		|| ((h->boat && t.terType->isLand() && t.blocked)
+		|| ((h->boat && h->boat->layer == EPathfindingLayer::SAIL && t.terType->isLand() && t.blocked)
 			&& complain("Cannot disembark hero, tile is blocked!"))
 		|| ((distance(h->pos, dst) >= 1.5 && !teleporting)
 			&& complain("Tiles are not neighboring!"))
@@ -2361,10 +2364,16 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
 	{
 		for (CGObjectInstance *obj : t.visitableObjects)
 		{
-			if (obj != h  &&  obj->blockVisit  &&  !obj->passableFor(h->tempOwner))
+			if(h->boat && !obj->blockVisit && !h->boat->onboardVisitAllowed)
+				return doMove(TryMoveHero::SUCCESS, this->IGNORE_GUARDS, DONT_VISIT_DEST, REMAINING_ON_TILE);
+			
+			if (obj != h && obj->blockVisit && !obj->passableFor(h->tempOwner))
 			{
-				return doMove(TryMoveHero::BLOCKING_VISIT, this->IGNORE_GUARDS, VISIT_DEST, REMAINING_ON_TILE);
-				//this-> is needed for MVS2010 to recognize scope (?)
+				EVisitDest visitDest = VISIT_DEST;
+				if(h->boat && !h->boat->onboardVisitAllowed)
+					visitDest = DONT_VISIT_DEST;
+				
+				return doMove(TryMoveHero::BLOCKING_VISIT, this->IGNORE_GUARDS, visitDest, REMAINING_ON_TILE);
 			}
 		}
 		return false;
@@ -2402,6 +2411,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
 
 		return true;
 	}
+	
 
 	//still here? it is standard movement!
 	{
@@ -2424,6 +2434,9 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
 		}
 		else if (blockingVisit())
 			return true;
+		
+		if(h->boat && !h->boat->onboardAssaultAllowed)
+		   lookForGuards = IGNORE_GUARDS;
 
 		doMove(TryMoveHero::SUCCESS, lookForGuards, visitDest, LEAVING_TILE);
 		return true;
@@ -5628,7 +5641,7 @@ bool CGameHandler::buildBoat(ObjectInstanceID objid, PlayerColor playerID)
 	//create boat
 	NewObject no;
 	no.ID = Obj::BOAT;
-	no.subID = obj->getBoatType();
+	no.subID = obj->getBoatType().getNum();
 	no.pos = tile + int3(1,0,0);
 	sendAndApply(&no);