Browse Source

Initial WIP of adventure map rendering rewrite

Ivan Savenko 2 years ago
parent
commit
bb6e1f7ee1

+ 5 - 0
client/CMakeLists.txt

@@ -11,6 +11,8 @@ set(client_SRCS
 	adventureMap/CMinimap.cpp
 	adventureMap/CResDataBar.cpp
 	adventureMap/CTerrainRect.cpp
+	adventureMap/MapRenderer.cpp
+	adventureMap/MapView.cpp
 	adventureMap/mapHandler.cpp
 
 	battle/BattleActionsController.cpp
@@ -121,6 +123,9 @@ set(client_HEADERS
 	adventureMap/CMinimap.h
 	adventureMap/CResDataBar.h
 	adventureMap/CTerrainRect.h
+	adventureMap/MapRenderer.h
+	adventureMap/MapRendererContext.h
+	adventureMap/MapView.h
 	adventureMap/mapHandler.h
 
 	battle/BattleActionsController.h

+ 2 - 15
client/CPlayerInterface.cpp

@@ -475,19 +475,6 @@ void CPlayerInterface::openTownWindow(const CGTownInstance * town)
 	GH.pushInt(newCastleInt);
 }
 
-int3 CPlayerInterface::repairScreenPos(int3 pos)
-{
-	if (pos.x<-CGI->mh->frameW)
-		pos.x = -CGI->mh->frameW;
-	if (pos.y<-CGI->mh->frameH)
-		pos.y = -CGI->mh->frameH;
-	if (pos.x>CGI->mh->sizes.x - adventureInt->terrain->tilesw + CGI->mh->frameW)
-		pos.x = CGI->mh->sizes.x - adventureInt->terrain->tilesw + CGI->mh->frameW;
-	if (pos.y>CGI->mh->sizes.y - adventureInt->terrain->tilesh + CGI->mh->frameH)
-		pos.y = CGI->mh->sizes.y - adventureInt->terrain->tilesh + CGI->mh->frameH;
-	return pos;
-}
-
 void CPlayerInterface::activateForSpectator()
 {
 	adventureInt->state = CAdvMapInt::INGAME;
@@ -1719,8 +1706,8 @@ void CPlayerInterface::movementPxStep( const TryMoveHero &details, int i, const
 		}
 	}
 
-	adventureInt->terrain->moveX = (32 - i) * (heroImageNewX - heroImageOldX) / 32;
-	adventureInt->terrain->moveY = (32 - i) * (heroImageNewY - heroImageOldY) / 32;
+	//adventureInt->terrain->moveX = (32 - i) * (heroImageNewX - heroImageOldX) / 32;
+	//adventureInt->terrain->moveY = (32 - i) * (heroImageNewY - heroImageOldY) / 32;
 }
 
 void CPlayerInterface::finishMovement( const TryMoveHero &details, const int3 &hp, const CGHeroInstance * ho )

+ 0 - 1
client/CPlayerInterface.h

@@ -220,7 +220,6 @@ public:
 	void openHeroWindow(const CGHeroInstance * hero); //shows hero window with given hero
 	void updateInfo(const CGObjectInstance * specific);
 	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
-	int3 repairScreenPos(int3 pos); //returns position closest to pos we can center screen on
 	void activateForSpectator(); // TODO: spectator probably need own player interface class
 
 	// show dialogs

+ 22 - 55
client/adventureMap/CAdvMapInt.cpp

@@ -90,11 +90,11 @@ CAdvMapInt::CAdvMapInt():
 	resdatabar(new CResDataBar),
 	terrain(new CTerrainRect),
 	state(NA),
-	spellBeingCasted(nullptr), position(int3(0, 0, 0)), selection(nullptr),
+	spellBeingCasted(nullptr), selection(nullptr),
 	redrawOnNextFrame(false), anim(0), animValHitCount(0), heroAnim(0), heroAnimValHitCount(0),
 	activeMapPanel(nullptr), duringAITurn(false), scrollingDir(0), scrollingState(false),
 	swipeEnabled(settings["general"]["swipe"].Bool()), swipeMovementRequested(false),
-	swipeTargetPosition(int3(-1, -1, -1))
+	swipeTargetPosition(Point(0, 0))
 {
 	pos.x = pos.y = 0;
 	pos.w = GH.screenDimensions().x;
@@ -285,16 +285,16 @@ void CAdvMapInt::fswitchLevel()
 	if (maxLevels < 2)
 		return;
 
-	position.z = (position.z + 1) % maxLevels;
+	terrain->setLevel((terrain->getLevel() + 1) % maxLevels);
 
-	underground->setIndex(position.z, true);
+	underground->setIndex(terrain->getLevel(), true);
 	underground->redraw();
 
-	worldViewUnderground->setIndex(position.z, true);
+	worldViewUnderground->setIndex(terrain->getLevel(), true);
 	worldViewUnderground->redraw();
 
 	redrawOnNextFrame = true;
-	minimap->setLevel(position.z);
+	minimap->setLevel(terrain->getLevel());
 
 	if (mode == EAdvMapMode::WORLD_VIEW)
 		terrain->redraw();
@@ -520,8 +520,6 @@ void CAdvMapInt::showAll(SDL_Surface * to)
 		infoBar->showAll(to);
 		break;
 	case EAdvMapMode::WORLD_VIEW:
-
-		terrain->showAll(to);
 		break;
 	}
 	activeMapPanel->showAll(to);
@@ -594,25 +592,15 @@ void CAdvMapInt::show(SDL_Surface * to)
 	}
 	if(redrawOnNextFrame)
 	{
-		int3 betterPos = LOCPLINT->repairScreenPos(position);
-		if (betterPos != position)
-		{
-			logGlobal->warn("Incorrect position for adventure map!");
-			position = betterPos;
-		}
-
-		terrain->show(to);
 		for(int i = 0; i < 4; i++)
 			gems[i]->showAll(to);
 		redrawOnNextFrame=false;
 		LOCPLINT->cingconsole->show(to);
 	}
-	else
-	{
-		terrain->showAnim(to);
-		for(int i = 0; i < 4; i++)
-			gems[i]->showAll(to);
-	}
+
+	terrain->show(to);
+	for(int i = 0; i < 4; i++)
+		gems[i]->showAll(to);
 
 	infoBar->show(to);
 	statusbar->showAll(to);
@@ -624,26 +612,22 @@ void CAdvMapInt::handleMapScrollingUpdate()
 	//if advmap needs updating AND (no dialog is shown OR ctrl is pressed)
 	if((animValHitCount % (4 / scrollSpeed)) == 0)
 	{
-		if((scrollingDir & LEFT) && (position.x > -CGI->mh->frameW))
-			position.x--;
+		if(scrollingDir & LEFT)
+			terrain->moveViewBy(Point(-4, 0));
 
-		if((scrollingDir & RIGHT) && (position.x < CGI->mh->map->width - CGI->mh->tilesW + CGI->mh->frameW))
-			position.x++;
+		if(scrollingDir & RIGHT)
+			terrain->moveViewBy(Point(+4, 0));
 
-		if((scrollingDir & UP) && (position.y > -CGI->mh->frameH))
-			position.y--;
+		if(scrollingDir & UP)
+			terrain->moveViewBy(Point(0, -4));
 
-		if((scrollingDir & DOWN) && (position.y < CGI->mh->map->height - CGI->mh->tilesH + CGI->mh->frameH))
-			position.y++;
+		if(scrollingDir & DOWN)
+			terrain->moveViewBy(Point(0, +4));
 
 		if(scrollingDir)
 		{
 			setScrollingCursor(scrollingDir);
 			scrollingState = true;
-			redrawOnNextFrame = true;
-			minimap->redraw();
-			if(mode == EAdvMapMode::WORLD_VIEW)
-				terrain->redraw();
 		}
 		else if(scrollingState)
 		{
@@ -657,9 +641,7 @@ void CAdvMapInt::handleSwipeUpdate()
 {
 	if(swipeMovementRequested)
 	{
-		auto fixedPos = LOCPLINT->repairScreenPos(swipeTargetPosition);
-		position.x = fixedPos.x;
-		position.y = fixedPos.y;
+		terrain->setViewCenter(swipeTargetPosition, terrain->getLevel());
 		CCS->curh->set(Cursor::Map::POINTER);
 		redrawOnNextFrame = true;
 		minimap->redraw();
@@ -676,37 +658,22 @@ void CAdvMapInt::selectionChanged()
 
 void CAdvMapInt::centerOn(int3 on, bool fade)
 {
-	bool switchedLevels = on.z != position.z;
+	bool switchedLevels = on.z != terrain->getLevel();
 
 	if (fade)
 	{
 		terrain->fadeFromCurrentView();
 	}
 
-	switch (mode)
-	{
-	default:
-	case EAdvMapMode::NORMAL:
-		on.x -= CGI->mh->frameW; // is this intentional? frame size doesn't really have to correspond to camera size...
-		on.y -= CGI->mh->frameH;
-		break;
-	case EAdvMapMode::WORLD_VIEW:
-		on.x -= static_cast<si32>(CGI->mh->tilesW / 2 / worldViewScale);
-		on.y -= static_cast<si32>(CGI->mh->tilesH / 2 / worldViewScale);
-		break;
-	}
-
-
-	on = LOCPLINT->repairScreenPos(on);
+	terrain->setViewCenter(on);
 
-	position = on;
 	redrawOnNextFrame=true;
 	underground->setIndex(on.z,true); //change underground switch button image
 	underground->redraw();
 	worldViewUnderground->setIndex(on.z, true);
 	worldViewUnderground->redraw();
 	if (switchedLevels)
-		minimap->setLevel(position.z);
+		minimap->setLevel(terrain->getLevel());
 	minimap->redraw();
 
 	if (mode == EAdvMapMode::WORLD_VIEW)

+ 2 - 2
client/adventureMap/CAdvMapInt.h

@@ -72,7 +72,7 @@ private:
 
 	bool swipeEnabled;
 	bool swipeMovementRequested;
-	int3 swipeTargetPosition;
+	Point swipeTargetPosition;
 
 	EGameStates state;
 
@@ -80,7 +80,7 @@ private:
 	ui8 heroAnim, heroAnimValHitCount; //animation frame
 
 	/// top left corner of visible map part
-	int3 position;
+	//int3 position;
 
 	EAdvMapMode mode;
 	float worldViewScale;

+ 1 - 0
client/adventureMap/CMinimap.cpp

@@ -180,6 +180,7 @@ void CMinimap::showAll(SDL_Surface * to)
 		};
 
 		Canvas clippedTarget(target, pos);
+		CSDL_Ext::CClipRectGuard guard(to, pos);
 		clippedTarget.drawBorderDashed(radar, CSDL_Ext::fromSDL(Colors::PURPLE));
 	}
 }

+ 64 - 42
client/adventureMap/CTerrainRect.cpp

@@ -11,6 +11,7 @@
 #include "CTerrainRect.h"
 
 #include "mapHandler.h"
+#include "MapView.h"
 #include "CAdvMapInt.h"
 
 #include "../CGameInfo.h"
@@ -40,14 +41,15 @@ CTerrainRect::CTerrainRect()
 	  curHoveredTile(-1,-1,-1),
 	  currentPath(nullptr)
 {
-	tilesw=(ADVOPT.advmapW+31)/32;
-	tilesh=(ADVOPT.advmapH+31)/32;
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
 	pos.x=ADVOPT.advmapX;
 	pos.y=ADVOPT.advmapY;
 	pos.w=ADVOPT.advmapW;
 	pos.h=ADVOPT.advmapH;
-	moveX = moveY = 0;
 	addUsedEvents(LCLICK | RCLICK | MCLICK | HOVER | MOVE);
+
+	renderer = std::make_shared<MapView>( Point(0,0), pos.dimensions() );
 }
 
 CTerrainRect::~CTerrainRect()
@@ -56,6 +58,16 @@ CTerrainRect::~CTerrainRect()
 		SDL_FreeSurface(fadeSurface);
 }
 
+void CTerrainRect::setViewCenter(const int3 &coordinates)
+{
+	renderer->setViewCenter(coordinates);
+}
+
+void CTerrainRect::setViewCenter(const Point & position, int level)
+{
+	renderer->setViewCenter(position, level);
+}
+
 void CTerrainRect::deactivate()
 {
 	CIntObject::deactivate();
@@ -143,10 +155,8 @@ void CTerrainRect::handleSwipeMove(const Point & cursorPosition)
 
 	if(isSwiping)
 	{
-		adventureInt->swipeTargetPosition.x =
-			swipeInitialMapPos.x + static_cast<si32>(swipeInitialRealPos.x - cursorPosition.x) / 32;
-		adventureInt->swipeTargetPosition.y =
-			swipeInitialMapPos.y + static_cast<si32>(swipeInitialRealPos.y - cursorPosition.y) / 32;
+		adventureInt->swipeTargetPosition.x = swipeInitialViewPos.x + swipeInitialRealPos.x - cursorPosition.x;
+		adventureInt->swipeTargetPosition.y = swipeInitialViewPos.y + swipeInitialRealPos.y - cursorPosition.y;
 		adventureInt->swipeMovementRequested = true;
 	}
 }
@@ -156,7 +166,7 @@ bool CTerrainRect::handleSwipeStateChange(bool btnPressed)
 	if(btnPressed)
 	{
 		swipeInitialRealPos = Point(GH.getCursorPosition().x, GH.getCursorPosition().y);
-		swipeInitialMapPos = int3(adventureInt->position);
+		swipeInitialViewPos = getViewCenter();
 		return true;
 	}
 	else if(isSwiping) // only accept this touch if it wasn't a swipe
@@ -169,7 +179,7 @@ bool CTerrainRect::handleSwipeStateChange(bool btnPressed)
 
 void CTerrainRect::handleHover(const Point & cursorPosition)
 {
-	int3 tHovered = whichTileIsIt(cursorPosition.x, cursorPosition.y);
+	int3 tHovered = whichTileIsIt(cursorPosition);
 	int3 pom = adventureInt->verifyPos(tHovered);
 
 	if(tHovered != pom) //tile outside the map
@@ -196,6 +206,7 @@ void CTerrainRect::hover(bool on)
 }
 void CTerrainRect::showPath(const Rect & extRect, SDL_Surface * to)
 {
+/*
 	const static int pns[9][9] = {
 				{16, 17, 18,  7, -1, 19,  6,  5, -1},
 				{ 8,  9, 18,  7, -1, 19,  6, -1, 20},
@@ -228,18 +239,18 @@ void CTerrainRect::showPath(const Rect & extRect, SDL_Surface * to)
 			const int3 &prevPos = currentPath->nodes[i-1].coord;
 			std::vector<CGPathNode> & cv = currentPath->nodes;
 
-			/* Vector directions
-			 *  0   1   2
-			 *    \ | /
-			 *  3 - 4 - 5
-			 *    / | \
-			 *  6   7  8
-			 *For example:
-			 *  |
-			 *  |__\
-			 *     /
-			 * is id1=7, id2=5 (pns[7][5])
-			*/
+			// Vector directions
+			//  0   1   2
+			//    \ | /
+			//  3 - 4 - 5
+			//    / | \
+			//  6   7  8
+			//For example:
+			//  |
+			//  |__\
+			//     /
+			// is id1=7, id2=5 (pns[7][5])
+			//
 			bool pathContinuous = curPos.areNeighbours(nextPos) && curPos.areNeighbours(prevPos);
 			if(pathContinuous && cv[i].action != CGPathNode::EMBARK && cv[i].action != CGPathNode::DISEMBARK)
 			{
@@ -317,8 +328,8 @@ void CTerrainRect::showPath(const Rect & extRect, SDL_Surface * to)
 
 		}
 	} //for (int i=0;i<currentPath->nodes.size()-1;i++)
-}
-
+*/}
+/*
 void CTerrainRect::show(SDL_Surface * to)
 {
 	if (adventureInt->mode == EAdvMapMode::NORMAL)
@@ -338,7 +349,7 @@ void CTerrainRect::show(SDL_Surface * to)
 			fadeAnim->draw(to, r.topLeft());
 		}
 
-		if (currentPath/* && adventureInt->position.z==currentPath->startPos().z*/) //drawing path
+		if (currentPath) //drawing path
 		{
 			showPath(pos, to);
 		}
@@ -366,33 +377,20 @@ void CTerrainRect::showAnim(SDL_Surface * to)
 	if (fadeAnim->isFading() || lastRedrawStatus == EMapAnimRedrawStatus::REDRAW_REQUESTED)
 		show(to);
 }
-
-int3 CTerrainRect::whichTileIsIt(const int x, const int y)
+*/
+int3 CTerrainRect::whichTileIsIt(const Point &position)
 {
-	int3 ret;
-	ret.x = adventureInt->position.x + ((x-CGI->mh->offsetX-pos.x)/32);
-	ret.y = adventureInt->position.y + ((y-CGI->mh->offsetY-pos.y)/32);
-	ret.z = adventureInt->position.z;
-	return ret;
+	return renderer->getTileAtPoint(position - pos);
 }
 
 int3 CTerrainRect::whichTileIsIt()
 {
-	return whichTileIsIt(GH.getCursorPosition().x, GH.getCursorPosition().y);
+	return whichTileIsIt(GH.getCursorPosition());
 }
 
 Rect CTerrainRect::visibleTilesArea()
 {
-	switch (adventureInt->mode)
-	{
-	default:
-		logGlobal->error("Unknown map mode %d", (int)adventureInt->mode);
-		return Rect();
-	case EAdvMapMode::NORMAL:
-		return Rect(adventureInt->position.x, adventureInt->position.y, tilesw, tilesh);
-	case EAdvMapMode::WORLD_VIEW:
-		return Rect(adventureInt->position.x, adventureInt->position.y, tilesw / adventureInt->worldViewScale, tilesh / adventureInt->worldViewScale);
-	}
+	return renderer->getVisibleAreaTiles();
 }
 
 void CTerrainRect::fadeFromCurrentView()
@@ -413,3 +411,27 @@ bool CTerrainRect::needsAnimUpdate()
 	return fadeAnim->isFading() || lastRedrawStatus == EMapAnimRedrawStatus::REDRAW_REQUESTED;
 }
 
+void CTerrainRect::setLevel(int level)
+{
+	renderer->setViewCenter(renderer->getViewCenter(), level);
+}
+
+void CTerrainRect::moveViewBy(const Point & delta)
+{
+	renderer->setViewCenter(renderer->getViewCenter() + delta, getLevel());
+}
+
+int3 CTerrainRect::getTileCenter()
+{
+	return renderer->getTileCenter();
+}
+
+Point CTerrainRect::getViewCenter()
+{
+	return renderer->getViewCenter();
+}
+
+int CTerrainRect::getLevel()
+{
+	return renderer->getTileCenter().z;
+}

+ 23 - 9
client/adventureMap/CTerrainRect.h

@@ -18,18 +18,26 @@ VCMI_LIB_NAMESPACE_END
 
 enum class EMapAnimRedrawStatus;
 class CFadeAnimation;
+class MapView;
 
 /// Holds information about which tiles of the terrain are shown/not shown at the screen
 class CTerrainRect : public CIntObject
 {
+	std::shared_ptr<MapView> renderer;
+
 	SDL_Surface * fadeSurface;
 	EMapAnimRedrawStatus lastRedrawStatus;
 	std::shared_ptr<CFadeAnimation> fadeAnim;
 
-	int3 swipeInitialMapPos;
+	Point swipeInitialViewPos;
 	Point swipeInitialRealPos;
 	bool isSwiping;
-	static constexpr float SwipeTouchSlop = 16.0f;
+
+#if defined(VCMI_ANDROID) || defined(VCMI_IOS)
+	static constexpr float SwipeTouchSlop = 16.0f; // touch UI
+#else
+	static constexpr float SwipeTouchSlop = 1.0f; // mouse UI
+#endif
 
 	void handleHover(const Point & cursorPosition);
 	void handleSwipeMove(const Point & cursorPosition);
@@ -37,19 +45,26 @@ class CTerrainRect : public CIntObject
 	bool handleSwipeStateChange(bool btnPressed);
 	int3 curHoveredTile;
 
-	int3 whichTileIsIt(const int x, const int y); //x,y are cursor position
+	int3 whichTileIsIt(const Point & position); //x,y are cursor position
 	int3 whichTileIsIt(); //uses current cursor pos
 	void showPath(const Rect &extRect, SDL_Surface * to);
 
 	bool needsAnimUpdate();
 public:
-	int tilesw, tilesh; //width and height of terrain to blit in tiles
-	int moveX, moveY; //shift between actual position of screen and the one we wil blit; ranges from -31 to 31 (in pixels)
 	CGPath * currentPath;
 
 	CTerrainRect();
 	~CTerrainRect();
 
+	void moveViewBy(const Point & delta);
+	void setViewCenter(const int3 & coordinates);
+	void setViewCenter(const Point & position, int level);
+	void setLevel(int level);
+
+	int3  getTileCenter();
+	Point getViewCenter();
+	int getLevel();
+
 	// CIntObject interface implementation
 	void deactivate() override;
 	void clickLeft(tribool down, bool previousState) override;
@@ -57,10 +72,10 @@ public:
 	void clickMiddle(tribool down, bool previousState) override;
 	void hover(bool on) override;
 	void mouseMoved (const Point & cursorPosition) override;
-	void show(SDL_Surface * to) override;
-	void showAll(SDL_Surface * to) override;
+	//void show(SDL_Surface * to) override;
+	//void showAll(SDL_Surface * to) override;
 
-	void showAnim(SDL_Surface * to);
+	//void showAnim(SDL_Surface * to);
 
 	/// @returns number of visible tiles on screen respecting current map scaling
 	Rect visibleTilesArea();
@@ -68,4 +83,3 @@ public:
 	/// animates view by caching current surface and crossfading it with normal screen
 	void fadeFromCurrentView();
 };
-

+ 578 - 0
client/adventureMap/MapRenderer.cpp

@@ -0,0 +1,578 @@
+/*
+ * MapRenderer.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 "MapRenderer.h"
+
+#include "MapRendererContext.h"
+#include "mapHandler.h"
+
+#include "../CGameInfo.h"
+#include "../render/CAnimation.h"
+#include "../render/Canvas.h"
+#include "../render/IImage.h"
+
+#include "../../CCallback.h"
+
+#include "../../lib/RiverHandler.h"
+#include "../../lib/RoadHandler.h"
+#include "../../lib/TerrainHandler.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/mapping/CMap.h"
+
+struct NeighborTilesInfo
+{
+	bool d7, //789
+		 d8, //456
+		 d9, //123
+		 d4,
+		 d5,
+		 d6,
+		 d1,
+		 d2,
+		 d3;
+	NeighborTilesInfo( const IMapRendererContext & context, const int3 & pos)
+	{
+		auto getTile = [&](int dx, int dy)->bool
+		{
+			if ( dx + pos.x < 0 || dx + pos.x >= context.getMapSize().x
+			  || dy + pos.y < 0 || dy + pos.y >= context.getMapSize().y)
+				return false;
+
+			//FIXME: please do not read settings for every tile...
+			return context.isVisible( pos + int3(dx, dy, 0));
+		};
+		d7 = getTile(-1, -1); //789
+		d8 = getTile( 0, -1); //456
+		d9 = getTile(+1, -1); //123
+		d4 = getTile(-1, 0);
+		d5 = getTile( 0, 0);
+		d6 = getTile(+1, 0);
+		d1 = getTile(-1, +1);
+		d2 = getTile( 0, +1);
+		d3 = getTile(+1, +1);
+	}
+
+	bool areAllHidden() const
+	{
+		return !(d1 || d2 || d3 || d4 || d5 || d6 || d7 || d8 || d9);
+	}
+
+	int getBitmapID() const
+	{
+		//NOTE: some images have unused in VCMI pair (same blockmap but a bit different look)
+		// 0-1, 2-3, 4-5, 11-13, 12-14
+		static const int visBitmaps[256] = {
+			-1,  34,   4,   4,  22,  23,   4,   4,  36,  36,  38,  38,  47,  47,  38,  38, //16
+			 3,  25,  12,  12,   3,  25,  12,  12,   9,   9,   6,   6,   9,   9,   6,   6, //32
+			35,  39,  48,  48,  41,  43,  48,  48,  36,  36,  38,  38,  47,  47,  38,  38, //48
+			26,  49,  28,  28,  26,  49,  28,  28,   9,   9,   6,   6,   9,   9,   6,   6, //64
+			 0,  45,  29,  29,  24,  33,  29,  29,  37,  37,   7,   7,  50,  50,   7,   7, //80
+			13,  27,  44,  44,  13,  27,  44,  44,   8,   8,  10,  10,   8,   8,  10,  10, //96
+			 0,  45,  29,  29,  24,  33,  29,  29,  37,  37,   7,   7,  50,  50,   7,   7, //112
+			13,  27,  44,  44,  13,  27,  44,  44,   8,   8,  10,  10,   8,   8,  10,  10, //128
+			15,  17,  30,  30,  16,  19,  30,  30,  46,  46,  40,  40,  32,  32,  40,  40, //144
+			 2,  25,  12,  12,   2,  25,  12,  12,   9,   9,   6,   6,   9,   9,   6,   6, //160
+			18,  42,  31,  31,  20,  21,  31,  31,  46,  46,  40,  40,  32,  32,  40,  40, //176
+			26,  49,  28,  28,  26,  49,  28,  28,   9,   9,   6,   6,   9,   9,   6,   6, //192
+			 0,  45,  29,  29,  24,  33,  29,  29,  37,  37,   7,   7,  50,  50,   7,   7, //208
+			13,  27,  44,  44,  13,  27,  44,  44,   8,   8,  10,  10,   8,   8,  10,  10, //224
+			 0,  45,  29,  29,  24,  33,  29,  29,  37,  37,   7,   7,  50,  50,   7,   7, //240
+			13,  27,  44,  44,  13,  27,  44,  44,   8,   8,  10,  10,   8,   8,  10,  10  //256
+		};
+
+		return visBitmaps[d1 + d2 * 2 + d3 * 4 + d4 * 8 + d6 * 16 + d7 * 32 + d8 * 64 + d9 * 128]; // >=0 -> partial hide, <0 - full hide
+	}
+};
+
+MapObjectsSorter::MapObjectsSorter(const IMapRendererContext & context)
+	: context(context)
+{
+}
+
+bool MapObjectsSorter::operator()(const ObjectInstanceID & left, const ObjectInstanceID & right) const
+{
+	return (*this)(context.getObject(left), context.getObject(right));
+}
+
+bool MapObjectsSorter::operator()(const CGObjectInstance * left, const CGObjectInstance * right) const
+{
+	//FIXME: remove mh access
+	return CGI->mh->compareObjectBlitOrder(left, right);
+}
+
+MapTileStorage::MapTileStorage(size_t capacity)
+	: animations(capacity)
+{
+}
+
+void MapTileStorage::load(size_t index, const std::string & filename)
+{
+	auto & terrainAnimations = animations[index];
+
+	for(auto & entry : terrainAnimations)
+	{
+		entry = std::make_unique<CAnimation>(filename);
+		entry->preload();
+	}
+
+	for(size_t i = 0; i < terrainAnimations[0]->size(); ++i)
+	{
+		terrainAnimations[1]->getImage(i)->verticalFlip();
+		terrainAnimations[3]->getImage(i)->verticalFlip();
+
+		terrainAnimations[2]->getImage(i)->horizontalFlip();
+		terrainAnimations[3]->getImage(i)->horizontalFlip();
+	}
+}
+
+std::shared_ptr<IImage> MapTileStorage::find(size_t fileIndex, size_t rotationIndex, size_t imageIndex)
+{
+	const auto & animation = animations[fileIndex][rotationIndex];
+	return animation->getImage(imageIndex);
+}
+
+MapRendererTerrain::MapRendererTerrain()
+	: storage(VLC->terrainTypeHandler->objects.size())
+{
+	for(const auto & terrain : VLC->terrainTypeHandler->objects)
+		storage.load(terrain->getIndex(), terrain->tilesFilename);
+}
+
+void MapRendererTerrain::renderTile(const IMapRendererContext & context, Canvas & target, const int3 & coordinates)
+{
+	const TerrainTile & mapTile = context.getMapTile(coordinates);
+
+	int32_t terrainIndex = mapTile.terType->getIndex();
+	int32_t imageIndex = mapTile.terView;
+	int32_t rotationIndex = mapTile.extTileFlags % 4;
+
+	const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex);
+
+	target.draw(image, Point(0, 0));
+}
+
+MapRendererRiver::MapRendererRiver()
+	: storage(VLC->riverTypeHandler->objects.size())
+{
+	for(const auto & river : VLC->riverTypeHandler->objects)
+		storage.load(river->getIndex(), river->tilesFilename);
+}
+
+void MapRendererRiver::renderTile(const IMapRendererContext & context, Canvas & target, const int3 & coordinates)
+{
+	const TerrainTile & mapTile = context.getMapTile(coordinates);
+
+	if(mapTile.riverType->getId() != River::NO_RIVER)
+	{
+		int32_t terrainIndex = mapTile.riverType->getIndex();
+		int32_t imageIndex = mapTile.riverDir;
+		int32_t rotationIndex = (mapTile.extTileFlags >> 2) % 4;
+
+		const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex);
+		target.draw(image, Point(0, 0));
+	}
+}
+
+MapRendererRoad::MapRendererRoad():
+	storage(VLC->roadTypeHandler->objects.size())
+{
+	for(const auto & road : VLC->roadTypeHandler->objects)
+		storage.load(road->getIndex(), road->tilesFilename);
+}
+
+void MapRendererRoad::renderTile(const IMapRendererContext & context, Canvas & target, const int3 & coordinates)
+{
+	const int3 coordinatesAbove = coordinates - int3(0,1,0);
+
+	if (context.isInMap(coordinatesAbove))
+	{
+		const TerrainTile & mapTileAbove = context.getMapTile(coordinatesAbove);
+		if (mapTileAbove.roadType->getId() != Road::NO_ROAD)
+		{
+			int32_t terrainIndex = mapTileAbove.roadType->getIndex();
+			int32_t imageIndex = mapTileAbove.roadDir;
+			int32_t rotationIndex = (mapTileAbove.extTileFlags >> 4) % 4;
+
+			const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex);
+			target.draw(image, Point(0,0), Rect(0, 16, 32, 16));
+		}
+	}
+
+	const TerrainTile & mapTile = context.getMapTile(coordinates);
+	if(mapTile.roadType->getId() != Road::NO_ROAD)
+	{
+		int32_t terrainIndex = mapTile.roadType->getIndex();
+		int32_t imageIndex = mapTile.roadDir;
+		int32_t rotationIndex = (mapTile.extTileFlags >> 4) % 4;
+
+		const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex);
+		target.draw(image, Point(0,16), Rect(0, 0, 32, 16));
+	}
+}
+
+MapRendererBorder::MapRendererBorder()
+{
+	animation = std::make_unique<CAnimation>("EDG");
+	animation->preload();
+}
+
+size_t MapRendererBorder::getIndexForTile(const IMapRendererContext & context, const int3 & tile)
+{
+	assert(!context.isInMap(tile));
+
+	int3 size = context.getMapSize();
+
+	if (tile.x < -1 || tile.x > size.x || tile.y < -1 || tile.y > size.y)
+		return std::abs(tile.x) % 4 + 4*(std::abs(tile.y) % 4);
+
+	if (tile.x == -1 && tile.y == -1)
+		return 16;
+
+	if (tile.x == size.x && tile.y == -1)
+		return 17;
+
+	if (tile.x == size.x && tile.y == size.y)
+		return 18;
+
+	if (tile.x == -1 && tile.y == size.y)
+		return 19;
+
+	if (tile.y == -1)
+		return 20 + (tile.x % 4);
+
+	if (tile.x == size.x)
+		return 24 + (tile.y % 4);
+
+	if (tile.y == size.y)
+		return 28 + (tile.x % 4);
+
+	if (tile.x == -1)
+		return 32 + (tile.y % 4);
+
+	//else - visible area, no renderable border
+	assert(0);
+	return 0;
+}
+
+void MapRendererBorder::renderTile(const IMapRendererContext & context, Canvas & target, const int3 & coordinates)
+{
+	const auto & image = animation->getImage(getIndexForTile(context, coordinates));
+	target.draw(image, Point(0,0));
+}
+
+MapRendererFow::MapRendererFow()
+{
+	fogOfWarFullHide = std::make_unique<CAnimation>("TSHRC");
+	fogOfWarFullHide->preload();
+	fogOfWarPartialHide = std::make_unique<CAnimation>("TSHRE");
+	fogOfWarPartialHide->preload();
+
+	static const std::vector<int> rotations = {22, 15, 2, 13, 12, 16, 28, 17, 20, 19, 7, 24, 26, 25, 30, 32, 27};
+
+	size_t size = fogOfWarPartialHide->size(0);//group size after next rotation
+
+	for(const int rotation : rotations)
+	{
+		fogOfWarPartialHide->duplicateImage(0, rotation, 0);
+		auto image = fogOfWarPartialHide->getImage(size, 0);
+		image->verticalFlip();
+		size++;
+	}
+}
+
+void MapRendererFow::renderTile(const IMapRendererContext & context, Canvas & target, const int3 & coordinates)
+{
+	assert(!context.isVisible(coordinates));
+
+	const NeighborTilesInfo neighborInfo(context, coordinates);
+
+	int retBitmapID = neighborInfo.getBitmapID();// >=0 -> partial hide, <0 - full hide
+	if (retBitmapID < 0)
+	{
+		// generate a number that is predefined for each tile,
+		// but appears random to break visible pattern in large areas of fow
+		// current approach (use primes as magic numbers for formula) looks to be suitable
+		size_t pseudorandomNumber = ((coordinates.x * 997) ^ (coordinates.y * 1009)) / 101;
+		size_t imageIndex = pseudorandomNumber % fogOfWarFullHide->size();
+
+		target.draw(fogOfWarFullHide->getImage(imageIndex), Point(0,0));
+	}
+	else
+	{
+		target.draw(fogOfWarPartialHide->getImage(retBitmapID), Point(0,0));
+	}
+}
+
+std::shared_ptr<CAnimation> MapRendererObjects::getAnimation(const CGObjectInstance* obj)
+{
+	const auto & info = obj->appearance;
+
+	//the only(?) invisible object
+	if(info->id == Obj::EVENT)
+		return std::shared_ptr<CAnimation>();
+
+	if(info->animationFile.empty())
+	{
+		logGlobal->warn("Def name for obj (%d,%d) is empty!", info->id, info->subid);
+		return std::shared_ptr<CAnimation>();
+	}
+
+	return getAnimation(info->animationFile);
+}
+
+std::shared_ptr<CAnimation> MapRendererObjects::getAnimation(const std::string & filename)
+{
+	if (animations.count(filename))
+		return animations[filename];
+
+	auto ret = std::make_shared<CAnimation>(filename);
+	animations[filename] = ret;
+	ret->preload();
+
+	return ret;
+}
+
+void MapRendererObjects::initializeObjects(const IMapRendererContext & context)
+{
+	auto mapSize = context.getMapSize();
+
+	objects.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]);
+
+	for(const auto & obj : context.getAllObjects())
+	{
+		if(!obj)
+			continue;
+
+		if(obj->ID == Obj::HERO && dynamic_cast<const CGHeroInstance *>(obj.get())->inTownGarrison)
+			continue;
+
+		if(obj->ID == Obj::BOAT && dynamic_cast<const CGBoat *>(obj.get())->hero)
+			continue;
+
+		std::shared_ptr<CAnimation> animation = getAnimation(obj);
+
+		//no animation at all, e.g. Event
+		if(!animation)
+			continue;
+
+		//empty animation. Illegal?
+		assert(animation->size(0) > 0);
+		if(animation->size(0) == 0)
+			continue;
+
+		auto image = animation->getImage(0, 0);
+
+		int imageWidthTiles = (image->width() + 31) / 32;
+		int imageHeightTiles = (image->height() + 31) / 32;
+
+		int objectWidth = std::min(obj->getWidth(), imageWidthTiles);
+		int objectHeight = std::min(obj->getHeight(), imageHeightTiles);
+
+		for(int fx = 0; fx < objectWidth; ++fx)
+		{
+			for(int fy = 0; fy < objectHeight; ++fy)
+			{
+				int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z);
+
+				if(context.isInMap(currTile) && obj->coveringAt(currTile.x, currTile.y))
+					objects[currTile.z][currTile.x][currTile.y].push_back(obj->id);
+			}
+		}
+	}
+
+	for(int z = 0; z < mapSize.z; z++)
+	{
+		for(int x = 0; x < mapSize.x; x++)
+		{
+			for(int y = 0; y < mapSize.y; y++)
+			{
+				auto & array = objects[z][x][y];
+				std::sort(array.begin(), array.end(), MapObjectsSorter(context));
+			}
+		}
+	}
+}
+
+MapRendererObjects::MapRendererObjects(const IMapRendererContext & context)
+{
+	initializeObjects(context);
+}
+
+std::shared_ptr<CAnimation> MapRendererObjects::getFlagAnimation(const CGObjectInstance* obj)
+{
+	//TODO: relocate to config file?
+	static const std::vector<std::string> heroFlags = {
+		"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);
+		assert(obj->tempOwner.isValidPlayer());
+		return getAnimation(heroFlags[obj->tempOwner.getNum()]);
+	}
+	if(obj->ID == Obj::BOAT)
+	{
+		const auto * boat = dynamic_cast<const CGBoat *>(obj);
+		assert(boat);
+		assert(boat->subID < boatFlags.size());
+		assert(!boat->hero || boat->hero->tempOwner.isValidPlayer());
+
+		if(boat->hero)
+			return getAnimation(boatFlags[obj->subID][boat->hero->tempOwner.getNum()]);
+	}
+	return nullptr;
+}
+
+std::shared_ptr<IImage> MapRendererObjects::getImage(const IMapRendererContext & context, const CGObjectInstance * obj, const std::shared_ptr<CAnimation>& animation) const
+{
+	if(!animation)
+		return nullptr;
+
+	size_t groupIndex = getAnimationGroup(context, obj);
+
+	if(animation->size(groupIndex) == 0)
+		return nullptr;
+
+	size_t frameCounter = context.getAnimationTime() / context.getAnimationPeriod();
+	size_t frameIndex = frameCounter % animation->size(groupIndex);
+
+	return animation->getImage(frameIndex, groupIndex);
+}
+
+size_t MapRendererObjects::getAnimationGroup(const IMapRendererContext & context, const CGObjectInstance * obj) const
+{
+	// TODO
+	//static const std::vector<size_t> moveGroups = {99, 10, 5, 6, 7, 8, 9, 12, 11};
+	static const std::vector<size_t> idleGroups = {99, 13, 0, 1, 2, 3, 4, 15, 14};
+
+	if(obj->ID == Obj::HERO)
+	{
+		const auto * hero = dynamic_cast<const CGHeroInstance *>(obj);
+		return idleGroups[hero->moveDir];
+	}
+
+	if(obj->ID == Obj::BOAT)
+	{
+		const auto * boat = dynamic_cast<const CGBoat *>(obj);
+		return idleGroups[boat->direction];
+	}
+
+	return 0;
+}
+
+void MapRendererObjects::renderImage(Canvas & target, const int3 & coordinates, const CGObjectInstance * object, const std::shared_ptr<IImage>& image)
+{
+	if(!image)
+		return;
+
+	image->setFlagColor(object->tempOwner);
+
+	int3 offsetTiles(object->getPosition() - coordinates);
+
+	Point offsetPixels(offsetTiles.x * 32, offsetTiles.y * 32);
+	Point imagePos = image->dimensions() - offsetPixels - Point(32, 32);
+	Point tileDimensions(32, 32);
+
+	target.draw(image, Point(0, 0), Rect(imagePos, tileDimensions));
+}
+
+void MapRendererObjects::renderObject(const IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance* instance)
+{
+	renderImage(target, coordinates, instance, getImage(context, instance, getAnimation(instance)));
+	renderImage(target, coordinates, instance, getImage(context, instance, getFlagAnimation(instance)));
+}
+
+void MapRendererObjects::renderTile(const IMapRendererContext & context, Canvas & target, const int3 & coordinates)
+{
+	for(const auto & objectID : objects[coordinates.z][coordinates.x][coordinates.y])
+	{
+		const auto * objectInstance = context.getObject(objectID);
+
+		assert(objectInstance);
+		if(!objectInstance)
+		{
+			logGlobal->error("Stray map object that isn't fading");
+			continue;
+		}
+
+		renderObject(context, target, coordinates, objectInstance);
+	}
+}
+
+void MapRendererObjects::addObject(const IMapRendererContext & context, const CGObjectInstance * object)
+{
+
+}
+
+void MapRendererObjects::removeObject(const IMapRendererContext & context, const CGObjectInstance * object)
+{
+
+}
+
+void MapRendererDebugGrid::renderTile(const IMapRendererContext & context, Canvas & target, const int3 & coordinates)
+{
+	if(context.showGrid())
+	{
+		target.drawLine(Point(0, 0), Point(0, 31), {128, 128, 128, 128}, {128, 128, 128, 128});
+		target.drawLine(Point(0, 0), Point(31, 0), {128, 128, 128, 128}, {128, 128, 128, 128});
+	}
+}
+
+MapRenderer::MapRenderer(const IMapRendererContext & context)
+	: rendererObjects(context)
+{
+
+}
+
+void MapRenderer::renderTile(const IMapRendererContext & context, Canvas & target, const int3 & coordinates)
+{
+	if (!context.isInMap(coordinates))
+	{
+		rendererBorder.renderTile(context, target, coordinates);
+		return;
+	}
+
+	const NeighborTilesInfo neighborInfo(context, coordinates);
+
+	if (neighborInfo.areAllHidden())
+	{
+		rendererFow.renderTile(context, target, coordinates);
+	}
+	else
+	{
+		rendererTerrain.renderTile(context, target, coordinates);
+		rendererRiver.renderTile(context, target, coordinates);
+		rendererRoad.renderTile(context, target, coordinates);
+		rendererObjects.renderTile(context, target, coordinates);
+
+		if (!context.isVisible(coordinates))
+			rendererFow.renderTile(context, target, coordinates);
+	}
+	rendererDebugGrid.renderTile(context, target,coordinates);
+}
+
+void MapRenderer::addObject(const IMapRendererContext & context, const CGObjectInstance * object)
+{
+	rendererObjects.addObject(context, object);
+}
+
+void MapRenderer::removeObject(const IMapRendererContext & context, const CGObjectInstance * object)
+{
+	rendererObjects.addObject(context, object);
+}

+ 140 - 0
client/adventureMap/MapRenderer.h

@@ -0,0 +1,140 @@
+/*
+ * MapRenderer.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
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class int3;
+class ObjectInstanceID;
+class CGObjectInstance;
+
+VCMI_LIB_NAMESPACE_END
+
+class CAnimation;
+class IImage;
+class Canvas;
+class IMapRendererContext;
+
+class MapObjectsSorter
+{
+	const IMapRendererContext & context;
+public:
+	explicit MapObjectsSorter(const IMapRendererContext & context);
+
+	bool operator ()(const ObjectInstanceID & left, const ObjectInstanceID & right) const;
+	bool operator ()(const CGObjectInstance * left, const CGObjectInstance * right) const;
+};
+
+class MapTileStorage
+{
+	using TerrainAnimation = std::array<std::unique_ptr<CAnimation>, 4>;
+	std::vector<TerrainAnimation> animations;
+public:
+	explicit MapTileStorage( size_t capacity);
+	void load(size_t index, const std::string& filename);
+	std::shared_ptr<IImage> find(size_t fileIndex, size_t rotationIndex, size_t imageIndex );
+};
+
+class MapRendererTerrain
+{
+	MapTileStorage storage;
+public:
+	MapRendererTerrain();
+	void renderTile(const IMapRendererContext & context, Canvas & target, const int3 & coordinates);
+};
+
+class MapRendererRiver
+{
+	MapTileStorage storage;
+public:
+	MapRendererRiver();
+	void renderTile(const IMapRendererContext & context, Canvas & target, const int3 & coordinates);
+};
+
+class MapRendererRoad
+{
+	MapTileStorage storage;
+public:
+	MapRendererRoad();
+	void renderTile(const IMapRendererContext & context, Canvas & target, const int3 & coordinates);
+};
+
+class MapRendererObjects
+{
+	using MapObject = ObjectInstanceID;
+	using MapTile   = std::vector<MapObject>;
+
+	boost::multi_array<MapTile, 3> objects;
+	std::map<std::string, std::shared_ptr<CAnimation>> animations;
+
+	std::shared_ptr<CAnimation> getFlagAnimation(const CGObjectInstance* obj);
+	std::shared_ptr<CAnimation> getAnimation(const CGObjectInstance* obj);
+	std::shared_ptr<CAnimation> getAnimation(const std::string & filename);
+
+	std::shared_ptr<IImage> getImage(const IMapRendererContext & context, const CGObjectInstance * obj, const std::shared_ptr<CAnimation>& animation) const;
+	size_t getAnimationGroup(const IMapRendererContext & context, const CGObjectInstance * obj) const;
+
+	void initializeObjects(const IMapRendererContext & context);
+	void renderImage(Canvas & target, const int3 & coordinates, const CGObjectInstance * object, const std::shared_ptr<IImage>& image);
+
+	void renderObject(const IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance* obj);
+public:
+	explicit MapRendererObjects(const IMapRendererContext & context);
+	void renderTile(const IMapRendererContext & context, Canvas & target, const int3 & coordinates);
+
+	void addObject(const IMapRendererContext & context, const CGObjectInstance * object);
+	void removeObject(const IMapRendererContext & context, const CGObjectInstance * object);
+};
+
+class MapRendererBorder
+{
+	std::unique_ptr<CAnimation> animation;
+
+	size_t getIndexForTile(const IMapRendererContext & context, const int3 & coordinates);
+public:
+	MapRendererBorder();
+	void renderTile(const IMapRendererContext & context, Canvas & target, const int3 & coordinates);
+};
+
+class MapRendererFow
+{
+	std::unique_ptr<CAnimation> fogOfWarFullHide;
+	std::unique_ptr<CAnimation> fogOfWarPartialHide;
+
+public:
+	MapRendererFow();
+	void renderTile(const IMapRendererContext & context, Canvas & target, const int3 & coordinates);
+};
+
+class MapRendererDebugGrid
+{
+public:
+	void renderTile(const IMapRendererContext & context, Canvas & target, const int3 & coordinates);
+};
+
+class MapRenderer
+{
+	MapRendererTerrain rendererTerrain;
+	MapRendererRiver rendererRiver;
+	MapRendererRoad rendererRoad;
+	MapRendererBorder rendererBorder;
+	MapRendererFow rendererFow;
+	MapRendererObjects rendererObjects;
+	MapRendererDebugGrid rendererDebugGrid;
+
+public:
+	explicit MapRenderer(const IMapRendererContext & context);
+
+	void renderTile(const IMapRendererContext & context, Canvas & target, const int3 & coordinates);
+
+	void addObject(const IMapRendererContext & context, const CGObjectInstance * object);
+	void removeObject(const IMapRendererContext & context, const CGObjectInstance * object);
+
+};

+ 47 - 0
client/adventureMap/MapRendererContext.h

@@ -0,0 +1,47 @@
+/*
+ * MapRenderer.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/ConstTransitivePtr.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class int3;
+class Point;
+class ObjectInstanceID;
+class CGObjectInstance;
+struct TerrainTile;
+
+VCMI_LIB_NAMESPACE_END
+
+class IMapRendererContext
+{
+public:
+	virtual ~IMapRendererContext() = default;
+
+	using VisibilityMap = std::shared_ptr<const boost::multi_array<ui8, 3>>;
+	using ObjectsVector = std::vector< ConstTransitivePtr<CGObjectInstance> >;
+
+	virtual int3 getMapSize() const = 0;
+	virtual bool isInMap(const int3 & coordinates) const = 0;
+	virtual const TerrainTile & getMapTile(const int3 & coordinates) const = 0;
+
+	virtual ObjectsVector getAllObjects() const = 0;
+	virtual const CGObjectInstance * getObject( ObjectInstanceID objectID ) const = 0;
+
+	virtual bool isVisible(const int3 & coordinates) const = 0;
+	virtual VisibilityMap getVisibilityMap() const = 0;
+
+	virtual uint32_t getAnimationPeriod() const = 0;
+	virtual uint32_t getAnimationTime() const = 0;
+	virtual Point tileSize() const = 0;
+
+	virtual bool showGrid() const = 0;
+};

+ 297 - 0
client/adventureMap/MapView.cpp

@@ -0,0 +1,297 @@
+/*
+ * MapView.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 "MapView.h"
+
+#include "MapRenderer.h"
+#include "mapHandler.h"
+
+#include "../CGameInfo.h"
+#include "../CMusicHandler.h"
+#include "../CPlayerInterface.h"
+#include "../gui/CGuiHandler.h"
+#include "../render/CAnimation.h"
+#include "../render/CFadeAnimation.h"
+#include "../render/Canvas.h"
+#include "../render/Colors.h"
+#include "../render/Graphics.h"
+#include "../render/IImage.h"
+#include "../renderSDL/SDL_Extensions.h"
+
+#include "../../CCallback.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/CRandomGenerator.h"
+#include "../../lib/CStopWatch.h"
+#include "../../lib/Color.h"
+#include "../../lib/RiverHandler.h"
+#include "../../lib/RoadHandler.h"
+#include "../../lib/TerrainHandler.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/mapObjects/CObjectClassesHandler.h"
+#include "../../lib/mapping/CMap.h"
+
+MapCache::~MapCache() = default;
+
+MapCache::MapCache(const Point & tileSize, const Point & dimensions)
+	: tileSize(tileSize)
+	, context(new MapRendererContext())
+	, mapRenderer(new MapRenderer(*context))
+	, targetDimensionsPixels(dimensions)
+	, viewCenter(0, 0)
+	, mapLevel(0)
+{
+	// total number of potentially visible tiles is:
+	// 1) number of completely visible tiles
+	// 2) additional tile that might be partially visible from left/top size
+	// 3) additional tile that might be partially visible from right/bottom size
+	Point visibleTiles{
+		dimensions.x / tileSize.x + 2,
+		dimensions.y / tileSize.y + 2,
+	};
+
+	viewDimensionsTiles = visibleTiles;
+	viewDimensionsPixels = visibleTiles * tileSize;
+
+	terrain = std::make_unique<Canvas>(viewDimensionsPixels);
+}
+
+void MapCache::setViewCenter(const Point & center, int newLevel)
+{
+	viewCenter = center;
+	mapLevel = newLevel;
+
+	int3 mapSize = LOCPLINT->cb->getMapSize();
+	Point viewMax = Point(mapSize) * tileSize;
+
+	vstd::abetween(viewCenter.x, 0, viewMax.x);
+	vstd::abetween(viewCenter.y, 0, viewMax.y);
+}
+
+Canvas MapCache::getTile(const int3 & coordinates)
+{
+	assert(mapLevel == coordinates.z);
+	assert(viewDimensionsTiles.x + coordinates.x >= 0);
+	assert(viewDimensionsTiles.y + coordinates.y >= 0);
+
+	Point tileIndex{
+		(viewDimensionsTiles.x + coordinates.x) % viewDimensionsTiles.x,
+		(viewDimensionsTiles.y + coordinates.y) % viewDimensionsTiles.y
+	};
+
+	Rect terrainSection(tileIndex * tileSize, tileSize);
+
+	return Canvas(*terrain, terrainSection);
+}
+
+void MapCache::updateTile(const int3 & coordinates)
+{
+	Canvas target = getTile(coordinates);
+
+	mapRenderer->renderTile(*context, target, coordinates);
+}
+
+Rect MapCache::getVisibleAreaTiles() const
+{
+	Rect visibleAreaPixels = {
+		viewCenter.x - targetDimensionsPixels.x / 2,
+		viewCenter.y - targetDimensionsPixels.y / 2,
+		targetDimensionsPixels.x,
+		targetDimensionsPixels.y
+	};
+
+	// NOTE: use division via double in order to use floor (which rounds to negative infinity and not towards zero)
+	Point topLeftTile{
+		static_cast<int>(std::floor(static_cast<double>(visibleAreaPixels.left()) / tileSize.x)),
+		static_cast<int>(std::floor(static_cast<double>(visibleAreaPixels.top()) / tileSize.y)),
+	};
+
+	Point bottomRightTile{
+		visibleAreaPixels.right() / tileSize.x,
+		visibleAreaPixels.bottom() / tileSize.y
+	};
+
+	return Rect(topLeftTile, bottomRightTile - topLeftTile + Point(1, 1));
+}
+
+int3 MapCache::getTileAtPoint(const Point & position) const
+{
+	Point topLeftOffset = viewCenter - targetDimensionsPixels / 2;
+
+	Point absolutePosition = position + topLeftOffset;
+
+	return {
+		static_cast<int>(std::floor(static_cast<double>(absolutePosition.x) / tileSize.x)),
+		static_cast<int>(std::floor(static_cast<double>(absolutePosition.y) / tileSize.y)),
+		mapLevel
+	};
+}
+
+int3 MapCache::getTileCenter() const
+{
+	return getTileAtPoint(getViewCenter());
+}
+
+Point MapCache::getViewCenter() const
+{
+	return viewCenter;
+}
+
+void MapCache::update(uint32_t timeDelta)
+{
+	context->advanceAnimations(timeDelta);
+
+	Rect dimensions = getVisibleAreaTiles();
+
+	for(int y = dimensions.top(); y < dimensions.bottom(); ++y)
+		for(int x = dimensions.left(); x < dimensions.right(); ++x)
+			updateTile({x, y, mapLevel});
+}
+
+void MapCache::render(Canvas & target)
+{
+	update(GH.mainFPSmng->getElapsedMilliseconds());
+
+	Rect dimensions = getVisibleAreaTiles();
+
+	for(int y = dimensions.top(); y < dimensions.bottom(); ++y)
+	{
+		for(int x = dimensions.left(); x < dimensions.right(); ++x)
+		{
+			Point topLeftOffset = viewCenter - targetDimensionsPixels / 2;
+			Point tilePosAbsolute = Point(x, y) * tileSize;
+			Point tilePosRelative = tilePosAbsolute - topLeftOffset;
+
+			Canvas source = getTile(int3(x, y, mapLevel));
+			target.draw(source, tilePosRelative);
+		}
+	}
+}
+
+MapView::MapView(const Point & offset, const Point & dimensions)
+	: tilesCache(new MapCache(Point(32, 32), dimensions))
+	, tileSize(Point(32, 32))
+{
+	pos += offset;
+	pos.w = dimensions.x;
+	pos.h = dimensions.y;
+}
+
+Rect MapView::getVisibleAreaTiles() const
+{
+	return tilesCache->getVisibleAreaTiles();
+}
+
+int3 MapView::getTileCenter() const
+{
+	return tilesCache->getTileCenter();
+}
+
+int3 MapView::getTileAtPoint(const Point & position) const
+{
+	return tilesCache->getTileAtPoint(position);
+}
+
+Point MapView::getViewCenter() const
+{
+	return tilesCache->getViewCenter();
+}
+
+void MapView::setViewCenter(const int3 & position)
+{
+	setViewCenter(Point(position.x, position.y) * tileSize, position.z);
+}
+
+void MapView::setViewCenter(const Point & position, int level)
+{
+	tilesCache->setViewCenter(position, level);
+}
+
+void MapView::show(SDL_Surface * to)
+{
+	Canvas target(to);
+	Canvas targetClipped(target, pos);
+
+	CSDL_Ext::CClipRectGuard guard(to, pos);
+
+	tilesCache->render(targetClipped);
+}
+
+void MapView::showAll(SDL_Surface * to)
+{
+	show(to);
+}
+
+void MapRendererContext::advanceAnimations(uint32_t ms)
+{
+	animationTime += ms;
+}
+
+int3 MapRendererContext::getMapSize() const
+{
+	return LOCPLINT->cb->getMapSize();
+}
+
+bool MapRendererContext::isInMap(const int3 & coordinates) const
+{
+	return LOCPLINT->cb->isInTheMap(coordinates);
+}
+
+const TerrainTile & MapRendererContext::getMapTile(const int3 & coordinates) const
+{
+	return CGI->mh->map->getTile(coordinates);
+}
+
+MapRendererContext::ObjectsVector MapRendererContext::getAllObjects() const
+{
+	return CGI->mh->map->objects;
+}
+
+const CGObjectInstance * MapRendererContext::getObject(ObjectInstanceID objectID) const
+{
+	return CGI->mh->map->objects.at(objectID.getNum());
+}
+
+bool MapRendererContext::isVisible(const int3 & coordinates) const
+{
+	return LOCPLINT->cb->isVisible(coordinates) || settings["session"]["spectate"].Bool();
+}
+
+MapRendererContext::VisibilityMap MapRendererContext::getVisibilityMap() const
+{
+	return LOCPLINT->cb->getVisibilityMap();
+}
+
+uint32_t MapRendererContext::getAnimationPeriod() const
+{
+	// H3 timing for adventure map objects animation is 180 ms
+	// Terrain animations also use identical interval, however it is only present in HotA and/or HD Mod
+	// TODO: duration of fade-in/fade-out for teleport, entering/leaving boat, removal of objects
+	// TOOD: duration of hero movement animation, frame timing of hero movement animation, effect of hero speed option
+	// TOOD: duration of enemy hero movement animation, frame timing of enemy hero movement animation, effect of enemy hero speed option
+	return 180;
+}
+
+uint32_t MapRendererContext::getAnimationTime() const
+{
+	return animationTime;
+}
+
+Point MapRendererContext::tileSize() const
+{
+	return Point(32, 32);
+}
+
+bool MapRendererContext::showGrid() const
+{
+	return true; // settings["session"]["showGrid"].Bool();
+}

+ 96 - 0
client/adventureMap/MapView.h

@@ -0,0 +1,96 @@
+/*
+ * MapView.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 "../gui/CIntObject.h"
+
+#include "MapRendererContext.h"
+
+class Canvas;
+class MapRenderer;
+
+class MapRendererContext : public IMapRendererContext
+{
+	uint32_t animationTime = 0;
+
+public:
+	void advanceAnimations(uint32_t ms);
+
+	int3 getMapSize() const override;
+	bool isInMap(const int3 & coordinates) const override;
+	const TerrainTile & getMapTile(const int3 & coordinates) const override;
+
+	ObjectsVector getAllObjects() const override;
+	const CGObjectInstance * getObject(ObjectInstanceID objectID) const override;
+
+	bool isVisible(const int3 & coordinates) const override;
+	VisibilityMap getVisibilityMap() const override;
+
+	uint32_t getAnimationPeriod() const override;
+	uint32_t getAnimationTime() const override;
+	Point tileSize() const override;
+
+	bool showGrid() const override;
+};
+
+class MapCache
+{
+	std::unique_ptr<Canvas> terrain;
+
+	Point tileSize;
+	Point viewCenter;
+	Point viewDimensionsTiles;
+	Point viewDimensionsPixels;
+	Point targetDimensionsPixels;
+
+	int mapLevel;
+
+	std::unique_ptr<MapRendererContext> context;
+	std::unique_ptr<MapRenderer> mapRenderer;
+
+	Canvas getTile(const int3 & coordinates);
+
+	void updateTile(const int3 & coordinates);
+
+public:
+	explicit MapCache(const Point & tileSize, const Point & dimensions);
+	~MapCache();
+
+	void setViewCenter(const Point & center, int level);
+
+	Rect getVisibleAreaTiles() const;
+	int3 getTileCenter() const;
+	int3 getTileAtPoint(const Point & position) const;
+	Point getViewCenter() const;
+
+	void update(uint32_t timeDelta);
+	void render(Canvas & target);
+};
+
+class MapView : public CIntObject
+{
+	std::unique_ptr<MapCache> tilesCache;
+
+	Point tileSize;
+
+public:
+	MapView(const Point & offset, const Point & dimensions);
+
+	Rect getVisibleAreaTiles() const;
+	Point getViewCenter() const;
+	int3 getTileCenter() const;
+	int3 getTileAtPoint(const Point & position) const;
+
+	void setViewCenter(const int3 & position);
+	void setViewCenter(const Point & position, int level);
+
+	void show(SDL_Surface * to) override;
+	void showAll(SDL_Surface * to) override;
+};

+ 6 - 0
client/adventureMap/mapHandler.cpp

@@ -14,11 +14,16 @@
 #include "../render/CAnimation.h"
 #include "../render/CFadeAnimation.h"
 #include "../render/Colors.h"
+#include "../gui/CGuiHandler.h"
 #include "../renderSDL/SDL_Extensions.h"
 #include "../CGameInfo.h"
 #include "../render/Graphics.h"
 #include "../render/IImage.h"
+#include "../render/Canvas.h"
 #include "../CMusicHandler.h"
+#include "../CPlayerInterface.h"
+
+#include "../../CCallback.h"
 
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/CObjectClassesHandler.h"
@@ -1483,4 +1488,5 @@ TerrainTileObject::TerrainTileObject(const CGObjectInstance * obj_, Rect rect_,
 
 TerrainTileObject::~TerrainTileObject()
 {
+
 }

+ 3 - 1
client/adventureMap/mapHandler.h

@@ -14,6 +14,8 @@
 #include "../../lib/spells/ViewSpellInt.h"
 #include "../../lib/Rect.h"
 
+#include "../gui/CIntObject.h"
+
 #ifdef IN
 #undef IN
 #endif
@@ -346,7 +348,7 @@ public: //TODO: make private
 	boost::multi_array<TerrainTile2, 3> ttiles; //informations about map tiles [z][x][y]
 	int3 sizes; //map size (x = width, y = height, z = number of levels)
 	const CMap * map;
-
+private:
 	// Max number of tiles that will fit in the map screen. Tiles
 	// can be partial on each edges.
 	int tilesW;

+ 21 - 23
client/render/Canvas.cpp

@@ -18,14 +18,14 @@
 
 Canvas::Canvas(SDL_Surface * surface):
 	surface(surface),
-	renderOffset(0,0)
+	renderArea(0,0, surface->w, surface->h)
 {
 	surface->refcount++;
 }
 
 Canvas::Canvas(const Canvas & other):
 	surface(other.surface),
-	renderOffset(other.renderOffset)
+	renderArea(other.renderArea)
 {
 	surface->refcount++;
 }
@@ -33,25 +33,23 @@ Canvas::Canvas(const Canvas & other):
 Canvas::Canvas(const Canvas & other, const Rect & newClipRect):
 	Canvas(other)
 {
-	clipRect.emplace();
-	CSDL_Ext::getClipRect(surface, clipRect.get());
+	//clipRect.emplace();
+	//CSDL_Ext::getClipRect(surface, clipRect.get());
 
-	Rect currClipRect = newClipRect + renderOffset;
-	CSDL_Ext::setClipRect(surface, currClipRect);
-
-	renderOffset += newClipRect.topLeft();
+	renderArea = other.renderArea.intersect(newClipRect + other.renderArea.topLeft());
+	//CSDL_Ext::setClipRect(surface, currClipRect);
 }
 
 Canvas::Canvas(const Point & size):
-	renderOffset(0,0),
+	renderArea(Point(0,0), size),
 	surface(CSDL_Ext::newSurface(size.x, size.y))
 {
 }
 
 Canvas::~Canvas()
 {
-	if (clipRect)
-		CSDL_Ext::setClipRect(surface, clipRect.get());
+	//if (clipRect)
+	//	CSDL_Ext::setClipRect(surface, clipRect.get());
 
 	SDL_FreeSurface(surface);
 }
@@ -60,19 +58,19 @@ void Canvas::draw(std::shared_ptr<IImage> image, const Point & pos)
 {
 	assert(image);
 	if (image)
-		image->draw(surface, renderOffset.x + pos.x, renderOffset.y + pos.y);
+		image->draw(surface, renderArea.x + pos.x, renderArea.y + pos.y);
 }
 
 void Canvas::draw(std::shared_ptr<IImage> image, const Point & pos, const Rect & sourceRect)
 {
 	assert(image);
 	if (image)
-		image->draw(surface, renderOffset.x + pos.x, renderOffset.y + pos.y, &sourceRect);
+		image->draw(surface, renderArea.x + pos.x, renderArea.y + pos.y, &sourceRect);
 }
 
 void Canvas::draw(Canvas & image, const Point & pos)
 {
-	CSDL_Ext::blitAt(image.surface, renderOffset.x + pos.x, renderOffset.y + pos.y, surface);
+	CSDL_Ext::blitSurface(image.surface, image.renderArea, surface, renderArea.topLeft() + pos);
 }
 
 void Canvas::draw(Canvas & image, const Point & pos, const Point & targetSize)
@@ -88,17 +86,17 @@ void Canvas::drawPoint(const Point & dest, const ColorRGBA & color)
 
 void Canvas::drawLine(const Point & from, const Point & dest, const ColorRGBA & colorFrom, const ColorRGBA & colorDest)
 {
-	CSDL_Ext::drawLine(surface, renderOffset + from, renderOffset + dest, CSDL_Ext::toSDL(colorFrom), CSDL_Ext::toSDL(colorDest));
+	CSDL_Ext::drawLine(surface, renderArea.topLeft() + from, renderArea.topLeft() + dest, CSDL_Ext::toSDL(colorFrom), CSDL_Ext::toSDL(colorDest));
 }
 
 void Canvas::drawLineDashed(const Point & from, const Point & dest, const ColorRGBA & color)
 {
-	CSDL_Ext::drawLineDashed(surface, renderOffset + from, renderOffset + dest, CSDL_Ext::toSDL(color));
+	CSDL_Ext::drawLineDashed(surface, renderArea.topLeft() + from, renderArea.topLeft() + dest, CSDL_Ext::toSDL(color));
 }
 
 void Canvas::drawBorderDashed(const Rect & target, const ColorRGBA & color)
 {
-	Rect realTarget = target + renderOffset;
+	Rect realTarget = target + renderArea.topLeft();
 
 	CSDL_Ext::drawLineDashed(surface, realTarget.topLeft(),    realTarget.topRight(),    CSDL_Ext::toSDL(color));
 	CSDL_Ext::drawLineDashed(surface, realTarget.bottomLeft(), realTarget.bottomRight(), CSDL_Ext::toSDL(color));
@@ -110,9 +108,9 @@ void Canvas::drawText(const Point & position, const EFonts & font, const SDL_Col
 {
 	switch (alignment)
 	{
-	case ETextAlignment::TOPLEFT:      return graphics->fonts[font]->renderTextLeft  (surface, text, colorDest, renderOffset + position);
-	case ETextAlignment::CENTER:       return graphics->fonts[font]->renderTextCenter(surface, text, colorDest, renderOffset + position);
-	case ETextAlignment::BOTTOMRIGHT:  return graphics->fonts[font]->renderTextRight (surface, text, colorDest, renderOffset + position);
+	case ETextAlignment::TOPLEFT:      return graphics->fonts[font]->renderTextLeft  (surface, text, colorDest, renderArea.topLeft() + position);
+	case ETextAlignment::CENTER:       return graphics->fonts[font]->renderTextCenter(surface, text, colorDest, renderArea.topLeft() + position);
+	case ETextAlignment::BOTTOMRIGHT:  return graphics->fonts[font]->renderTextRight (surface, text, colorDest, renderArea.topLeft() + position);
 	}
 }
 
@@ -120,9 +118,9 @@ void Canvas::drawText(const Point & position, const EFonts & font, const SDL_Col
 {
 	switch (alignment)
 	{
-	case ETextAlignment::TOPLEFT:      return graphics->fonts[font]->renderTextLinesLeft  (surface, text, colorDest, renderOffset + position);
-	case ETextAlignment::CENTER:       return graphics->fonts[font]->renderTextLinesCenter(surface, text, colorDest, renderOffset + position);
-	case ETextAlignment::BOTTOMRIGHT:  return graphics->fonts[font]->renderTextLinesRight (surface, text, colorDest, renderOffset + position);
+	case ETextAlignment::TOPLEFT:      return graphics->fonts[font]->renderTextLinesLeft  (surface, text, colorDest, renderArea.topLeft() + position);
+	case ETextAlignment::CENTER:       return graphics->fonts[font]->renderTextLinesCenter(surface, text, colorDest, renderArea.topLeft() + position);
+	case ETextAlignment::BOTTOMRIGHT:  return graphics->fonts[font]->renderTextLinesRight (surface, text, colorDest, renderArea.topLeft() + position);
 	}
 }
 

+ 2 - 2
client/render/Canvas.h

@@ -27,8 +27,8 @@ class Canvas
 	/// Clip rect that was in use on surface originally and needs to be restored on destruction
 	boost::optional<Rect> clipRect;
 
-	/// Current rendering area offset, all rendering operations will be moved into selected area
-	Point renderOffset;
+	/// Current rendering area, all rendering operations will be moved into selected area
+	Rect renderArea;
 
 	Canvas & operator = (Canvas & other) = delete;
 public:

+ 6 - 1
lib/Point.h

@@ -28,7 +28,7 @@ public:
 		:x(X),y(Y)
 	{};
 
-	Point(const int3 &a);
+	explicit DLL_LINKAGE Point(const int3 &a);
 
 	template<typename T>
 	Point operator+(const T &b) const
@@ -48,6 +48,11 @@ public:
 		return Point(x*mul, y*mul);
 	}
 
+	Point operator*(const Point &b) const
+	{
+		return Point(x*b.x,y*b.y);
+	}
+
 	template<typename T>
 	Point& operator+=(const T &b)
 	{

+ 7 - 0
lib/Rect.cpp

@@ -10,9 +10,16 @@
 
 #include "StdInc.h"
 #include "Rect.h"
+#include "int3.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+Point::Point(const int3 & a)
+	: x(a.x)
+	, y(a.x)
+{
+}
+
 /// Returns rect union - rect that covers both this rect and provided rect
 Rect Rect::include(const Rect & other) const
 {