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