|
@@ -1,5 +1,5 @@
|
|
|
/*
|
|
|
- * CPlayerInterface.cpp, part of VCMI engine
|
|
|
+ * HeroMovementController.cpp, part of VCMI engine
|
|
|
*
|
|
|
* Authors: listed in file AUTHORS in main folder
|
|
|
*
|
|
@@ -24,37 +24,19 @@
|
|
|
|
|
|
#include "../lib/pathfinder/CGPathNode.h"
|
|
|
#include "../lib/mapObjects/CGHeroInstance.h"
|
|
|
-#include "../lib/mapObjects/MiscObjects.h"
|
|
|
#include "../lib/RoadHandler.h"
|
|
|
#include "../lib/TerrainHandler.h"
|
|
|
#include "../lib/NetPacks.h"
|
|
|
-#include "../lib/CondSh.h"
|
|
|
-
|
|
|
-std::optional<int3> HeroMovementController::getLastTile(const CGHeroInstance * hero) const
|
|
|
-{
|
|
|
- if (!LOCPLINT->localState->hasPath(hero))
|
|
|
- return std::nullopt;
|
|
|
-
|
|
|
- return LOCPLINT->localState->getPath(hero).endPos();
|
|
|
-}
|
|
|
-
|
|
|
-std::optional<int3> HeroMovementController::getNextTile(const CGHeroInstance * hero) const
|
|
|
-{
|
|
|
- if (!LOCPLINT->localState->hasPath(hero))
|
|
|
- return std::nullopt;
|
|
|
-
|
|
|
- return LOCPLINT->localState->getPath(hero).nextNode().coord;
|
|
|
-}
|
|
|
|
|
|
bool HeroMovementController::isHeroMovingThroughGarrison(const CGHeroInstance * hero, const CArmedInstance * garrison) const
|
|
|
{
|
|
|
if(!duringMovement)
|
|
|
return false;
|
|
|
|
|
|
- if (!LOCPLINT->localState->hasPath(hero))
|
|
|
+ if(!LOCPLINT->localState->hasPath(hero))
|
|
|
return false;
|
|
|
|
|
|
- if (garrison->visitableAt(*getLastTile(hero)))
|
|
|
+ if(garrison->visitableAt(LOCPLINT->localState->getPath(hero).lastNode().coord))
|
|
|
return false; // hero want to enter garrison, not pass through it
|
|
|
|
|
|
return true;
|
|
@@ -83,9 +65,13 @@ void HeroMovementController::onBattleStarted()
|
|
|
|
|
|
void HeroMovementController::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID)
|
|
|
{
|
|
|
- assert(hero == currentlyMovingHero);
|
|
|
+ // Player entered teleporter
|
|
|
+ // Check whether hero that has entered teleporter has paths that goes through teleporter and select appropriate exit
|
|
|
+ // othervice, ask server to select one randomly by sending invalid (-1) value as answer
|
|
|
+ assert(waitingForQueryApplyReply == false);
|
|
|
+ waitingForQueryApplyReply = true;
|
|
|
|
|
|
- if (!LOCPLINT->localState->hasPath(hero))
|
|
|
+ if(!LOCPLINT->localState->hasPath(hero))
|
|
|
{
|
|
|
// Hero enters teleporter without specifying exit - select it randomly
|
|
|
LOCPLINT->cb->selectionMade(-1, askID);
|
|
@@ -95,12 +81,14 @@ void HeroMovementController::showTeleportDialog(const CGHeroInstance * hero, Tel
|
|
|
const auto & heroPath = LOCPLINT->localState->getPath(hero);
|
|
|
const auto & nextNode = heroPath.nextNode();
|
|
|
|
|
|
- for (size_t i = 0; i < exits.size(); ++i)
|
|
|
+ for(size_t i = 0; i < exits.size(); ++i)
|
|
|
{
|
|
|
const auto * teleporter = LOCPLINT->cb->getObj(exits[i].first);
|
|
|
|
|
|
- if (teleporter && teleporter->visitableAt(nextNode.coord))
|
|
|
+ if(teleporter && teleporter->visitableAt(nextNode.coord))
|
|
|
{
|
|
|
+ // Remove this node from path - it will be covered by teleportation
|
|
|
+ //LOCPLINT->localState->removeLastNode(hero);
|
|
|
LOCPLINT->cb->selectionMade(i, askID);
|
|
|
return;
|
|
|
}
|
|
@@ -113,27 +101,28 @@ void HeroMovementController::showTeleportDialog(const CGHeroInstance * hero, Tel
|
|
|
|
|
|
void HeroMovementController::updatePath(const CGHeroInstance * hero, const TryMoveHero & details)
|
|
|
{
|
|
|
- if (hero->tempOwner != LOCPLINT->playerID)
|
|
|
+ // Once hero moved (or attempted to move) we need to update path
|
|
|
+ // to make sure that it is still valid or remove it completely if destination has been reached
|
|
|
+
|
|
|
+ if(hero->tempOwner != LOCPLINT->playerID)
|
|
|
return;
|
|
|
|
|
|
- if (!LOCPLINT->localState->hasPath(hero))
|
|
|
+ if(!LOCPLINT->localState->hasPath(hero))
|
|
|
return; // may happen when hero teleports
|
|
|
|
|
|
- assert(hero == currentlyMovingHero);
|
|
|
assert(LOCPLINT->makingTurn);
|
|
|
- assert(getNextTile(hero).has_value());
|
|
|
|
|
|
- bool directlyAttackingCreature = details.attackedFrom.has_value() && getLastTile(hero) == details.attackedFrom;
|
|
|
+ bool directlyAttackingCreature = details.attackedFrom.has_value() && LOCPLINT->localState->getPath(hero).lastNode().coord == details.attackedFrom;
|
|
|
|
|
|
- auto desiredTarget = getNextTile(hero);
|
|
|
- auto actualTarget = hero->convertToVisitablePos(details.end);
|
|
|
+ int3 desiredTarget = LOCPLINT->localState->getPath(hero).nextNode().coord;
|
|
|
+ int3 actualTarget = hero->convertToVisitablePos(details.end);
|
|
|
|
|
|
//don't erase path when revisiting with spacebar
|
|
|
bool heroChangedTile = details.start != details.end;
|
|
|
|
|
|
- if (desiredTarget && heroChangedTile)
|
|
|
+ if(heroChangedTile)
|
|
|
{
|
|
|
- if (*desiredTarget != actualTarget)
|
|
|
+ if(desiredTarget != actualTarget)
|
|
|
{
|
|
|
//invalidate path - movement was not along current path
|
|
|
//possible reasons: teleport, visit of object with "blocking visit" property
|
|
@@ -152,12 +141,20 @@ void HeroMovementController::updatePath(const CGHeroInstance * hero, const TryMo
|
|
|
|
|
|
void HeroMovementController::heroMoved(const CGHeroInstance * hero, const TryMoveHero & details)
|
|
|
{
|
|
|
- if (details.result == TryMoveHero::EMBARK || details.result == TryMoveHero::DISEMBARK)
|
|
|
+ // Server initiated movement -> start movement animation
|
|
|
+ // Note that this movement is not necessarily of owned heroes - other players movement will also pass through this method
|
|
|
+
|
|
|
+ if(details.result == TryMoveHero::EMBARK || details.result == TryMoveHero::DISEMBARK)
|
|
|
{
|
|
|
if(hero->getRemovalSound() && hero->tempOwner == LOCPLINT->playerID)
|
|
|
CCS->soundh->playSound(hero->getRemovalSound().value());
|
|
|
}
|
|
|
|
|
|
+ bool directlyAttackingCreature =
|
|
|
+ details.attackedFrom.has_value() &&
|
|
|
+ LOCPLINT->localState->hasPath(hero) &&
|
|
|
+ LOCPLINT->localState->getPath(hero).lastNode().coord == details.attackedFrom;
|
|
|
+
|
|
|
std::unordered_set<int3> changedTiles {
|
|
|
hero->convertToVisitablePos(details.start),
|
|
|
hero->convertToVisitablePos(details.end)
|
|
@@ -167,61 +164,86 @@ void HeroMovementController::heroMoved(const CGHeroInstance * hero, const TryMov
|
|
|
|
|
|
updatePath(hero, details);
|
|
|
|
|
|
- if(details.stopMovement()) //hero failed to move
|
|
|
+ if(details.stopMovement())
|
|
|
{
|
|
|
- if (duringMovement)
|
|
|
+ if(duringMovement)
|
|
|
endHeroMove(hero);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ // We are in network thread
|
|
|
+ // Block netpack processing until movement animation is over
|
|
|
CGI->mh->waitForOngoingAnimations();
|
|
|
|
|
|
//move finished
|
|
|
adventureInt->onHeroChanged(hero);
|
|
|
|
|
|
-// // Hero attacked creature directly, set direction to face it.
|
|
|
-// if (directlyAttackingCreature)
|
|
|
-// {
|
|
|
-// // Get direction to attacker.
|
|
|
-// int3 posOffset = *details.attackedFrom - details.end + int3(2, 1, 0);
|
|
|
-// static const ui8 dirLookup[3][3] =
|
|
|
-// {
|
|
|
-// { 1, 2, 3 },
|
|
|
-// { 8, 0, 4 },
|
|
|
-// { 7, 6, 5 }
|
|
|
-// };
|
|
|
-// // FIXME: Avoid const_cast, make moveDir mutable in some other way?
|
|
|
-// const_cast<CGHeroInstance *>(hero)->moveDir = dirLookup[posOffset.y][posOffset.x];
|
|
|
-// }
|
|
|
+ // Hero attacked creature, set direction to face it.
|
|
|
+ if(directlyAttackingCreature)
|
|
|
+ {
|
|
|
+ // Get direction to attacker.
|
|
|
+ int3 posOffset = *details.attackedFrom - details.end + int3(2, 1, 0);
|
|
|
+ static const ui8 dirLookup[3][3] =
|
|
|
+ {
|
|
|
+ { 1, 2, 3 },
|
|
|
+ { 8, 0, 4 },
|
|
|
+ { 7, 6, 5 }
|
|
|
+ };
|
|
|
+
|
|
|
+ //FIXME: better handling of this case without const_cast
|
|
|
+ const_cast<CGHeroInstance *>(hero)->moveDir = dirLookup[posOffset.y][posOffset.x];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void HeroMovementController::onQueryReplyApplied()
|
|
|
+{
|
|
|
+ if(duringMovement)
|
|
|
+ {
|
|
|
+ // Server accepted our TeleportDialog query reply and moved hero
|
|
|
+ // Continue moving alongside our path, if any
|
|
|
+
|
|
|
+ assert(waitingForQueryApplyReply);
|
|
|
+ waitingForQueryApplyReply = false;
|
|
|
+ onMoveHeroApplied();
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
void HeroMovementController::onMoveHeroApplied()
|
|
|
{
|
|
|
- //check if user cancelled movement
|
|
|
- if (GH.input().ignoreEventsUntilInput())
|
|
|
+ // at this point, server have finished processing of hero movement request
|
|
|
+ // as well as all side effectes from movement, such as object visit or combat start
|
|
|
+
|
|
|
+ // this was request to move alongside path from player, but either another player or teleport action
|
|
|
+ if(!duringMovement)
|
|
|
+ return;
|
|
|
+
|
|
|
+ // hero has moved onto teleporter and activated it
|
|
|
+ // in this case next movement should be done only after query reply has been acknowledged
|
|
|
+ // and hero has been moved to teleport destination
|
|
|
+ if(waitingForQueryApplyReply)
|
|
|
+ return;
|
|
|
+
|
|
|
+ if(GH.input().ignoreEventsUntilInput())
|
|
|
stoppingMovement = true;
|
|
|
|
|
|
- if (duringMovement)
|
|
|
- {
|
|
|
- assert(currentlyMovingHero);
|
|
|
- auto const * hero = currentlyMovingHero;
|
|
|
+ assert(currentlyMovingHero);
|
|
|
+ const auto * hero = currentlyMovingHero;
|
|
|
|
|
|
- bool canMove = LOCPLINT->localState->hasPath(hero) && LOCPLINT->localState->getPath(hero).nextNode().turns == 0;
|
|
|
- bool wantStop = stoppingMovement;
|
|
|
- bool canStop = !canMove || canHeroStopAtNode(LOCPLINT->localState->getPath(hero).currNode());
|
|
|
+ bool canMove = LOCPLINT->localState->hasPath(hero) && LOCPLINT->localState->getPath(hero).nextNode().turns == 0;
|
|
|
+ bool wantStop = stoppingMovement;
|
|
|
+ bool canStop = !canMove || canHeroStopAtNode(LOCPLINT->localState->getPath(hero).currNode());
|
|
|
|
|
|
- if (!canMove)
|
|
|
- {
|
|
|
- endHeroMove(hero);
|
|
|
- }
|
|
|
- else if ( wantStop && canStop )
|
|
|
- {
|
|
|
- endHeroMove(hero);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- moveHeroOnce(hero, LOCPLINT->localState->getPath(hero));
|
|
|
- }
|
|
|
+ if(!canMove)
|
|
|
+ {
|
|
|
+ endHeroMove(hero);
|
|
|
+ }
|
|
|
+ else if(wantStop && canStop)
|
|
|
+ {
|
|
|
+ endHeroMove(hero);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ moveHeroOnce(hero, LOCPLINT->localState->getPath(hero));
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -234,6 +256,7 @@ void HeroMovementController::movementAbortRequested()
|
|
|
void HeroMovementController::endHeroMove(const CGHeroInstance * hero)
|
|
|
{
|
|
|
assert(duringMovement == true);
|
|
|
+ assert(currentlyMovingHero != nullptr);
|
|
|
duringMovement = false;
|
|
|
stoppingMovement = false;
|
|
|
currentlyMovingHero = nullptr;
|
|
@@ -244,17 +267,17 @@ void HeroMovementController::endHeroMove(const CGHeroInstance * hero)
|
|
|
|
|
|
AudioPath HeroMovementController::getMovementSoundFor(const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType)
|
|
|
{
|
|
|
- if (moveType == EPathNodeAction::TELEPORT_BATTLE || moveType == EPathNodeAction::TELEPORT_BLOCKING_VISIT || moveType == EPathNodeAction::TELEPORT_NORMAL)
|
|
|
+ if(moveType == EPathNodeAction::TELEPORT_BATTLE || moveType == EPathNodeAction::TELEPORT_BLOCKING_VISIT || moveType == EPathNodeAction::TELEPORT_NORMAL)
|
|
|
return {};
|
|
|
|
|
|
- if (moveType == EPathNodeAction::EMBARK || moveType == EPathNodeAction::DISEMBARK)
|
|
|
+ if(moveType == EPathNodeAction::EMBARK || moveType == EPathNodeAction::DISEMBARK)
|
|
|
return {};
|
|
|
|
|
|
- if (moveType == EPathNodeAction::BLOCKING_VISIT)
|
|
|
+ if(moveType == EPathNodeAction::BLOCKING_VISIT)
|
|
|
return {};
|
|
|
|
|
|
// flying movement sound
|
|
|
- if (hero->hasBonusOfType(BonusType::FLYING_MOVEMENT))
|
|
|
+ if(hero->hasBonusOfType(BonusType::FLYING_MOVEMENT))
|
|
|
return AudioPath::builtin("HORSE10.wav");
|
|
|
|
|
|
auto prevTile = LOCPLINT->cb->getTile(posPrev);
|
|
@@ -264,7 +287,7 @@ AudioPath HeroMovementController::getMovementSoundFor(const CGHeroInstance * her
|
|
|
auto nextRoad = nextTile->roadType;
|
|
|
bool movingOnRoad = prevRoad->getId() != Road::NO_ROAD && nextRoad->getId() != Road::NO_ROAD;
|
|
|
|
|
|
- if (movingOnRoad)
|
|
|
+ if(movingOnRoad)
|
|
|
return nextTile->terType->horseSound;
|
|
|
else
|
|
|
return nextTile->terType->horseSoundPenalty;
|
|
@@ -279,10 +302,10 @@ void HeroMovementController::updateMovementSound(const CGHeroInstance * h, int3
|
|
|
{
|
|
|
currentMovementSoundName = newSoundName;
|
|
|
|
|
|
- if (currentMovementSoundChannel != -1)
|
|
|
+ if(currentMovementSoundChannel != -1)
|
|
|
CCS->soundh->stopSound(currentMovementSoundChannel);
|
|
|
|
|
|
- if (!currentMovementSoundName.empty())
|
|
|
+ if(!currentMovementSoundName.empty())
|
|
|
currentMovementSoundChannel = CCS->soundh->playSound(currentMovementSoundName, -1, true);
|
|
|
else
|
|
|
currentMovementSoundChannel = -1;
|
|
@@ -291,17 +314,18 @@ void HeroMovementController::updateMovementSound(const CGHeroInstance * h, int3
|
|
|
|
|
|
void HeroMovementController::stopMovementSound()
|
|
|
{
|
|
|
- CCS->soundh->stopSound(currentMovementSoundChannel);
|
|
|
+ if(currentMovementSoundChannel != -1)
|
|
|
+ CCS->soundh->stopSound(currentMovementSoundChannel);
|
|
|
currentMovementSoundChannel = -1;
|
|
|
currentMovementSoundName = AudioPath();
|
|
|
}
|
|
|
|
|
|
bool HeroMovementController::canHeroStopAtNode(const CGPathNode & node) const
|
|
|
{
|
|
|
- if (node.layer != EPathfindingLayer::LAND && node.layer != EPathfindingLayer::SAIL)
|
|
|
+ if(node.layer != EPathfindingLayer::LAND && node.layer != EPathfindingLayer::SAIL)
|
|
|
return false;
|
|
|
|
|
|
- if (node.accessible != EPathAccessibility::ACCESSIBLE)
|
|
|
+ if(node.accessible != EPathAccessibility::ACCESSIBLE)
|
|
|
return false;
|
|
|
|
|
|
return true;
|
|
@@ -313,12 +337,15 @@ void HeroMovementController::movementStartRequested(const CGHeroInstance * h, co
|
|
|
duringMovement = true;
|
|
|
currentlyMovingHero = h;
|
|
|
|
|
|
- CCS->curh->show();
|
|
|
+ CCS->curh->hide();
|
|
|
moveHeroOnce(h, path);
|
|
|
}
|
|
|
|
|
|
void HeroMovementController::moveHeroOnce(const CGHeroInstance * h, const CGPath & path)
|
|
|
{
|
|
|
+ // Moves hero once, sends request to server and immediately returns
|
|
|
+ // movement alongside paths will be done on receiving response from server
|
|
|
+
|
|
|
assert(duringMovement == true);
|
|
|
|
|
|
const auto & currNode = path.currNode();
|
|
@@ -329,9 +356,10 @@ void HeroMovementController::moveHeroOnce(const CGHeroInstance * h, const CGPath
|
|
|
|
|
|
int3 nextCoord = h->convertFromVisitablePos(nextNode.coord);
|
|
|
|
|
|
- if (nextNode.isTeleportAction())
|
|
|
+ if(nextNode.isTeleportAction())
|
|
|
{
|
|
|
stopMovementSound();
|
|
|
+ logGlobal->trace("Requesting hero teleportation to %s", nextNode.coord.toString());
|
|
|
LOCPLINT->cb->moveHero(h, nextCoord, false);
|
|
|
return;
|
|
|
}
|