1 /*
2  * @(#) src/games/stendhal/server/entity/GuidedEntity.java
3  *
4  * $Id$
5  */
6 
7 package games.stendhal.server.entity;
8 
9 //
10 //
11 
12 import static games.stendhal.common.constants.General.PATHSET;
13 
14 import java.awt.Point;
15 import java.awt.geom.Rectangle2D;
16 import java.util.LinkedList;
17 import java.util.List;
18 
19 import org.apache.log4j.Logger;
20 
21 import games.stendhal.common.Direction;
22 import games.stendhal.common.Rand;
23 import games.stendhal.server.core.events.TurnListener;
24 import games.stendhal.server.core.events.TurnNotifier;
25 import games.stendhal.server.core.pathfinder.EntityGuide;
26 import games.stendhal.server.core.pathfinder.FixedPath;
27 import games.stendhal.server.core.pathfinder.Node;
28 import games.stendhal.server.core.pathfinder.Path;
29 import games.stendhal.server.entity.npc.ConversationStates;
30 import games.stendhal.server.entity.npc.SpeakerNPC;
31 import marauroa.common.game.RPObject;
32 
33 /**
34  * An entity that has speed/direction and is guided via a Path.
35  */
36 public abstract class GuidedEntity extends ActiveEntity {
37 
38 	// logger instance
39 	private static final Logger logger = Logger.getLogger(GuidedEntity.class);
40 
41 	/** The entity's default speed value */
42 	protected double baseSpeed;
43 
44 	private final EntityGuide guide = new EntityGuide();
45 
46 	public Registrator pathnotifier = new Registrator();
47 
48 	/** Action entity will take after collision */
49     private CollisionAction collisionAction;
50 
51     /**
52      * The entity is using a random path
53      */
54     private boolean randomPath = false;
55     private boolean returnToOrigin = false;
56 
57     /**
58      * The radius at which the entity will walk
59      */
60     private int movementRadius = 0;
61 
62 
63     // used to store & restore the entity's base speed for suspension
64     private Double storedSpeed = null;
65 
66 	/**
67 	 * Create a guided entity.
68 	 */
GuidedEntity()69 	public GuidedEntity() {
70 		baseSpeed = 0;
71 		guide.guideMe(this);
72 	}
73 
74 	/**
75 	 * Create a guided entity.
76 	 *
77 	 * @param object
78 	 *            The source object.
79 	 */
GuidedEntity(final RPObject object)80 	public GuidedEntity(final RPObject object) {
81 		super(object);
82 		baseSpeed = 0;
83 		guide.guideMe(this);
84 		update();
85 	}
86 
87 	//
88 	// TEMP for Transition
89 	//
90 
91 	/**
92 	 * Get the normal movement speed.
93 	 *
94 	 * @return The normal speed when moving.
95 	 */
getBaseSpeed()96 	public final double getBaseSpeed() {
97 		return this.baseSpeed;
98 	}
99 
100 	/**
101 	 * Set the normal movement speed.
102 	 *
103 	 * @param bs - New normal speed for moving.
104 	 */
setBaseSpeed(final double bs)105 	public final void setBaseSpeed(final double bs) {
106 		this.baseSpeed = bs;
107 	}
108 
109 	//
110 	// GuidedEntity
111 	//
112 
113 	/**
114 	 * Set a path for this entity to follow. Any previous path is cleared and
115 	 * the entity starts at the first node (so the first node should be its
116 	 * position, of course). The speed will be set to the default for the
117 	 * entity.
118 	 *
119 	 * @param path
120 	 *            The path.
121 	 */
setPath(final FixedPath path)122 	public final void setPath(final FixedPath path) {
123 		if ((path != null) && !path.isFinished()) {
124 			setSpeed(getBaseSpeed());
125 
126 			if (!this.has(PATHSET)) {
127 				this.put(PATHSET, "");
128 			}
129 
130 			guide.path = path;
131 			guide.pathPosition = 0;
132 			guide.followPath(this);
133 
134 			return;
135 		}
136 
137 		if (this.has(PATHSET)) {
138 			this.remove(PATHSET);
139 		}
140 		guide.clearPath();
141 	}
142 
143 	/**
144 	 * Set path & starting position for entity. The starting position is
145 	 * the first node in the path.
146 	 *
147 	 * @param path
148 	 * 		Path to set.
149 	 */
setPathAndPosition(final FixedPath path)150 	public void setPathAndPosition(final FixedPath path) {
151 		setPath(path);
152 
153 		final Node[] nodes = path.getNodes();
154 		if (nodes.length < 1) {
155 			logger.error("Path is empty, cannot set entity position");
156 			return;
157 		}
158 
159 		// set initial position to first node
160 		final Node start = path.getNodes()[0];
161 		setPosition(start.getX(), start.getY());
162 	}
163 
164 	/**
165 	 * Causes entity to retrace its path backwards when it reaches the end.
166 	 */
retracePath()167 	public void retracePath() {
168 		if (!hasPath()) {
169 			logger.warn("Cannot set path to be retraced when entity does not have path set");
170 			return;
171 		}
172 
173 		final List<Node> nodes = guide.path.getNodeList();
174 		for (int idx = nodes.size() - 2; idx > 1 - 1; idx--) {
175 			nodes.add(nodes.get(idx));
176 		}
177 
178 		setPath(new FixedPath(nodes, guide.path.isLoop()), guide.pathPosition);
179 	}
180 
181 	/**
182      * Set a path for this entity to follow. Any previous path is cleared and
183      * the entity starts at the first node (so the first node should be its
184      * position, of course). The speed will be set to the default for the
185      * entity.
186 	 *
187 	 * @param path
188 	 *         The path.
189 	 * @param position
190 	 *         The position of the path where the entity should start
191 	 */
setPath(final FixedPath path, final int position)192     public final void setPath(final FixedPath path, final int position) {
193         if ((path != null) && !path.isFinished()) {
194             setSpeed(getBaseSpeed());
195 
196 			if (!this.has(PATHSET)) {
197 				this.put(PATHSET, "");
198 			}
199 
200             guide.path = path;
201             guide.pathPosition = position;
202             guide.followPath(this);
203 
204             return;
205         }
206 
207 		if (this.has(PATHSET)) {
208 			this.remove(PATHSET);
209 		}
210         guide.clearPath();
211     }
212 
213 	/**
214 	 * Remove PATHSET attribute if available and stop entity movement.
215 	 */
216 	@Override
stop()217 	public void stop() {
218 		/* Clear entity's path if set. */
219 		if (this.has(PATHSET)) {
220 			/* Remove PATHSET attribute here instead of in clearPath(). */
221 			this.remove(PATHSET);
222 		}
223 		super.stop();
224 	}
225 
226 	/**
227 	 * Set the action type to take when entity collides.
228 	 *
229 	 * @param action
230 	 *        Type of action to execute
231 	 */
setCollisionAction(final CollisionAction action)232 	public void setCollisionAction(final CollisionAction action) {
233 		collisionAction = action;
234 	}
235 
236 	/**
237 	 * function return current entity's path.
238 	 * @return path
239 	 */
getPath()240 	public FixedPath getPath() {
241 		return guide.path;
242 	}
243 
244 	/**
245 	 * Clear the entity's path.
246 	 */
clearPath()247 	public void clearPath() {
248 		guide.clearPath();
249 	}
250 
251 	/**
252 	 * Determine if the entity has a path.
253 	 *
254 	 * @return <code>true</code> if there is a path.
255 	 */
hasPath()256 	public boolean hasPath() {
257 		return (guide.path != null);
258 	}
259 
260 	/**
261 	 * Is the path a loop.
262 	 * @return true if running in circles
263 	 */
isPathLoop()264 	public boolean isPathLoop() {
265 		if (guide.path == null) {
266 			return false;
267 		} else {
268 			return guide.path.isLoop();
269 		}
270 	}
271 
272 	/**
273 	 * Get the path nodes position.
274 	 * @return position in path
275 	 */
getPathPosition()276 	public int getPathPosition() {
277 		return guide.pathPosition;
278 	}
279 
280 	/**
281 	 * Set the path nodes position.
282 	 * @param pathPos
283 	 */
setPathPosition(final int pathPos)284 	public void setPathPosition(final int pathPos) {
285 		onNodeReached();
286 
287 		guide.pathPosition = pathPos;
288 	}
289 
290 	/**
291 	 * Plan a new path to the old destination.
292 	 */
reroute()293 	public void reroute() {
294 		if (hasPath()) {
295 			Node node = guide.path.getDestination();
296 			final List<Node> path = Path.searchPath(this, node.getX(), node.getY());
297 
298 			if (path.size() >= 1) {
299 				setPath(new FixedPath(path, false));
300 			} else {
301 				/*
302 				 * It can happen that some other entity goes to occupy the
303 				 * target position after the path has been planned. Just
304 				 * stop if that happens and we are next to the goal.
305 				 */
306 				clearPath();
307 				stop();
308 			}
309 		}
310 	}
311 
312 	//
313 	// ActiveEntity
314 	//
315 
316 	/**
317 	 * Apply movement and process it's reactions.
318 	 */
319 	@Override
applyMovement()320 	public void applyMovement() {
321 		if (hasPath()) {
322 			followPath();
323 			super.applyMovement();
324 			faceNext();
325 		} else {
326 			super.applyMovement();
327 		}
328 	}
329 
330 	/**
331 	 * Set facing to next <code>Node</code>, if any
332 	 */
faceNext()333 	private void faceNext() {
334 		guide.faceNext(this);
335 	}
336 
followPath()337 	public boolean followPath() {
338 		return guide.followPath(this);
339 	}
340 
getGuide()341 	public EntityGuide getGuide() {
342 		return guide;
343 	}
344 
345 	@Override
onMoved(final int oldX, final int oldY, final int newX, final int newY)346 	protected void onMoved(final int oldX, final int oldY, final int newX, final int newY) {
347 		super.onMoved(oldX, oldY, newX, newY);
348 
349 		/*
350 		 * Adjust speed based on the resisting entities at the same coordinate.
351 		 */
352 		if (getSpeed() > 0) {
353 			int resistance = getLocalResistance();
354 
355 			if ((getSpeed() < getBaseSpeed()) || (resistance != 0)) {
356 				setSpeed(getBaseSpeed() * (100 - resistance) / 100.0);
357 			}
358 		}
359 	}
360 
361 	/**
362 	 * Suspends the entity's movement if the path position is marked for suspension.
363 	 */
onNodeReached()364 	protected void onNodeReached() {
365 		if (!isSuspended()) {
366 			if (guide.path.suspendAt(guide.pathPosition)) {
367 				stop();
368 				storedSpeed = getBaseSpeed();
369 				setBaseSpeed(0.0);
370 
371 				final Direction suspendDir = guide.path.getSuspendDirection(guide.pathPosition);
372 				if (suspendDir != null) {
373 					// FIXME: direction appears to be set, but client does not reflect it
374 					setDirection(suspendDir);
375 				}
376 
377 				final GuidedEntity tmp = this;
378 
379 				TurnNotifier.get().notifyInTurns(guide.path.getSuspendValue(guide.pathPosition), new TurnListener() {
380 					@Override
381 					public void onTurnReached(final int currentTurn) {
382 						setBaseSpeed(storedSpeed);
383 						storedSpeed = null;
384 
385 						// make sure the entity is not in conversation
386 						if (tmp instanceof SpeakerNPC) {
387 							if (((SpeakerNPC) tmp).getEngine().getCurrentState() != ConversationStates.IDLE) {
388 								return;
389 							}
390 						}
391 
392 						setSpeed(getBaseSpeed());
393 					}
394 				});
395 			}
396 		}
397 	}
398 
399 	/**
400 	 * Checks if the entity is in suspended state.
401 	 *
402 	 * @return
403 	 * 		<code>true</code> if the entity is stopped & its base speed has been stored.
404 	 */
isSuspended()405 	private boolean isSuspended() {
406 		return storedSpeed != null && stopped();
407 	}
408 
409 	/**
410 	 * Add a suspension to the entity's path.
411 	 *
412 	 * @param duration
413 	 * 		Amount of time (in turns) the entity will be suspended.
414 	 * @param dir
415 	 * 		Direction to face while suspended, or <code>null</code>
416 	 * 		if direction should not be changed.
417 	 * @param pos
418 	 * 		The position(s) in the path where to add the suspension.
419 	 */
addSuspend(final int duration, final Direction dir, final int... pos)420 	public void addSuspend(final int duration, final Direction dir, final int... pos) {
421 		guide.path.addSuspend(duration, dir, pos);
422 	}
423 
424 	/**
425 	 * Add a suspension to the entity's path.
426 	 *
427 	 * @param duration
428 	 * 		Amount of time (in turns) the entity will be suspended.
429 	 * 		if direction should not be changed.
430 	 * @param pos
431 	 * 		The position(s) in the path where to add the suspension.
432 	 */
addSuspend(final int duration, final int... pos)433 	public void addSuspend(final int duration, final int... pos) {
434 		guide.path.addSuspend(duration, pos);
435 	}
436 
437 	/**
438 	 * Removes suspension value from path position.
439 	 *
440 	 * @param pos
441 	 * 		The position(s) in the path from where to remove the suspension.
442 	 */
removeSuspend(final int... pos)443 	public void removeSuspend(final int... pos) {
444 		guide.path.removeSuspend(pos);
445 	}
446 
447 	/**
448 	 *
449 	 */
onFinishedPath()450 	public void onFinishedPath() {
451 		pathnotifier.setChanges();
452 		pathnotifier.notifyObservers();
453 	}
454 
455 	/**
456 	 * Get resistance caused by other entities occupying the same, or part
457 	 * of the same space.
458 	 *
459 	 * @return resistance
460 	 */
getLocalResistance()461 	private int getLocalResistance() {
462 		int resistance = 0;
463 		double size = getWidth() * getHeight();
464 
465 		Rectangle2D thisArea = getArea();
466 		Rectangle2D otherArea;
467 		Rectangle2D intersect = new Rectangle2D.Double();
468 		for (final RPObject obj : getZone()) {
469 			final Entity entity = (Entity) obj;
470 			if (this != entity) {
471 				otherArea = entity.getArea();
472 				Rectangle2D.intersect(thisArea, otherArea, intersect);
473 				// skip entities far away
474 				if (!intersect.isEmpty()) {
475 					int r = getResistance(entity);
476 					if (r != 0) {
477 						/*
478 						 * Only count resistance by the proportion the resisting
479 						 * entity covers the area of this entity. Allows large
480 						 * monsters trample over small obstacles faster than a
481 						 * small one trying to run right through it.
482 						 */
483 						double part = intersect.getWidth() * intersect.getHeight() / size;
484 						r *= part;
485 
486 						/*
487 						 * Add up like probabilities to avoid small resistance
488 						 * quickly resulting in a massive slow down.
489 						 */
490 						resistance = 100 - ((100 - resistance)) * (100 - r) / 100;
491 					}
492 				}
493 			}
494 		}
495 
496 		return resistance;
497 	}
498 
499 	@Override
handleObjectCollision()500 	protected void handleObjectCollision() {
501 		stop();
502 		clearPath();
503 	}
504 
updateModifiedAttributes()505 	public void updateModifiedAttributes() {
506 		//TODO base speed does not get transfered to the client? testing showed, that speed is used at client side
507 	}
508 
509 	//
510 	// START - Methods controlling random movement (alphabetical)
511 	// Currently may only work for SilentNPC.
512 	//
513 
514     /**
515      * Checks if the entity has reached a set radius
516      *
517      * @return <code>true</code> if the entity has moved outside its movement
518      * 	area, othwewise <code>false</code>
519      */
atMovementRadius()520     public final boolean atMovementRadius() {
521         Point difference = getDistanceFromOrigin();
522 
523         // Set the maximum movement distance at exact radius
524         int max = movementRadius - 1;
525         return (movementRadius > 0 && (difference.getX() > max || difference.getY() > max));
526     }
527 
getDirectionFromOrigin()528     protected final Direction getDirectionFromOrigin() {
529         Direction dir;
530         dir = Direction.LEFT;
531 
532         return dir;
533     }
534     /**
535      * Get the distance that the entity has moved away from its starting point
536      *
537      * @return The distance from entity's starting point
538      */
getDistanceFromOrigin()539     protected final Point getDistanceFromOrigin() {
540         int originX = getOrigin().x;
541         int originY = getOrigin().y;
542         int currentX = getX();
543         int currentY = getY();
544 
545         int Xdiff = Math.abs(currentX - originX);
546         int Ydiff = Math.abs(currentY - originY);
547 
548         return new Point(Xdiff, Ydiff);
549     }
550 
551     /**
552      * @return Action to take on collision
553      */
getCollisionAction()554     public CollisionAction getCollisionAction() {
555     	return collisionAction;
556     }
557 
558 	/**
559 	 * Changed path of entity when radius is reached
560 	 */
onOutsideMovementRadius()561     public void onOutsideMovementRadius() {
562         List<Node> nodes = new LinkedList<Node>();
563 
564         if (returnToOrigin) {
565             nodes.add(new Node(getOrigin().x, getOrigin().y));
566         } else {
567             // FIXME: Does not change direction smoothly
568             // Generate a random distanct to walk somwhere within the radius
569             // We should already know from atMovementRadius() that movementRadius is greater than 0
570             int walkBack = Rand.randUniform(1, movementRadius);
571             int newX = getX();
572             int newY = getY();
573 
574             Direction dir = getDirection();
575             if (dir == Direction.RIGHT) {
576                 newX -= walkBack;
577             } else if (dir == Direction.LEFT) {
578                 newX += walkBack;
579             } else if (dir == Direction.DOWN) {
580                 newY -= walkBack;
581             } else if (dir == Direction.UP) {
582                 newY += walkBack;
583             }
584 
585             nodes.add(new Node(newX, newY));
586         }
587 
588         this.setPath(new FixedPath(nodes, false));
589     }
590 
591     /**
592      * Sets the maximum distance the entity will move from its origin
593      *
594      * @param radius max movable distance
595      */
setRandomMovementRadius(final int radius)596     public void setRandomMovementRadius(final int radius) {
597         movementRadius = radius;
598     }
599 
600     /**
601      * Sets the maximum distance an entity will move away from its original
602      * position
603      *
604      * @param radius
605      *     distance entity will move away from its origin
606      * @param ret
607      *     if "true" entity will return to origin when radius border reached
608      */
setRandomMovementRadius(final int radius, final boolean ret)609     public void setRandomMovementRadius(final int radius, final boolean ret) {
610         movementRadius = radius;
611         returnToOrigin = ret;
612     }
613 
614     /**
615      * Sets or unsets entity's path as random.
616      *
617      * @param random
618      *      <code>true</code> if entity's path is random
619      */
setUsesRandomPath(boolean random)620     protected void setUsesRandomPath(boolean random) {
621         randomPath = random;
622     }
623 
624 	/**
625 	 * Determines whether the entity is using a random path.
626 	 *
627 	 * @return <code>true</code> if the entity uses random paths, otherwise
628 	 *  <code>false</code>
629 	 */
usesRandomPath()630 	protected boolean usesRandomPath() {
631 	    return randomPath;
632 	}
633 
634 	//
635 	// END - Methods controlling random movement
636 	//
637 }
638