File: PathfindingRules.cpp

package info (click to toggle)
vcmi 1.6.5%2Bdfsg-2
  • links: PTS, VCS
  • area: contrib
  • in suites: forky, sid, trixie
  • size: 32,060 kB
  • sloc: cpp: 238,971; python: 265; sh: 224; xml: 157; ansic: 78; objc: 61; makefile: 49
file content (426 lines) | stat: -rw-r--r-- 13,828 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
/*
 * PathfindingRules.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 "PathfindingRules.h"

#include "CGPathNode.h"
#include "CPathfinder.h"
#include "INodeStorage.h"
#include "PathfinderOptions.h"

#include "../mapObjects/CGHeroInstance.h"
#include "../mapObjects/MiscObjects.h"
#include "../mapping/CMapDefines.h"

VCMI_LIB_NAMESPACE_BEGIN

void MovementCostRule::process(
	const PathNodeInfo & source,
	CDestinationNodeInfo & destination,
	const PathfinderConfig * pathfinderConfig,
	CPathfinderHelper * pathfinderHelper) const
{
	const float currentCost = destination.cost;
	const int currentTurnsUsed = destination.turn;
	const int currentMovePointsLeft = destination.movementLeft;
	const int sourceLayerMaxMovePoints = pathfinderHelper->getMaxMovePoints(source.node->layer);

	int moveCostPoints = pathfinderHelper->getMovementCost(source, destination, currentMovePointsLeft);
	float destinationCost = currentCost;
	int destTurnsUsed = currentTurnsUsed;
	int destMovePointsLeft = currentMovePointsLeft;

	if(currentMovePointsLeft < moveCostPoints)
	{
		// occurs rarely, when hero with low movepoints tries to leave the road
		// in this case, all remaining movement points from current turn are spent
		// and actual movement will happen on next turn, spending points from next turn pool

		destinationCost += static_cast<float>(currentMovePointsLeft) / sourceLayerMaxMovePoints;
		destTurnsUsed += 1;
		destMovePointsLeft = sourceLayerMaxMovePoints;

		// update move cost - it might have changed since hero now makes next turn and replenished his pool
		moveCostPoints = pathfinderHelper->getMovementCost(source, destination, destMovePointsLeft);

		pathfinderHelper->updateTurnInfo(destTurnsUsed);
	}

	if(destination.action == EPathNodeAction::EMBARK || destination.action == EPathNodeAction::DISEMBARK)
	{
		// FREE_SHIP_BOARDING bonus only remove additional penalty
		// land <-> sail transition still cost movement points as normal movement

		const int movementPointsAfterEmbark = pathfinderHelper->movementPointsAfterEmbark(destMovePointsLeft, moveCostPoints, (destination.action == EPathNodeAction::DISEMBARK));

		const int destinationLayerMaxMovePoints = pathfinderHelper->getMaxMovePoints(destination.node->layer);
		const float costBeforeConversion = static_cast<float>(destMovePointsLeft) / sourceLayerMaxMovePoints;
		const float costAfterConversion = static_cast<float>(movementPointsAfterEmbark) / destinationLayerMaxMovePoints;
		const float costDelta = costBeforeConversion - costAfterConversion;

		assert(costDelta >= 0);
		destMovePointsLeft = movementPointsAfterEmbark;
		destinationCost += costDelta;
	}
	else
	{
		// Standard movement
		assert(destMovePointsLeft >= moveCostPoints);
		destMovePointsLeft -= moveCostPoints;
		destinationCost += static_cast<float>(moveCostPoints) / sourceLayerMaxMovePoints;
	}

	// pathfinder / priority queue does not supports negative costs
	assert(destinationCost >= currentCost);

	destination.cost = destinationCost;
	destination.turn = destTurnsUsed;
	destination.movementLeft = destMovePointsLeft;

	if(destination.isBetterWay() &&
		((source.node->turns == destTurnsUsed && destMovePointsLeft) || pathfinderHelper->passOneTurnLimitCheck(source)))
	{
		pathfinderConfig->nodeStorage->commit(destination, source);

		return;
	}

	destination.blocked = true;
}

void PathfinderBlockingRule::process(
	const PathNodeInfo & source,
	CDestinationNodeInfo & destination,
	const PathfinderConfig * pathfinderConfig,
	CPathfinderHelper * pathfinderHelper) const
{
	auto blockingReason = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper);

	destination.blocked = blockingReason != BlockingReason::NONE;
}

void DestinationActionRule::process(
	const PathNodeInfo & source,
	CDestinationNodeInfo & destination,
	const PathfinderConfig * pathfinderConfig,
	CPathfinderHelper * pathfinderHelper) const
{
	if(destination.action != EPathNodeAction::UNKNOWN)
	{
#ifdef VCMI_TRACE_PATHFINDER
		logAi->trace("Accepted precalculated action at %s", destination.coord.toString());
#endif
		return;
	}

	EPathNodeAction action = EPathNodeAction::NORMAL;
	const auto * hero = pathfinderHelper->hero;

	switch(destination.node->layer.toEnum())
	{
	case EPathfindingLayer::LAND:
		if(source.node->layer == EPathfindingLayer::SAIL)
		{
			// TODO: Handle dismebark into guarded areaa
			action = EPathNodeAction::DISEMBARK;
			break;
		}

		/// don't break - next case shared for both land and sail layers
		[[fallthrough]];

	case EPathfindingLayer::SAIL:
		if(destination.isNodeObjectVisitable())
		{
			auto objRel = destination.objectRelations;

			if(destination.nodeObject->ID == Obj::BOAT)
				action = EPathNodeAction::EMBARK;
			else if(destination.nodeHero)
			{
				if(destination.heroRelations == PlayerRelations::ENEMIES)
					action = EPathNodeAction::BATTLE;
				else
					action = EPathNodeAction::BLOCKING_VISIT;
			}
			else if(destination.nodeObject->ID == Obj::TOWN)
			{
				if(destination.nodeObject->passableFor(hero->tempOwner))
					action = EPathNodeAction::VISIT;
				else if(objRel == PlayerRelations::ENEMIES)
					action = EPathNodeAction::BATTLE;
			}
			else if(destination.nodeObject->ID == Obj::GARRISON || destination.nodeObject->ID == Obj::GARRISON2)
			{
				if(destination.nodeObject->passableFor(hero->tempOwner))
				{
					if(destination.guarded)
						action = EPathNodeAction::BATTLE;
				}
				else if(objRel == PlayerRelations::ENEMIES)
					action = EPathNodeAction::BATTLE;
			}
			else if(destination.nodeObject->ID == Obj::BORDER_GATE)
			{
				if(destination.nodeObject->passableFor(hero->tempOwner))
				{
					if(destination.guarded)
						action = EPathNodeAction::BATTLE;
				}
				else
					action = EPathNodeAction::BLOCKING_VISIT;
			}
			else if(destination.isGuardianTile)
				action = EPathNodeAction::BATTLE;
			else if(destination.nodeObject->isBlockedVisitable() && !(pathfinderConfig->options.useCastleGate && destination.nodeObject->ID == Obj::TOWN))
				action = EPathNodeAction::BLOCKING_VISIT;

			if(action == EPathNodeAction::NORMAL)
			{
				if(destination.guarded)
					action = EPathNodeAction::BATTLE;
				else
					action = EPathNodeAction::VISIT;
			}
		}
		else if(destination.guarded)
			action = EPathNodeAction::BATTLE;

		break;
	}

	destination.action = action;
}

void MovementAfterDestinationRule::process(
	const PathNodeInfo & source,
	CDestinationNodeInfo & destination,
	const PathfinderConfig * config,
	CPathfinderHelper * pathfinderHelper) const
{
	auto blocker = getBlockingReason(source, destination, config, pathfinderHelper);

	if(blocker == BlockingReason::DESTINATION_GUARDED && destination.action == EPathNodeAction::BATTLE)
	{
		return; // allow bypass guarded tile but only in direction of guard, a bit UI related thing
	}

	destination.blocked = blocker != BlockingReason::NONE;
}


PathfinderBlockingRule::BlockingReason MovementAfterDestinationRule::getBlockingReason(
	const PathNodeInfo & source,
	const CDestinationNodeInfo & destination,
	const PathfinderConfig * config,
	const CPathfinderHelper * pathfinderHelper) const
{
	switch(destination.action)
	{
	/// TODO: Investigate what kind of limitation is possible to apply on movement from visitable tiles
	/// Likely in many cases we don't need to add visitable tile to queue when hero doesn't fly
	case EPathNodeAction::VISIT:
	{
		/// For now we only add visitable tile into queue when it's teleporter that allow transit
		/// Movement from visitable tile when hero is standing on it is possible into any layer
		const auto * objTeleport = dynamic_cast<const CGTeleport *>(destination.nodeObject);
		if(pathfinderHelper->isAllowedTeleportEntrance(objTeleport))
		{
			/// For now we'll always allow transit over teleporters
			/// Transit over whirlpools only allowed when hero is protected
			return BlockingReason::NONE;
		}
		else if(destination.nodeObject->ID == Obj::GARRISON
			|| destination.nodeObject->ID == Obj::GARRISON2
			|| destination.nodeObject->ID == Obj::BORDER_GATE)
		{
			/// Transit via unguarded garrisons is always possible
			return BlockingReason::NONE;
		}

		return BlockingReason::DESTINATION_VISIT;
	}

	case EPathNodeAction::BLOCKING_VISIT:
		return BlockingReason::DESTINATION_BLOCKVIS;

	case EPathNodeAction::NORMAL:
		return BlockingReason::NONE;

	case EPathNodeAction::EMBARK:
		if(pathfinderHelper->options.useEmbarkAndDisembark)
			return BlockingReason::NONE;

		return BlockingReason::DESTINATION_BLOCKED;

	case EPathNodeAction::DISEMBARK:
		if(pathfinderHelper->options.useEmbarkAndDisembark)
			return destination.guarded ? BlockingReason::DESTINATION_GUARDED : BlockingReason::NONE;

		return BlockingReason::DESTINATION_BLOCKED;

	case EPathNodeAction::BATTLE:
		// H3 rule: do not allow direct attack on wandering monsters if hero lands on visitable object
		if (config->options.originalFlyRules && destination.nodeObject && source.node->layer == EPathfindingLayer::AIR)
			return BlockingReason::DESTINATION_BLOCKED;

		// Movement after BATTLE action only possible from guarded tile to guardian tile
		if(destination.guarded)
		{
			if (pathfinderHelper->options.ignoreGuards)
				return BlockingReason::NONE;
			else
				return BlockingReason::DESTINATION_GUARDED;
		}
		break;
	}

	return BlockingReason::DESTINATION_BLOCKED;
}


PathfinderBlockingRule::BlockingReason MovementToDestinationRule::getBlockingReason(
	const PathNodeInfo & source,
	const CDestinationNodeInfo & destination,
	const PathfinderConfig * pathfinderConfig,
	const CPathfinderHelper * pathfinderHelper) const
{

	if(destination.node->accessible == EPathAccessibility::BLOCKED)
		return BlockingReason::DESTINATION_BLOCKED;

	switch(destination.node->layer.toEnum())
	{
	case EPathfindingLayer::LAND:
		if(!pathfinderHelper->canMoveBetween(source.coord, destination.coord))
			return BlockingReason::DESTINATION_BLOCKED;

		if(source.guarded)
		{
			if(source.node->layer != EPathfindingLayer::AIR // zone of control is ignored when flying
				&& !pathfinderConfig->options.ignoreGuards
				&&	(!destination.isGuardianTile || pathfinderHelper->getGuardiansCount(source.coord) > 1)) // Can step into tile of guard
			{
				return BlockingReason::SOURCE_GUARDED;
			}
		}

		break;

	case EPathfindingLayer::SAIL:
		if(!pathfinderHelper->canMoveBetween(source.coord, destination.coord))
			return BlockingReason::DESTINATION_BLOCKED;

		if(source.guarded)
		{
			// Hero embarked a boat standing on a guarded tile -> we must allow to move away from that tile
			if(source.node->action != EPathNodeAction::EMBARK && !destination.isGuardianTile)
				return BlockingReason::SOURCE_GUARDED;
		}

		if(source.node->layer == EPathfindingLayer::LAND)
		{
			if(!destination.isNodeObjectVisitable())
				return BlockingReason::DESTINATION_BLOCKED;

			if(!destination.nodeHero && !destination.nodeObject->isCoastVisitable())
				return BlockingReason::DESTINATION_BLOCKED;
		}
		else if(destination.isNodeObjectVisitable() && destination.nodeObject->ID == Obj::BOAT)
		{
			/// Hero in boat can't visit empty boats
			return BlockingReason::DESTINATION_BLOCKED;
		}

		break;

	case EPathfindingLayer::WATER:
		if(!pathfinderHelper->canMoveBetween(source.coord, destination.coord))
			return BlockingReason::DESTINATION_BLOCKED;

		if (destination.node->accessible != EPathAccessibility::ACCESSIBLE)
			return BlockingReason::DESTINATION_BLOCKED;

		if(destination.guarded)
			return BlockingReason::DESTINATION_BLOCKED;

		break;
	}

	return BlockingReason::NONE;
}

void LayerTransitionRule::process(
	const PathNodeInfo & source,
	CDestinationNodeInfo & destination,
	const PathfinderConfig * pathfinderConfig,
	CPathfinderHelper * pathfinderHelper) const
{
	if(source.node->layer == destination.node->layer)
		return;

	switch(source.node->layer.toEnum())
	{
	case EPathfindingLayer::LAND:
		if(destination.node->layer == EPathfindingLayer::SAIL)
		{
			/// Cannot enter empty water tile from land -> it has to be visitable
			if(destination.node->accessible == EPathAccessibility::ACCESSIBLE)
				destination.blocked = true;
		}

		break;

	case EPathfindingLayer::SAIL:
		// have to disembark first before visiting objects on land
		if (destination.tile->visitable())
			destination.blocked = true;

		//can disembark only on accessible tiles or tiles guarded by nearby monster
		if((destination.node->accessible != EPathAccessibility::ACCESSIBLE && destination.node->accessible != EPathAccessibility::GUARDED))
			destination.blocked = true;

		break;

	case EPathfindingLayer::AIR:
		if(pathfinderConfig->options.originalFlyRules)
		{
			if(source.node->accessible != EPathAccessibility::ACCESSIBLE && source.node->accessible != EPathAccessibility::VISITABLE)
			{
				if (destination.node->accessible == EPathAccessibility::BLOCKVIS)
				{
					// Can't visit 'blockvisit' objects on coast if hero will end up on water terrain
					if (source.tile->blocked() || !destination.tile->entrableTerrain(source.tile))
						destination.blocked = true;
				}
			}
		}
		else
		{
			// Hero that fly can only land on accessible tiles
			if(destination.node->accessible != EPathAccessibility::ACCESSIBLE && destination.nodeObject)
				destination.blocked = true;
		}

		break;

	case EPathfindingLayer::WATER:
		if(destination.node->accessible != EPathAccessibility::ACCESSIBLE && destination.node->accessible != EPathAccessibility::VISITABLE)
		{
			/// Hero that walking on water can transit to accessible and visitable tiles
			/// Though hero can't interact with blocking visit objects while standing on water
			destination.blocked = true;
		}

		break;
	}
}

VCMI_LIB_NAMESPACE_END