/* * MapViewController.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 "MapViewController.h" #include "MapRendererContext.h" #include "MapRendererContextState.h" #include "MapViewCache.h" #include "MapViewModel.h" #include "../CCallback.h" #include "../CPlayerInterface.h" #include "../adventureMap/AdventureMapInterface.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" #include "../eventsSDL/InputHandler.h" #include "../../lib/CConfigHandler.h" #include "../../lib/StartInfo.h" #include "../../lib/UnlockGuard.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/MiscObjects.h" #include "../../lib/pathfinder/CGPathNode.h" #include "../../lib/spells/ViewSpellInt.h" void MapViewController::setViewCenter(const int3 & position) { setViewCenter(Point(position) * model->getSingleTileSize() + model->getSingleTileSize() / 2, position.z); } void MapViewController::setViewCenter(const Point & position, int level) { Point upperLimit = Point(context->getMapSize()) * model->getSingleTileSize(); Point lowerLimit = Point(0, 0); if(worldViewContext) { Point area = model->getPixelsVisibleDimensions(); Point mapCenter = upperLimit / 2; Point desiredLowerLimit = lowerLimit + area / 2; Point desiredUpperLimit = upperLimit - area / 2; Point actualLowerLimit{ std::min(desiredLowerLimit.x, mapCenter.x), std::min(desiredLowerLimit.y, mapCenter.y) }; Point actualUpperLimit{ std::max(desiredUpperLimit.x, mapCenter.x), std::max(desiredUpperLimit.y, mapCenter.y) }; upperLimit = actualUpperLimit; lowerLimit = actualLowerLimit; } Point betterPosition = {std::clamp(position.x, lowerLimit.x, upperLimit.x), std::clamp(position.y, lowerLimit.y, upperLimit.y)}; model->setViewCenter(betterPosition); model->setLevel(std::clamp(level, 0, context->getMapSize().z)); if(adventureInt && !puzzleMapContext) // may be called before adventureInt is initialized adventureInt->onMapViewMoved(model->getTilesTotalRect(), model->getLevel()); } void MapViewController::setTileSize(const Point & tileSize) { Point oldSize = model->getSingleTileSize(); model->setTileSize(tileSize); double scaleChangeX = 1.0 * tileSize.x / oldSize.x; double scaleChangeY = 1.0 * tileSize.y / oldSize.y; Point newViewCenter { static_cast(std::round(model->getMapViewCenter().x * scaleChangeX)), static_cast(std::round(model->getMapViewCenter().y * scaleChangeY)) }; // force update of view center since changing tile size may invalidated it setViewCenter(newViewCenter, model->getLevel()); } void MapViewController::modifyTileSize(int stepsChange, bool useDeadZone) { // we want to zoom in/out in fixed 10% steps, to allow player to return back to exactly 100% zoom just by scrolling // so, zooming in for 5 steps will put game at 1.1^5 = 1.61 scale // try to determine current zooming level and change it by requested number of steps double currentZoomFactor = targetTileSize.x / static_cast(defaultTileSize); double currentZoomSteps = std::round(std::log(currentZoomFactor) / std::log(1.01)); double newZoomSteps = stepsChange != 0 ? currentZoomSteps + stepsChange : stepsChange; double newZoomFactor = std::pow(1.01, newZoomSteps); Point currentZoom = targetTileSize; Point desiredZoom = Point(defaultTileSize,defaultTileSize) * newZoomFactor; if (desiredZoom == currentZoom && stepsChange < 0) desiredZoom -= Point(1,1); if (desiredZoom == currentZoom && stepsChange > 0) desiredZoom += Point(1,1); Point minimal = model->getSingleTileSizeLowerLimit(); Point maximal = model->getSingleTileSizeUpperLimit(); Point actualZoom = { std::clamp(desiredZoom.x, minimal.x, maximal.x), std::clamp(desiredZoom.y, minimal.y, maximal.y) }; if (actualZoom != currentZoom) { targetTileSize = actualZoom; if (useDeadZone) { if(actualZoom.x >= defaultTileSize - zoomTileDeadArea && actualZoom.x <= defaultTileSize + zoomTileDeadArea) actualZoom.x = defaultTileSize; if(actualZoom.y >= defaultTileSize - zoomTileDeadArea && actualZoom.y <= defaultTileSize + zoomTileDeadArea) actualZoom.y = defaultTileSize; } bool isInDeadZone = targetTileSize != actualZoom || actualZoom == Point(defaultTileSize, defaultTileSize); if(!wasInDeadZone && isInDeadZone) GH.input().hapticFeedback(); wasInDeadZone = isInDeadZone; setTileSize(actualZoom); } } MapViewController::MapViewController(std::shared_ptr model, std::shared_ptr view) : state(new MapRendererContextState()) , model(std::move(model)) , view(view) { adventureContext = std::make_shared(*state); context = adventureContext; } std::shared_ptr MapViewController::getContext() const { return context; } void MapViewController::tick(uint32_t timeDelta) { // confirmed to match H3 for // - hero embarking on boat (500 ms) // - hero disembarking from boat (500 ms) // - TODO: picking up resources // - TODO: killing mosters // - teleporting ( 250 ms) static const double fadeOutDuration = 500; static const double fadeInDuration = 500; static const double heroTeleportDuration = 250; if(movementContext) { const auto * object = context->getObject(movementContext->target); const auto * hero = dynamic_cast(object); const auto * boat = dynamic_cast(object); assert(boat || hero); if(!hero) hero = boat->hero; double heroMoveTime = LOCPLINT->playerID == hero->getOwner() ? settings["adventure"]["heroMoveTime"].Float() : settings["adventure"]["enemyMoveTime"].Float(); movementContext->progress += timeDelta / heroMoveTime; movementContext->progress = std::min( 1.0, movementContext->progress); Point positionFrom = Point(hero->convertToVisitablePos(movementContext->tileFrom)) * model->getSingleTileSize() + model->getSingleTileSize() / 2; Point positionDest = Point(hero->convertToVisitablePos(movementContext->tileDest)) * model->getSingleTileSize() + model->getSingleTileSize() / 2; Point positionCurr = vstd::lerp(positionFrom, positionDest, movementContext->progress); setViewCenter(positionCurr, movementContext->tileDest.z); } if(teleportContext) { teleportContext->progress += timeDelta / heroTeleportDuration; teleportContext->progress = std::min( 1.0, teleportContext->progress); } if(fadingOutContext) { fadingOutContext->progress -= timeDelta / fadeOutDuration; fadingOutContext->progress = std::max( 0.0, fadingOutContext->progress); } if(fadingInContext) { fadingInContext->progress += timeDelta / fadeInDuration; fadingInContext->progress = std::min( 1.0, fadingInContext->progress); } if (adventureContext) adventureContext->animationTime += timeDelta; updateState(); } void MapViewController::updateState() { if(adventureContext) { adventureContext->settingsSessionSpectate = settings["session"]["spectate"].Bool(); adventureContext->settingsAdventureObjectAnimation = settings["adventure"]["objectAnimation"].Bool(); adventureContext->settingsAdventureTerrainAnimation = settings["adventure"]["terrainAnimation"].Bool(); adventureContext->settingShowGrid = settings["gameTweaks"]["showGrid"].Bool(); adventureContext->settingShowVisitable = settings["session"]["showVisitable"].Bool(); adventureContext->settingShowBlocked = settings["session"]["showBlocked"].Bool(); adventureContext->settingSpellRange = settings["session"]["showSpellRange"].Bool(); adventureContext->settingTextOverlay = GH.isKeyboardAltDown(); } } void MapViewController::afterRender() { if(movementContext) { const auto * object = context->getObject(movementContext->target); const auto * hero = dynamic_cast(object); const auto * boat = dynamic_cast(object); assert(boat || hero); if(!hero) hero = boat->hero; if(movementContext->progress >= 0.999) { logGlobal->debug("Ending movement animation"); setViewCenter(hero->getSightCenter()); removeObject(context->getObject(movementContext->target)); addObject(context->getObject(movementContext->target)); activateAdventureContext(movementContext->animationTime); } } if(teleportContext && teleportContext->progress >= 0.999) { logGlobal->debug("Ending teleport animation"); activateAdventureContext(teleportContext->animationTime); } if(fadingOutContext && fadingOutContext->progress <= 0.001) { logGlobal->debug("Ending fade out animation"); removeObject(context->getObject(fadingOutContext->target)); activateAdventureContext(fadingOutContext->animationTime); } if(fadingInContext && fadingInContext->progress >= 0.999) { logGlobal->debug("Ending fade in animation"); activateAdventureContext(fadingInContext->animationTime); } } bool MapViewController::isEventInstant(const CGObjectInstance * obj, const PlayerColor & initiator) { if(settings["gameTweaks"]["skipAdventureMapAnimations"].Bool()) return true; if (!isEventVisible(obj, initiator)) return true; if (!initiator.isValidPlayer()) return true; // skip effects such as new monsters on new month if(initiator != LOCPLINT->playerID && settings["adventure"]["enemyMoveTime"].Float() <= 0) return true; // instant movement speed if(initiator == LOCPLINT->playerID && settings["adventure"]["heroMoveTime"].Float() <= 0) return true; // instant movement speed return false; } bool MapViewController::isEventVisible(const CGObjectInstance * obj, const PlayerColor & initiator) { if(adventureContext == nullptr) return false; if(initiator != LOCPLINT->playerID && settings["adventure"]["enemyMoveTime"].Float() < 0) return false; // enemy move speed set to "hidden/none" if(!GH.windows().isTopWindow(adventureInt)) return false; // do not focus on actions of other players except for AI with simturns off if (initiator != LOCPLINT->playerID && initiator.isValidPlayer()) { if (LOCPLINT->makingTurn) return false; if (LOCPLINT->cb->getStartInfo()->playerInfos.at(initiator).isControlledByHuman() && !settings["session"]["adventureTrackHero"].Bool()) return false; } if(obj->isVisitable()) return context->isVisible(obj->visitablePos()); else return context->isVisible(obj->anchorPos()); } bool MapViewController::isEventVisible(const CGHeroInstance * obj, const int3 & from, const int3 & dest) { if(adventureContext == nullptr) return false; if(obj->getOwner() != LOCPLINT->playerID && settings["adventure"]["enemyMoveTime"].Float() < 0) return false; // enemy move speed set to "hidden/none" if(!GH.windows().isTopWindow(adventureInt)) return false; // do not focus on actions of other players except for AI with simturns off if (obj->getOwner() != LOCPLINT->playerID) { if (LOCPLINT->makingTurn) return false; if (LOCPLINT->cb->getStartInfo()->playerInfos.at(obj->getOwner()).isControlledByHuman() && !settings["session"]["adventureTrackHero"].Bool()) return false; } if(context->isVisible(obj->convertToVisitablePos(from))) return true; if(context->isVisible(obj->convertToVisitablePos(dest))) return true; return false; } void MapViewController::fadeOutObject(const CGObjectInstance * obj) { animationWait.setBusy(); logGlobal->debug("Starting fade out animation"); fadingOutContext = std::make_shared(*state); fadingOutContext->animationTime = adventureContext->animationTime; adventureContext = fadingOutContext; context = fadingOutContext; const CGObjectInstance * movingObject = obj; if (obj->ID == Obj::HERO) { auto * hero = dynamic_cast(obj); if (hero->boat) movingObject = hero->boat; } fadingOutContext->target = movingObject->id; fadingOutContext->progress = 1.0; } void MapViewController::fadeInObject(const CGObjectInstance * obj) { animationWait.setBusy(); logGlobal->debug("Starting fade in animation"); fadingInContext = std::make_shared(*state); fadingInContext->animationTime = adventureContext->animationTime; adventureContext = fadingInContext; context = fadingInContext; const CGObjectInstance * movingObject = obj; if (obj->ID == Obj::HERO) { auto * hero = dynamic_cast(obj); if (hero->boat) movingObject = hero->boat; } fadingInContext->target = movingObject->id; fadingInContext->progress = 0.0; } void MapViewController::removeObject(const CGObjectInstance * obj) { if (obj->ID == Obj::BOAT) { auto * boat = dynamic_cast(obj); if (boat->hero) { view->invalidate(context, boat->hero->id); state->removeObject(boat->hero); } } if (obj->ID == Obj::HERO) { auto * hero = dynamic_cast(obj); if (hero->boat) { view->invalidate(context, hero->boat->id); state->removeObject(hero->boat); } } view->invalidate(context, obj->id); state->removeObject(obj); } void MapViewController::addObject(const CGObjectInstance * obj) { state->addObject(obj); view->invalidate(context, obj->id); } void MapViewController::onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) { if(isEventVisible(obj, from, dest)) { if (!isEventInstant(obj, obj->getOwner())) fadeOutObject(obj); setViewCenter(obj->getSightCenter()); } else removeObject(obj); } void MapViewController::onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) { if(isEventVisible(obj, from, dest)) setViewCenter(obj->getSightCenter()); } void MapViewController::onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) { if(isEventVisible(obj, from, dest)) setViewCenter(obj->getSightCenter()); } void MapViewController::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) { if(isEventVisible(obj, from, dest)) { if (!isEventInstant(obj, obj->getOwner())) fadeInObject(obj); setViewCenter(obj->getSightCenter()); } addObject(obj); } void MapViewController::onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) { assert(!hasOngoingAnimations()); if(isEventVisible(obj, initiator) && !isEventInstant(obj, initiator) ) fadeInObject(obj); addObject(obj); } void MapViewController::onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) { assert(!hasOngoingAnimations()); if(isEventVisible(obj, initiator) && !isEventInstant(obj, initiator) ) fadeOutObject(obj); else removeObject(obj); } void MapViewController::onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) { addObject(obj); }; void MapViewController::onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) { removeObject(obj); }; void MapViewController::onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) { assert(!hasOngoingAnimations()); if(isEventVisible(obj, from, dest)) { setViewCenter(obj->getSightCenter()); view->createTransitionSnapshot(context); } } void MapViewController::onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) { assert(!hasOngoingAnimations()); const CGObjectInstance * movingObject = obj; if(obj->boat) movingObject = obj->boat; removeObject(movingObject); addObject(movingObject); if(isEventVisible(obj, from, dest)) { animationWait.setBusy(); logGlobal->debug("Starting teleport animation"); teleportContext = std::make_shared(*state); teleportContext->animationTime = adventureContext->animationTime; adventureContext = teleportContext; context = teleportContext; setViewCenter(movingObject->getSightCenter()); } } void MapViewController::onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) { assert(!hasOngoingAnimations()); // revisiting via spacebar, no need to animate if(from == dest) return; const CGObjectInstance * movingObject = obj; if(obj->boat) movingObject = obj->boat; removeObject(movingObject); if(!isEventVisible(obj, from, dest)) { addObject(movingObject); return; } double movementTime = LOCPLINT->playerID == obj->tempOwner ? settings["adventure"]["heroMoveTime"].Float() : settings["adventure"]["enemyMoveTime"].Float(); if(movementTime > 1) { animationWait.setBusy(); logGlobal->debug("Starting movement animation"); movementContext = std::make_shared(*state); movementContext->animationTime = adventureContext->animationTime; adventureContext = movementContext; context = movementContext; state->addMovingObject(movingObject, from, dest); movementContext->target = movingObject->id; movementContext->tileFrom = from; movementContext->tileDest = dest; movementContext->progress = 0.0; } else // instant movement { addObject(movingObject); setViewCenter(movingObject->visitablePos()); } } bool MapViewController::hasOngoingAnimations() { if(movementContext) return true; if(fadingOutContext) return true; if(fadingInContext) return true; if(teleportContext) return true; return false; } void MapViewController::waitForOngoingAnimations() { auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); animationWait.waitWhileBusy(); } void MapViewController::endNetwork() { animationWait.requestTermination(); } void MapViewController::activateAdventureContext(uint32_t animationTime) { resetContext(); adventureContext = std::make_shared(*state); adventureContext->animationTime = animationTime; context = adventureContext; updateState(); } void MapViewController::activateAdventureContext() { activateAdventureContext(0); } void MapViewController::activateWorldViewContext() { if(worldViewContext) return; resetContext(); worldViewContext = std::make_shared(*state); context = worldViewContext; } void MapViewController::activateSpellViewContext() { if(spellViewContext) return; resetContext(); spellViewContext = std::make_shared(*state); worldViewContext = spellViewContext; context = spellViewContext; } void MapViewController::activatePuzzleMapContext(const int3 & grailPosition) { resetContext(); puzzleMapContext = std::make_shared(*state); context = puzzleMapContext; CGPathNode fakeNode; fakeNode.coord = grailPosition; puzzleMapContext->grailPos = std::make_unique(); // create two nodes since 1st one is normally not visible puzzleMapContext->grailPos->nodes.push_back(fakeNode); puzzleMapContext->grailPos->nodes.push_back(fakeNode); } void MapViewController::resetContext() { adventureContext.reset(); movementContext.reset(); fadingOutContext.reset(); fadingInContext.reset(); teleportContext.reset(); worldViewContext.reset(); spellViewContext.reset(); puzzleMapContext.reset(); animationWait.setFree(); } void MapViewController::setTerrainVisibility(bool showAllTerrain) { assert(spellViewContext); spellViewContext->showAllTerrain = showAllTerrain; } void MapViewController::setOverlayVisibility(const std::vector & objectPositions) { assert(spellViewContext); spellViewContext->additionalOverlayIcons = objectPositions; }