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