1 /* $Id$ */ 2 /*************************************************************************** 3 * (C) Copyright 2003 - Marauroa * 4 *************************************************************************** 5 *************************************************************************** 6 * * 7 * This program is free software; you can redistribute it and/or modify * 8 * it under the terms of the GNU General Public License as published by * 9 * the Free Software Foundation; either version 2 of the License, or * 10 * (at your option) any later version. * 11 * * 12 ***************************************************************************/ 13 package games.stendhal.server.entity; 14 15 import static games.stendhal.common.Constants.KARMA_SETTINGS; 16 import static games.stendhal.common.constants.General.COMBAT_KARMA; 17 18 import java.awt.geom.Rectangle2D; 19 import java.util.ArrayList; 20 import java.util.Collections; 21 import java.util.LinkedList; 22 import java.util.List; 23 import java.util.Map; 24 import java.util.Map.Entry; 25 import java.util.Objects; 26 import java.util.WeakHashMap; 27 import java.util.function.Predicate; 28 import java.util.stream.Collectors; 29 import java.util.stream.Stream; 30 import java.util.stream.StreamSupport; 31 32 import org.apache.log4j.Logger; 33 34 import com.google.common.collect.ImmutableList; 35 import com.google.common.collect.ImmutableList.Builder; 36 37 import games.stendhal.common.EquipActionConsts; 38 import games.stendhal.common.Level; 39 import games.stendhal.common.NotificationType; 40 import games.stendhal.common.Rand; 41 import games.stendhal.common.constants.Nature; 42 import games.stendhal.common.constants.SoundLayer; 43 import games.stendhal.common.constants.Testing; 44 import games.stendhal.common.grammar.Grammar; 45 import games.stendhal.common.parser.WordList; 46 import games.stendhal.server.actions.equip.DropAction; 47 import games.stendhal.server.core.engine.GameEvent; 48 import games.stendhal.server.core.engine.ItemLogger; 49 import games.stendhal.server.core.engine.SingletonRepository; 50 import games.stendhal.server.core.engine.StendhalRPZone; 51 import games.stendhal.server.core.engine.db.StendhalKillLogDAO; 52 import games.stendhal.server.core.engine.dbcommand.LogKillEventCommand; 53 import games.stendhal.server.core.events.TurnListener; 54 import games.stendhal.server.core.events.TutorialNotifier; 55 import games.stendhal.server.entity.creature.Creature; 56 import games.stendhal.server.entity.creature.Pet; 57 import games.stendhal.server.entity.item.CaptureTheFlagFlag; 58 import games.stendhal.server.entity.item.Corpse; 59 import games.stendhal.server.entity.item.Item; 60 import games.stendhal.server.entity.item.StackableItem; 61 import games.stendhal.server.entity.mapstuff.portal.Portal; 62 import games.stendhal.server.entity.npc.TrainingDummy; 63 import games.stendhal.server.entity.player.Player; 64 import games.stendhal.server.entity.slot.EntitySlot; 65 import games.stendhal.server.entity.slot.Slots; 66 import games.stendhal.server.entity.status.Status; 67 import games.stendhal.server.entity.status.StatusAttacker; 68 import games.stendhal.server.entity.status.StatusList; 69 import games.stendhal.server.entity.status.StatusType; 70 import games.stendhal.server.events.AttackEvent; 71 import games.stendhal.server.events.SoundEvent; 72 import games.stendhal.server.events.TextEvent; 73 import games.stendhal.server.util.CounterMap; 74 import marauroa.common.game.RPAction; 75 import marauroa.common.game.RPObject; 76 import marauroa.common.game.RPSlot; 77 import marauroa.common.game.SyntaxException; 78 import marauroa.server.db.command.DBCommandPriority; 79 import marauroa.server.db.command.DBCommandQueue; 80 import marauroa.server.game.Statistics; 81 import marauroa.server.game.db.DAORegister; 82 83 public abstract class RPEntity extends GuidedEntity { 84 /** 85 * The title attribute name. 86 */ 87 protected static final String ATTR_TITLE = "title"; 88 private static final float WEAPON_DEF_MULTIPLIER = 4.0f; 89 private static final float BOOTS_DEF_MULTIPLIER = 1.0f; 90 private static final float LEG_DEF_MULTIPLIER = 1.0f; 91 private static final float HELMET_DEF_MULTIPLIER = 1.0f; 92 private static final float CLOAK_DEF_MULTIPLIER = 1.5f; 93 private static final float ARMOR_DEF_MULTIPLIER = 2.0f; 94 private static final float SHIELD_DEF_MULTIPLIER = 4.0f; 95 private static final float RING_DEF_MULTIPLIER = 1.0f; 96 /** 97 * To prevent players from gaining attack and defense experience by fighting 98 * against very weak creatures, they only gain atk and def xp for so many 99 * turns after they have actually been damaged by the enemy. // 100 */ 101 private static final int TURNS_WHILE_FIGHT_XP_INCREASES = 12; 102 /** the logger instance. */ 103 private static final Logger logger = Logger.getLogger(RPEntity.class); 104 private static Statistics stats; 105 106 private String name; 107 protected int atk; 108 private int atk_xp; 109 protected int def; 110 private int def_xp; 111 protected int ratk; 112 private int ratk_xp; 113 private int base_hp; 114 private int hp; 115 protected int lv_cap; 116 private int xp; 117 protected int level; 118 private int mana; 119 private int base_mana; 120 121 private String deathSound; 122 private String bloodClass; 123 124 /** Entity uses a status attack */ 125 protected ImmutableList<StatusAttacker> statusAttackers = ImmutableList.of(); 126 /** a list of current statuses */ 127 protected StatusList statusList; 128 /** 129 * Maps each enemy which has recently damaged this RPEntity to the turn when 130 * the last damage has occurred. 131 * 132 * You only get ATK and DEF experience by fighting against a creature that 133 * is in this list. 134 */ 135 private final Map<RPEntity, Integer> enemiesThatGiveFightXP; 136 /** List of all enemies that are currently attacking this entity. */ 137 private final List<Entity> attackSources; 138 /** the enemy that is currently attacked by this entity. */ 139 private RPEntity attackTarget; 140 141 /** 142 * Maps each attacker to the sum of hitpoint loss it has caused to this 143 * RPEntity. 144 */ 145 protected CounterMap<Entity> damageReceived; 146 protected int totalDamageReceived; 147 148 /** 149 * To avoid using karma for damage calculations when the natural ability of 150 * the fighters would mean they need no luck, we only use karma when the 151 * levels are significantly different. 152 */ 153 154 private static final double IGNORE_KARMA_MULTIPLIER = 0.2; 155 156 /** 157 * Level bonus for defence given to everyone. Prevents newbies killing each 158 * other too fast. 159 */ 160 private static final double NEWBIE_DEF = 10.0; 161 /** 162 * Armor value of no armor. Prevents unarmored or lightly armored entities 163 * from being completely helpless 164 */ 165 private static final double SKIN_DEF = 10.0; 166 /** Adjusts the weight of level. Larger means weight more */ 167 private static final double LEVEL_ATK = 0.03; 168 /** Adjusts the weight of level. Larger means weight more */ 169 private static final double LEVEL_DEF = 0.03; 170 /** General parameter for damage. Larger means more damage. */ 171 private static final double WEIGHT_ATK = 8.0; 172 /** the level where relative damage curves start being linear. */ 173 private static final double EVEN_POINT = 1.2; 174 /** 175 * Steepness of the damage vs level curves. The maximum bonus/penalty with 176 * weak enemies 177 */ 178 private static final double WEIGHT_EFFECT = 0.5; 179 180 /** 181 * A helper class for building a size limited list of killer names. If there 182 * are more killers than the limit, then "others" is set as the last killer. 183 * Only living creatures and online players are included in the killer name 184 * list. RPEntities on other zones are not included. 185 */ 186 private class KillerList { 187 /** Maximum amount of killer names. */ 188 private static final int MAX_SIZE = 10; 189 /** List of killer names. */ 190 private final LinkedList<String> list = new LinkedList<>(); 191 /** 192 * A flag for detecting when the killer list has grown over the 193 * maximum size. 194 */ 195 private boolean more; 196 197 /** 198 * Add an entity to the killer list. 199 * 200 * @param e entity 201 */ addEntity(Entity e)202 void addEntity(Entity e) { 203 if (e instanceof RPEntity) { 204 // Only list the killers on the zone where the death happened. 205 if (e.getZone() != getZone()) { 206 return; 207 } 208 // Try to keep player names at the start of the list 209 if (e instanceof Player) { 210 if (((Player) e).isDisconnected()) { 211 return; 212 } 213 list.addFirst(e.getName()); 214 } else { 215 if (((RPEntity) e).getHP() <= 0) { 216 return; 217 } 218 list.add(e.getName()); 219 } 220 } else { 221 list.add(e.getName()); 222 } 223 trim(); 224 } 225 226 /** 227 * Set the official killer. If the killer was already on the list, move 228 * it first. Otherwise prepend the list with the official killer. This 229 * means that a creature can appear before players if it is the official 230 * killer. Also an item "poison" can be the first on the list this way. 231 * (And, as of this writing (2015-04-01) it is the only way anything but 232 * RPEntities can be shown on the killer list). 233 * 234 * @param killer The official killer 235 */ setKiller(String killer)236 void setKiller(String killer) { 237 if (list.contains(killer)) { 238 list.remove(killer); 239 list.addFirst(killer); 240 } else { 241 list.addFirst(killer); 242 trim(); 243 } 244 } 245 246 /** 247 * Keep the name list at most {@link #MAX_SIZE}. 248 */ trim()249 private void trim() { 250 if (list.size() > MAX_SIZE) { 251 list.remove(list.size() - 1); 252 more = true; 253 } 254 } 255 256 /** 257 * Get the name list of the added entities. 258 * 259 * @return name list. 260 */ asList()261 List<String> asList() { 262 if (more) { 263 list.set(list.size() - 1, "others"); 264 } 265 return Collections.unmodifiableList(list); 266 } 267 } 268 269 @Override handlePortal(final Portal portal)270 protected boolean handlePortal(final Portal portal) { 271 if (isZoneChangeAllowed()) { 272 if (logger.isDebugEnabled() || Testing.DEBUG) { 273 logger.debug("Using portal " + portal); 274 } 275 276 return portal.onUsed(this); 277 } 278 return super.handlePortal(portal); 279 } 280 generateRPClass()281 public static void generateRPClass() { 282 try { 283 stats = Statistics.getStatistics(); 284 RPEntityRPClass.generateRPClass(ATTR_TITLE); 285 } catch (final SyntaxException e) { 286 logger.error("cannot generateRPClass", e); 287 } 288 } 289 RPEntity(final RPObject object)290 public RPEntity(final RPObject object) { 291 super(object); 292 attackSources = new ArrayList<>(); 293 damageReceived = new CounterMap<>(true); 294 enemiesThatGiveFightXP = new WeakHashMap<>(); 295 totalDamageReceived = 0; 296 } 297 RPEntity()298 public RPEntity() { 299 super(); 300 attackSources = new ArrayList<>(); 301 damageReceived = new CounterMap<>(true); 302 enemiesThatGiveFightXP = new WeakHashMap<>(); 303 totalDamageReceived = 0; 304 } 305 306 /** 307 * Give the player some karma (good or bad). 308 * 309 * @param karma 310 * An amount of karma to add/subtract. 311 */ addKarma(final double karma)312 public void addKarma(final double karma) { 313 // No nothing 314 } 315 316 /** 317 * Get the current amount of karma. 318 * 319 * @return The current amount of karma. 320 * 321 * @see #addKarma(double) 322 */ getKarma()323 public double getKarma() { 324 // No karma (yet) 325 return 0.0; 326 } 327 328 /** 329 * Get some of the player's karma. A positive value indicates good 330 * luck/energy. A negative value indicates bad luck/energy. A value of zero 331 * should cause no change on an action or outcome. 332 * 333 * @param scale 334 * A positive number. 335 * 336 * @return A number between -scale and scale. 337 */ useKarma(final double scale)338 public double useKarma(final double scale) { 339 // No impact 340 return 0.0; 341 } 342 343 /** 344 * Get some of the player's karma. A positive value indicates good 345 * luck/energy. A negative value indicates bad luck/energy. A value of zero 346 * should cause no change on an action or outcome. 347 * 348 * @param negLimit 349 * The lowest negative value returned. 350 * @param posLimit 351 * The highest positive value returned. 352 * 353 * @return A number within negLimit <= 0 <= posLimit. 354 */ useKarma(final double negLimit, final double posLimit)355 public double useKarma(final double negLimit, final double posLimit) { 356 // No impact 357 return 0.0; 358 } 359 360 /** 361 * Use some of the player's karma. A positive value indicates good 362 * luck/energy. A negative value indicates bad luck/energy. A value of zero 363 * should cause no change on an action or outcome. 364 * 365 * @param negLimit 366 * The lowest negative value returned. 367 * @param posLimit 368 * The highest positive value returned. 369 * @param granularity 370 * The amount that any extracted karma is a multiple of. 371 * 372 * @return A number within negLimit <= 0 <= posLimit. 373 */ useKarma(final double negLimit, final double posLimit, final double granularity)374 public double useKarma(final double negLimit, final double posLimit, 375 final double granularity) { 376 // No impact 377 return 0.0; 378 } 379 380 /** 381 * Heal this entity completely. 382 * 383 * @return The amount actually healed. 384 */ heal()385 public int heal() { 386 final int baseHP = getBaseHP(); 387 final int given = baseHP - getHP(); 388 389 if (given != 0) { 390 put("heal", given); 391 setHP(baseHP); 392 } 393 394 return given; 395 } 396 397 /** 398 * Heal this entity. 399 * 400 * @param amount 401 * The [maximum] amount to heal by. 402 * 403 * @return The amount actually healed. 404 */ heal(final int amount)405 public int heal(final int amount) { 406 return heal(amount, false); 407 } 408 409 /** 410 * Heal this entity. 411 * 412 * @param amount 413 * The [maximum] amount to heal by. 414 * @param tell 415 * Whether to tell the entity they've been healed. 416 * 417 * @return The amount actually healed. 418 */ heal(final int amount, final boolean tell)419 public int heal(final int amount, final boolean tell) { 420 int tempHp = getHP(); 421 int given = 0; 422 423 // Avoid creating zombies out of dead creatures 424 if (tempHp > 0) { 425 given = Math.min(amount, getBaseHP() - tempHp); 426 427 if (given != 0) { 428 tempHp += given; 429 430 if (tell) { 431 put("heal", given); 432 } 433 434 setHP(tempHp); 435 } 436 } 437 438 return given; 439 } 440 441 /** 442 * Give mana to the entity. 443 * 444 * @param mana 445 * The amount of mana to add/substract. 446 * @param tell 447 * Whether to tell the entity that mana has been added. 448 * 449 * @return Amount of mana actually refilled. 450 */ addMana(int mana, boolean tell)451 public int addMana(int mana, boolean tell) { 452 int old_mana = getMana(); 453 int new_mana = old_mana + mana; 454 int given = 0; 455 456 // no negative mana 457 new_mana = Math.max(new_mana, 0); 458 459 // maximum is base_mana 460 new_mana = Math.min(new_mana, getBaseMana()); 461 462 given = new_mana - old_mana; 463 464 if(tell) { 465 //TODO: Add notification for increased mana 466 } 467 468 setMana(new_mana); 469 470 return given; 471 } 472 473 @Override update()474 public void update() { 475 super.update(); 476 477 if (has("name")) { 478 final String newName = get("name"); 479 registerNewName(newName, name); 480 name = newName; 481 } 482 483 if (has("atk_xp")) { 484 atk_xp = getInt("atk_xp"); 485 setAtkXpInternal(atk_xp, false); 486 } 487 488 if (has("def_xp")) { 489 def_xp = getInt("def_xp"); 490 setDefXpInternal(def_xp, false); 491 } 492 493 if (Testing.COMBAT && has("ratk_xp")) { 494 ratk_xp = getInt("ratk_xp"); 495 setRatkXPInternal(ratk_xp, false); 496 } 497 498 if (has("base_hp")) { 499 base_hp = getInt("base_hp"); 500 } 501 if (has("hp")) { 502 hp = getInt("hp"); 503 } 504 505 if (has("lv_cap")) { 506 lv_cap = getInt("lv_cap"); 507 } 508 if (has("level")) { 509 level = getInt("level"); 510 } 511 if (has("xp")) { 512 xp = getInt("xp"); 513 } 514 if (has("mana")) { 515 mana = getInt("mana"); 516 } 517 if (has("base_mana")) { 518 base_mana = getInt("base_mana"); 519 } 520 if (has("base_speed")) { 521 setBaseSpeed(getDouble("base_speed")); 522 } 523 } 524 525 /** 526 * Register the new name in the conversation parser word list. 527 * 528 * @param newName 529 * @param oldName 530 */ registerNewName(final String newName, final String oldName)531 private static void registerNewName(final String newName, final String oldName) { 532 if ((oldName != null) && !oldName.equals(newName)) { 533 WordList.getInstance().unregisterSubjectName(oldName); 534 } 535 536 if ((oldName == null) || !oldName.equals(newName)) { 537 WordList.getInstance().registerSubjectName(newName); 538 } 539 } 540 541 /** 542 * Is called when this has hit the given defender. Determines how much 543 * hitpoints the defender will lose, based on this's ATK experience and 544 * weapon(s), the defender's DEF experience and defensive items, and a 545 * random generator. 546 * 547 * @param defender 548 * The defender. 549 * @param attackingWeaponsValue 550 * ATK-value of all attacking weapons/spells 551 * @param damageType nature of damage 552 * @param isRanged <code>true</code> if this is a ranged attack, otherwise 553 * <code>false</code> 554 * @param maxRange maximum range of a ranged attack 555 * 556 * @return The number of hitpoints that the target should lose. 0 if the 557 * attack was completely blocked by the defender. 558 */ damageDone(RPEntity defender, double attackingWeaponsValue, Nature damageType, boolean isRanged, int maxRange)559 int damageDone(RPEntity defender, double attackingWeaponsValue, Nature damageType, 560 boolean isRanged, int maxRange) { 561 // Don't start from 0 to mitigate weird behaviour at very low levels 562 final int effectiveAttackerLevel = getLevel() + 5; 563 final int effectiveDefenderLevel = defender.getLevel() + 5; 564 565 // Defending side 566 final double armor = defender.getItemDef(); 567 final int targetDef = defender.getCappedDef(); 568 // Even strong players are vulnerable without any armor. 569 // Armor def gets much higher with high level players unlike 570 // weapon atk, so it can not be treated similarly. Using geometric 571 // / mean to balance things a bit. 572 final double maxDefence = Math.sqrt(targetDef * (SKIN_DEF + armor)) 573 * (NEWBIE_DEF + LEVEL_DEF * effectiveDefenderLevel); 574 575 double defence = Rand.rand() * maxDefence; 576 /* 577 * Account for karma (+/-10%) But, the defender doesn't need luck to 578 * help him defend if he's a much higher level than this attacker 579 */ 580 final int levelDifferenceToNotNeedKarmaDefending = (int) (IGNORE_KARMA_MULTIPLIER * defender.getLevel()); 581 582 // this attribute determines how karma is used in combat 583 String karmaMode = null; 584 if (defender.has(COMBAT_KARMA)) { 585 karmaMode = defender.get(COMBAT_KARMA); 586 } 587 588 boolean useKarma = false; 589 if (karmaMode == null || karmaMode.equals(KARMA_SETTINGS.get(1))) { 590 if (!(effectiveDefenderLevel - levelDifferenceToNotNeedKarmaDefending > effectiveAttackerLevel)) { 591 useKarma = true; 592 } 593 } else if (karmaMode.equals(KARMA_SETTINGS.get(2))) { 594 useKarma = true; 595 } 596 597 // using karma here decreases damage done by enemy 598 if (useKarma) { 599 defence += defence * defender.useKarma(0.1); 600 } 601 602 /* Attacking with ranged weapon uses a separate strength value. 603 * 604 * XXX: atkStrength never used outside of debugger. 605 */ 606 final int atkStrength, sourceAtk; 607 if (Testing.COMBAT && isRanged) { 608 atkStrength = this.getRatk(); 609 sourceAtk = this.getCappedRatk(); 610 } else { 611 atkStrength = this.getAtk(); 612 sourceAtk = this.getCappedAtk(); 613 } 614 615 // Attacking 616 if (logger.isDebugEnabled() || Testing.DEBUG) { 617 logger.debug("attacker has " + atkStrength + " (" + getCappedAtk() 618 + ") and uses a weapon of " + getItemAtk()); 619 } 620 621 // Make fast weapons efficient against weak enemies, and heavy 622 // better against strong enemies. 623 // Half a parabola; desceding for rate < 5; ascending for > 5 624 double speedEffect = 1.0; 625 if (effectiveDefenderLevel < EVEN_POINT * effectiveAttackerLevel) { 626 final double levelPart = 1.0 - effectiveDefenderLevel 627 / (EVEN_POINT * effectiveAttackerLevel); 628 // Gets values -1 at rate = 1, 0 at rate = 5, 629 // and approaches 1 when rate approaches infinity. 630 // We can't use a much simpler function as long as we need 631 // to deal with open ended rate values. 632 final double speedPart = 1 - 8 / (getAttackRate() + 3.0); 633 634 speedEffect = 1.0 - WEIGHT_EFFECT * speedPart * levelPart 635 * levelPart; 636 } 637 638 final double weaponComponent = 1.0 + attackingWeaponsValue; 639 // XXX: Is correct to use sourceAtk here instead of atkStrength? 640 final double maxAttack = sourceAtk * weaponComponent 641 * (1 + LEVEL_ATK * effectiveAttackerLevel) * speedEffect; 642 double attack = Rand.rand() * maxAttack; 643 644 /* 645 * Account for karma (+/-10%) But, don't need luck to help you attack if 646 * you're a much higher level than what you attack 647 */ 648 final int levelDifferenceToNotNeedKarmaAttacking = (int) (IGNORE_KARMA_MULTIPLIER * getLevel()); 649 650 karmaMode = null; 651 if (this.has(COMBAT_KARMA)) { 652 karmaMode = this.get(COMBAT_KARMA); 653 } 654 655 useKarma = false; 656 if (karmaMode == null || karmaMode.equals(KARMA_SETTINGS.get(1))) { 657 if (!(effectiveAttackerLevel - levelDifferenceToNotNeedKarmaAttacking > effectiveDefenderLevel)) { 658 useKarma = true; 659 } 660 } else if (karmaMode.equals(KARMA_SETTINGS.get(2))) { 661 useKarma = true; 662 } 663 664 // using karma here increases damage to enemy 665 if (useKarma) { 666 attack += attack * useKarma(0.1); 667 } 668 669 if (logger.isDebugEnabled() || Testing.DEBUG) { 670 logger.debug("DEF MAX: " + maxDefence + "\t DEF VALUE: " + defence); 671 } 672 673 // Apply defense and damage type effect 674 int damage = (int) (defender.getSusceptibility(damageType) 675 * (WEIGHT_ATK * attack - defence) / maxDefence); 676 677 /* FIXME: Can argument be removed and just use 678 * RPEntity.usingRangedAttack() here? 679 */ 680 if (isRanged) { 681 // The attacker is attacking either using a range weapon with 682 // ammunition such as a bow and arrows, or a missile such as a 683 // spear. 684 damage = applyDistanceAttackModifiers(damage, 685 squaredDistance(defender), maxRange); 686 } 687 688 return damage; 689 } 690 691 /** 692 * Is called when this has hit the given defender. Determines how much 693 * hitpoints the defender will lose, based on this's ATK experience and 694 * weapon(s), the defender's DEF experience and defensive items, and a 695 * random generator. 696 * 697 * @param defender 698 * The defender. 699 * @param attackingWeaponsValue 700 * ATK-value of all attacking weapons/spells 701 * @param damageType nature of damage 702 * @return The number of hitpoints that the target should lose. 0 if the 703 * attack was completely blocked by the defender. 704 */ damageDone(final RPEntity defender, double attackingWeaponsValue, Nature damageType)705 public int damageDone(final RPEntity defender, double attackingWeaponsValue, Nature damageType) { 706 final int maxRange = getMaxRangeForArcher(); 707 boolean isRanged = ((maxRange > 0) && canDoRangeAttack(defender, maxRange)); 708 709 return damageDone(defender, attackingWeaponsValue, damageType, isRanged, maxRange); 710 } 711 712 /** 713 * Calculates the damage that will be done in a distance attack (bow and 714 * arrows, spear, etc.). 715 * 716 * @param damage 717 * The damage that would have been done if there would be no 718 * modifiers for distance attacks. 719 * @param squareDistance 720 * the distance 721 * @param maxrange maximum attack range 722 * @return The damage that will be done with the distance attack. 723 */ applyDistanceAttackModifiers(final int damage, final double squareDistance, final double maxrange)724 public static int applyDistanceAttackModifiers(final int damage, 725 final double squareDistance, final double maxrange) { 726 final double maxRangeSquared = maxrange * maxrange; 727 if (maxRangeSquared < squareDistance) { 728 return 0; 729 } else if (squareDistance == 0) { 730 // as a special case, make archers switch to melee when the enemy is 731 // next to them 732 return (int) (0.8 * damage); 733 } 734 735 final double outOfRange = maxrange + 1; 736 final double distance = Math.sqrt(squareDistance); 737 738 // a downward parabola with zero points at 0 and outOfRange 739 return (int) (damage * ((distance * 4) / outOfRange - 4 740 * squareDistance / (outOfRange * outOfRange))); 741 } 742 743 /** 744 * Set the entity's name. 745 * 746 * @param name 747 * The new name. 748 */ setName(final String name)749 public void setName(final String name) { 750 registerNewName(name, this.name); 751 752 this.name = name; 753 put("name", name); 754 } 755 756 /** 757 * Get the entity's name. 758 * 759 * @return The entity's name. 760 */ 761 @Override getName()762 public String getName() { 763 if (name != null) { 764 return name; 765 } 766 return super.getName(); 767 } 768 769 @Override onAdded(final StendhalRPZone zone)770 public void onAdded(final StendhalRPZone zone) { 771 super.onAdded(zone); 772 this.updateItemAtkDef(); 773 } 774 775 setLevel(final int level)776 public void setLevel(final int level) { 777 this.level = level; 778 put("level", level); 779 this.updateModifiedAttributes(); 780 } 781 getLevel()782 public int getLevel() { 783 return this.level; 784 } 785 setAtk(final int atk)786 public void setAtk(final int atk) { 787 setAtkInternal(atk, true); 788 } 789 setAtkInternal(final int atk, boolean notify)790 protected void setAtkInternal(final int atk, boolean notify) { 791 this.atk = atk; 792 put("atk", atk); // visible atk 793 if(notify) { 794 this.updateModifiedAttributes(); 795 } 796 } 797 getAtk()798 public int getAtk() { 799 return this.atk; 800 } 801 802 /** 803 * gets the capped atk level, which prevent players from training their atk way beyond what is reasonable for their level 804 * 805 * @return capped atk 806 */ getCappedAtk()807 public int getCappedAtk() { 808 return this.atk; 809 } 810 811 /** 812 * Set attack XP. 813 * 814 * @param atk the new value 815 */ setAtkXP(final int atk)816 public void setAtkXP(final int atk) { 817 setAtkXpInternal(atk, true); 818 } 819 setAtkXpInternal(final int atk, boolean notify)820 private void setAtkXpInternal(final int atk, boolean notify) { 821 this.atk_xp = atk; 822 put("atk_xp", atk_xp); 823 824 // Handle level changes 825 final int newLevel = Level.getLevel(atk_xp); 826 final int levels = newLevel - (this.atk - 10); 827 if (levels != 0) { 828 setAtkInternal(this.atk + levels, notify); 829 new GameEvent(getName(), "atk", Integer.toString(getAtk())).raise(); 830 } 831 } 832 833 /** 834 * Adjust entity's ATK XP by specified amount. 835 * 836 * @param xp 837 * Amount to add. 838 */ addAtkXP(final int xp)839 public void addAtkXP(final int xp) { 840 setAtkXP(getAtkXP() + xp); 841 } 842 getAtkXP()843 public int getAtkXP() { 844 return atk_xp; 845 } 846 847 /** 848 * Increase attack XP by 1. 849 */ incAtkXP()850 public void incAtkXP() { 851 setAtkXP(atk_xp + 1); 852 } 853 setDef(final int def)854 public void setDef(final int def) { 855 setDefInternal(def, true); 856 } 857 setDefInternal(final int def, boolean notify)858 protected void setDefInternal(final int def, boolean notify) { 859 this.def = def; 860 put("def", def); // visible def 861 if(notify) { 862 this.updateModifiedAttributes(); 863 } 864 } 865 getDef()866 public int getDef() { 867 return this.def; 868 } 869 870 /** 871 * gets the capped def level, which prevent players from training their def way beyond what is reasonable for their level 872 * 873 * @return capped def 874 */ getCappedDef()875 public int getCappedDef() { 876 return this.def; 877 } 878 879 /** 880 * Set defense XP. 881 * 882 * @param defXp the new value 883 */ setDefXP(final int defXp)884 public void setDefXP(final int defXp) { 885 setDefXpInternal(defXp, true); 886 } 887 setDefXpInternal(final int defXp, boolean notify)888 private void setDefXpInternal(final int defXp, boolean notify) { 889 this.def_xp = defXp; 890 put("def_xp", def_xp); 891 892 // Handle level changes 893 final int newLevel = Level.getLevel(def_xp); 894 final int levels = newLevel - (this.def - 10); 895 if (levels != 0) { 896 setDefInternal(this.def + levels, notify); 897 new GameEvent(getName(), "def", Integer.toString(this.def)).raise(); 898 } 899 } 900 901 /** 902 * Adjust entity's DEF XP by specified amount. 903 * 904 * @param xp 905 * Amount to add. 906 */ addDefXP(final int xp)907 public void addDefXP(final int xp) { 908 setDefXP(getDefXP() + xp); 909 } 910 getDefXP()911 public int getDefXP() { 912 return def_xp; 913 } 914 915 /** 916 * Increase defense XP by 1. 917 */ incDefXP()918 public void incDefXP() { 919 setDefXP(def_xp + 1); 920 } 921 922 923 /* ### --- START RANGED --- ### */ 924 925 /** 926 * Set the value of the entity's ranged attack level. 927 * 928 * @param ratk 929 * Integer value representing new ranged attack level 930 */ setRatk(final int ratk)931 public void setRatk(final int ratk) { 932 setRatkInternal(ratk, true); 933 } 934 935 /** 936 * Set the entity's ranged attack level. 937 * 938 * @param ratk 939 * Integer value representing new ranged attack level 940 * @param notify 941 * Update stat in real-time 942 */ setRatkInternal(final int ratk, boolean notify)943 protected void setRatkInternal(final int ratk, boolean notify) { 944 this.ratk = ratk; 945 put("ratk", ratk); // visible ratk 946 if(notify) { 947 this.updateModifiedAttributes(); 948 } 949 } 950 951 /** 952 * Gets the entity's current ranged attack level. 953 * 954 * @return 955 * Integer value of ranged attack level 956 */ getRatk()957 public int getRatk() { 958 return this.ratk; 959 } 960 961 /** 962 * gets the capped ranged attack level which prevents players from training 963 * ratk way beyond what is reasonable for their level. 964 * 965 * @return 966 * The maximum value player's ranged attack level can be at current 967 * level 968 */ getCappedRatk()969 public int getCappedRatk() { 970 return this.ratk; 971 } 972 973 /** 974 * Sets the entity's ranged attack experience. 975 * 976 * @param ratkXP 977 * Integer value of the target experience 978 */ setRatkXP(final int ratkXP)979 public void setRatkXP(final int ratkXP) { 980 setRatkXPInternal(ratkXP, true); 981 } 982 983 /** 984 * Sets the entity's ranged attack experience. 985 * 986 * @param ratkXP 987 * Integer value of the target experience 988 * @param notify 989 * Update ranged attack experience in real-time 990 */ setRatkXPInternal(final int ratkXP, boolean notify)991 protected void setRatkXPInternal(final int ratkXP, boolean notify) { 992 this.ratk_xp = ratkXP; 993 put("ratk_xp", ratk_xp); 994 995 // Handle level changes 996 final int newLevel = Level.getLevel(ratk_xp); 997 final int levels = newLevel - (this.ratk - 10); 998 999 // In case we level up several levels at a single time. 1000 if (levels != 0) { 1001 setRatkInternal(this.ratk + levels, notify); 1002 new GameEvent(getName(), "ratk", Integer.toString(this.ratk)).raise(); 1003 } 1004 } 1005 1006 /** 1007 * Adjust entity's RATK XP by specified amount. 1008 * 1009 * @param xp 1010 * Amount to add. 1011 */ addRatkXP(final int xp)1012 public void addRatkXP(final int xp) { 1013 setRatkXP(getRatkXP() + xp); 1014 } 1015 1016 /** 1017 * Get's the entity's current ranged attack experience. 1018 * 1019 * @return 1020 * Integer representation of current experience 1021 */ getRatkXP()1022 public int getRatkXP() { 1023 return ratk_xp; 1024 } 1025 1026 /** 1027 * Increase ranged XP by 1. 1028 */ incRatkXP()1029 public void incRatkXP() { 1030 setRatkXP(ratk_xp + 1); 1031 } 1032 1033 /* ### --- END RANGED --- ### */ 1034 1035 1036 /** 1037 * Set the base and current HP. 1038 * 1039 * @param hp 1040 * The HP to set. 1041 */ initHP(final int hp)1042 public void initHP(final int hp) { 1043 setBaseHP(hp); 1044 setHP(hp); 1045 } 1046 1047 /** 1048 * Set the base HP. 1049 * 1050 * @param newhp 1051 * The base HP to set. 1052 */ setBaseHP(final int newhp)1053 public void setBaseHP(final int newhp) { 1054 this.base_hp = newhp; 1055 try { 1056 put("base_hp", newhp); 1057 } catch (IllegalArgumentException e) { 1058 logger.error("Failed to set base HP to " + newhp + ". Entity was: " + this, e); 1059 } 1060 this.updateModifiedAttributes(); 1061 } 1062 1063 /** 1064 * Get the base HP. 1065 * 1066 * @return The current HP. 1067 */ getBaseHP()1068 public int getBaseHP() { 1069 return this.base_hp; 1070 } 1071 1072 /** 1073 * Set the HP. <br> 1074 * DO NOT USE THIS UNLESS YOU REALLY KNOW WHAT YOU ARE DOING. <br> 1075 * Use the appropriate damage(), and heal() methods instead. 1076 * 1077 * @param hp 1078 * The HP to set. 1079 */ setHP(final int hp)1080 public void setHP(final int hp) { 1081 setHpInternal(hp, true); 1082 } 1083 setHpInternal(final int hp, final boolean notify)1084 private void setHpInternal(final int hp, final boolean notify) { 1085 this.hp = hp; 1086 try { 1087 put("hp", hp); 1088 } catch (IllegalArgumentException e) { 1089 logger.error("Failed to set HP to " + hp + ". Entity was: " + this, e); 1090 } 1091 if(notify) { 1092 this.updateModifiedAttributes(); 1093 } 1094 } 1095 1096 /** 1097 * Get the current HP. 1098 * 1099 * @return The current HP. 1100 */ getHP()1101 public int getHP() { 1102 return this.hp; 1103 } 1104 1105 /** 1106 * Get the lv_cap. 1107 * 1108 * @return The current lv_cap. 1109 */ getLVCap()1110 public int getLVCap() { 1111 return this.lv_cap; 1112 } 1113 1114 /** 1115 * Gets the mana (magic). 1116 * 1117 * @return mana 1118 */ getMana()1119 public int getMana() { 1120 return this.mana; 1121 } 1122 1123 /** 1124 * Gets the base mana (like base_hp). 1125 * 1126 * @return base mana 1127 */ getBaseMana()1128 public int getBaseMana() { 1129 return this.base_mana; 1130 } 1131 1132 /** 1133 * Sets the available mana. 1134 * 1135 * @param newMana 1136 * new amount of mana 1137 */ setMana(final int newMana)1138 public void setMana(final int newMana) { 1139 setManaInternal(newMana, true); 1140 } 1141 setManaInternal(final int newMana, boolean notify)1142 private void setManaInternal(final int newMana, boolean notify) { 1143 mana = newMana; 1144 put("mana", newMana); 1145 if(notify) { 1146 this.updateModifiedAttributes(); 1147 } 1148 } 1149 1150 /** 1151 * Sets the base mana (like base_hp). 1152 * 1153 * @param newBaseMana 1154 * new amount of base mana 1155 */ setBaseMana(final int newBaseMana)1156 public void setBaseMana(final int newBaseMana) { 1157 base_mana = newBaseMana; 1158 put("base_mana", newBaseMana); 1159 this.updateModifiedAttributes(); 1160 } 1161 1162 /** 1163 * adds to base mana (like addXP). 1164 * 1165 * @param newBaseMana 1166 * amount of base mana to be added 1167 */ addBaseMana(final int newBaseMana)1168 public void addBaseMana(final int newBaseMana) { 1169 base_mana += newBaseMana; 1170 put("base_mana", base_mana); 1171 } 1172 setLVCap(final int newLVCap)1173 public void setLVCap(final int newLVCap) { 1174 lv_cap = newLVCap; 1175 put("lv_cap", newLVCap); 1176 this.updateModifiedAttributes(); 1177 } 1178 setXP(final int newxp)1179 public final void setXP(final int newxp) { 1180 if (newxp < 0) { 1181 return; 1182 } 1183 this.xp = newxp; 1184 put("xp", xp); 1185 } 1186 subXP(final int newxp)1187 public void subXP(final int newxp) { 1188 addXP(-newxp); 1189 } 1190 addXP(final int newxp)1191 public void addXP(final int newxp) { 1192 if (Integer.MAX_VALUE - this.xp <= newxp) { 1193 return; 1194 } 1195 if (newxp == 0) { 1196 return; 1197 } 1198 1199 // Increment experience points 1200 this.xp += newxp; 1201 put("xp", xp); 1202 String[] params = { Integer.toString(newxp) }; 1203 1204 new GameEvent(getName(), "added xp", params).raise(); 1205 new GameEvent(getName(), "xp", String.valueOf(xp)).raise(); 1206 1207 updateLevel(); 1208 } 1209 1210 /** 1211 * Change the level to match the XP, if needed. 1212 */ updateLevel()1213 protected void updateLevel() { 1214 final int newLevel = Level.getLevel(getXP()); 1215 final int oldLevel = has("level") ? getInt("level") : 0; 1216 final int levels = newLevel - oldLevel; 1217 1218 // In case we level up several levels at a single time. 1219 for (int i = 0; i < Math.abs(levels); i++) { 1220 setBaseHP(getBaseHP() + (int) Math.signum(levels) * 10); 1221 setHP(getBaseHP()); 1222 new GameEvent(getName(), "level", Integer.toString(oldLevel+(i+1)*((int) Math.signum(levels)))).raise(); 1223 setLevel(newLevel); 1224 } 1225 } 1226 getXP()1227 public int getXP() { 1228 return xp; 1229 } 1230 1231 /** 1232 * Get a multiplier for a given damage type when this 1233 * entity is damaged. 1234 * 1235 * @param type Type of the damage 1236 * @return damage multiplier 1237 */ getSusceptibility(Nature type)1238 protected double getSusceptibility(Nature type) { 1239 return 1.0; 1240 } 1241 1242 /** 1243 * Get the type of the damage this entity inflicts 1244 * 1245 * @return type of damage 1246 */ getDamageType()1247 protected Nature getDamageType() { 1248 return Nature.CUT; 1249 } 1250 1251 /** 1252 * Get the nature of the damage the entity inflicts in ranged attacks. 1253 * 1254 * @return type of damage 1255 */ getRangedDamageType()1256 protected Nature getRangedDamageType() { 1257 /* 1258 * Default to the same as the base damage type. Entities needing more 1259 * complicated behavior (ie. fire breathing dragons) should override the 1260 * method. 1261 */ 1262 return getDamageType(); 1263 } 1264 1265 /*************************************************************************** 1266 * * Attack handling code. * * 1267 **************************************************************************/ 1268 1269 /** 1270 * @return true if this RPEntity is attackable. 1271 */ isAttackable()1272 public boolean isAttackable() { 1273 return true; 1274 } 1275 1276 /** 1277 * Modify the entity to order to attack the target entity. 1278 * 1279 * @param target 1280 */ setTarget(final RPEntity target)1281 public void setTarget(final RPEntity target) { 1282 put("target", target.getID().getObjectID()); 1283 if (attackTarget != null) { 1284 attackTarget.attackSources.remove(this); 1285 } 1286 attackTarget = target; 1287 } 1288 1289 /** Modify the entity to stop attacking. */ stopAttack()1290 public void stopAttack() { 1291 if (has("heal")) { 1292 remove("heal"); 1293 } 1294 if (has("target")) { 1295 remove("target"); 1296 } 1297 1298 if (attackTarget != null) { 1299 attackTarget.attackSources.remove(this); 1300 1301 // remove opponent here to avoid memory leak 1302 enemiesThatGiveFightXP.remove(attackTarget); 1303 1304 attackTarget = null; 1305 } 1306 } 1307 getsFightXpFrom(final RPEntity enemy)1308 public boolean getsFightXpFrom(final RPEntity enemy) { 1309 if (enemy instanceof TrainingDummy) { 1310 // training dummies always give fight XP 1311 return true; 1312 } 1313 1314 final Integer turnWhenLastDamaged = enemiesThatGiveFightXP.get(enemy); 1315 if (turnWhenLastDamaged == null) { 1316 return false; 1317 } 1318 final int currentTurn = SingletonRepository.getRuleProcessor() 1319 .getTurn(); 1320 if (currentTurn - turnWhenLastDamaged > TURNS_WHILE_FIGHT_XP_INCREASES) { 1321 enemiesThatGiveFightXP.remove(enemy); 1322 return false; 1323 } 1324 return true; 1325 } 1326 stopAttacking(final Entity attacker)1327 public void stopAttacking(final Entity attacker) { 1328 if (attacker.has("target")) { 1329 attacker.remove("target"); 1330 } 1331 } 1332 rememberAttacker(final Entity attacker)1333 public void rememberAttacker(final Entity attacker) { 1334 if (!attackSources.contains(attacker)) { 1335 attackSources.add(attacker); 1336 } 1337 } 1338 1339 /** 1340 * sets the blood class 1341 * 1342 * @param name name of blood class 1343 */ setBlood(final String name)1344 public final void setBlood(final String name) { 1345 this.bloodClass = name; 1346 } 1347 1348 /** 1349 * gets the name of the blood class 1350 * 1351 * @return bloodClass or <code>null</code> 1352 */ getBloodClass()1353 public final String getBloodClass() { 1354 return this.bloodClass; 1355 } 1356 1357 /** 1358 * Creates a blood pool on the ground under this entity, but only if there 1359 * isn't a blood pool at that position already. 1360 */ bleedOnGround()1361 private void bleedOnGround() { 1362 final Rectangle2D rect = getArea(); 1363 final int bx = (int) rect.getX(); 1364 final int by = (int) rect.getY(); 1365 final StendhalRPZone zone = getZone(); 1366 1367 if (zone.getBlood(bx, by) == null) { 1368 final Blood blood = new Blood(bloodClass); 1369 blood.setPosition(bx, by); 1370 1371 zone.add(blood); 1372 } 1373 } 1374 1375 /** 1376 * return list of all droppable items in entity's hands. 1377 * 1378 * currently only considers items in hands. no other part of body 1379 * 1380 * currently, there is only one type of droppable item - CaptureTheFlagFlag. 1381 * need some more general solution 1382 * 1383 * @return list of droppable items. returns null if no droppable items found 1384 */ getDroppables()1385 public List<Item> getDroppables() { 1386 final String[] slots = { "lhand", "rhand" }; 1387 Stream<Item> items = Stream.of(slots).map(this::getSlot).filter(Objects::nonNull).flatMap(this::slotStream); 1388 return items.filter(CaptureTheFlagFlag.class::isInstance).collect(Collectors.toList()); 1389 } 1390 1391 /** 1392 * Drop specified item from entity's equipment 1393 * 1394 * note: seems like this.drop(droppable) should work, but 1395 * the item just disappears - does not end up on ground. 1396 * 1397 * TODO: probably need to refactor this in to the general drop system 1398 * (maybe fixing some of the other code paths) 1399 * 1400 * @param droppable item to be dropped 1401 */ dropDroppableItem(Item droppable)1402 public void dropDroppableItem(Item droppable) { 1403 1404 // note: this.drop() does not do all necessary operations - 1405 // item disappears from hand, but disappears competely 1406 1407 Player player = (Player) this; 1408 RPObject parent = droppable.getContainer(); 1409 RPAction action = new RPAction(); 1410 1411 action.put("type", "drop"); 1412 action.put("baseitem", droppable.getID().getObjectID()); 1413 action.put(EquipActionConsts.BASE_OBJECT, parent.getID().getObjectID()); 1414 action.put(EquipActionConsts.BASE_SLOT, droppable.getContainerSlot().getName()); 1415 1416 // TODO: better to drop "behind" the player, if they have been running 1417 action.put("x", this.getX()); 1418 action.put("y", this.getY() + 1); 1419 1420 DropAction dropAction = new DropAction(); 1421 dropAction.onAction(player, action); 1422 1423 // TODO: send message to player - you dropped ... 1424 1425 this.notifyWorldAboutChanges(); 1426 } 1427 1428 /** 1429 * if defender (this entity) is carrying a droppable item, 1430 * then attacker and defender both roll d20, and if attacker 1431 * rolls higher, the defender drops the droppable. 1432 * 1433 * note that separate rolls are performed for each droppable 1434 * that the entity is carrying. 1435 * 1436 * XXX this does not belong here - should be in some Effect framework 1437 * 1438 * returns string - what happened. no effect returns null 1439 * 1440 * @param attacker 1441 * @return event description 1442 */ maybeDropDroppables(RPEntity attacker)1443 public String maybeDropDroppables(RPEntity attacker) { 1444 List<Item> droppables = getDroppables(); 1445 if (droppables.isEmpty()) { 1446 return null; 1447 } 1448 1449 for (Item droppable : droppables) { 1450 // roll two dice, tie goes to defender 1451 // TODO: integrate skills, ctf atk/def 1452 int attackerRoll = Rand.roll1D20(); 1453 int defenderRoll = Rand.roll1D20(); 1454 1455 System.out.printf(" drop: %2d %2d\n", attackerRoll, defenderRoll); 1456 1457 if (attackerRoll > defenderRoll) { 1458 this.dropDroppableItem(droppable); 1459 // XXX get description from droppable - what color, ... 1460 return "dropped the flag"; 1461 } 1462 } 1463 return null; 1464 } 1465 1466 /** 1467 * This method is called when this entity has been attacked by Entity 1468 * attacker and it has been damaged with damage points. 1469 * 1470 * @param attacker 1471 * @param damage 1472 */ onDamaged(final Entity attacker, final int damage)1473 public void onDamaged(final Entity attacker, final int damage) { 1474 if (logger.isDebugEnabled() || Testing.DEBUG) { 1475 logger.debug("Damaged " + damage + " points by " + attacker.getID()); 1476 } 1477 1478 bleedOnGround(); 1479 if (attacker instanceof RPEntity) { 1480 final int currentTurn = SingletonRepository.getRuleProcessor() 1481 .getTurn(); 1482 enemiesThatGiveFightXP.put((RPEntity) attacker, currentTurn); 1483 } 1484 1485 final int leftHP = getHP() - damage; 1486 1487 totalDamageReceived += damage; 1488 1489 // remember the damage done so that the attacker can later be rewarded 1490 // XP etc. 1491 damageReceived.add(attacker, damage); 1492 1493 if (leftHP > 0) { 1494 setHP(leftHP); 1495 } else { 1496 kill(attacker); 1497 } 1498 1499 notifyWorldAboutChanges(); 1500 } 1501 1502 /** 1503 * Apply damage to this entity. This is normally called from one of the 1504 * other damage() methods to account for death. 1505 * 1506 * @param amount 1507 * The HP to take. 1508 * 1509 * @return The damage actually taken (in case HP was < amount). 1510 */ damage(final int amount)1511 private int damage(final int amount) { 1512 int tempHp = getHP(); 1513 final int taken = Math.min(amount, tempHp); 1514 1515 tempHp -= taken; 1516 setHP(tempHp); 1517 1518 return taken; 1519 } 1520 1521 /** 1522 * Apply damage to this entity, and call onDead() if HP reaches 0. 1523 * 1524 * @param amount 1525 * The HP to take. 1526 * @param attacker 1527 * The attacking entity. 1528 * 1529 * @return The damage actually taken (in case HP was < amount). 1530 */ damage(final int amount, final Killer attacker)1531 public int damage(final int amount, final Killer attacker) { 1532 final int taken = damage(amount); 1533 1534 if (hp <= 0) { 1535 onDead(attacker); 1536 } 1537 1538 return taken; 1539 } 1540 1541 /** 1542 * Apply damage to this entity, delaying the damage to happen in a turn 1543 * notifier. To be used when dying could result in concurrent modification 1544 * in the zone's entity list, such as sheep starving. Call onDead() if HP 1545 * reaches 0. 1546 * 1547 * @param amount 1548 * The HP to take. 1549 * @param attackerName 1550 * The name of the attacker. 1551 */ delayedDamage(final int amount, final String attackerName)1552 public void delayedDamage(final int amount, final String attackerName) { 1553 final RPEntity me = this; 1554 /* 1555 * Use a dummy damager rpentity, so that we can follow the 1556 * normal code path. Important when dying. 1557 */ 1558 final Entity attacker = new RPEntity(this) { 1559 @Override 1560 public String getTitle() { 1561 return attackerName; 1562 } 1563 1564 @Override 1565 protected void dropItemsOn(Corpse corpse) { 1566 1567 } 1568 1569 @Override 1570 public void logic() { 1571 1572 } 1573 }; 1574 1575 SingletonRepository.getTurnNotifier().notifyInTurns(1, new TurnListener() { 1576 @Override 1577 public void onTurnReached(int turn) { 1578 me.damage(amount, attacker); 1579 } 1580 }); 1581 } 1582 1583 /** 1584 * Kills this RPEntity. 1585 * 1586 * @param killer 1587 * The killer 1588 */ kill(final Entity killer)1589 private void kill(final Entity killer) { 1590 setHP(0); 1591 SingletonRepository.getRuleProcessor().killRPEntity(this, killer); 1592 } 1593 1594 /** 1595 * For rewarding killers. Get the entity as a Player, if the entity is a 1596 * Player. If the player has logged out, try to get the corresponding online 1597 * player. 1598 * 1599 * @param entity entity to be checked 1600 * @return online Player corresponding to the entity, or {@code null} if the 1601 * entity is not a Player, or if the equivalent player is not online 1602 */ entityAsOnlinePlayer(Entity entity)1603 protected Player entityAsOnlinePlayer(Entity entity) { 1604 if (!(entity instanceof Player)) { 1605 return null; 1606 } 1607 Player killer = (Player) entity; 1608 if (killer.isDisconnected()) { 1609 // Try to get the corresponding online player: 1610 killer = SingletonRepository.getRuleProcessor().getPlayer(killer.getName()); 1611 } 1612 return killer; 1613 } 1614 entityAsPet(Entity entity)1615 protected Pet entityAsPet(Entity entity) { 1616 if (!(entity instanceof Pet)) { 1617 return null; 1618 } 1619 Pet killerPet = (Pet) entity; 1620 /* isDisconnected is undefined in object Pet; 1621 if (killer.isDisconnected()) { 1622 // Try to get the corresponding online player: 1623 killer = SingletonRepository.getRuleProcessor().getPlayer(killer.getName()); 1624 } 1625 */ 1626 return killerPet; 1627 } 1628 1629 /** 1630 * Gives XP to every player who has helped killing this RPEntity. 1631 * 1632 * @param oldXP 1633 * The XP that this RPEntity had before being killed. 1634 */ rewardKillers(final int oldXP)1635 protected void rewardKillers(final int oldXP) { 1636 final int xpReward = (int) (oldXP * 0.05); 1637 1638 for (Entry<Entity, Integer> entry : damageReceived.entrySet()) { 1639 final int damageDone = entry.getValue(); 1640 if (damageDone == 0) { 1641 continue; 1642 } 1643 1644 Player killer = entityAsOnlinePlayer(entry.getKey()); 1645 if (killer == null) { 1646 continue; 1647 } 1648 1649 TutorialNotifier.killedSomething(killer); 1650 1651 if (logger.isDebugEnabled() || Testing.DEBUG) { 1652 final String killName; 1653 if (killer.has("name")) { 1654 killName = killer.get("name"); 1655 } else { 1656 killName = killer.get("type"); 1657 } 1658 1659 logger.debug(killName + " did " + damageDone + " of " 1660 + totalDamageReceived + ". Reward was " + xpReward); 1661 } 1662 1663 final int xpEarn = (int) (xpReward * ((float) damageDone / (float) totalDamageReceived)); 1664 1665 if (logger.isDebugEnabled() || Testing.DEBUG) { 1666 logger.debug("OnDead: " + xpReward + "\t" + damageDone + "\t" 1667 + totalDamageReceived + "\t"); 1668 } 1669 1670 int reward = xpEarn; 1671 1672 // We ensure that the player gets at least 1 experience 1673 // point, because getting nothing lowers motivation. 1674 if (reward == 0) { 1675 reward = 1; 1676 } 1677 1678 killer.addXP(reward); 1679 1680 // For some quests etc., it is required that the player kills a 1681 // certain creature without the help of others. 1682 // Find out if the player killed this RPEntity on his own, but 1683 // don't overwrite solo with shared. 1684 final String killedName = getName(); 1685 1686 if (killedName == null) { 1687 logger.warn("This entity returns null as name: " + this); 1688 } else { 1689 if (damageDone == totalDamageReceived) { 1690 killer.setSoloKill(killedName); 1691 } else { 1692 killer.setSharedKill(killedName); 1693 } 1694 } 1695 1696 SingletonRepository.getAchievementNotifier().onKill(killer); 1697 1698 killer.notifyWorldAboutChanges(); 1699 } 1700 } 1701 1702 /* 1703 * Reward pets who kill enemies. don't perks like AchievementNotifier that players. 1704 */ rewardKillerAnimals(final int oldXP)1705 protected void rewardKillerAnimals(final int oldXP) { 1706 if (!System.getProperty("stendhal.petleveling", "false").equals("true")) { 1707 return; 1708 } 1709 final int xpReward = (int) (oldXP * 0.05); 1710 1711 for (Entry<Entity, Integer> entry : damageReceived.entrySet()) { 1712 final int damageDone = entry.getValue(); 1713 if (damageDone == 0) { 1714 continue; 1715 } 1716 1717 Pet killer = entityAsPet(entry.getKey()); 1718 if (killer == null) { 1719 continue; 1720 } 1721 1722 if (logger.isDebugEnabled() || Testing.DEBUG) { 1723 final String killName; 1724 if (killer.has("name")) { 1725 killName = killer.get("name"); 1726 } else { 1727 killName = killer.get("type"); 1728 } 1729 1730 logger.debug(killName + " did " + damageDone + " of " 1731 + totalDamageReceived + ". Reward was " + xpReward); 1732 } 1733 1734 final int xpEarn = (int) (xpReward * ((float) damageDone / (float) totalDamageReceived)); 1735 1736 if (logger.isDebugEnabled() || Testing.DEBUG) { 1737 logger.debug("OnDead: " + xpReward + "\t" + damageDone + "\t" 1738 + totalDamageReceived + "\t"); 1739 } 1740 1741 int reward = xpEarn; 1742 1743 // We ensure it gets at least 1 experience 1744 // point, because getting nothing lowers motivation. 1745 if (reward == 0) { 1746 reward = 1; 1747 } 1748 1749 if (killer.getLevel() >= killer.getLVCap()) 1750 { 1751 reward = 0; 1752 } 1753 1754 killer.addXP(reward); 1755 1756 /* 1757 // For some quests etc., it is required that the player kills a 1758 // certain creature without the help of others. 1759 // Find out if the player killed this RPEntity on his own, but 1760 // don't overwrite solo with shared. 1761 final String killedName = getName(); 1762 1763 if (killedName == null) { 1764 logger.warn("This entity returns null as name: " + this); 1765 } else { 1766 if (damageDone == totalDamageReceived) { 1767 killer.setSoloKill(killedName); 1768 } else { 1769 killer.setSharedKill(killedName); 1770 } 1771 } 1772 1773 SingletonRepository.getAchievementNotifier().onKill(killer); 1774 */ 1775 1776 killer.notifyWorldAboutChanges(); 1777 } 1778 } 1779 1780 /** 1781 * This method is called when the entity has been killed ( hp==0 ). 1782 * 1783 * @param killer 1784 * The entity who caused the death 1785 */ onDead(final Killer killer)1786 public final void onDead(final Killer killer) { 1787 onDead(killer, true); 1788 } 1789 1790 /** 1791 * This method is called when this entity has been killed (hp == 0). 1792 * 1793 * @param killer 1794 * The entity who caused the death, i.e. who did the last hit. 1795 * @param remove 1796 * true iff this entity should be removed from the world. For 1797 * almost everything remove is true, but not for the players, who 1798 * are instead moved to afterlife ("reborn"). 1799 */ onDead(final Killer killer, final boolean remove)1800 public void onDead(final Killer killer, final boolean remove) { 1801 StendhalKillLogDAO killLog = DAORegister.get().get(StendhalKillLogDAO.class); 1802 String killerName = killer.getName(); 1803 1804 if (killer instanceof RPEntity) { 1805 new GameEvent(killerName, "killed", this.getName(), killLog.entityToType(killer), killLog.entityToType(this)).raise(); 1806 } 1807 1808 DBCommandQueue.get().enqueue(new LogKillEventCommand(this, killer), DBCommandPriority.LOW); 1809 1810 die(killer, remove); 1811 } 1812 1813 /** 1814 * Build a list of killer names. 1815 * 1816 * @param killerName The "official" killer. This will be always included in 1817 * the list 1818 * @return list of killers 1819 */ buildKillerList(String killerName)1820 private List<String> buildKillerList(String killerName) { 1821 KillerList killers = new KillerList(); 1822 1823 for (Entry<Entity, Integer> entry : damageReceived.entrySet()) { 1824 final int damageDone = entry.getValue(); 1825 if (damageDone == 0) { 1826 continue; 1827 } 1828 1829 killers.addEntity(entry.getKey()); 1830 } 1831 if (killerName != null) { 1832 killers.setKiller(killerName); 1833 } 1834 return killers.asList(); 1835 } 1836 1837 /** 1838 * This method is called when this entity has been killed (hp == 0). 1839 * 1840 * @param killer the "official" killer 1841 * @param remove 1842 * <code>true</code> to remove entity from world. 1843 */ die(Killer killer, final boolean remove)1844 private void die(Killer killer, final boolean remove) { 1845 StendhalRPZone zone = this.getZone(); 1846 if ((zone == null) || !zone.has(this.getID())) { 1847 logger.warn("RPEntity died but is not in a zone"); 1848 return; 1849 } 1850 1851 String killerName = killer.getName(); 1852 // Needs to be done while the killer map still has the contents 1853 List<String> killers = buildKillerList(killerName); 1854 1855 final int oldXP = this.getXP(); 1856 1857 // Establish how much xp points your are rewarded 1858 // give XP to everyone who helped killing this RPEntity 1859 rewardKillers(oldXP); 1860 rewardKillerAnimals(oldXP); 1861 1862 if (!(killer instanceof Player) && !(killer instanceof Status) && !(killer instanceof Pet)) { 1863 /* 1864 * Prettify the killer name for the corpse. Should be done only 1865 * after the more plain version has been used for the killer list. 1866 * Players are unique, so they should not get an article. Also 1867 * statuses should not, so that "killed by poison" does not become 1868 * "killed by a bottle of poison". 1869 */ 1870 killerName = Grammar.a_noun(killerName); 1871 } 1872 // Add a corpse 1873 final Corpse corpse = makeCorpse(killerName); 1874 damageReceived.clear(); 1875 totalDamageReceived = 0; 1876 1877 // Stats about dead 1878 if (has("name")) { 1879 stats.add("Killed " + get("name"), 1); 1880 } else { 1881 stats.add("Killed " + get("type"), 1); 1882 } 1883 1884 // Add some reward inside the corpse 1885 dropItemsOn(corpse); 1886 updateItemAtkDef(); 1887 1888 // Adding to zone clears events, so the sound needs to be added after that. 1889 zone.add(corpse); 1890 if (deathSound != null) { 1891 corpse.addEvent(new SoundEvent(deathSound, 23, 100, SoundLayer.FIGHTING_NOISE)); 1892 corpse.notifyWorldAboutChanges(); 1893 } 1894 1895 StringBuilder deathMessage = new StringBuilder(getName()); 1896 deathMessage.append(" has been killed"); 1897 if (!killers.isEmpty()) { 1898 deathMessage.append(" by "); 1899 deathMessage.append(Grammar.enumerateCollection(killers)); 1900 } 1901 corpse.addEvent(new TextEvent(deathMessage.toString())); 1902 1903 // Corpse may want to know who this entity was attacking (RaidCreatureCorpse does), 1904 // so defer stopping. 1905 stopAttack(); 1906 if (statusList != null) { 1907 statusList.removeAll(); 1908 } 1909 if (remove) { 1910 zone.remove(this); 1911 } 1912 } 1913 1914 /** 1915 * Make a corpse belonging to this entity 1916 * 1917 * @param killer Name of the killer 1918 * @return The corpse of a dead RPEntity 1919 */ makeCorpse(String killer)1920 protected Corpse makeCorpse(String killer) { 1921 return new Corpse(this, killer); 1922 } 1923 1924 /** 1925 * Get the corpse image name to be used for the entity. 1926 * Defaults to a player corpse. 1927 * 1928 * @return Identification string for corpse. This is the corpse 1929 * image shown by the client without the path or file extension. 1930 */ getCorpseName()1931 public String getCorpseName() { 1932 return "player"; 1933 } 1934 getHarmlessCorpseName()1935 public String getHarmlessCorpseName() { 1936 return "harmless_player"; 1937 } 1938 getCorpseWidth()1939 public int getCorpseWidth() { 1940 return 1; 1941 } 1942 getCorpseHeight()1943 public int getCorpseHeight() { 1944 return 1; 1945 } 1946 dropItemsOn(Corpse corpse)1947 protected abstract void dropItemsOn(Corpse corpse); 1948 1949 /** 1950 * Determine if the entity is invisible to creatures. 1951 * 1952 * @return <code>true</code> if invisible. 1953 */ isInvisibleToCreatures()1954 public boolean isInvisibleToCreatures() { 1955 return false; 1956 } 1957 1958 /** 1959 * Return true if this entity is attacked. 1960 * 1961 * @return true if no attack sources found 1962 */ isAttacked()1963 public boolean isAttacked() { 1964 return !attackSources.isEmpty(); 1965 } 1966 1967 /** 1968 * Returns the Entities that are attacking this character. 1969 * 1970 * @return list of all attacking entities 1971 */ getAttackSources()1972 public List<Entity> getAttackSources() { 1973 return attackSources; 1974 } 1975 1976 /** 1977 * Returns the RPEntities that are attacking this character. 1978 * 1979 * @return list of all attacking RPEntities 1980 */ getAttackingRPEntities()1981 public List<RPEntity> getAttackingRPEntities() { 1982 final List<RPEntity> list = new ArrayList<>(); 1983 1984 for (final Entity entity : getAttackSources()) { 1985 if (entity instanceof RPEntity) { 1986 list.add((RPEntity) entity); 1987 } 1988 } 1989 return list; 1990 } 1991 1992 /** 1993 * Checks whether the attacktarget is null. Sets attacktarget to null if hp 1994 * of attacktarget <=0; 1995 * 1996 * @return true if attacktarget != null and not dead 1997 */ isAttacking()1998 public boolean isAttacking() { 1999 if (attackTarget != null) { 2000 if (attackTarget.getHP() <= 0) { 2001 attackTarget = null; 2002 } 2003 } else { 2004 return false; 2005 } 2006 return attackTarget != null; 2007 } 2008 2009 /** 2010 * Return the RPEntity that this entity is attacking. 2011 * 2012 * @return the attack target of this 2013 */ getAttackTarget()2014 public RPEntity getAttackTarget() { 2015 return attackTarget; 2016 } 2017 2018 /*************************************************************************** 2019 * * Equipment handling. * * 2020 **************************************************************************/ 2021 2022 /** 2023 * Tries to equip an item in the appropriate slot. 2024 * 2025 * @param item 2026 * the item 2027 * @return true if the item can be equipped, else false 2028 */ equipToInventoryOnly(final Item item)2029 public final boolean equipToInventoryOnly(final Item item) { 2030 final RPSlot slot = getSlotToEquip(item); 2031 if (slot != null) { 2032 return equipIt(slot, item); 2033 } else { 2034 return false; 2035 } 2036 } 2037 2038 /** 2039 * Check if an object is a stackable item that can be merged to an existing 2040 * item stack. 2041 * 2042 * @param item stackable item 2043 * @param object merge candidate 2044 * @return <code>true</code> if the items can be merged, <code>false</code> 2045 * otherwise 2046 */ canMergeItems(StackableItem item, RPObject object)2047 private boolean canMergeItems(StackableItem item, RPObject object) { 2048 if (object instanceof StackableItem) { 2049 final StackableItem other = (StackableItem) object; 2050 if (other.isStackable(item)) { 2051 return true; 2052 } 2053 } 2054 return false; 2055 } 2056 2057 /** 2058 * Find slot where an item could be merged, looking recursively inside a 2059 * slot and the content slots of the items in that slot. 2060 * 2061 * @param item item for which the merge location is sought for 2062 * @param slot starting location slot 2063 * @return slot where the item can be merged, or <code>null</code> if no 2064 * suitable location was found 2065 */ getSlotToMerge(StackableItem item, RPSlot slot)2066 private RPSlot getSlotToMerge(StackableItem item, RPSlot slot) { 2067 if (slot instanceof EntitySlot) { 2068 if (!((EntitySlot) slot).isReachableForThrowingThingsIntoBy(this)) { 2069 return null; 2070 } 2071 } 2072 // Try first merging the item in the parent slot, so that the item 2073 // appears as visibly as possible 2074 if (item.getPossibleSlots().contains(slot.getName())) { 2075 for (RPObject obj : slot) { 2076 if (canMergeItems(item, obj)) { 2077 return slot; 2078 } 2079 } 2080 } 2081 // Then check the slots of the contained items 2082 for (RPObject obj : slot) { 2083 for (RPSlot childSlot : obj.slots()) { 2084 RPSlot tmp = getSlotToMerge(item, childSlot); 2085 if (tmp != null) { 2086 return tmp; 2087 } 2088 } 2089 } 2090 2091 return null; 2092 } 2093 2094 /** 2095 * Find a target slot where an item can be equipped. The slots are sought 2096 * recursively starting from a specified initial slot, and then proceeding 2097 * to the content slots of the items in that slot. 2098 * 2099 * @param item item to be equipped 2100 * @param slot starting slot 2101 * @return slot where the item can be equipped, or <code>null</code> if no 2102 * suitable location was found 2103 */ getSlotToEquip(Item item, RPSlot slot)2104 private RPSlot getSlotToEquip(Item item, RPSlot slot) { 2105 if (slot instanceof EntitySlot) { 2106 if (!((EntitySlot) slot).isReachableForThrowingThingsIntoBy(this)) { 2107 return null; 2108 } 2109 } 2110 if (item.getPossibleSlots().contains(slot.getName())) { 2111 if (!slot.isFull()) { 2112 return slot; 2113 } 2114 } 2115 for (RPObject obj : slot) { 2116 for (RPSlot childSlot : obj.slots()) { 2117 RPSlot tmp = getSlotToEquip(item, childSlot); 2118 if (tmp != null) { 2119 return tmp; 2120 } 2121 } 2122 } 2123 2124 return null; 2125 } 2126 2127 /** 2128 * Gets the slot in which the entity can equip the item, preferring 2129 * locations where the item can be merged with existing item stacks. 2130 * 2131 * @param item 2132 * @return the slot for the item or null if there is no matching slot 2133 * in the entity 2134 */ getSlotToEquip(final Item item)2135 public final RPSlot getSlotToEquip(final Item item) { 2136 if (item instanceof StackableItem) { 2137 // Try merging the item first 2138 for (RPSlot slot : slots()) { 2139 RPSlot tmp = getSlotToMerge((StackableItem) item, slot); 2140 if (tmp != null) { 2141 return tmp; 2142 } 2143 } 2144 } 2145 2146 // We can't stack it on another item. Check if we can simply 2147 // add it to an empty cell. 2148 for (RPSlot slot : slots()) { 2149 RPSlot tmp = getSlotToEquip(item, slot); 2150 if (tmp != null) { 2151 return tmp; 2152 } 2153 } 2154 return null; 2155 } 2156 2157 /** 2158 * Tries to equip an item in the appropriate slot. 2159 * 2160 * @param item the item 2161 * @return true if the item can be equipped, else false 2162 */ equipOrPutOnGround(final Item item)2163 public final boolean equipOrPutOnGround(final Item item) { 2164 if (equipToInventoryOnly(item)) { 2165 return true; 2166 } else { 2167 item.setPosition(getX(), getY()); 2168 getZone().add(item); 2169 this.sendPrivateText("You dropped the new item onto the ground because your bag is full."); 2170 return false; 2171 } 2172 } 2173 2174 2175 2176 /** 2177 * Tries to equip one unit of an item in the given slot. Note: This doesn't 2178 * check if it is allowed to put the given item into the given slot, e.g. it 2179 * is possible to wear your helmet at your feet using this method. 2180 * 2181 * @param slotName 2182 * the name of the slot 2183 * @param item 2184 * the item 2185 * @return true if the item can be equipped, else false 2186 */ equip(final String slotName, final Item item)2187 public final boolean equip(final String slotName, final Item item) { 2188 RPSlot slot = getSlot(slotName); 2189 if (equipIt(slot, item)) { 2190 updateItemAtkDef(); 2191 return true; 2192 } 2193 return false; 2194 } 2195 2196 /** 2197 * Removes a specific amount of an item from the RPEntity. The item can 2198 * either be stackable or non-stackable. The units can be distributed over 2199 * different slots. If the RPEntity doesn't have enough units of the item, 2200 * doesn't remove anything. 2201 * 2202 * @param name 2203 * The name of the item 2204 * @param amount 2205 * The number of units that should be dropped 2206 * @return true iff dropping the desired amount was successful. 2207 */ drop(final String name, final int amount)2208 public boolean drop(final String name, final int amount) { 2209 return drop(nameMatches(name), amount); 2210 } 2211 isEquipped(Predicate<Item> condition, int amount)2212 private boolean isEquipped(Predicate<Item> condition, int amount) { 2213 Iterable<Item> matching = getAllEquipped(condition)::iterator; 2214 int count = 0; 2215 for (Item item : matching) { 2216 count += item.getQuantity(); 2217 if (count >= amount) { 2218 return true; 2219 } 2220 } 2221 return false; 2222 } 2223 drop(Predicate<Item> condition, int amount)2224 private boolean drop(Predicate<Item> condition, int amount) { 2225 if (!isEquipped(condition, amount)) { 2226 return false; 2227 } 2228 2229 int toDrop = amount; 2230 Iterable<Item> matchingItems = equippedStream().filter(condition)::iterator; 2231 for (Item item : matchingItems) { 2232 toDrop -= dropItem(item, toDrop); 2233 if (toDrop == 0) { 2234 return true; 2235 } 2236 } 2237 2238 logger.error("Not enough items dropped even though the entity was checked to have them", new Throwable()); 2239 return false; 2240 } 2241 2242 /** 2243 * Low level drop. <b>Does not check the containing slot or owner. This is 2244 * meant to be used only by higher level drop() methods.</b> 2245 * 2246 * @param item dropped item 2247 * @param amount maximum amout to drop 2248 * @return dropped amount 2249 */ dropItem(Item item, int amount)2250 private int dropItem(Item item, int amount) { 2251 RPSlot slot = item.getContainerSlot(); 2252 if (item instanceof StackableItem) { 2253 // The item is stackable, we try to remove 2254 // multiple ones. 2255 final int quantity = item.getQuantity(); 2256 if (amount >= quantity) { 2257 new ItemLogger().destroy(this, slot, item); 2258 slot.remove(item.getID()); 2259 return quantity; 2260 } else { 2261 ((StackableItem) item).setQuantity(quantity - amount); 2262 new ItemLogger().splitOff(this, item, amount); 2263 return amount; 2264 } 2265 } else { 2266 // The item is not stackable, so we only remove a 2267 // single one. 2268 slot.remove(item.getID()); 2269 new ItemLogger().destroy(this, slot, item); 2270 return 1; 2271 } 2272 } 2273 2274 /** 2275 * Removes one unit of an item from the RPEntity. The item can either be 2276 * stackable or non-stackable. If the RPEntity doesn't have enough the item, 2277 * doesn't remove anything. 2278 * 2279 * @param name 2280 * The name of the item 2281 * @return true iff dropping the item was successful. 2282 */ drop(final String name)2283 public boolean drop(final String name) { 2284 return drop(name, 1); 2285 } 2286 2287 /** 2288 * Removes the given item from the RPEntity. The item can either be 2289 * stackable or non-stackable. If the RPEntity doesn't have the item, 2290 * doesn't remove anything. 2291 * 2292 * @param item 2293 * the item that should be removed 2294 * @return true iff dropping the item was successful. 2295 */ drop(final Item item)2296 public boolean drop(final Item item) { 2297 return drop(it -> item == it, 1); 2298 } 2299 2300 /** 2301 * Removes a specific amount of an item with matching info string from 2302 * the RPEntity. The item can either be stackable or non-stackable. 2303 * The units can be distributed over different slots. If the RPEntity 2304 * doesn't have enough units of the item, doesn't remove anything. 2305 * 2306 * @param name 2307 * Name of item to remove. 2308 * @param infostring 2309 * Required item info string to match. 2310 * @param amount 2311 * Number of items to remove from entity. 2312 * @return 2313 * <code>true</code> if dropping the item(s) was successful. 2314 */ dropWithInfostring(final String name, final String infostring, final int amount)2315 public boolean dropWithInfostring(final String name, final String infostring, final int amount) { 2316 return drop(item -> (name.equals(item.getName()) && infostring.equals(item.getInfoString())), amount); 2317 } 2318 2319 /** 2320 * Removes a single item with matching info string from the RPEntity. 2321 * The item can either be stackable or non-stackable. The units can 2322 * be distributed over different slots. If the RPEntity doesn't have 2323 * enough units of the item, doesn't remove anything. 2324 * 2325 * @param name 2326 * Name of item to remove. 2327 * @param infostring 2328 * Required item info string to match. 2329 * @return 2330 * <code>true</code> if dropping the item(s) was successful. 2331 */ dropWithInfostring(final String name, final String infostring)2332 public boolean dropWithInfostring(final String name, final String infostring) { 2333 return dropWithInfostring(name, infostring, 1); 2334 } 2335 2336 /** 2337 * Determine if this entity is equipped with a minimum quantity of an item. 2338 * 2339 * @param name 2340 * The item name. 2341 * @param amount 2342 * The minimum amount. 2343 * 2344 * @return <code>true</code> if the item is equipped with the minimum 2345 * number. 2346 */ isEquipped(final String name, final int amount)2347 public boolean isEquipped(final String name, final int amount) { 2348 return isEquipped(nameMatches(name), amount); 2349 } 2350 2351 /** 2352 * Determine if this entity is equipped with an item. 2353 * 2354 * @param name 2355 * The item name. 2356 * 2357 * @return <code>true</code> if the item is equipped. 2358 */ isEquipped(final String name)2359 public boolean isEquipped(final String name) { 2360 return isEquipped(name, 1); 2361 } 2362 2363 /** 2364 * Checks if entity carry a number of items with specified info string. 2365 * 2366 * @param name 2367 * Name of item to check. 2368 * @param infostring 2369 * Info string of item to check. 2370 * @param amount 2371 * Quantity of carried items to check. 2372 * @return 2373 * <code>true</code> if entity is carrying at least specified amount of items matching name & infostring. 2374 */ isEquippedWithInfostring(final String name, final String infostring, final int amount)2375 public boolean isEquippedWithInfostring(final String name, final String infostring, final int amount) { 2376 return getAllEquippedWithInfostring(name, infostring).size() >= amount; 2377 } 2378 2379 /** 2380 * Checks if entity carry a number of items with specified info string. 2381 * 2382 * @param name 2383 * Name of item to check. 2384 * @param infostring 2385 * Info string of item to check. 2386 * @return 2387 * <code>true</code> if entity is carrying at least one of items matching name & infostring. 2388 */ isEquippedWithInfostring(final String name, final String infostring)2389 public boolean isEquippedWithInfostring(final String name, final String infostring) { 2390 return isEquippedWithInfostring(name, infostring, 1); 2391 } 2392 2393 /** 2394 * Gets the number of items of the given name that are carried by the 2395 * RPEntity. The item can either be stackable or non-stackable. 2396 * 2397 * @param name 2398 * The item's name 2399 * @return The number of carried items 2400 */ getNumberOfEquipped(final String name)2401 public int getNumberOfEquipped(final String name) { 2402 return equippedStream().filter(nameMatches(name)) 2403 .mapToInt(Item::getQuantity).sum(); 2404 } 2405 2406 /** 2407 * Gets the number of items of the given name including bank. 2408 * The item can either be stackable or non-stackable. 2409 * 2410 * @param name 2411 * The item's name 2412 * @return The number of carried items 2413 */ getTotalNumberOf(final String name)2414 public int getTotalNumberOf(final String name) { 2415 Stream<Item> allItems = slots().stream().flatMap(this::slotStream); 2416 return allItems.filter(nameMatches(name)).mapToInt(Item::getQuantity).sum(); 2417 } 2418 2419 /** 2420 * Gets an item that is carried by the RPEntity. If the item is stackable, 2421 * gets all that are on the first stack that is found. 2422 * 2423 * @param name 2424 * The item's name 2425 * @return The item, or a stack of stackable items, or null if nothing was 2426 * found 2427 */ getFirstEquipped(final String name)2428 public Item getFirstEquipped(final String name) { 2429 return equippedStream().filter(nameMatches(name)).findFirst().orElse(null); 2430 } 2431 2432 /** 2433 * Gets an item that is carried by the RPEntity. If the item is stackable, 2434 * gets all that are on the first stack that is found. 2435 * 2436 * @param name 2437 * The item's name 2438 * @return The item, or a stack of stackable items, or an empty list if nothing was 2439 * found 2440 */ getAllEquipped(final String name)2441 public List<Item> getAllEquipped(final String name) { 2442 return getAllEquipped(nameMatches(name)); 2443 } 2444 getAllEquipped(Predicate<Item> condition)2445 private List<Item> getAllEquipped(Predicate<Item> condition) { 2446 return equippedStream().filter(condition).collect(Collectors.toList()); 2447 } 2448 2449 /** 2450 * Retrieves all of an item with matching info string. 2451 * 2452 * @param name 2453 * Name of item to match. 2454 * @param infostring 2455 * Info string of item to match. 2456 * @return 2457 * List<Item> 2458 */ getAllEquippedWithInfostring(String name, String infostring)2459 public List<Item> getAllEquippedWithInfostring(String name, String infostring) { 2460 return getAllEquipped(item -> name.equals(item.getName()) 2461 && infostring.equalsIgnoreCase(item.getInfoString())); 2462 } 2463 2464 /** 2465 * checks if an item of class <i>clazz</i> is equipped in slot <i>slot</i> 2466 * returns true if it is, else false. 2467 * 2468 * @param slot 2469 * @param clazz 2470 * @return true if so false otherwise 2471 */ isEquippedItemClass(final String slot, final String clazz)2472 public boolean isEquippedItemClass(final String slot, final String clazz) { 2473 if (hasSlot(slot)) { 2474 // get slot if the this entity has one 2475 final RPSlot rpslot = getSlot(slot); 2476 // traverse all slot items 2477 for (final RPObject item : rpslot) { 2478 if ((item instanceof Item) && ((Item) item).isOfClass(clazz)) { 2479 return true; 2480 } 2481 } 2482 } 2483 2484 // no slot, free slot or wrong item type 2485 return false; 2486 } 2487 2488 2489 /** 2490 * checks if an item is equipped in a slot 2491 * 2492 * @param slot 2493 * @param item 2494 * @return true if so false otherwise 2495 */ isEquippedItemInSlot(final String slot, final String item)2496 public boolean isEquippedItemInSlot(final String slot, final String item) { 2497 if (hasSlot(slot)) { 2498 final RPSlot rpslot = getSlot(slot); 2499 for (final RPObject object : rpslot) { 2500 if ((object instanceof Item) && ((Item) object).getName().equals(item)) { 2501 return true; 2502 } 2503 } 2504 } 2505 2506 // no slot, free slot or wrong item type 2507 return false; 2508 } 2509 2510 /** 2511 * Finds the first item of class <i>clazz</i> from the slot. 2512 * 2513 * @param slot 2514 * @param clazz 2515 * @return the item or <code>null</code> if there is no item with the 2516 * requested clazz. 2517 */ getEquippedItemClass(final String slot, final String clazz)2518 public Item getEquippedItemClass(final String slot, final String clazz) { 2519 if (hasSlot(slot)) { 2520 // get slot if the this entity has one 2521 final RPSlot rpslot = getSlot(slot); 2522 // traverse all slot items 2523 for (final RPObject object : rpslot) { 2524 // is it the right type 2525 if (object instanceof Item) { 2526 final Item item = (Item) object; 2527 if (item.isOfClass(clazz)) { 2528 return item; 2529 } 2530 } 2531 } 2532 } 2533 2534 // no slot, free slot or wrong item type 2535 return null; 2536 } 2537 2538 2539 /** 2540 * Gets the weapon that this entity is holding in its hands. 2541 * 2542 * @return The weapon, or null if this entity is not holding a weapon. If 2543 * the entity has a weapon in each hand, returns the weapon in its 2544 * left hand. 2545 */ getWeapon()2546 public Item getWeapon() { 2547 final String[] weaponsClasses = {"club", "sword", "axe", "ranged", "missile"}; 2548 2549 for (final String weaponClass : weaponsClasses) { 2550 final String[] slots = { "lhand", "rhand" }; 2551 for (final String slot : slots) { 2552 final Item item = getEquippedItemClass(slot, weaponClass); 2553 if (item != null) { 2554 return item; 2555 } 2556 } 2557 } 2558 2559 return null; 2560 } 2561 getWeapons()2562 public List<Item> getWeapons() { 2563 final List<Item> weapons = new ArrayList<>(); 2564 Item weaponItem = getWeapon(); 2565 if (weaponItem != null) { 2566 weapons.add(weaponItem); 2567 2568 // pair weapons 2569 if (weaponItem.getName().startsWith("l hand ")) { 2570 // check if there is a matching right-hand weapon in 2571 // the other hand. 2572 final String rpclass = weaponItem.getItemClass(); 2573 weaponItem = getEquippedItemClass("rhand", rpclass); 2574 if ((weaponItem != null) 2575 && (weaponItem.getName().startsWith("r hand "))) { 2576 weapons.add(weaponItem); 2577 } else { 2578 // You can't use a left-hand weapon without the matching 2579 // right-hand weapon. Hmmm... but why not? 2580 weapons.clear(); 2581 } 2582 } else { 2583 // You can't hold a right-hand weapon with your left hand, for 2584 // ergonomic reasons ;) 2585 if (weaponItem.getName().startsWith("r hand ")) { 2586 weapons.clear(); 2587 } 2588 } 2589 } 2590 2591 return weapons; 2592 } 2593 2594 /** 2595 * Gets the range weapon (bow etc.) that this entity is holding in its 2596 * hands. 2597 * 2598 * @return The range weapon, or null if this entity is not holding a range 2599 * weapon. If the entity has a range weapon in each hand, returns 2600 * one in its left hand. 2601 */ getRangeWeapon()2602 public Item getRangeWeapon() { 2603 for (final Item weapon : getWeapons()) { 2604 if (weapon.isOfClass("ranged")) { 2605 return weapon; 2606 } 2607 } 2608 2609 return null; 2610 } 2611 2612 /** 2613 * Gets the stack of ammunition (arrows or similar) that this entity is 2614 * holding in its hands. 2615 * 2616 * @return The ammunition, or null if this entity is not holding ammunition. 2617 * If the entity has ammunition in each hand, returns the ammunition 2618 * in its left hand. 2619 */ getAmmunition()2620 public StackableItem getAmmunition() { 2621 final String[] slots = { "lhand", "rhand" }; 2622 2623 for (final String slot : slots) { 2624 final StackableItem item = (StackableItem) getEquippedItemClass( 2625 slot, "ammunition"); 2626 if (item != null) { 2627 return item; 2628 } 2629 } 2630 2631 return null; 2632 } 2633 2634 /** 2635 * Gets the stack of missiles (spears or similar) that this entity is 2636 * holding in its hands, but only if it is not holding another, non-missile 2637 * weapon in the other hand. 2638 * 2639 * You can only throw missiles while you're not holding another weapon. This 2640 * restriction is a workaround because of the way attack strength is 2641 * determined; otherwise, one could increase one's spear attack strength by 2642 * holding an ice sword in the other hand. 2643 * 2644 * @return The missiles, or null if this entity is not holding missiles. If 2645 * the entity has missiles in each hand, returns the missiles in its 2646 * left hand. 2647 */ getMissileIfNotHoldingOtherWeapon()2648 public StackableItem getMissileIfNotHoldingOtherWeapon() { 2649 StackableItem missileWeaponItem = null; 2650 boolean holdsOtherWeapon = false; 2651 2652 for (final Item weaponItem : getWeapons()) { 2653 if (weaponItem.isOfClass("missile")) { 2654 missileWeaponItem = (StackableItem) weaponItem; 2655 } else { 2656 holdsOtherWeapon = true; 2657 } 2658 } 2659 2660 if (holdsOtherWeapon) { 2661 return null; 2662 } else { 2663 return missileWeaponItem; 2664 } 2665 } 2666 2667 /** @return true if the entity has an item of class shield equipped. */ hasShield()2668 public boolean hasShield() { 2669 return isEquippedItemClass("lhand", "shield") 2670 || isEquippedItemClass("rhand", "shield"); 2671 } 2672 getShield()2673 public Item getShield() { 2674 final Item item = getEquippedItemClass("lhand", "shield"); 2675 if (item != null) { 2676 return item; 2677 } else { 2678 return getEquippedItemClass("rhand", "shield"); 2679 } 2680 } 2681 hasArmor()2682 public boolean hasArmor() { 2683 return isEquippedItemClass("armor", "armor"); 2684 } 2685 getArmor()2686 public Item getArmor() { 2687 return getEquippedItemClass("armor", "armor"); 2688 } 2689 hasHelmet()2690 public boolean hasHelmet() { 2691 return isEquippedItemClass("head", "helmet"); 2692 } 2693 getHelmet()2694 public Item getHelmet() { 2695 return getEquippedItemClass("head", "helmet"); 2696 } 2697 hasLegs()2698 public boolean hasLegs() { 2699 return isEquippedItemClass("legs", "legs"); 2700 } 2701 getLegs()2702 public Item getLegs() { 2703 return getEquippedItemClass("legs", "legs"); 2704 } 2705 hasBoots()2706 public boolean hasBoots() { 2707 return isEquippedItemClass("feet", "boots"); 2708 } 2709 getBoots()2710 public Item getBoots() { 2711 return getEquippedItemClass("feet", "boots"); 2712 } 2713 hasCloak()2714 public boolean hasCloak() { 2715 return isEquippedItemClass("cloak", "cloak"); 2716 } 2717 getCloak()2718 public Item getCloak() { 2719 return getEquippedItemClass("cloak", "cloak"); 2720 } 2721 hasRing()2722 public boolean hasRing() { 2723 return isEquippedItemClass("finger", "ring"); 2724 } 2725 getRing()2726 public Item getRing() { 2727 return getEquippedItemClass("finger", "ring"); 2728 } 2729 2730 @Override describe()2731 public String describe() { 2732 String text = super.describe(); 2733 if (getLevel() > 0) { 2734 boolean showLevel = true; 2735 if (this instanceof Creature) { 2736 /** 2737 * Hide level information of chess pieces. 2738 * 2739 * Don't call "Creature.isAbnormal" method here since it also checks "rare" attribute. 2740 */ 2741 if (((Creature) this).getAIProfiles().containsKey("abnormal") && 2742 (this.getName().startsWith("chaos") || this.getName().startsWith("madaram"))) { 2743 showLevel = false; 2744 } 2745 } 2746 2747 if (showLevel) { 2748 text += " It is level " + getLevel() + "."; 2749 } 2750 } 2751 2752 return text; 2753 } 2754 2755 /** 2756 * Sends a message that only this RPEntity can read. In this default 2757 * implementation, this method does nothing; it can be overridden in 2758 * subclasses. 2759 * 2760 * @param text 2761 * The message. 2762 */ sendPrivateText(final String text)2763 public void sendPrivateText(final String text) { 2764 // does nothing in this implementation. 2765 } 2766 2767 /** 2768 * Sends a message that only this player can read. 2769 * 2770 * @param type 2771 * NotificationType 2772 * @param text 2773 * the message. 2774 */ sendPrivateText(final NotificationType type, final String text)2775 public void sendPrivateText(final NotificationType type, final String text) { 2776 // does nothing in this implementation. 2777 } 2778 2779 /** 2780 * Retrieves total ATK value of held weapons. 2781 */ getItemAtk()2782 public float getItemAtk() { 2783 int weapon = 0; 2784 int ring = 0; 2785 2786 final List<Item> weapons = getWeapons(); 2787 for (final Item weaponItem : weapons) { 2788 weapon += weaponItem.getAttack(); 2789 } 2790 2791 // calculate ammo when not using RATK stat 2792 if (!Testing.COMBAT && weapons.size() > 0) { 2793 if (getWeapons().get(0).isOfClass("ranged")) { 2794 weapon += getAmmoAtk(); 2795 } 2796 } 2797 2798 if (hasRing()) { 2799 ring = getRing().getAttack(); 2800 } 2801 2802 return weapon + ring; 2803 } 2804 2805 /** 2806 * Retrieves total range attack value of held weapon & ammunition. 2807 */ getItemRatk()2808 public float getItemRatk() { 2809 float ratk = 0; 2810 final List<Item> weapons = getWeapons(); 2811 2812 if (weapons.size() > 0) { 2813 final Item held = getWeapons().get(0); 2814 ratk += held.getRangedAttack(); 2815 2816 if (held.isOfClass("ranged")) { 2817 ratk += getAmmoAtk(); 2818 } 2819 } 2820 2821 return ratk; 2822 } 2823 2824 /** 2825 * Retrieves ATK or RATK (depending on testing.combat system property) value of equipped ammunition. 2826 */ getAmmoAtk()2827 private float getAmmoAtk() { 2828 float ammo = 0; 2829 2830 final StackableItem ammoItem = getAmmunition(); 2831 if (ammoItem != null) { 2832 if (Testing.COMBAT) { 2833 ammo = ammoItem.getRangedAttack(); 2834 } else { 2835 ammo = ammoItem.getAttack(); 2836 } 2837 } 2838 2839 return ammo; 2840 } 2841 getItemDef()2842 public float getItemDef() { 2843 int shield = 0; 2844 int armor = 0; 2845 int helmet = 0; 2846 int legs = 0; 2847 int boots = 0; 2848 int cloak = 0; 2849 int weapon = 0; 2850 int ring = 0; 2851 2852 Item item; 2853 2854 if (hasShield()) { 2855 item = getShield(); 2856 shield = (int) (item.getDefense() / getItemLevelModifier(item)); 2857 } 2858 2859 if (hasArmor()) { 2860 item = getArmor(); 2861 armor = (int) (item.getDefense() / getItemLevelModifier(item)); 2862 } 2863 2864 if (hasHelmet()) { 2865 item = getHelmet(); 2866 helmet = (int) (item.getDefense() / getItemLevelModifier(item)); 2867 } 2868 2869 if (hasLegs()) { 2870 item = getLegs(); 2871 legs = (int) (item.getDefense() / getItemLevelModifier(item)); 2872 } 2873 2874 if (hasBoots()) { 2875 item = getBoots(); 2876 boots = (int) (item.getDefense() / getItemLevelModifier(item)); 2877 } 2878 2879 if (hasCloak()) { 2880 item = getCloak(); 2881 cloak = (int) (item.getDefense() / getItemLevelModifier(item)); 2882 } 2883 2884 if (hasRing()) { 2885 item = getRing(); 2886 ring = (int) (item.getDefense() / getItemLevelModifier(item)); 2887 } 2888 2889 final List<Item> targetWeapons = getWeapons(); 2890 for (final Item weaponItem : targetWeapons) { 2891 weapon += weaponItem.getDefense() / getItemLevelModifier(weaponItem); 2892 } 2893 2894 return SHIELD_DEF_MULTIPLIER * shield + ARMOR_DEF_MULTIPLIER * armor 2895 + CLOAK_DEF_MULTIPLIER * cloak + HELMET_DEF_MULTIPLIER * helmet 2896 + LEG_DEF_MULTIPLIER * legs + BOOTS_DEF_MULTIPLIER * boots 2897 + WEAPON_DEF_MULTIPLIER * weapon + RING_DEF_MULTIPLIER * ring; 2898 } 2899 2900 /** 2901 * get all items that affect a player's defensive value except the weapon 2902 * 2903 * @return a list of all equipped defensive items 2904 */ getDefenseItems()2905 public List<Item> getDefenseItems() { 2906 List<Item> items = new LinkedList<>(); 2907 if (hasShield()) { 2908 items.add(getShield()); 2909 } 2910 if (hasArmor()) { 2911 items.add(getArmor()); 2912 } 2913 if (hasHelmet()) { 2914 items.add(getHelmet()); 2915 } 2916 if (hasLegs()) { 2917 items.add(getLegs()); 2918 } 2919 2920 if (hasBoots()) { 2921 items.add(getBoots()); 2922 } 2923 if (hasCloak()) { 2924 items.add(getCloak()); 2925 } 2926 return items; 2927 } 2928 2929 /** 2930 * Recalculates item based atk and def. 2931 */ updateItemAtkDef()2932 public void updateItemAtkDef() { 2933 put("atk_item", ((int) getItemAtk())); 2934 if (Testing.COMBAT) { 2935 put("ratk_item", ((int) getItemRatk())); 2936 } 2937 put("def_item", ((int) getItemDef())); 2938 notifyWorldAboutChanges(); 2939 } 2940 2941 /** 2942 * Can this entity do a distance attack on the given target? 2943 * 2944 * @param target 2945 * @param maxrange maximum attack distance 2946 * 2947 * @return true if this entity is armed with a distance weapon and if the 2948 * target is in range. 2949 */ canDoRangeAttack(final RPEntity target, final int maxrange)2950 public boolean canDoRangeAttack(final RPEntity target, final int maxrange) { 2951 // the target's in range 2952 return (squaredDistance(target) <= maxrange * maxrange); 2953 } 2954 2955 /** 2956 * Check if the entity has a line of sight to the the center of another 2957 * entity. Only static collisions are checked. 2958 * 2959 * @param target target entity 2960 * @return <code>true</code> if there are no collisions blocking the line 2961 * of sight, <code>false</code> otherwise 2962 */ hasLineOfSight(final Entity target)2963 public boolean hasLineOfSight(final Entity target) { 2964 return !getZone().collidesOnLine((int) (getX() + getWidth() / 2), 2965 (int) (getY() + getHeight() / 2), 2966 (int) (target.getX() + target.getWidth() / 2), 2967 (int) (target.getY() + target.getHeight() / 2)); 2968 } 2969 2970 /** 2971 * Get the maximum distance attack range. 2972 * 2973 * @return maximum range, or 0 if the entity can't attack from distance 2974 */ getMaxRangeForArcher()2975 public int getMaxRangeForArcher() { 2976 final Item rangeWeapon = getRangeWeapon(); 2977 final StackableItem ammunition = getAmmunition(); 2978 final StackableItem missiles = getMissileIfNotHoldingOtherWeapon(); 2979 int maxRange; 2980 if ((rangeWeapon != null) && (ammunition != null) 2981 && (ammunition.getQuantity() > 0)) { 2982 maxRange = rangeWeapon.getInt("range") + ammunition.getInt("range"); 2983 } else if ((missiles != null) && (missiles.getQuantity() > 0)) { 2984 maxRange = missiles.getInt("range"); 2985 } else { 2986 // The entity doesn't hold the necessary distance weapons. 2987 maxRange = 0; 2988 } 2989 return maxRange; 2990 } 2991 2992 /** 2993 * Set the entity's formatted title. 2994 * 2995 * @param title 2996 * The title, or <code>null</code>. 2997 */ setTitle(final String title)2998 public void setTitle(final String title) { 2999 if (title != null) { 3000 put(ATTR_TITLE, title); 3001 } else if (has(ATTR_TITLE)) { 3002 remove(ATTR_TITLE); 3003 } 3004 } 3005 3006 // 3007 // Entity 3008 // 3009 3010 /** 3011 * Returns the name or something that can be used to identify the entity for 3012 * the player. 3013 * 3014 * @param definite 3015 * <code>true</code> for "the", and <code>false</code> for "a/an" 3016 * in case the entity has no name. 3017 * 3018 * @return The description name. 3019 */ 3020 @Override getDescriptionName(final boolean definite)3021 public String getDescriptionName(final boolean definite) { 3022 if (name != null) { 3023 return name; 3024 } else { 3025 return super.getDescriptionName(definite); 3026 } 3027 } 3028 3029 /** 3030 * Get the nicely formatted entity title/name. 3031 * 3032 * @return The title, or <code>null</code> if unknown. 3033 */ 3034 @Override getTitle()3035 public String getTitle() { 3036 if (has(ATTR_TITLE)) { 3037 return get(ATTR_TITLE); 3038 } else if (name != null) { 3039 return name; 3040 } else { 3041 return super.getTitle(); 3042 } 3043 } 3044 3045 /** 3046 * Perform cycle logic. 3047 */ logic()3048 public abstract void logic(); 3049 3050 /** 3051 * Chooses randomly if this has hit the defender, or if this missed him. 3052 * Note that, even if this method returns true, the damage done might be 0 3053 * (if the defender blocks the attack). 3054 * 3055 * @param defender 3056 * The attacked RPEntity. 3057 * @return true if the attacker has hit the defender (the defender may still 3058 * block this); false if the attacker has missed the defender. 3059 */ canHit(final RPEntity defender)3060 public boolean canHit(final RPEntity defender) { 3061 int roll = Rand.roll1D20(); 3062 final int defenderDEF = defender.getCappedDef(); 3063 3064 // Check if attacking from distance 3065 boolean usesRanged = false; 3066 if (!this.nextTo(defender)) { 3067 usesRanged = true; 3068 } 3069 3070 final int attackerATK; 3071 if (Testing.COMBAT && usesRanged) { 3072 attackerATK = this.getCappedRatk(); // player is using ranged weapon 3073 } else { 3074 attackerATK = this.getCappedAtk(); // player is using hand-to-hand 3075 } 3076 3077 /* 3078 * Use some karma unless attacker is much stronger than defender, in 3079 * which case attacker doesn't need luck to help him hit. 3080 */ 3081 final int levelDifferenceToNotNeedKarmaAttacking = (int) (IGNORE_KARMA_MULTIPLIER * getLevel()); 3082 3083 String karmaMode = null; 3084 if (this.has(COMBAT_KARMA)) { 3085 karmaMode = this.get(COMBAT_KARMA); 3086 } 3087 3088 boolean useKarma = false; 3089 if (karmaMode == null || karmaMode.equals(KARMA_SETTINGS.get(1))) { 3090 if (!(getLevel() - levelDifferenceToNotNeedKarmaAttacking > defender.getLevel())) { 3091 useKarma = true; 3092 } 3093 } else if (karmaMode.equals(KARMA_SETTINGS.get(2))) { 3094 useKarma = true; 3095 } 3096 3097 // using karma here increases chance to hit enemy 3098 if (useKarma) { 3099 final double karmaMultiplier = this.useKarma(0.1); 3100 // the karma effect must be cast to an integer to affect the roll 3101 // but in most cases this means the karma use was lost. so multiply by 2 to 3102 // make the same amount of karma use be more useful 3103 final double karmaEffect = roll * karmaMultiplier * 2.0; 3104 roll -= (int) karmaEffect; 3105 } 3106 3107 int risk = calculateRiskForCanHit(roll, defenderDEF, attackerATK); 3108 3109 if (logger.isDebugEnabled() || Testing.DEBUG) { 3110 logger.debug("attack from " + this + " to " + defender 3111 + ": Risk to strike: " + risk); 3112 } 3113 3114 if (risk < 0) { 3115 risk = 0; 3116 } 3117 3118 if (risk > 1) { 3119 risk = 1; 3120 } 3121 3122 return (risk != 0); 3123 } 3124 calculateRiskForCanHit(final int roll, final int defenderDEF, final int attackerATK)3125 int calculateRiskForCanHit(final int roll, final int defenderDEF, 3126 final int attackerATK) { 3127 return 20 * attackerATK - roll * defenderDEF; 3128 } 3129 3130 /** 3131 * Returns the attack rate, the lower the better. 3132 * 3133 * @return the attack rate 3134 */ getAttackRate()3135 public int getAttackRate() { 3136 3137 boolean meleeDistance = isAttacking() && nextTo(getAttackTarget()); 3138 3139 final List<Item> weapons = getWeapons(); 3140 3141 if (weapons.isEmpty()) { 3142 return Item.getDefaultAttackRate(); 3143 } 3144 int best = weapons.get(0).getAttackRate(meleeDistance); 3145 for (final Item weapon : weapons) { 3146 final int res = weapon.getAttackRate(meleeDistance); 3147 if (res < best) { 3148 best = res; 3149 } 3150 } 3151 3152 // Level effect 3153 best = (int) Math.ceil(best * getItemLevelModifier(weapons.get(0))); 3154 3155 return best; 3156 } 3157 3158 /** 3159 * Get a modifier to be used when an item has a higher min_level 3160 * than the entity's level. For any item where the entity's level 3161 * is high enough, the modifier is 1. For anything else, > 1 depending 3162 * on the ratio between the required, and the possessed level. 3163 * 3164 * @param item the item to be examined 3165 * @return modifier for item properties 3166 */ getItemLevelModifier(Item item)3167 private double getItemLevelModifier(Item item) { 3168 final String minLevelS = item.get("min_level"); 3169 3170 if (minLevelS != null) { 3171 final int minLevel = Integer.parseInt(minLevelS); 3172 final int level = getLevel(); 3173 if (minLevel > level) { 3174 return 1 - Math.log(((double) level + 1) / (minLevel + 1)); 3175 } 3176 } 3177 3178 return 1.0; 3179 } 3180 3181 /** 3182 * Lets the attacker attack its target. 3183 * 3184 * @return true iff the attacker has done damage to the defender. 3185 * 3186 */ attack()3187 public boolean attack() { 3188 boolean result = false; 3189 final RPEntity defender = this.getAttackTarget(); 3190 3191 // isInZoneandNotDead(defender); 3192 3193 defender.rememberAttacker(this); 3194 3195 final int maxRange = getMaxRangeForArcher(); 3196 /* 3197 * The second part (damage type check) ensures that normal archers need 3198 * distance attack modifiers for melee, but creatures with special 3199 * ranged attacks (dragons) pay the price only when using their ranged 3200 * powers (yes, it's a bit of a hack). 3201 */ 3202 boolean isRanged = ((maxRange > 0) && canDoRangeAttack(defender, maxRange)) 3203 && (((getDamageType() == getRangedDamageType()) || squaredDistance(defender) > 0)); 3204 3205 Nature nature; 3206 final float itemAtk; 3207 if (isRanged) { 3208 nature = getRangedDamageType(); 3209 itemAtk = getItemRatk(); 3210 } else { 3211 nature = getDamageType(); 3212 itemAtk = getItemAtk(); 3213 } 3214 3215 // Try to inflict a status effect 3216 for (StatusAttacker statusAttacker : statusAttackers) { 3217 statusAttacker.onAttackAttempt(defender, this); 3218 } 3219 3220 // Weapon for the use in the attack event 3221 Item attackWeapon = getWeapon(); 3222 String weaponName = null; 3223 if (attackWeapon != null) { 3224 weaponName = attackWeapon.getWeaponType(); 3225 } 3226 3227 if (this.canHit(defender)) { 3228 defender.applyDefXP(this); 3229 3230 int damage = damageDone(defender, itemAtk, nature, isRanged, maxRange); 3231 3232 if (damage > 0) { 3233 3234 // limit damage to target HP 3235 damage = Math.min(damage, defender.getHP()); 3236 this.handleLifesteal(this, this.getWeapons(), damage); 3237 3238 defender.onDamaged(this, damage); 3239 3240 if (logger.isDebugEnabled() || Testing.DEBUG) { 3241 logger.debug("attack from " + this.getID() + " to " 3242 + defender.getID() + ": Damage: " + damage); 3243 } 3244 3245 result = true; 3246 } else { 3247 // The attack was too weak, it was blocked 3248 3249 if (logger.isDebugEnabled() || Testing.DEBUG) { 3250 logger.debug("attack from " + this.getID() + " to " 3251 + defender.getID() + ": Damage: " + 0); 3252 } 3253 } 3254 this.addEvent(new AttackEvent(true, damage, nature, weaponName, isRanged)); 3255 3256 // Try to inflict a status effect 3257 for (StatusAttacker statusAttacker : statusAttackers) { 3258 statusAttacker.onHit(defender, this, damage); 3259 } 3260 3261 } else { 3262 // Missed 3263 if (logger.isDebugEnabled() || Testing.DEBUG) { 3264 logger.debug("attack from " + this.getID() + " to " 3265 + defender.getID() + ": Missed"); 3266 } 3267 3268 this.addEvent(new AttackEvent(false, 0, nature, weaponName, isRanged)); 3269 } 3270 3271 this.notifyWorldAboutChanges(); 3272 return result; 3273 } 3274 applyDefXP(final RPEntity entity)3275 protected void applyDefXP(final RPEntity entity) { 3276 // implemented in sub classes 3277 } 3278 3279 /** 3280 * Calculate lifesteal and update hp of source. 3281 * 3282 * @param attacker 3283 * the RPEntity doing the hit 3284 * @param attackerWeapons 3285 * the weapons of the RPEntity doing the hit 3286 * @param damage 3287 * the damage done by this hit. 3288 */ handleLifesteal(final RPEntity attacker, final List<Item> attackerWeapons, final int damage)3289 public void handleLifesteal(final RPEntity attacker, 3290 final List<Item> attackerWeapons, final int damage) { 3291 3292 // Calculate the lifesteal value based on the configured factor 3293 // In case of a lifesteal weapon used together with a non-lifesteal 3294 // weapon, 3295 // weight it based on the atk-values of the weapons. 3296 float sumAll = 0; 3297 float sumLifesteal = 0; 3298 3299 // Creature with lifesteal profile? 3300 if (attacker instanceof Creature) { 3301 sumAll = 1; 3302 final String value = ((Creature) attacker) 3303 .getAIProfile("lifesteal"); 3304 if (value == null) { 3305 // The creature doesn't steal life. 3306 return; 3307 } 3308 sumLifesteal = Float.parseFloat(value); 3309 } else { 3310 // weapons with lifesteal attribute for players 3311 for (final Item weaponItem : attackerWeapons) { 3312 sumAll += weaponItem.getAttack(); 3313 if (weaponItem.has("lifesteal")) { 3314 sumLifesteal += weaponItem.getAttack() 3315 * weaponItem.getDouble("lifesteal"); 3316 } 3317 } 3318 } 3319 3320 // process the lifesteal 3321 if (sumLifesteal != 0) { 3322 // 0.5f is used for rounding 3323 final int lifesteal = (int) (damage * sumLifesteal / sumAll + 0.5f); 3324 3325 if (lifesteal >= 0) { 3326 attacker.heal(lifesteal, true); 3327 } else { 3328 /* 3329 * Negative lifesteal means that we hurt ourselves. 3330 */ 3331 attacker.damage(-lifesteal, attacker); 3332 } 3333 3334 attacker.notifyWorldAboutChanges(); 3335 } 3336 } 3337 3338 /** 3339 * Equips the item in the specified slot. 3340 * 3341 * @param rpslot 3342 * @param item 3343 * @return true if successful*/ equipIt(final RPSlot rpslot, final Item item)3344 private boolean equipIt(final RPSlot rpslot, final Item item) { 3345 if (rpslot == null || (item == null)) { 3346 return false; 3347 } 3348 3349 if (item instanceof StackableItem) { 3350 final StackableItem stackEntity = (StackableItem) item; 3351 // find a stackable item of the same type 3352 for (final RPObject object : rpslot) { 3353 if (object instanceof StackableItem) { 3354 // found another stackable 3355 final StackableItem other = (StackableItem) object; 3356 if (other.isStackable(stackEntity)) { 3357 // other is the same type...merge them 3358 new ItemLogger().merge(this, stackEntity, other); 3359 other.add(stackEntity); 3360 updateItemAtkDef(); 3361 return true; 3362 } 3363 } 3364 } 3365 } 3366 3367 // We can't stack it on another item. Check if we can simply 3368 // add it to an empty cell. 3369 if (rpslot.isFull()) { 3370 return false; 3371 } else { 3372 rpslot.add(item); 3373 updateItemAtkDef(); 3374 return true; 3375 } 3376 } 3377 3378 /** 3379 * Gets the name of the player who deserves the corpse. 3380 * 3381 * @return name of player who deserves the corpse or <code>null</code>. 3382 */ getCorpseDeserver()3383 public String getCorpseDeserver() { 3384 return null; 3385 } 3386 3387 /** 3388 * gets the language 3389 * 3390 * @return language 3391 */ getLanguage()3392 public String getLanguage() { 3393 return null; 3394 } 3395 3396 /** 3397 * Sets the sound played at entity death 3398 * 3399 * @param sound Name of sound 3400 */ setDeathSound(final String sound)3401 public void setDeathSound(final String sound) { 3402 deathSound = sound; 3403 } 3404 3405 /** 3406 * @return Name of sound played at entity death 3407 */ getDeathSound()3408 public String getDeathSound() { 3409 return deathSound; 3410 } 3411 3412 /** 3413 * Add a status attack type to the entity 3414 * 3415 * @param statusAttacker Status attacker 3416 */ addStatusAttacker(final StatusAttacker statusAttacker)3417 public void addStatusAttacker(final StatusAttacker statusAttacker) { 3418 // the immutable statusAttackers list is shared between multiple instances of Creatures to reduce memory usage 3419 Builder<StatusAttacker> builder = ImmutableList.builder(); 3420 statusAttackers = builder.addAll(statusAttackers).add(statusAttacker).build(); 3421 } 3422 3423 /** 3424 * gets the status list 3425 * 3426 * @return StatusList 3427 */ getStatusList()3428 public StatusList getStatusList() { 3429 if (statusList == null) { 3430 statusList = new StatusList(this); 3431 } 3432 return statusList; 3433 } 3434 3435 /** 3436 * Find if the entity has a specified status 3437 * 3438 * @param statusType the status type to check for 3439 * @return true, if the entity has status; false otherwise 3440 */ hasStatus(StatusType statusType)3441 public boolean hasStatus(StatusType statusType) { 3442 if (statusList == null) { 3443 return false; 3444 } 3445 return statusList.hasStatus(statusType); 3446 } 3447 3448 @Override onRemoved(StendhalRPZone zone)3449 public void onRemoved(StendhalRPZone zone) { 3450 super.onRemoved(zone); 3451 3452 // Stop other creatures and players attacks on me. 3453 // Probably I am dead, and I don't want to die again with a second corpse. 3454 for (Entity attacker : new LinkedList<>(attackSources)) { 3455 if (attacker instanceof RPEntity) { 3456 ((RPEntity) attacker).stopAttack(); 3457 } 3458 } 3459 } 3460 3461 /** 3462 * Gets an items as a stream of items, followed by any contained items 3463 * recursively. 3464 * 3465 * @param item 3466 * @return stream of items 3467 */ itemStream(Item item)3468 private Stream<Item> itemStream(Item item) { 3469 Stream<Item> stream = Stream.of(item); 3470 if (item.slots().isEmpty()) { 3471 return stream; 3472 } 3473 Stream<RPSlot> slots = item.slots().stream(); 3474 Stream<Item> internalItems = slots.flatMap(this::slotStream); 3475 return Stream.concat(stream, internalItems); 3476 } 3477 3478 /** 3479 * Get a stream of all items in a slot. 3480 * 3481 * @param slot 3482 * @return items in the slot 3483 */ slotStream(RPSlot slot)3484 private Stream<Item> slotStream(RPSlot slot) { 3485 Stream<RPObject> objects = StreamSupport.stream(slot.spliterator(), false); 3486 Stream<Item> items = objects.filter(Item.class::isInstance).map(Item.class::cast); 3487 return items.flatMap(this::itemStream); 3488 } 3489 3490 /** 3491 * Get a stream of all equipped items. 3492 * 3493 * @return equipped items 3494 */ equippedStream()3495 private Stream<Item> equippedStream() { 3496 Stream<String> slotNames = Slots.CARRYING.getNames().stream(); 3497 Stream<RPSlot> slots = slotNames.map(this::getSlot).filter(Objects::nonNull); 3498 return slots.flatMap(this::slotStream); 3499 } 3500 3501 /** 3502 * A convenience method for getting a method for matching item names. 3503 * 3504 * @param name name to match 3505 * @return a predicate for matching the name 3506 */ nameMatches(String name)3507 private Predicate<Item> nameMatches(String name) { 3508 return it -> name.equals(it.getName()); 3509 } 3510 3511 /** 3512 * Sets the attribute to define the shadow that the client should use. 3513 * 3514 * @param st 3515 * String name of the shadow to use. 3516 */ setShadowStyle(final String st)3517 public void setShadowStyle(final String st) { 3518 if (st == null || st.equals("none")) { 3519 remove("shadow_style"); 3520 put("no_shadow", ""); 3521 return; 3522 } 3523 3524 put("shadow_style", st); 3525 remove("no_shadow"); 3526 } 3527 } 3528