diff --git a/src/pocketmine/entity/Living.php b/src/pocketmine/entity/Living.php index ca1943312..88341aef8 100644 --- a/src/pocketmine/entity/Living.php +++ b/src/pocketmine/entity/Living.php @@ -33,6 +33,7 @@ use pocketmine\event\entity\EntityEffectRemoveEvent; use pocketmine\event\entity\EntityRegainHealthEvent; use pocketmine\event\Timings; use pocketmine\inventory\ArmorInventory; +use pocketmine\item\Armor; use pocketmine\item\Consumable; use pocketmine\item\enchantment\Enchantment; use pocketmine\item\Item as ItemItem; @@ -389,6 +390,22 @@ abstract class Living extends Entity implements Damageable{ return $total; } + /** + * Returns the highest level of the specified enchantment on any armour piece that the entity is currently wearing. + * + * @param int $enchantmentId + * + * @return int + */ + public function getHighestArmorEnchantmentLevel(int $enchantmentId) : int{ + $result = 0; + foreach($this->armorInventory->getContents() as $item){ + $result = max($result, $item->getEnchantmentLevel($enchantmentId)); + } + + return $result; + } + /** * @return ArmorInventory */ @@ -396,6 +413,10 @@ abstract class Living extends Entity implements Damageable{ return $this->armorInventory; } + public function setOnFire(int $seconds){ + parent::setOnFire($seconds - (int) min($seconds, $seconds * $this->getHighestArmorEnchantmentLevel(Enchantment::FIRE_PROTECTION) * 0.15)); + } + /** * Called prior to EntityDamageEvent execution to apply modifications to the event's damage, such as reduction due * to effects or armour. @@ -413,7 +434,13 @@ abstract class Living extends Entity implements Damageable{ $source->setDamage(-($source->getFinalDamage() * 0.20 * $this->getEffect(Effect::DAMAGE_RESISTANCE)->getEffectLevel()), EntityDamageEvent::MODIFIER_RESISTANCE); } - //TODO: armour protection enchantments should be checked here (after effect damage reduction) + $totalEpf = 0; + foreach($this->armorInventory->getContents() as $item){ + if($item instanceof Armor){ + $totalEpf += $item->getEnchantmentProtectionFactor($source); + } + } + $source->setDamage(-$source->getFinalDamage() * min(ceil(min($totalEpf, 25) * (mt_rand(50, 100) / 100)), 20) * 0.04, EntityDamageEvent::MODIFIER_ARMOR_ENCHANTMENTS); $source->setDamage(-min($this->getAbsorption(), $source->getFinalDamage()), EntityDamageEvent::MODIFIER_ABSORPTION); } @@ -447,6 +474,16 @@ abstract class Living extends Entity implements Damageable{ $this->applyDamageModifiers($source); + if($source instanceof EntityDamageByEntityEvent and ( + $source->getCause() === EntityDamageEvent::CAUSE_BLOCK_EXPLOSION or + $source->getCause() === EntityDamageEvent::CAUSE_ENTITY_EXPLOSION) + ){ + //TODO: knockback should not just apply for entity damage sources + //this doesn't matter for TNT right now because the PrimedTNT entity is considered the source, not the block. + $base = $source->getKnockBack(); + $source->setKnockBack($base - min($base, $base * $this->getHighestArmorEnchantmentLevel(Enchantment::BLAST_PROTECTION) * 0.15)); + } + parent::attack($source); if($source->isCancelled()){ diff --git a/src/pocketmine/event/entity/EntityDamageEvent.php b/src/pocketmine/event/entity/EntityDamageEvent.php index 9f53b7312..6baf640a0 100644 --- a/src/pocketmine/event/entity/EntityDamageEvent.php +++ b/src/pocketmine/event/entity/EntityDamageEvent.php @@ -38,6 +38,7 @@ class EntityDamageEvent extends EntityEvent implements Cancellable{ public const MODIFIER_WEAKNESS = 3; public const MODIFIER_RESISTANCE = 4; public const MODIFIER_ABSORPTION = 5; + public const MODIFIER_ARMOR_ENCHANTMENTS = 6; public const CAUSE_CONTACT = 0; public const CAUSE_ENTITY_ATTACK = 1; diff --git a/src/pocketmine/item/Armor.php b/src/pocketmine/item/Armor.php index 42dd91a09..7ca373820 100644 --- a/src/pocketmine/item/Armor.php +++ b/src/pocketmine/item/Armor.php @@ -24,6 +24,8 @@ declare(strict_types=1); namespace pocketmine\item; +use pocketmine\event\entity\EntityDamageEvent; +use pocketmine\item\enchantment\ProtectionEnchantment; use pocketmine\nbt\tag\IntTag; use pocketmine\utils\Binary; use pocketmine\utils\Color; @@ -55,4 +57,25 @@ abstract class Armor extends Item{ public function setCustomColor(Color $color) : void{ $this->setNamedTagEntry(new IntTag(self::TAG_CUSTOM_COLOR, Binary::signInt($color->toARGB()))); } + + /** + * Returns the total enchantment protection factor this armour piece offers from all applicable protection + * enchantments on the item. + * + * @param EntityDamageEvent $event + * + * @return int + */ + public function getEnchantmentProtectionFactor(EntityDamageEvent $event) : int{ + $epf = 0; + + foreach($this->getEnchantments() as $enchantment){ + $type = $enchantment->getType(); + if($type instanceof ProtectionEnchantment and $type->isApplicable($event)){ + $epf += $type->getProtectionFactor($enchantment->getLevel()); + } + } + + return $epf; + } } diff --git a/src/pocketmine/item/enchantment/Enchantment.php b/src/pocketmine/item/enchantment/Enchantment.php index ff79d1a8f..e915071df 100644 --- a/src/pocketmine/item/enchantment/Enchantment.php +++ b/src/pocketmine/item/enchantment/Enchantment.php @@ -23,6 +23,8 @@ declare(strict_types=1); namespace pocketmine\item\enchantment; +use pocketmine\event\entity\EntityDamageEvent; + /** * Manages enchantment type data. */ @@ -88,9 +90,23 @@ class Enchantment{ public static function init(){ self::$enchantments = new \SplFixedArray(256); - self::registerEnchantment(new Enchantment(self::PROTECTION, "%enchantment.protect.all", self::RARITY_COMMON, self::SLOT_ARMOR, 4)); - self::registerEnchantment(new Enchantment(self::FIRE_PROTECTION, "%enchantment.protect.fire", self::RARITY_UNCOMMON, self::SLOT_ARMOR, 4)); - self::registerEnchantment(new Enchantment(self::FEATHER_FALLING, "%enchantment.protect.fall", self::RARITY_UNCOMMON, self::SLOT_FEET, 4)); + self::registerEnchantment(new ProtectionEnchantment(self::PROTECTION, "%enchant.protect.all", self::RARITY_COMMON, self::SLOT_ARMOR, 4, 0.75, null)); + self::registerEnchantment(new ProtectionEnchantment(self::FIRE_PROTECTION, "%enchantment.protect.fire", self::RARITY_UNCOMMON, self::SLOT_ARMOR, 4, 1.25, [ + EntityDamageEvent::CAUSE_FIRE, + EntityDamageEvent::CAUSE_FIRE_TICK, + EntityDamageEvent::CAUSE_LAVA + //TODO: check fireballs + ])); + self::registerEnchantment(new ProtectionEnchantment(self::FEATHER_FALLING, "%enchantment.protect.fall", self::RARITY_UNCOMMON, self::SLOT_FEET, 4, 2.5, [ + EntityDamageEvent::CAUSE_FALL + ])); + self::registerEnchantment(new ProtectionEnchantment(self::BLAST_PROTECTION, "%enchantment.protect.explosion", self::RARITY_RARE, self::SLOT_ARMOR, 4, 1.5, [ + EntityDamageEvent::CAUSE_BLOCK_EXPLOSION, + EntityDamageEvent::CAUSE_ENTITY_EXPLOSION + ])); + self::registerEnchantment(new ProtectionEnchantment(self::PROJECTILE_PROTECTION, "%enchantment.protect.projectile", self::RARITY_UNCOMMON, self::SLOT_ARMOR, 4, 1.5, [ + EntityDamageEvent::CAUSE_PROJECTILE + ])); self::registerEnchantment(new Enchantment(self::RESPIRATION, "%enchantment.oxygen", self::RARITY_RARE, self::SLOT_HEAD, 3)); diff --git a/src/pocketmine/item/enchantment/ProtectionEnchantment.php b/src/pocketmine/item/enchantment/ProtectionEnchantment.php new file mode 100644 index 000000000..5ac5e7878 --- /dev/null +++ b/src/pocketmine/item/enchantment/ProtectionEnchantment.php @@ -0,0 +1,81 @@ +typeModifier = $typeModifier; + if($applicableDamageTypes !== null){ + $this->applicableDamageTypes = array_flip($applicableDamageTypes); + } + } + + /** + * Returns the multiplier by which this enchantment type's EPF increases with each enchantment level. + * @return float + */ + public function getTypeModifier() : float{ + return $this->typeModifier; + } + + /** + * Returns the base EPF this enchantment type offers for the given enchantment level. + * @param int $level + * + * @return int + */ + public function getProtectionFactor(int $level) : int{ + return (int) floor((6 + $level ** 2) * $this->typeModifier / 3); + } + + /** + * Returns whether this enchantment type offers protection from the specified damage source's cause. + * @param EntityDamageEvent $event + * + * @return bool + */ + public function isApplicable(EntityDamageEvent $event) : bool{ + return $this->applicableDamageTypes === null or isset($this->applicableDamageTypes[$event->getCause()]); + } +}