2023-09-07 21:37:42 +02:00
/*
2023-09-16 19:07:02 +02:00
* HeroMovementController . cpp , part of VCMI engine
2023-09-07 21:37:42 +02:00
*
* 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 "HeroMovementController.h"
# include "CGameInfo.h"
# include "CPlayerInterface.h"
# include "PlayerLocalState.h"
# include "adventureMap/AdventureMapInterface.h"
# include "eventsSDL/InputHandler.h"
# include "gui/CGuiHandler.h"
# include "gui/CursorHandler.h"
# include "mapView/mapHandler.h"
2024-05-02 14:55:20 +02:00
# include "media/ISoundPlayer.h"
2023-09-07 21:37:42 +02:00
# include "../CCallback.h"
2024-05-18 13:04:10 +02:00
# include "ConditionalWait.h"
2024-05-02 14:55:20 +02:00
# include "../lib/CConfigHandler.h"
2024-06-01 17:28:17 +02:00
# include "../lib/CRandomGenerator.h"
2023-09-07 21:37:42 +02:00
# include "../lib/pathfinder/CGPathNode.h"
# include "../lib/mapObjects/CGHeroInstance.h"
2023-10-23 12:59:15 +02:00
# include "../lib/networkPacks/PacksForClient.h"
2023-09-07 21:37:42 +02:00
# include "../lib/RoadHandler.h"
# include "../lib/TerrainHandler.h"
2023-09-08 17:02:36 +02:00
bool HeroMovementController : : isHeroMovingThroughGarrison ( const CGHeroInstance * hero , const CArmedInstance * garrison ) const
2023-09-07 21:37:42 +02:00
{
2023-09-16 12:40:49 +02:00
if ( ! duringMovement )
return false ;
2023-09-08 17:02:36 +02:00
2023-09-16 19:07:02 +02:00
if ( ! LOCPLINT - > localState - > hasPath ( hero ) )
2023-09-08 17:02:36 +02:00
return false ;
2023-09-16 19:07:02 +02:00
if ( garrison - > visitableAt ( LOCPLINT - > localState - > getPath ( hero ) . lastNode ( ) . coord ) )
2023-09-08 17:02:36 +02:00
return false ; // hero want to enter garrison, not pass through it
return true ;
2023-09-07 21:37:42 +02:00
}
bool HeroMovementController : : isHeroMoving ( ) const
{
return duringMovement ;
}
void HeroMovementController : : onPlayerTurnStarted ( )
{
2023-09-15 21:18:36 +02:00
assert ( duringMovement = = false ) ;
assert ( stoppingMovement = = false ) ;
duringMovement = false ;
2023-09-16 12:40:49 +02:00
currentlyMovingHero = nullptr ;
2023-09-07 21:37:42 +02:00
}
void HeroMovementController : : onBattleStarted ( )
{
// when battle starts, game will send battleStart pack *before* movement confirmation
// and since network thread wait for battle intro to play, movement confirmation will only happen after intro
// leading to several bugs, such as blocked input during intro
2023-09-18 17:17:26 +02:00
requestMovementAbort ( ) ;
2023-09-07 21:37:42 +02:00
}
2023-09-15 21:18:36 +02:00
void HeroMovementController : : showTeleportDialog ( const CGHeroInstance * hero , TeleportChannelID channel , TTeleportExitsList exits , bool impassable , QueryID askID )
2023-09-07 21:37:42 +02:00
{
2023-09-18 17:17:26 +02:00
if ( impassable | | exits . empty ( ) ) //FIXME: why we even have this dialog in such case?
{
LOCPLINT - > cb - > selectionMade ( - 1 , askID ) ;
return ;
}
2023-09-16 19:07:02 +02:00
// 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 ;
2023-09-16 12:40:49 +02:00
2023-09-16 19:07:02 +02:00
if ( ! LOCPLINT - > localState - > hasPath ( hero ) )
2023-09-15 21:18:36 +02:00
{
2023-09-16 12:40:49 +02:00
// Hero enters teleporter without specifying exit - select it randomly
LOCPLINT - > cb - > selectionMade ( - 1 , askID ) ;
2023-09-15 21:18:36 +02:00
return ;
}
const auto & heroPath = LOCPLINT - > localState - > getPath ( hero ) ;
const auto & nextNode = heroPath . nextNode ( ) ;
2023-09-08 17:02:36 +02:00
2023-09-16 19:07:02 +02:00
for ( size_t i = 0 ; i < exits . size ( ) ; + + i )
2023-09-15 21:18:36 +02:00
{
2024-06-11 16:31:11 +02:00
if ( exits [ i ] . second = = nextNode . coord )
2023-09-15 21:18:36 +02:00
{
2023-09-16 19:07:02 +02:00
// Remove this node from path - it will be covered by teleportation
//LOCPLINT->localState->removeLastNode(hero);
2023-09-15 21:18:36 +02:00
LOCPLINT - > cb - > selectionMade ( i , askID ) ;
return ;
}
}
2023-11-27 23:17:25 +02:00
// may happen when hero has path but does not moves alongside it
// for example, while standing on teleporter set path that does not leads throught teleporter and press space
2023-09-16 12:40:49 +02:00
LOCPLINT - > cb - > selectionMade ( - 1 , askID ) ;
2023-09-15 21:18:36 +02:00
return ;
2023-09-07 21:37:42 +02:00
}
2023-09-11 11:54:25 +02:00
void HeroMovementController : : updatePath ( const CGHeroInstance * hero , const TryMoveHero & details )
{
2023-09-16 19:07:02 +02:00
// 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 )
2023-09-11 11:54:25 +02:00
return ;
2023-09-16 19:07:02 +02:00
if ( ! LOCPLINT - > localState - > hasPath ( hero ) )
2023-09-15 21:18:36 +02:00
return ; // may happen when hero teleports
2023-09-11 11:54:25 +02:00
assert ( LOCPLINT - > makingTurn ) ;
2023-09-16 19:07:02 +02:00
bool directlyAttackingCreature = details . attackedFrom . has_value ( ) & & LOCPLINT - > localState - > getPath ( hero ) . lastNode ( ) . coord = = details . attackedFrom ;
2023-09-11 11:54:25 +02:00
2023-09-16 19:07:02 +02:00
int3 desiredTarget = LOCPLINT - > localState - > getPath ( hero ) . nextNode ( ) . coord ;
int3 actualTarget = hero - > convertToVisitablePos ( details . end ) ;
2023-09-11 11:54:25 +02:00
//don't erase path when revisiting with spacebar
bool heroChangedTile = details . start ! = details . end ;
2023-09-16 19:07:02 +02:00
if ( heroChangedTile )
2023-09-11 11:54:25 +02:00
{
2023-09-16 19:07:02 +02:00
if ( desiredTarget ! = actualTarget )
2023-09-11 11:54:25 +02:00
{
//invalidate path - movement was not along current path
//possible reasons: teleport, visit of object with "blocking visit" property
LOCPLINT - > localState - > erasePath ( hero ) ;
}
else
{
//movement along desired path - remove one node and keep rest of path
LOCPLINT - > localState - > removeLastNode ( hero ) ;
}
if ( directlyAttackingCreature )
LOCPLINT - > localState - > erasePath ( hero ) ;
}
}
2023-09-18 17:17:26 +02:00
void HeroMovementController : : onTryMoveHero ( const CGHeroInstance * hero , const TryMoveHero & details )
2023-09-07 21:37:42 +02:00
{
2023-09-16 19:07:02 +02:00
// 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 )
2023-09-07 21:37:42 +02:00
{
2024-06-01 17:28:17 +02:00
if ( hero - > tempOwner = = LOCPLINT - > playerID )
{
auto removalSound = hero - > getRemovalSound ( CRandomGenerator : : getDefault ( ) ) ;
if ( removalSound )
CCS - > soundh - > playSound ( removalSound . value ( ) ) ;
}
2023-09-07 21:37:42 +02:00
}
2023-09-16 19:07:02 +02:00
bool directlyAttackingCreature =
details . attackedFrom . has_value ( ) & &
LOCPLINT - > localState - > hasPath ( hero ) & &
LOCPLINT - > localState - > getPath ( hero ) . lastNode ( ) . coord = = details . attackedFrom ;
2023-09-07 21:37:42 +02:00
std : : unordered_set < int3 > changedTiles {
hero - > convertToVisitablePos ( details . start ) ,
hero - > convertToVisitablePos ( details . end )
} ;
adventureInt - > onMapTilesChanged ( changedTiles ) ;
adventureInt - > onHeroMovementStarted ( hero ) ;
2023-09-11 11:54:25 +02:00
updatePath ( hero , details ) ;
2023-09-07 21:37:42 +02:00
2023-09-16 19:07:02 +02:00
if ( details . stopMovement ( ) )
2023-09-07 21:37:42 +02:00
{
2023-09-16 19:07:02 +02:00
if ( duringMovement )
2023-09-18 17:17:26 +02:00
endMove ( hero ) ;
2023-09-07 21:37:42 +02:00
return ;
}
2023-09-16 19:07:02 +02:00
// We are in network thread
// Block netpack processing until movement animation is over
2023-09-07 21:37:42 +02:00
CGI - > mh - > waitForOngoingAnimations ( ) ;
//move finished
adventureInt - > onHeroChanged ( hero ) ;
2023-09-16 19:07:02 +02:00
// 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 ( )
{
2024-05-02 17:05:57 +02:00
if ( ! waitingForQueryApplyReply )
return ;
2023-09-27 16:17:06 +02:00
waitingForQueryApplyReply = false ;
2023-09-16 19:07:02 +02:00
2023-09-27 16:17:06 +02:00
// Server accepted our TeleportDialog query reply and moved hero
// Continue moving alongside our path, if any
if ( duringMovement )
2023-09-16 19:07:02 +02:00
onMoveHeroApplied ( ) ;
2023-09-07 21:37:42 +02:00
}
2023-09-15 21:18:36 +02:00
void HeroMovementController : : onMoveHeroApplied ( )
2023-09-07 21:37:42 +02:00
{
2023-09-16 19:07:02 +02:00
// 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 ( ) )
2023-09-15 21:18:36 +02:00
stoppingMovement = true ;
2023-09-16 19:07:02 +02:00
assert ( currentlyMovingHero ) ;
const auto * hero = currentlyMovingHero ;
2023-09-16 12:40:49 +02:00
2024-05-18 13:04:10 +02:00
bool canMove = LOCPLINT - > localState - > hasPath ( hero ) & & LOCPLINT - > localState - > getPath ( hero ) . nextNode ( ) . turns = = 0 & & ! LOCPLINT - > showingDialog - > isBusy ( ) ;
2023-09-16 19:07:02 +02:00
bool wantStop = stoppingMovement ;
bool canStop = ! canMove | | canHeroStopAtNode ( LOCPLINT - > localState - > getPath ( hero ) . currNode ( ) ) ;
2023-09-15 21:18:36 +02:00
2023-09-18 17:17:26 +02:00
if ( ! canMove | | ( wantStop & & canStop ) )
2023-09-16 19:07:02 +02:00
{
2023-09-18 17:17:26 +02:00
endMove ( hero ) ;
2023-09-16 19:07:02 +02:00
}
else
{
2024-04-20 13:20:54 +02:00
sendMovementRequest ( hero , LOCPLINT - > localState - > getPath ( hero ) ) ;
2023-09-15 21:18:36 +02:00
}
}
2023-09-18 17:17:26 +02:00
void HeroMovementController : : requestMovementAbort ( )
2023-09-15 21:18:36 +02:00
{
if ( duringMovement )
2023-09-18 17:17:26 +02:00
endMove ( currentlyMovingHero ) ;
2023-09-15 21:18:36 +02:00
}
2023-09-18 17:17:26 +02:00
void HeroMovementController : : endMove ( const CGHeroInstance * hero )
2023-09-15 21:18:36 +02:00
{
assert ( duringMovement = = true ) ;
2023-09-16 19:07:02 +02:00
assert ( currentlyMovingHero ! = nullptr ) ;
2023-09-15 21:18:36 +02:00
duringMovement = false ;
stoppingMovement = false ;
2023-09-16 12:40:49 +02:00
currentlyMovingHero = nullptr ;
2023-09-15 21:18:36 +02:00
stopMovementSound ( ) ;
adventureInt - > onHeroChanged ( hero ) ;
CCS - > curh - > show ( ) ;
2023-09-07 21:37:42 +02:00
}
2023-09-11 11:54:25 +02:00
AudioPath HeroMovementController : : getMovementSoundFor ( const CGHeroInstance * hero , int3 posPrev , int3 posNext , EPathNodeAction moveType )
{
2023-09-16 19:07:02 +02:00
if ( moveType = = EPathNodeAction : : TELEPORT_BATTLE | | moveType = = EPathNodeAction : : TELEPORT_BLOCKING_VISIT | | moveType = = EPathNodeAction : : TELEPORT_NORMAL )
2023-09-11 11:54:25 +02:00
return { } ;
2023-09-16 19:07:02 +02:00
if ( moveType = = EPathNodeAction : : EMBARK | | moveType = = EPathNodeAction : : DISEMBARK )
2023-09-11 11:54:25 +02:00
return { } ;
2023-09-16 19:07:02 +02:00
if ( moveType = = EPathNodeAction : : BLOCKING_VISIT )
2023-09-11 11:54:25 +02:00
return { } ;
// flying movement sound
2023-09-16 19:07:02 +02:00
if ( hero - > hasBonusOfType ( BonusType : : FLYING_MOVEMENT ) )
2023-09-11 11:54:25 +02:00
return AudioPath : : builtin ( " HORSE10.wav " ) ;
2023-09-15 21:18:36 +02:00
auto prevTile = LOCPLINT - > cb - > getTile ( posPrev ) ;
auto nextTile = LOCPLINT - > cb - > getTile ( posNext ) ;
2023-09-11 11:54:25 +02:00
auto prevRoad = prevTile - > roadType ;
auto nextRoad = nextTile - > roadType ;
bool movingOnRoad = prevRoad - > getId ( ) ! = Road : : NO_ROAD & & nextRoad - > getId ( ) ! = Road : : NO_ROAD ;
2023-09-16 19:07:02 +02:00
if ( movingOnRoad )
2023-09-11 11:54:25 +02:00
return nextTile - > terType - > horseSound ;
else
return nextTile - > terType - > horseSoundPenalty ;
} ;
2023-09-15 21:18:36 +02:00
void HeroMovementController : : updateMovementSound ( const CGHeroInstance * h , int3 posPrev , int3 nextCoord , EPathNodeAction moveType )
2023-09-11 11:54:25 +02:00
{
// Start a new sound for the hero movement or let the existing one carry on.
2023-09-15 21:18:36 +02:00
AudioPath newSoundName = getMovementSoundFor ( h , posPrev , nextCoord , moveType ) ;
2023-09-11 11:54:25 +02:00
2023-09-15 21:18:36 +02:00
if ( newSoundName ! = currentMovementSoundName )
2023-09-11 11:54:25 +02:00
{
2023-09-15 21:18:36 +02:00
currentMovementSoundName = newSoundName ;
2023-09-11 11:54:25 +02:00
2023-09-16 19:07:02 +02:00
if ( currentMovementSoundChannel ! = - 1 )
2023-09-15 21:18:36 +02:00
CCS - > soundh - > stopSound ( currentMovementSoundChannel ) ;
2023-09-16 19:07:02 +02:00
if ( ! currentMovementSoundName . empty ( ) )
2023-09-15 21:18:36 +02:00
currentMovementSoundChannel = CCS - > soundh - > playSound ( currentMovementSoundName , - 1 , true ) ;
2023-09-11 11:54:25 +02:00
else
2023-09-15 21:18:36 +02:00
currentMovementSoundChannel = - 1 ;
2023-09-11 11:54:25 +02:00
}
}
void HeroMovementController : : stopMovementSound ( )
{
2023-09-16 19:07:02 +02:00
if ( currentMovementSoundChannel ! = - 1 )
CCS - > soundh - > stopSound ( currentMovementSoundChannel ) ;
2023-09-15 21:18:36 +02:00
currentMovementSoundChannel = - 1 ;
currentMovementSoundName = AudioPath ( ) ;
2023-09-11 11:54:25 +02:00
}
2023-09-15 21:18:36 +02:00
bool HeroMovementController : : canHeroStopAtNode ( const CGPathNode & node ) const
2023-09-07 21:37:42 +02:00
{
2023-09-16 19:07:02 +02:00
if ( node . layer ! = EPathfindingLayer : : LAND & & node . layer ! = EPathfindingLayer : : SAIL )
2023-09-15 21:18:36 +02:00
return false ;
2023-09-07 21:37:42 +02:00
2023-09-16 19:07:02 +02:00
if ( node . accessible ! = EPathAccessibility : : ACCESSIBLE )
2023-09-15 21:18:36 +02:00
return false ;
2023-09-07 21:37:42 +02:00
2023-09-15 21:18:36 +02:00
return true ;
}
2023-09-07 21:37:42 +02:00
2023-09-18 17:17:26 +02:00
void HeroMovementController : : requestMovementStart ( const CGHeroInstance * h , const CGPath & path )
2023-09-15 21:18:36 +02:00
{
assert ( duringMovement = = false ) ;
2023-09-07 21:37:42 +02:00
2024-04-20 13:20:54 +02:00
duringMovement = true ;
currentlyMovingHero = h ;
CCS - > curh - > hide ( ) ;
sendMovementRequest ( h , path ) ;
}
void HeroMovementController : : sendMovementRequest ( const CGHeroInstance * h , const CGPath & path )
{
assert ( duringMovement = = true ) ;
2024-04-17 17:18:28 +02:00
int heroMovementSpeed = settings [ " adventure " ] [ " heroMoveTime " ] . Integer ( ) ;
2024-04-20 13:20:54 +02:00
bool useMovementBatching = heroMovementSpeed = = 0 ;
2024-04-17 17:18:28 +02:00
2024-04-20 13:20:54 +02:00
const auto & currNode = path . currNode ( ) ;
const auto & nextNode = path . nextNode ( ) ;
assert ( nextNode . turns = = 0 ) ;
assert ( currNode . coord = = h - > visitablePos ( ) ) ;
2024-04-17 17:18:28 +02:00
2024-04-20 13:20:54 +02:00
if ( nextNode . isTeleportAction ( ) )
{
stopMovementSound ( ) ;
logGlobal - > trace ( " Requesting hero teleportation to %s " , nextNode . coord . toString ( ) ) ;
LOCPLINT - > cb - > moveHero ( h , h - > pos , false ) ;
return ;
2024-04-17 17:18:28 +02:00
}
2024-04-20 13:20:54 +02:00
if ( ! useMovementBatching )
2024-04-17 17:18:28 +02:00
{
2024-04-20 13:20:54 +02:00
updateMovementSound ( h , currNode . coord , nextNode . coord , nextNode . action ) ;
2024-10-02 18:40:06 +02:00
assert ( h - > anchorPos ( ) . z = = nextNode . coord . z ) ; // Z should change only if it's movement via teleporter and in this case this code shouldn't be executed at all
2024-04-20 13:20:54 +02:00
logGlobal - > trace ( " Requesting hero movement to %s " , nextNode . coord . toString ( ) ) ;
bool useTransit = nextNode . layer = = EPathfindingLayer : : AIR | | nextNode . layer = = EPathfindingLayer : : WATER ;
int3 nextCoord = h - > convertFromVisitablePos ( nextNode . coord ) ;
LOCPLINT - > cb - > moveHero ( h , nextCoord , useTransit ) ;
return ;
2024-04-17 17:18:28 +02:00
}
2024-04-20 13:20:54 +02:00
bool useTransitAtStart = path . nextNode ( ) . layer = = EPathfindingLayer : : AIR | | path . nextNode ( ) . layer = = EPathfindingLayer : : WATER ;
2024-04-18 14:13:16 +02:00
std : : vector < int3 > pathToMove ;
2024-04-17 17:18:28 +02:00
for ( auto const & node : boost : : adaptors : : reverse ( path . nodes ) )
{
if ( node . coord = = h - > visitablePos ( ) )
continue ; // first node, ignore - this is hero current position
if ( node . isTeleportAction ( ) )
2024-04-18 14:13:16 +02:00
break ; // pause after monolith / subterra gates
2024-04-17 17:18:28 +02:00
if ( node . turns ! = 0 )
2024-04-18 14:13:16 +02:00
break ; // ran out of move points
bool useTransitHere = node . layer = = EPathfindingLayer : : AIR | | node . layer = = EPathfindingLayer : : WATER ;
2024-04-20 13:20:54 +02:00
if ( useTransitHere ! = useTransitAtStart )
2024-04-18 14:13:16 +02:00
break ;
2024-04-17 17:18:28 +02:00
int3 coord = h - > convertFromVisitablePos ( node . coord ) ;
2024-04-18 14:13:16 +02:00
pathToMove . push_back ( coord ) ;
if ( LOCPLINT - > cb - > guardingCreaturePosition ( node . coord ) ! = int3 ( - 1 , - 1 , - 1 ) )
break ; // we reached zone-of-control of wandering monster
2024-04-17 17:18:28 +02:00
2024-04-18 14:13:16 +02:00
if ( ! LOCPLINT - > cb - > getVisitableObjs ( node . coord ) . empty ( ) )
break ; // we reached event, garrison or some other visitable object - end this movement batch
}
2024-04-20 13:20:54 +02:00
assert ( ! pathToMove . empty ( ) ) ;
2024-04-18 14:13:16 +02:00
if ( ! pathToMove . empty ( ) )
2023-09-15 21:18:36 +02:00
{
updateMovementSound ( h , currNode . coord , nextNode . coord , nextNode . action ) ;
2024-04-20 13:20:54 +02:00
LOCPLINT - > cb - > moveHero ( h , pathToMove , useTransitAtStart ) ;
2023-09-07 21:37:42 +02:00
}
}