PocketMine-MP 5.18.1 git-9381fc4172e5dce4cada1cb356050c8a2ab57b94
Living.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\entity;
25
66use function array_shift;
67use function atan2;
68use function ceil;
69use function count;
70use function floor;
71use function ksort;
72use function lcg_value;
73use function max;
74use function min;
75use function mt_getrandmax;
76use function mt_rand;
77use function round;
78use function sqrt;
79use const M_PI;
80use const SORT_NUMERIC;
81
82abstract class Living extends Entity{
83 protected const DEFAULT_BREATH_TICKS = 300;
84
89 public const DEFAULT_KNOCKBACK_FORCE = 0.4;
95
96 private const TAG_LEGACY_HEALTH = "HealF"; //TAG_Float
97 private const TAG_HEALTH = "Health"; //TAG_Float
98 private const TAG_BREATH_TICKS = "Air"; //TAG_Short
99 private const TAG_ACTIVE_EFFECTS = "ActiveEffects"; //TAG_List<TAG_Compound>
100 private const TAG_EFFECT_ID = "Id"; //TAG_Byte
101 private const TAG_EFFECT_DURATION = "Duration"; //TAG_Int
102 private const TAG_EFFECT_AMPLIFIER = "Amplifier"; //TAG_Byte
103 private const TAG_EFFECT_SHOW_PARTICLES = "ShowParticles"; //TAG_Byte
104 private const TAG_EFFECT_AMBIENT = "Ambient"; //TAG_Byte
105
106 protected int $attackTime = 0;
107
108 public int $deadTicks = 0;
109 protected int $maxDeadTicks = 25;
110
111 protected float $jumpVelocity = 0.42;
112
113 protected EffectManager $effectManager;
114
115 protected ArmorInventory $armorInventory;
116
117 protected bool $breathing = true;
118 protected int $breathTicks = self::DEFAULT_BREATH_TICKS;
119 protected int $maxBreathTicks = self::DEFAULT_BREATH_TICKS;
120
121 protected Attribute $healthAttr;
122 protected Attribute $absorptionAttr;
123 protected Attribute $knockbackResistanceAttr;
124 protected Attribute $moveSpeedAttr;
125
126 protected bool $sprinting = false;
127 protected bool $sneaking = false;
128 protected bool $gliding = false;
129 protected bool $swimming = false;
130
131 protected function getInitialDragMultiplier() : float{ return 0.02; }
132
133 protected function getInitialGravity() : float{ return 0.08; }
134
135 abstract public function getName() : string;
136
137 public function canBeRenamed() : bool{
138 return true;
139 }
140
141 protected function initEntity(CompoundTag $nbt) : void{
142 parent::initEntity($nbt);
143
144 $this->effectManager = new EffectManager($this);
145 $this->effectManager->getEffectAddHooks()->add(function() : void{ $this->networkPropertiesDirty = true; });
146 $this->effectManager->getEffectRemoveHooks()->add(function() : void{ $this->networkPropertiesDirty = true; });
147
148 $this->armorInventory = new ArmorInventory($this);
149 //TODO: load/save armor inventory contents
150 $this->armorInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
151 $this->getViewers(),
152 fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobArmorChange($recipients, $this)
153 )));
154
155 $health = $this->getMaxHealth();
156
157 if(($healFTag = $nbt->getTag(self::TAG_LEGACY_HEALTH)) instanceof FloatTag){
158 $health = $healFTag->getValue();
159 }elseif(($healthTag = $nbt->getTag(self::TAG_HEALTH)) instanceof ShortTag){
160 $health = $healthTag->getValue(); //Older versions of PocketMine-MP incorrectly saved this as a short instead of a float
161 }elseif($healthTag instanceof FloatTag){
162 $health = $healthTag->getValue();
163 }
164
165 $this->setHealth($health);
166
167 $this->setAirSupplyTicks($nbt->getShort(self::TAG_BREATH_TICKS, self::DEFAULT_BREATH_TICKS));
168
170 $activeEffectsTag = $nbt->getListTag(self::TAG_ACTIVE_EFFECTS);
171 if($activeEffectsTag !== null){
172 foreach($activeEffectsTag as $e){
173 $effect = EffectIdMap::getInstance()->fromId($e->getByte(self::TAG_EFFECT_ID));
174 if($effect === null){
175 continue;
176 }
177
178 $this->effectManager->add(new EffectInstance(
179 $effect,
180 $e->getInt(self::TAG_EFFECT_DURATION),
181 Binary::unsignByte($e->getByte(self::TAG_EFFECT_AMPLIFIER)),
182 $e->getByte(self::TAG_EFFECT_SHOW_PARTICLES, 1) !== 0,
183 $e->getByte(self::TAG_EFFECT_AMBIENT, 0) !== 0
184 ));
185 }
186 }
187 }
188
189 protected function addAttributes() : void{
190 $this->attributeMap->add($this->healthAttr = AttributeFactory::getInstance()->mustGet(Attribute::HEALTH));
191 $this->attributeMap->add(AttributeFactory::getInstance()->mustGet(Attribute::FOLLOW_RANGE));
192 $this->attributeMap->add($this->knockbackResistanceAttr = AttributeFactory::getInstance()->mustGet(Attribute::KNOCKBACK_RESISTANCE));
193 $this->attributeMap->add($this->moveSpeedAttr = AttributeFactory::getInstance()->mustGet(Attribute::MOVEMENT_SPEED));
194 $this->attributeMap->add(AttributeFactory::getInstance()->mustGet(Attribute::ATTACK_DAMAGE));
195 $this->attributeMap->add($this->absorptionAttr = AttributeFactory::getInstance()->mustGet(Attribute::ABSORPTION));
196 }
197
201 public function getDisplayName() : string{
202 return $this->nameTag !== "" ? $this->nameTag : $this->getName();
203 }
204
205 public function setHealth(float $amount) : void{
206 $wasAlive = $this->isAlive();
207 parent::setHealth($amount);
208 $this->healthAttr->setValue(ceil($this->getHealth()), true);
209 if($this->isAlive() && !$wasAlive){
210 $this->broadcastAnimation(new RespawnAnimation($this));
211 }
212 }
213
214 public function getMaxHealth() : int{
215 return (int) $this->healthAttr->getMaxValue();
216 }
217
218 public function setMaxHealth(int $amount) : void{
219 $this->healthAttr->setMaxValue($amount)->setDefaultValue($amount);
220 }
221
222 public function getAbsorption() : float{
223 return $this->absorptionAttr->getValue();
224 }
225
226 public function setAbsorption(float $absorption) : void{
227 $this->absorptionAttr->setValue($absorption);
228 }
229
230 public function isSneaking() : bool{
231 return $this->sneaking;
232 }
233
234 public function setSneaking(bool $value = true) : void{
235 $this->sneaking = $value;
236 $this->networkPropertiesDirty = true;
237 $this->recalculateSize();
238 }
239
240 public function isSprinting() : bool{
241 return $this->sprinting;
242 }
243
244 public function setSprinting(bool $value = true) : void{
245 if($value !== $this->isSprinting()){
246 $this->sprinting = $value;
247 $this->networkPropertiesDirty = true;
248 $moveSpeed = $this->getMovementSpeed();
249 $this->setMovementSpeed($value ? ($moveSpeed * 1.3) : ($moveSpeed / 1.3));
250 $this->moveSpeedAttr->markSynchronized(false); //TODO: reevaluate this hack
251 }
252 }
253
254 public function isGliding() : bool{
255 return $this->gliding;
256 }
257
258 public function setGliding(bool $value = true) : void{
259 $this->gliding = $value;
260 $this->networkPropertiesDirty = true;
261 $this->recalculateSize();
262 }
263
264 public function isSwimming() : bool{
265 return $this->swimming;
266 }
267
268 public function setSwimming(bool $value = true) : void{
269 $this->swimming = $value;
270 $this->networkPropertiesDirty = true;
271 $this->recalculateSize();
272 }
273
274 private function recalculateSize() : void{
275 $size = $this->getInitialSizeInfo();
276 if($this->isSwimming() || $this->isGliding()){
277 $width = $size->getWidth();
278 $this->setSize((new EntitySizeInfo($width, $width, $width * 0.9))->scale($this->getScale()));
279 }elseif($this->isSneaking()){
280 $this->setSize((new EntitySizeInfo(3 / 4 * $size->getHeight(), $size->getWidth(), 3 / 4 * $size->getEyeHeight()))->scale($this->getScale()));
281 }else{
282 $this->setSize($size->scale($this->getScale()));
283 }
284 }
285
286 public function getMovementSpeed() : float{
287 return $this->moveSpeedAttr->getValue();
288 }
289
290 public function setMovementSpeed(float $v, bool $fit = false) : void{
291 $this->moveSpeedAttr->setValue($v, $fit);
292 }
293
294 public function saveNBT() : CompoundTag{
295 $nbt = parent::saveNBT();
296 $nbt->setFloat(self::TAG_HEALTH, $this->getHealth());
297
298 $nbt->setShort(self::TAG_BREATH_TICKS, $this->getAirSupplyTicks());
299
300 if(count($this->effectManager->all()) > 0){
301 $effects = [];
302 foreach($this->effectManager->all() as $effect){
303 $effects[] = CompoundTag::create()
304 ->setByte(self::TAG_EFFECT_ID, EffectIdMap::getInstance()->toId($effect->getType()))
305 ->setByte(self::TAG_EFFECT_AMPLIFIER, Binary::signByte($effect->getAmplifier()))
306 ->setInt(self::TAG_EFFECT_DURATION, $effect->getDuration())
307 ->setByte(self::TAG_EFFECT_AMBIENT, $effect->isAmbient() ? 1 : 0)
308 ->setByte(self::TAG_EFFECT_SHOW_PARTICLES, $effect->isVisible() ? 1 : 0);
309 }
310
311 $nbt->setTag(self::TAG_ACTIVE_EFFECTS, new ListTag($effects));
312 }
313
314 return $nbt;
315 }
316
317 public function getEffects() : EffectManager{
318 return $this->effectManager;
319 }
320
325 public function consumeObject(Consumable $consumable) : bool{
326 $this->applyConsumptionResults($consumable);
327 return true;
328 }
329
334 protected function applyConsumptionResults(Consumable $consumable) : void{
335 foreach($consumable->getAdditionalEffects() as $effect){
336 $this->effectManager->add($effect);
337 }
338 if($consumable instanceof FoodSource){
339 $this->broadcastSound(new BurpSound());
340 }
341
342 $consumable->onConsume($this);
343 }
344
348 public function getJumpVelocity() : float{
349 return $this->jumpVelocity + ((($jumpBoost = $this->effectManager->get(VanillaEffects::JUMP_BOOST())) !== null ? $jumpBoost->getEffectLevel() : 0) / 10);
350 }
351
355 public function jump() : void{
356 if($this->onGround){
357 $this->motion = $this->motion->withComponents(null, $this->getJumpVelocity(), null); //Y motion should already be 0 if we're jumping from the ground.
358 }
359 }
360
361 protected function calculateFallDamage(float $fallDistance) : float{
362 return ceil($fallDistance - 3 - (($jumpBoost = $this->effectManager->get(VanillaEffects::JUMP_BOOST())) !== null ? $jumpBoost->getEffectLevel() : 0));
363 }
364
365 protected function onHitGround() : ?float{
366 $fallBlockPos = $this->location->floor();
367 $fallBlock = $this->getWorld()->getBlock($fallBlockPos);
368 if(count($fallBlock->getCollisionBoxes()) === 0){
369 $fallBlockPos = $fallBlockPos->down();
370 $fallBlock = $this->getWorld()->getBlock($fallBlockPos);
371 }
372 $newVerticalVelocity = $fallBlock->onEntityLand($this);
373
374 $damage = $this->calculateFallDamage($this->fallDistance);
375 if($damage > 0){
376 $ev = new EntityDamageEvent($this, EntityDamageEvent::CAUSE_FALL, $damage);
377 $this->attack($ev);
378
379 $this->broadcastSound($damage > 4 ?
380 new EntityLongFallSound($this) :
381 new EntityShortFallSound($this)
382 );
383 }elseif($fallBlock->getTypeId() !== BlockTypeIds::AIR){
384 $this->broadcastSound(new EntityLandSound($this, $fallBlock));
385 }
386 return $newVerticalVelocity;
387 }
388
394 public function getArmorPoints() : int{
395 $total = 0;
396 foreach($this->armorInventory->getContents() as $item){
397 $total += $item->getDefensePoints();
398 }
399
400 return $total;
401 }
402
406 public function getHighestArmorEnchantmentLevel(Enchantment $enchantment) : int{
407 $result = 0;
408 foreach($this->armorInventory->getContents() as $item){
409 $result = max($result, $item->getEnchantmentLevel($enchantment));
410 }
411
412 return $result;
413 }
414
415 public function getArmorInventory() : ArmorInventory{
416 return $this->armorInventory;
417 }
418
419 public function setOnFire(int $seconds) : void{
420 parent::setOnFire($seconds - (int) min($seconds, $seconds * $this->getHighestArmorEnchantmentLevel(VanillaEnchantments::FIRE_PROTECTION()) * 0.15));
421 }
422
427 public function applyDamageModifiers(EntityDamageEvent $source) : void{
428 if($this->lastDamageCause !== null && $this->attackTime > 0){
429 if($this->lastDamageCause->getBaseDamage() >= $source->getBaseDamage()){
430 $source->cancel();
431 }
432 $source->setModifier(-$this->lastDamageCause->getBaseDamage(), EntityDamageEvent::MODIFIER_PREVIOUS_DAMAGE_COOLDOWN);
433 }
434 if($source->canBeReducedByArmor()){
435 //MCPE uses the same system as PC did pre-1.9
436 $source->setModifier(-$source->getFinalDamage() * $this->getArmorPoints() * 0.04, EntityDamageEvent::MODIFIER_ARMOR);
437 }
438
439 $cause = $source->getCause();
440 if(($resistance = $this->effectManager->get(VanillaEffects::RESISTANCE())) !== null && $cause !== EntityDamageEvent::CAUSE_VOID && $cause !== EntityDamageEvent::CAUSE_SUICIDE){
441 $source->setModifier(-$source->getFinalDamage() * min(1, 0.2 * $resistance->getEffectLevel()), EntityDamageEvent::MODIFIER_RESISTANCE);
442 }
443
444 $totalEpf = 0;
445 foreach($this->armorInventory->getContents() as $item){
446 if($item instanceof Armor){
447 $totalEpf += $item->getEnchantmentProtectionFactor($source);
448 }
449 }
450 $source->setModifier(-$source->getFinalDamage() * min(ceil(min($totalEpf, 25) * (mt_rand(50, 100) / 100)), 20) * 0.04, EntityDamageEvent::MODIFIER_ARMOR_ENCHANTMENTS);
451
452 $source->setModifier(-min($this->getAbsorption(), $source->getFinalDamage()), EntityDamageEvent::MODIFIER_ABSORPTION);
453
454 if($cause === EntityDamageEvent::CAUSE_FALLING_BLOCK && $this->armorInventory->getHelmet() instanceof Armor){
455 $source->setModifier(-($source->getFinalDamage() / 4), EntityDamageEvent::MODIFIER_ARMOR_HELMET);
456 }
457 }
458
464 protected function applyPostDamageEffects(EntityDamageEvent $source) : void{
465 $this->setAbsorption(max(0, $this->getAbsorption() + $source->getModifier(EntityDamageEvent::MODIFIER_ABSORPTION)));
466 if($source->canBeReducedByArmor()){
467 $this->damageArmor($source->getBaseDamage());
468 }
469
470 if($source instanceof EntityDamageByEntityEvent && ($attacker = $source->getDamager()) !== null){
471 $damage = 0;
472 foreach($this->armorInventory->getContents() as $k => $item){
473 if($item instanceof Armor && ($thornsLevel = $item->getEnchantmentLevel(VanillaEnchantments::THORNS())) > 0){
474 if(mt_rand(0, 99) < $thornsLevel * 15){
475 $this->damageItem($item, 3);
476 $damage += ($thornsLevel > 10 ? $thornsLevel - 10 : 1 + mt_rand(0, 3));
477 }else{
478 $this->damageItem($item, 1); //thorns causes an extra +1 durability loss even if it didn't activate
479 }
480
481 $this->armorInventory->setItem($k, $item);
482 }
483 }
484
485 if($damage > 0){
486 $attacker->attack(new EntityDamageByEntityEvent($this, $attacker, EntityDamageEvent::CAUSE_MAGIC, $damage));
487 }
488
489 if($source->getModifier(EntityDamageEvent::MODIFIER_ARMOR_HELMET) < 0){
490 $helmet = $this->armorInventory->getHelmet();
491 if($helmet instanceof Armor){
492 $finalDamage = $source->getFinalDamage();
493 $this->damageItem($helmet, (int) round($finalDamage * 4 + lcg_value() * $finalDamage * 2));
494 $this->armorInventory->setHelmet($helmet);
495 }
496 }
497 }
498 }
499
504 public function damageArmor(float $damage) : void{
505 $durabilityRemoved = (int) max(floor($damage / 4), 1);
506
507 $armor = $this->armorInventory->getContents();
508 foreach($armor as $slotId => $item){
509 if($item instanceof Armor){
510 $oldItem = clone $item;
511 $this->damageItem($item, $durabilityRemoved);
512 if(!$item->equalsExact($oldItem)){
513 $this->armorInventory->setItem($slotId, $item);
514 }
515 }
516 }
517 }
518
519 private function damageItem(Durable $item, int $durabilityRemoved) : void{
520 $item->applyDamage($durabilityRemoved);
521 if($item->isBroken()){
522 $this->broadcastSound(new ItemBreakSound());
523 }
524 }
525
526 public function attack(EntityDamageEvent $source) : void{
527 if($this->noDamageTicks > 0 && $source->getCause() !== EntityDamageEvent::CAUSE_SUICIDE){
528 $source->cancel();
529 }
530
531 if($this->effectManager->has(VanillaEffects::FIRE_RESISTANCE()) && (
532 $source->getCause() === EntityDamageEvent::CAUSE_FIRE
533 || $source->getCause() === EntityDamageEvent::CAUSE_FIRE_TICK
534 || $source->getCause() === EntityDamageEvent::CAUSE_LAVA
535 )
536 ){
537 $source->cancel();
538 }
539
540 if($source->getCause() !== EntityDamageEvent::CAUSE_SUICIDE){
541 $this->applyDamageModifiers($source);
542 }
543
544 if($source instanceof EntityDamageByEntityEvent && (
545 $source->getCause() === EntityDamageEvent::CAUSE_BLOCK_EXPLOSION ||
546 $source->getCause() === EntityDamageEvent::CAUSE_ENTITY_EXPLOSION)
547 ){
548 //TODO: knockback should not just apply for entity damage sources
549 //this doesn't matter for TNT right now because the PrimedTNT entity is considered the source, not the block.
550 $base = $source->getKnockBack();
551 $source->setKnockBack($base - min($base, $base * $this->getHighestArmorEnchantmentLevel(VanillaEnchantments::BLAST_PROTECTION()) * 0.15));
552 }
553
554 parent::attack($source);
555
556 if($source->isCancelled()){
557 return;
558 }
559
560 if($this->attackTime <= 0){
561 //this logic only applies if the entity was cold attacked
562
563 $this->attackTime = $source->getAttackCooldown();
564
565 if($source instanceof EntityDamageByChildEntityEvent){
566 $e = $source->getChild();
567 if($e !== null){
568 $motion = $e->getMotion();
569 $this->knockBack($motion->x, $motion->z, $source->getKnockBack(), $source->getVerticalKnockBackLimit());
570 }
571 }elseif($source instanceof EntityDamageByEntityEvent){
572 $e = $source->getDamager();
573 if($e !== null){
574 $deltaX = $this->location->x - $e->location->x;
575 $deltaZ = $this->location->z - $e->location->z;
576 $this->knockBack($deltaX, $deltaZ, $source->getKnockBack(), $source->getVerticalKnockBackLimit());
577 }
578 }
579
580 if($this->isAlive()){
581 $this->doHitAnimation();
582 }
583 }
584
585 if($this->isAlive()){
586 $this->applyPostDamageEffects($source);
587 }
588 }
589
590 protected function doHitAnimation() : void{
591 $this->broadcastAnimation(new HurtAnimation($this));
592 }
593
594 public function knockBack(float $x, float $z, float $force = self::DEFAULT_KNOCKBACK_FORCE, ?float $verticalLimit = self::DEFAULT_KNOCKBACK_VERTICAL_LIMIT) : void{
595 $f = sqrt($x * $x + $z * $z);
596 if($f <= 0){
597 return;
598 }
599 if(mt_rand() / mt_getrandmax() > $this->knockbackResistanceAttr->getValue()){
600 $f = 1 / $f;
601
602 $motionX = $this->motion->x / 2;
603 $motionY = $this->motion->y / 2;
604 $motionZ = $this->motion->z / 2;
605 $motionX += $x * $f * $force;
606 $motionY += $force;
607 $motionZ += $z * $f * $force;
608
609 $verticalLimit ??= $force;
610 if($motionY > $verticalLimit){
611 $motionY = $verticalLimit;
612 }
613
614 $this->setMotion(new Vector3($motionX, $motionY, $motionZ));
615 }
616 }
617
618 protected function onDeath() : void{
619 $ev = new EntityDeathEvent($this, $this->getDrops(), $this->getXpDropAmount());
620 $ev->call();
621 foreach($ev->getDrops() as $item){
622 $this->getWorld()->dropItem($this->location, $item);
623 }
624
625 //TODO: check death conditions (must have been damaged by player < 5 seconds from death)
626 $this->getWorld()->dropExperience($this->location, $ev->getXpDropAmount());
627
628 $this->startDeathAnimation();
629 }
630
631 protected function onDeathUpdate(int $tickDiff) : bool{
632 if($this->deadTicks < $this->maxDeadTicks){
633 $this->deadTicks += $tickDiff;
634 if($this->deadTicks >= $this->maxDeadTicks){
635 $this->endDeathAnimation();
636 }
637 }
638
639 return $this->deadTicks >= $this->maxDeadTicks;
640 }
641
642 protected function startDeathAnimation() : void{
643 $this->broadcastAnimation(new DeathAnimation($this));
644 }
645
646 protected function endDeathAnimation() : void{
647 $this->despawnFromAll();
648 }
649
650 protected function entityBaseTick(int $tickDiff = 1) : bool{
651 Timings::$livingEntityBaseTick->startTiming();
652
653 $hasUpdate = parent::entityBaseTick($tickDiff);
654
655 if($this->isAlive()){
656 if($this->effectManager->tick($tickDiff)){
657 $hasUpdate = true;
658 }
659
660 if($this->isInsideOfSolid()){
661 $hasUpdate = true;
662 $ev = new EntityDamageEvent($this, EntityDamageEvent::CAUSE_SUFFOCATION, 1);
663 $this->attack($ev);
664 }
665
666 if($this->doAirSupplyTick($tickDiff)){
667 $hasUpdate = true;
668 }
669
670 foreach($this->armorInventory->getContents() as $index => $item){
671 $oldItem = clone $item;
672 if($item->onTickWorn($this)){
673 $hasUpdate = true;
674 if(!$item->equalsExact($oldItem)){
675 $this->armorInventory->setItem($index, $item);
676 }
677 }
678 }
679 }
680
681 if($this->attackTime > 0){
682 $this->attackTime -= $tickDiff;
683 }
684
685 Timings::$livingEntityBaseTick->stopTiming();
686
687 return $hasUpdate;
688 }
689
693 protected function doAirSupplyTick(int $tickDiff) : bool{
694 $ticks = $this->getAirSupplyTicks();
695 $oldTicks = $ticks;
696 if(!$this->canBreathe()){
697 $this->setBreathing(false);
698
699 if(($respirationLevel = $this->armorInventory->getHelmet()->getEnchantmentLevel(VanillaEnchantments::RESPIRATION())) <= 0 ||
700 lcg_value() <= (1 / ($respirationLevel + 1))
701 ){
702 $ticks -= $tickDiff;
703 if($ticks <= -20){
704 $ticks = 0;
705 $this->onAirExpired();
706 }
707 }
708 }elseif(!$this->isBreathing()){
709 if($ticks < ($max = $this->getMaxAirSupplyTicks())){
710 $ticks += $tickDiff * 5;
711 }
712 if($ticks >= $max){
713 $ticks = $max;
714 $this->setBreathing(true);
715 }
716 }
717
718 if($ticks !== $oldTicks){
719 $this->setAirSupplyTicks($ticks);
720 }
721
722 return $ticks !== $oldTicks;
723 }
724
728 public function canBreathe() : bool{
729 return $this->effectManager->has(VanillaEffects::WATER_BREATHING()) || $this->effectManager->has(VanillaEffects::CONDUIT_POWER()) || !$this->isUnderwater();
730 }
731
735 public function isBreathing() : bool{
736 return $this->breathing;
737 }
738
743 public function setBreathing(bool $value = true) : void{
744 $this->breathing = $value;
745 $this->networkPropertiesDirty = true;
746 }
747
752 public function getAirSupplyTicks() : int{
753 return $this->breathTicks;
754 }
755
759 public function setAirSupplyTicks(int $ticks) : void{
760 $this->breathTicks = $ticks;
761 $this->networkPropertiesDirty = true;
762 }
763
767 public function getMaxAirSupplyTicks() : int{
768 return $this->maxBreathTicks;
769 }
770
774 public function setMaxAirSupplyTicks(int $ticks) : void{
775 $this->maxBreathTicks = $ticks;
776 $this->networkPropertiesDirty = true;
777 }
778
783 public function onAirExpired() : void{
784 $ev = new EntityDamageEvent($this, EntityDamageEvent::CAUSE_DROWNING, 2);
785 $this->attack($ev);
786 }
787
791 public function getDrops() : array{
792 return [];
793 }
794
798 public function getXpDropAmount() : int{
799 return 0;
800 }
801
808 public function getLineOfSight(int $maxDistance, int $maxLength = 0, array $transparent = []) : array{
809 if($maxDistance > 120){
810 $maxDistance = 120;
811 }
812
813 if(count($transparent) === 0){
814 $transparent = null;
815 }
816
817 $blocks = [];
818 $nextIndex = 0;
819
820 foreach(VoxelRayTrace::inDirection($this->location->add(0, $this->size->getEyeHeight(), 0), $this->getDirectionVector(), $maxDistance) as $vector3){
821 $block = $this->getWorld()->getBlockAt($vector3->x, $vector3->y, $vector3->z);
822 $blocks[$nextIndex++] = $block;
823
824 if($maxLength !== 0 && count($blocks) > $maxLength){
825 array_shift($blocks);
826 --$nextIndex;
827 }
828
829 $id = $block->getTypeId();
830
831 if($transparent === null){
832 if($id !== BlockTypeIds::AIR){
833 break;
834 }
835 }else{
836 if(!isset($transparent[$id])){
837 break;
838 }
839 }
840 }
841
842 return $blocks;
843 }
844
849 public function getTargetBlock(int $maxDistance, array $transparent = []) : ?Block{
850 $line = $this->getLineOfSight($maxDistance, 1, $transparent);
851 if(count($line) > 0){
852 return array_shift($line);
853 }
854
855 return null;
856 }
857
862 public function lookAt(Vector3 $target) : void{
863 $horizontal = sqrt(($target->x - $this->location->x) ** 2 + ($target->z - $this->location->z) ** 2);
864 $vertical = $target->y - ($this->location->y + $this->getEyeHeight());
865 $pitch = -atan2($vertical, $horizontal) / M_PI * 180; //negative is up, positive is down
866
867 $xDist = $target->x - $this->location->x;
868 $zDist = $target->z - $this->location->z;
869
870 $yaw = atan2($zDist, $xDist) / M_PI * 180 - 90;
871 if($yaw < 0){
872 $yaw += 360.0;
873 }
874
875 $this->setRotation($yaw, $pitch);
876 }
877
878 protected function sendSpawnPacket(Player $player) : void{
879 parent::sendSpawnPacket($player);
880
881 $networkSession = $player->getNetworkSession();
882 $networkSession->getEntityEventBroadcaster()->onMobArmorChange([$networkSession], $this);
883 }
884
885 protected function syncNetworkData(EntityMetadataCollection $properties) : void{
886 parent::syncNetworkData($properties);
887
888 $visibleEffects = [];
889 foreach ($this->effectManager->all() as $effect) {
890 if (!$effect->isVisible() || !$effect->getType()->hasBubbles()) {
891 continue;
892 }
893 $visibleEffects[EffectIdMap::getInstance()->toId($effect->getType())] = $effect->isAmbient();
894 }
895
896 //TODO: HACK! the client may not be able to identify effects if they are not sorted.
897 ksort($visibleEffects, SORT_NUMERIC);
898
899 $effectsData = 0;
900 $packedEffectsCount = 0;
901 foreach ($visibleEffects as $effectId => $isAmbient) {
902 $effectsData = ($effectsData << 7) |
903 (($effectId & 0x3f) << 1) | //Why not use 7 bits instead of only 6? mojang...
904 ($isAmbient ? 1 : 0);
905
906 if (++$packedEffectsCount >= 8) {
907 break;
908 }
909 }
910 $properties->setLong(EntityMetadataProperties::VISIBLE_MOB_EFFECTS, $effectsData);
911
912 $properties->setShort(EntityMetadataProperties::AIR, $this->breathTicks);
913 $properties->setShort(EntityMetadataProperties::MAX_AIR, $this->maxBreathTicks);
914
915 $properties->setGenericFlag(EntityMetadataFlags::BREATHING, $this->breathing);
916 $properties->setGenericFlag(EntityMetadataFlags::SNEAKING, $this->sneaking);
917 $properties->setGenericFlag(EntityMetadataFlags::SPRINTING, $this->sprinting);
918 $properties->setGenericFlag(EntityMetadataFlags::GLIDING, $this->gliding);
919 $properties->setGenericFlag(EntityMetadataFlags::SWIMMING, $this->swimming);
920 }
921
922 protected function onDispose() : void{
923 $this->armorInventory->removeAllViewers();
924 $this->effectManager->getEffectAddHooks()->clear();
925 $this->effectManager->getEffectRemoveHooks()->clear();
926 parent::onDispose();
927 }
928
929 protected function destroyCycles() : void{
930 unset(
931 $this->armorInventory,
932 $this->effectManager
933 );
934 parent::destroyCycles();
935 }
936}
applyPostDamageEffects(EntityDamageEvent $source)
Definition: Living.php:464
sendSpawnPacket(Player $player)
Definition: Living.php:878
setMaxAirSupplyTicks(int $ticks)
Definition: Living.php:774
setBreathing(bool $value=true)
Definition: Living.php:743
lookAt(Vector3 $target)
Definition: Living.php:862
const DEFAULT_KNOCKBACK_FORCE
Definition: Living.php:89
onDeathUpdate(int $tickDiff)
Definition: Living.php:631
damageArmor(float $damage)
Definition: Living.php:504
setHealth(float $amount)
Definition: Living.php:205
getLineOfSight(int $maxDistance, int $maxLength=0, array $transparent=[])
Definition: Living.php:808
const DEFAULT_KNOCKBACK_VERTICAL_LIMIT
Definition: Living.php:94
applyDamageModifiers(EntityDamageEvent $source)
Definition: Living.php:427
getTargetBlock(int $maxDistance, array $transparent=[])
Definition: Living.php:849
consumeObject(Consumable $consumable)
Definition: Living.php:325
applyConsumptionResults(Consumable $consumable)
Definition: Living.php:334
setAirSupplyTicks(int $ticks)
Definition: Living.php:759
doAirSupplyTick(int $tickDiff)
Definition: Living.php:693
getHighestArmorEnchantmentLevel(Enchantment $enchantment)
Definition: Living.php:406
setTag(string $name, Tag $tag)
setFloat(string $name, float $value)
setShort(string $name, int $value)