PocketMine-MP 5.33.2 git-1133d49c924b4358c79d44eeb97dcbf56cb4d1eb
Loading...
Searching...
No Matches
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
71use function abs;
72use function array_shift;
73use function atan2;
74use function ceil;
75use function count;
76use function floor;
77use function ksort;
78use function max;
79use function min;
80use function mt_getrandmax;
81use function mt_rand;
82use function round;
83use function sqrt;
84use const M_PI;
85use const SORT_NUMERIC;
86
87abstract class Living extends Entity{
88 protected const DEFAULT_BREATH_TICKS = 300;
89
94 public const DEFAULT_KNOCKBACK_FORCE = 0.4;
100
101 private const TAG_LEGACY_HEALTH = "HealF"; //TAG_Float
102 private const TAG_HEALTH = "Health"; //TAG_Float
103 private const TAG_BREATH_TICKS = "Air"; //TAG_Short
104 private const TAG_ACTIVE_EFFECTS = "ActiveEffects"; //TAG_List<TAG_Compound>
105 private const TAG_EFFECT_ID = "Id"; //TAG_Byte
106 private const TAG_EFFECT_DURATION = "Duration"; //TAG_Int
107 private const TAG_EFFECT_AMPLIFIER = "Amplifier"; //TAG_Byte
108 private const TAG_EFFECT_SHOW_PARTICLES = "ShowParticles"; //TAG_Byte
109 private const TAG_EFFECT_AMBIENT = "Ambient"; //TAG_Byte
110
111 protected int $attackTime = 0;
112
113 public int $deadTicks = 0;
114 protected int $maxDeadTicks = 25;
115
116 protected float $jumpVelocity = 0.42;
117
118 protected EffectManager $effectManager;
119
120 protected ArmorInventory $armorInventory;
121
122 protected bool $breathing = true;
123 protected int $breathTicks = self::DEFAULT_BREATH_TICKS;
124 protected int $maxBreathTicks = self::DEFAULT_BREATH_TICKS;
125
126 protected Attribute $healthAttr;
127 protected Attribute $absorptionAttr;
128 protected Attribute $knockbackResistanceAttr;
129 protected Attribute $moveSpeedAttr;
130
131 protected bool $sprinting = false;
132 protected bool $sneaking = false;
133 protected bool $gliding = false;
134 protected bool $swimming = false;
135
136 private ?int $frostWalkerLevel = null;
137
138 protected function getInitialDragMultiplier() : float{ return 0.02; }
139
140 protected function getInitialGravity() : float{ return 0.08; }
141
142 abstract public function getName() : string;
143
144 public function canBeRenamed() : bool{
145 return true;
146 }
147
148 protected function initEntity(CompoundTag $nbt) : void{
149 parent::initEntity($nbt);
150
151 $this->effectManager = new EffectManager($this);
152 $this->effectManager->getEffectAddHooks()->add(function() : void{ $this->networkPropertiesDirty = true; });
153 $this->effectManager->getEffectRemoveHooks()->add(function() : void{ $this->networkPropertiesDirty = true; });
154
155 $this->armorInventory = new ArmorInventory();
156 //TODO: load/save armor inventory contents
157 $this->armorInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
158 $this->getViewers(),
159 fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobArmorChange($recipients, $this)
160 )));
161 $this->armorInventory->getListeners()->add(new CallbackInventoryListener(
162 onSlotChange: function(Inventory $inventory, int $slot) : void{
163 if($slot === ArmorInventory::SLOT_FEET){
164 $this->frostWalkerLevel = null;
165 }
166 },
167 onContentChange: function() : void{ $this->frostWalkerLevel = null; }
168 ));
169
170 $health = $this->getMaxHealth();
171
172 if(($healFTag = $nbt->getTag(self::TAG_LEGACY_HEALTH)) instanceof FloatTag){
173 $health = $healFTag->getValue();
174 }elseif(($healthTag = $nbt->getTag(self::TAG_HEALTH)) instanceof ShortTag){
175 $health = $healthTag->getValue(); //Older versions of PocketMine-MP incorrectly saved this as a short instead of a float
176 }elseif($healthTag instanceof FloatTag){
177 $health = $healthTag->getValue();
178 }
179
180 $this->setHealth($health);
181
182 $this->setAirSupplyTicks($nbt->getShort(self::TAG_BREATH_TICKS, self::DEFAULT_BREATH_TICKS));
183
185 $activeEffectsTag = $nbt->getListTag(self::TAG_ACTIVE_EFFECTS);
186 if($activeEffectsTag !== null){
187 foreach($activeEffectsTag as $e){
188 $effect = EffectIdMap::getInstance()->fromId($e->getByte(self::TAG_EFFECT_ID));
189 if($effect === null){
190 continue;
191 }
192
193 $this->effectManager->add(new EffectInstance(
194 $effect,
195 $e->getInt(self::TAG_EFFECT_DURATION),
196 Binary::unsignByte($e->getByte(self::TAG_EFFECT_AMPLIFIER)),
197 $e->getByte(self::TAG_EFFECT_SHOW_PARTICLES, 1) !== 0,
198 $e->getByte(self::TAG_EFFECT_AMBIENT, 0) !== 0
199 ));
200 }
201 }
202 }
203
204 protected function addAttributes() : void{
205 $this->attributeMap->add($this->healthAttr = AttributeFactory::getInstance()->mustGet(Attribute::HEALTH));
206 $this->attributeMap->add(AttributeFactory::getInstance()->mustGet(Attribute::FOLLOW_RANGE));
207 $this->attributeMap->add($this->knockbackResistanceAttr = AttributeFactory::getInstance()->mustGet(Attribute::KNOCKBACK_RESISTANCE));
208 $this->attributeMap->add($this->moveSpeedAttr = AttributeFactory::getInstance()->mustGet(Attribute::MOVEMENT_SPEED));
209 $this->attributeMap->add(AttributeFactory::getInstance()->mustGet(Attribute::ATTACK_DAMAGE));
210 $this->attributeMap->add($this->absorptionAttr = AttributeFactory::getInstance()->mustGet(Attribute::ABSORPTION));
211 }
212
216 public function getDisplayName() : string{
217 return $this->nameTag !== "" ? $this->nameTag : $this->getName();
218 }
219
220 public function setHealth(float $amount) : void{
221 $wasAlive = $this->isAlive();
222 parent::setHealth($amount);
223 $this->healthAttr->setValue(ceil($this->getHealth()), true);
224 if($this->isAlive() && !$wasAlive){
225 $this->broadcastAnimation(new RespawnAnimation($this));
226 }
227 }
228
229 public function getMaxHealth() : int{
230 return (int) $this->healthAttr->getMaxValue();
231 }
232
233 public function setMaxHealth(int $amount) : void{
234 $this->healthAttr->setMaxValue($amount)->setDefaultValue($amount);
235 }
236
237 public function getAbsorption() : float{
238 return $this->absorptionAttr->getValue();
239 }
240
241 public function setAbsorption(float $absorption) : void{
242 $this->absorptionAttr->setValue($absorption);
243 }
244
245 public function getSneakOffset() : float{
246 return 0.0;
247 }
248
249 public function isSneaking() : bool{
250 return $this->sneaking;
251 }
252
253 public function setSneaking(bool $value = true) : void{
254 $this->sneaking = $value;
255 $this->networkPropertiesDirty = true;
256 $this->recalculateSize();
257 }
258
259 public function isSprinting() : bool{
260 return $this->sprinting;
261 }
262
263 public function setSprinting(bool $value = true) : void{
264 if($value !== $this->isSprinting()){
265 $this->sprinting = $value;
266 $this->networkPropertiesDirty = true;
267 $moveSpeed = $this->getMovementSpeed();
268 $this->setMovementSpeed($value ? ($moveSpeed * 1.3) : ($moveSpeed / 1.3));
269 $this->moveSpeedAttr->markSynchronized(false); //TODO: reevaluate this hack
270 }
271 }
272
273 public function isGliding() : bool{
274 return $this->gliding;
275 }
276
277 public function setGliding(bool $value = true) : void{
278 $this->gliding = $value;
279 $this->networkPropertiesDirty = true;
280 $this->recalculateSize();
281 }
282
283 public function isSwimming() : bool{
284 return $this->swimming;
285 }
286
287 public function setSwimming(bool $value = true) : void{
288 $this->swimming = $value;
289 $this->networkPropertiesDirty = true;
290 $this->recalculateSize();
291 }
292
293 private function recalculateSize() : void{
294 $size = $this->getInitialSizeInfo();
295 if($this->isSwimming() || $this->isGliding()){
296 $width = $size->getWidth();
297 $this->setSize((new EntitySizeInfo($width, $width, $width * 0.9))->scale($this->getScale()));
298 }elseif($this->isSneaking()){
299 $this->setSize((new EntitySizeInfo($size->getHeight() - $this->getSneakOffset(), $size->getWidth(), $size->getEyeHeight() - $this->getSneakOffset()))->scale($this->getScale()));
300 }else{
301 $this->setSize($size->scale($this->getScale()));
302 }
303 }
304
305 public function getMovementSpeed() : float{
306 return $this->moveSpeedAttr->getValue();
307 }
308
309 public function setMovementSpeed(float $v, bool $fit = false) : void{
310 $this->moveSpeedAttr->setValue($v, $fit);
311 }
312
313 public function saveNBT() : CompoundTag{
314 $nbt = parent::saveNBT();
315 $nbt->setFloat(self::TAG_HEALTH, $this->getHealth());
316
317 $nbt->setShort(self::TAG_BREATH_TICKS, $this->getAirSupplyTicks());
318
319 if(count($this->effectManager->all()) > 0){
320 $effects = [];
321 foreach($this->effectManager->all() as $effect){
322 $effects[] = CompoundTag::create()
323 ->setByte(self::TAG_EFFECT_ID, EffectIdMap::getInstance()->toId($effect->getType()))
324 ->setByte(self::TAG_EFFECT_AMPLIFIER, Binary::signByte($effect->getAmplifier()))
325 ->setInt(self::TAG_EFFECT_DURATION, $effect->getDuration())
326 ->setByte(self::TAG_EFFECT_AMBIENT, $effect->isAmbient() ? 1 : 0)
327 ->setByte(self::TAG_EFFECT_SHOW_PARTICLES, $effect->isVisible() ? 1 : 0);
328 }
329
330 $nbt->setTag(self::TAG_ACTIVE_EFFECTS, new ListTag($effects));
331 }
332
333 return $nbt;
334 }
335
336 public function getEffects() : EffectManager{
337 return $this->effectManager;
338 }
339
344 public function consumeObject(Consumable $consumable) : bool{
345 $this->applyConsumptionResults($consumable);
346 return true;
347 }
348
353 protected function applyConsumptionResults(Consumable $consumable) : void{
354 foreach($consumable->getAdditionalEffects() as $effect){
355 $this->effectManager->add($effect);
356 }
357 if($consumable instanceof FoodSource){
358 $this->broadcastSound(new BurpSound());
359 }
360
361 $consumable->onConsume($this);
362 }
363
367 public function getJumpVelocity() : float{
368 return $this->jumpVelocity + ((($jumpBoost = $this->effectManager->get(VanillaEffects::JUMP_BOOST())) !== null ? $jumpBoost->getEffectLevel() : 0) / 10);
369 }
370
374 public function jump() : void{
375 if($this->onGround){
376 $this->motion = $this->motion->withComponents(null, $this->getJumpVelocity(), null); //Y motion should already be 0 if we're jumping from the ground.
377 }
378 }
379
380 protected function calculateFallDamage(float $fallDistance) : float{
381 return ceil($fallDistance - 3 - (($jumpBoost = $this->effectManager->get(VanillaEffects::JUMP_BOOST())) !== null ? $jumpBoost->getEffectLevel() : 0));
382 }
383
384 protected function onHitGround() : ?float{
385 $fallBlockPos = $this->location->floor();
386 $fallBlock = $this->getWorld()->getBlock($fallBlockPos);
387 if(count($fallBlock->getCollisionBoxes()) === 0){
388 $fallBlockPos = $fallBlockPos->down();
389 $fallBlock = $this->getWorld()->getBlock($fallBlockPos);
390 }
391 $newVerticalVelocity = $fallBlock->onEntityLand($this);
392
393 $damage = $this->calculateFallDamage($this->fallDistance);
394 if($damage > 0){
395 $ev = new EntityDamageEvent($this, EntityDamageEvent::CAUSE_FALL, $damage);
396 $this->attack($ev);
397
398 $this->broadcastSound($damage > 4 ?
399 new EntityLongFallSound($this) :
400 new EntityShortFallSound($this)
401 );
402 }elseif($fallBlock->getTypeId() !== BlockTypeIds::AIR){
403 $this->broadcastSound(new EntityLandSound($this, $fallBlock));
404 }
405 return $newVerticalVelocity;
406 }
407
413 public function getArmorPoints() : int{
414 $total = 0;
415 foreach($this->armorInventory->getContents() as $item){
416 $total += $item->getDefensePoints();
417 }
418
419 return $total;
420 }
421
425 public function getHighestArmorEnchantmentLevel(Enchantment $enchantment) : int{
426 $result = 0;
427 foreach($this->armorInventory->getContents() as $item){
428 $result = max($result, $item->getEnchantmentLevel($enchantment));
429 }
430
431 return $result;
432 }
433
434 public function getArmorInventory() : ArmorInventory{
435 return $this->armorInventory;
436 }
437
438 public function setOnFire(int $seconds) : void{
439 parent::setOnFire($seconds - (int) min($seconds, $seconds * $this->getHighestArmorEnchantmentLevel(VanillaEnchantments::FIRE_PROTECTION()) * 0.15));
440 }
441
446 public function applyDamageModifiers(EntityDamageEvent $source) : void{
447 if($this->lastDamageCause !== null && $this->attackTime > 0){
448 if($this->lastDamageCause->getBaseDamage() >= $source->getBaseDamage()){
449 $source->cancel();
450 }
451 $source->setModifier(-$this->lastDamageCause->getBaseDamage(), EntityDamageEvent::MODIFIER_PREVIOUS_DAMAGE_COOLDOWN);
452 }
453 if($source->canBeReducedByArmor()){
454 //MCPE uses the same system as PC did pre-1.9
455 $source->setModifier(-$source->getFinalDamage() * $this->getArmorPoints() * 0.04, EntityDamageEvent::MODIFIER_ARMOR);
456 }
457
458 $cause = $source->getCause();
459 if(($resistance = $this->effectManager->get(VanillaEffects::RESISTANCE())) !== null && $cause !== EntityDamageEvent::CAUSE_VOID && $cause !== EntityDamageEvent::CAUSE_SUICIDE){
460 $source->setModifier(-$source->getFinalDamage() * min(1, 0.2 * $resistance->getEffectLevel()), EntityDamageEvent::MODIFIER_RESISTANCE);
461 }
462
463 $totalEpf = 0;
464 foreach($this->armorInventory->getContents() as $item){
465 if($item instanceof Armor){
466 $totalEpf += $item->getEnchantmentProtectionFactor($source);
467 }
468 }
469 $source->setModifier(-$source->getFinalDamage() * min(ceil(min($totalEpf, 25) * (mt_rand(50, 100) / 100)), 20) * 0.04, EntityDamageEvent::MODIFIER_ARMOR_ENCHANTMENTS);
470
471 $source->setModifier(-min($this->getAbsorption(), $source->getFinalDamage()), EntityDamageEvent::MODIFIER_ABSORPTION);
472
473 if($cause === EntityDamageEvent::CAUSE_FALLING_BLOCK && $this->armorInventory->getHelmet() instanceof Armor){
474 $source->setModifier(-($source->getFinalDamage() / 4), EntityDamageEvent::MODIFIER_ARMOR_HELMET);
475 }
476 }
477
483 protected function applyPostDamageEffects(EntityDamageEvent $source) : void{
484 $this->setAbsorption(max(0, $this->getAbsorption() + $source->getModifier(EntityDamageEvent::MODIFIER_ABSORPTION)));
485 if($source->canBeReducedByArmor()){
486 $this->damageArmor($source->getBaseDamage());
487 }
488
489 if($source instanceof EntityDamageByEntityEvent && ($attacker = $source->getDamager()) !== null){
490 $damage = 0;
491 foreach($this->armorInventory->getContents() as $k => $item){
492 if($item instanceof Armor && ($thornsLevel = $item->getEnchantmentLevel(VanillaEnchantments::THORNS())) > 0){
493 if(mt_rand(0, 99) < $thornsLevel * 15){
494 $this->damageItem($item, 3);
495 $damage += ($thornsLevel > 10 ? $thornsLevel - 10 : 1 + mt_rand(0, 3));
496 }else{
497 $this->damageItem($item, 1); //thorns causes an extra +1 durability loss even if it didn't activate
498 }
499
500 $this->armorInventory->setItem($k, $item);
501 }
502 }
503
504 if($damage > 0){
505 $attacker->attack(new EntityDamageByEntityEvent($this, $attacker, EntityDamageEvent::CAUSE_MAGIC, $damage));
506 }
507
508 if($source->getModifier(EntityDamageEvent::MODIFIER_ARMOR_HELMET) < 0){
509 $helmet = $this->armorInventory->getHelmet();
510 if($helmet instanceof Armor){
511 $finalDamage = $source->getFinalDamage();
512 $this->damageItem($helmet, (int) round($finalDamage * 4 + Utils::getRandomFloat() * $finalDamage * 2));
513 $this->armorInventory->setHelmet($helmet);
514 }
515 }
516 }
517 }
518
523 public function damageArmor(float $damage) : void{
524 $durabilityRemoved = (int) max(floor($damage / 4), 1);
525
526 $armor = $this->armorInventory->getContents();
527 foreach($armor as $slotId => $item){
528 if($item instanceof Armor){
529 $oldItem = clone $item;
530 $this->damageItem($item, $durabilityRemoved);
531 if(!$item->equalsExact($oldItem)){
532 $this->armorInventory->setItem($slotId, $item);
533 }
534 }
535 }
536 }
537
538 private function damageItem(Durable $item, int $durabilityRemoved) : void{
539 $item->applyDamage($durabilityRemoved);
540 if($item->isBroken()){
541 $this->broadcastSound(new ItemBreakSound());
542 }
543 }
544
545 public function attack(EntityDamageEvent $source) : void{
546 if($this->noDamageTicks > 0 && $source->getCause() !== EntityDamageEvent::CAUSE_SUICIDE){
547 $source->cancel();
548 }
549
550 if($this->effectManager->has(VanillaEffects::FIRE_RESISTANCE()) && (
551 $source->getCause() === EntityDamageEvent::CAUSE_FIRE
552 || $source->getCause() === EntityDamageEvent::CAUSE_FIRE_TICK
553 || $source->getCause() === EntityDamageEvent::CAUSE_LAVA
554 )
555 ){
556 $source->cancel();
557 }
558
559 if($source->getCause() !== EntityDamageEvent::CAUSE_SUICIDE){
560 $this->applyDamageModifiers($source);
561 }
562
563 if($source instanceof EntityDamageByEntityEvent && (
564 $source->getCause() === EntityDamageEvent::CAUSE_BLOCK_EXPLOSION ||
565 $source->getCause() === EntityDamageEvent::CAUSE_ENTITY_EXPLOSION)
566 ){
567 //TODO: knockback should not just apply for entity damage sources
568 //this doesn't matter for TNT right now because the PrimedTNT entity is considered the source, not the block.
569 $base = $source->getKnockBack();
570 $source->setKnockBack($base - min($base, $base * $this->getHighestArmorEnchantmentLevel(VanillaEnchantments::BLAST_PROTECTION()) * 0.15));
571 }
572
573 parent::attack($source);
574
575 if($source->isCancelled()){
576 return;
577 }
578
579 if($this->attackTime <= 0){
580 //this logic only applies if the entity was cold attacked
581
582 $this->attackTime = $source->getAttackCooldown();
583
584 if($source instanceof EntityDamageByChildEntityEvent){
585 $e = $source->getChild();
586 if($e !== null){
587 $motion = $e->getMotion();
588 $this->knockBack($motion->x, $motion->z, $source->getKnockBack(), $source->getVerticalKnockBackLimit());
589 }
590 }elseif($source instanceof EntityDamageByEntityEvent){
591 $e = $source->getDamager();
592 if($e !== null){
593 $deltaX = $this->location->x - $e->location->x;
594 $deltaZ = $this->location->z - $e->location->z;
595 $this->knockBack($deltaX, $deltaZ, $source->getKnockBack(), $source->getVerticalKnockBackLimit());
596 }
597 }
598
599 if($this->isAlive()){
600 $this->doHitAnimation();
601 }
602 }
603
604 if($this->isAlive()){
605 $this->applyPostDamageEffects($source);
606 }
607 }
608
609 protected function doHitAnimation() : void{
610 $this->broadcastAnimation(new HurtAnimation($this));
611 }
612
613 public function knockBack(float $x, float $z, float $force = self::DEFAULT_KNOCKBACK_FORCE, ?float $verticalLimit = self::DEFAULT_KNOCKBACK_VERTICAL_LIMIT) : void{
614 $f = sqrt($x * $x + $z * $z);
615 if($f <= 0){
616 return;
617 }
618 if(mt_rand() / mt_getrandmax() > $this->knockbackResistanceAttr->getValue()){
619 $f = 1 / $f;
620
621 $motionX = $this->motion->x / 2;
622 $motionY = $this->motion->y / 2;
623 $motionZ = $this->motion->z / 2;
624 $motionX += $x * $f * $force;
625 $motionY += $force;
626 $motionZ += $z * $f * $force;
627
628 $verticalLimit ??= $force;
629 if($motionY > $verticalLimit){
630 $motionY = $verticalLimit;
631 }
632
633 $this->setMotion(new Vector3($motionX, $motionY, $motionZ));
634 }
635 }
636
637 protected function onDeath() : void{
638 $ev = new EntityDeathEvent($this, $this->getDrops(), $this->getXpDropAmount());
639 $ev->call();
640 foreach($ev->getDrops() as $item){
641 $this->getWorld()->dropItem($this->location, $item);
642 }
643
644 //TODO: check death conditions (must have been damaged by player < 5 seconds from death)
645 $this->getWorld()->dropExperience($this->location, $ev->getXpDropAmount());
646
647 $this->startDeathAnimation();
648 }
649
650 protected function onDeathUpdate(int $tickDiff) : bool{
651 if($this->deadTicks < $this->maxDeadTicks){
652 $this->deadTicks += $tickDiff;
653 if($this->deadTicks >= $this->maxDeadTicks){
654 $this->endDeathAnimation();
655 }
656 }
657
658 return $this->deadTicks >= $this->maxDeadTicks;
659 }
660
661 protected function startDeathAnimation() : void{
662 $this->broadcastAnimation(new DeathAnimation($this));
663 }
664
665 protected function endDeathAnimation() : void{
666 $this->despawnFromAll();
667 }
668
669 protected function entityBaseTick(int $tickDiff = 1) : bool{
670 Timings::$livingEntityBaseTick->startTiming();
671
672 $hasUpdate = parent::entityBaseTick($tickDiff);
673
674 if($this->isAlive()){
675 if($this->effectManager->tick($tickDiff)){
676 $hasUpdate = true;
677 }
678
679 if($this->isInsideOfSolid()){
680 $hasUpdate = true;
681 $ev = new EntityDamageEvent($this, EntityDamageEvent::CAUSE_SUFFOCATION, 1);
682 $this->attack($ev);
683 }
684
685 if($this->doAirSupplyTick($tickDiff)){
686 $hasUpdate = true;
687 }
688
689 foreach($this->armorInventory->getContents() as $index => $item){
690 $oldItem = clone $item;
691 if($item->onTickWorn($this)){
692 $hasUpdate = true;
693 if(!$item->equalsExact($oldItem)){
694 $this->armorInventory->setItem($index, $item);
695 }
696 }
697 }
698 }
699
700 if($this->attackTime > 0){
701 $this->attackTime -= $tickDiff;
702 }
703
704 Timings::$livingEntityBaseTick->stopTiming();
705
706 return $hasUpdate;
707 }
708
709 protected function move(float $dx, float $dy, float $dz) : void{
710 $oldX = $this->location->x;
711 $oldZ = $this->location->z;
712
713 parent::move($dx, $dy, $dz);
714
715 $frostWalkerLevel = $this->getFrostWalkerLevel();
716 if($frostWalkerLevel > 0 && (abs($this->location->x - $oldX) > self::MOTION_THRESHOLD || abs($this->location->z - $oldZ) > self::MOTION_THRESHOLD)){
717 $this->applyFrostWalker($frostWalkerLevel);
718 }
719 }
720
721 protected function applyFrostWalker(int $level) : void{
722 $radius = $level + 2;
723 $world = $this->getWorld();
724
725 $baseX = $this->location->getFloorX();
726 $y = $this->location->getFloorY() - 1;
727 $baseZ = $this->location->getFloorZ();
728
729 $liquid = VanillaBlocks::WATER();
730 $targetBlock = VanillaBlocks::FROSTED_ICE();
731 if(EntityFrostWalkerEvent::hasHandlers()){
732 $ev = new EntityFrostWalkerEvent($this, $radius, $liquid, $targetBlock);
733 $ev->call();
734 if($ev->isCancelled()){
735 return;
736 }
737 $radius = $ev->getRadius();
738 $liquid = $ev->getLiquid();
739 $targetBlock = $ev->getTargetBlock();
740 }
741
742 for($x = $baseX - $radius; $x <= $baseX + $radius; $x++){
743 for($z = $baseZ - $radius; $z <= $baseZ + $radius; $z++){
744 $block = $world->getBlockAt($x, $y, $z);
745 if(
746 !$block->isSameState($liquid) ||
747 $world->getBlockAt($x, $y + 1, $z)->getTypeId() !== BlockTypeIds::AIR ||
748 count($world->getNearbyEntities(AxisAlignedBB::one()->offsetCopy($x, $y, $z))) !== 0
749 ){
750 continue;
751 }
752 $world->setBlockAt($x, $y, $z, $targetBlock);
753 }
754 }
755 }
756
757 public function getFrostWalkerLevel() : int{
758 return $this->frostWalkerLevel ??= $this->armorInventory->getBoots()->getEnchantmentLevel(VanillaEnchantments::FROST_WALKER());
759 }
760
764 protected function doAirSupplyTick(int $tickDiff) : bool{
765 $ticks = $this->getAirSupplyTicks();
766 $oldTicks = $ticks;
767 if(!$this->canBreathe()){
768 $this->setBreathing(false);
769
770 if(($respirationLevel = $this->armorInventory->getHelmet()->getEnchantmentLevel(VanillaEnchantments::RESPIRATION())) <= 0 ||
771 Utils::getRandomFloat() <= (1 / ($respirationLevel + 1))
772 ){
773 $ticks -= $tickDiff;
774 if($ticks <= -20){
775 $ticks = 0;
776 $this->onAirExpired();
777 }
778 }
779 }elseif(!$this->isBreathing()){
780 if($ticks < ($max = $this->getMaxAirSupplyTicks())){
781 $ticks += $tickDiff * 5;
782 }
783 if($ticks >= $max){
784 $ticks = $max;
785 $this->setBreathing(true);
786 }
787 }
788
789 if($ticks !== $oldTicks){
790 $this->setAirSupplyTicks($ticks);
791 }
792
793 return $ticks !== $oldTicks;
794 }
795
799 public function canBreathe() : bool{
800 return $this->effectManager->has(VanillaEffects::WATER_BREATHING()) || $this->effectManager->has(VanillaEffects::CONDUIT_POWER()) || !$this->isUnderwater();
801 }
802
806 public function isBreathing() : bool{
807 return $this->breathing;
808 }
809
814 public function setBreathing(bool $value = true) : void{
815 $this->breathing = $value;
816 $this->networkPropertiesDirty = true;
817 }
818
823 public function getAirSupplyTicks() : int{
824 return $this->breathTicks;
825 }
826
830 public function setAirSupplyTicks(int $ticks) : void{
831 $this->breathTicks = $ticks;
832 $this->networkPropertiesDirty = true;
833 }
834
838 public function getMaxAirSupplyTicks() : int{
839 return $this->maxBreathTicks;
840 }
841
845 public function setMaxAirSupplyTicks(int $ticks) : void{
846 $this->maxBreathTicks = $ticks;
847 $this->networkPropertiesDirty = true;
848 }
849
854 public function onAirExpired() : void{
855 $ev = new EntityDamageEvent($this, EntityDamageEvent::CAUSE_DROWNING, 2);
856 $this->attack($ev);
857 }
858
862 public function getDrops() : array{
863 return [];
864 }
865
869 public function getXpDropAmount() : int{
870 return 0;
871 }
872
879 public function getLineOfSight(int $maxDistance, int $maxLength = 0, array $transparent = []) : array{
880 if($maxDistance > 120){
881 $maxDistance = 120;
882 }
883
884 if(count($transparent) === 0){
885 $transparent = null;
886 }
887
888 $blocks = [];
889 $nextIndex = 0;
890
891 foreach(VoxelRayTrace::inDirection($this->location->add(0, $this->size->getEyeHeight(), 0), $this->getDirectionVector(), $maxDistance) as $vector3){
892 $block = $this->getWorld()->getBlockAt($vector3->x, $vector3->y, $vector3->z);
893 $blocks[$nextIndex++] = $block;
894
895 if($maxLength !== 0 && count($blocks) > $maxLength){
896 array_shift($blocks);
897 --$nextIndex;
898 }
899
900 $id = $block->getTypeId();
901
902 if($transparent === null){
903 if($id !== BlockTypeIds::AIR){
904 break;
905 }
906 }else{
907 if(!isset($transparent[$id])){
908 break;
909 }
910 }
911 }
912
913 return $blocks;
914 }
915
920 public function getTargetBlock(int $maxDistance, array $transparent = []) : ?Block{
921 $line = $this->getLineOfSight($maxDistance, 1, $transparent);
922 if(count($line) > 0){
923 return array_shift($line);
924 }
925
926 return null;
927 }
928
933 public function lookAt(Vector3 $target) : void{
934 $horizontal = sqrt(($target->x - $this->location->x) ** 2 + ($target->z - $this->location->z) ** 2);
935 $vertical = $target->y - ($this->location->y + $this->getEyeHeight());
936 $pitch = -atan2($vertical, $horizontal) / M_PI * 180; //negative is up, positive is down
937
938 $xDist = $target->x - $this->location->x;
939 $zDist = $target->z - $this->location->z;
940
941 $yaw = atan2($zDist, $xDist) / M_PI * 180 - 90;
942 if($yaw < 0){
943 $yaw += 360.0;
944 }
945
946 $this->setRotation($yaw, $pitch);
947 }
948
949 protected function sendSpawnPacket(Player $player) : void{
950 parent::sendSpawnPacket($player);
951
952 $networkSession = $player->getNetworkSession();
953 $networkSession->getEntityEventBroadcaster()->onMobArmorChange([$networkSession], $this);
954 }
955
956 protected function syncNetworkData(EntityMetadataCollection $properties) : void{
957 parent::syncNetworkData($properties);
958
959 $visibleEffects = [];
960 foreach ($this->effectManager->all() as $effect) {
961 if (!$effect->isVisible() || !$effect->getType()->hasBubbles()) {
962 continue;
963 }
964 $visibleEffects[EffectIdMap::getInstance()->toId($effect->getType())] = $effect->isAmbient();
965 }
966
967 //TODO: HACK! the client may not be able to identify effects if they are not sorted.
968 ksort($visibleEffects, SORT_NUMERIC);
969
970 $effectsData = 0;
971 $packedEffectsCount = 0;
972 foreach ($visibleEffects as $effectId => $isAmbient) {
973 $effectsData = ($effectsData << 7) |
974 (($effectId & 0x3f) << 1) | //Why not use 7 bits instead of only 6? mojang...
975 ($isAmbient ? 1 : 0);
976
977 if (++$packedEffectsCount >= 8) {
978 break;
979 }
980 }
981 $properties->setLong(EntityMetadataProperties::VISIBLE_MOB_EFFECTS, $effectsData);
982
983 $properties->setShort(EntityMetadataProperties::AIR, $this->breathTicks);
984 $properties->setShort(EntityMetadataProperties::MAX_AIR, $this->maxBreathTicks);
985
986 $properties->setGenericFlag(EntityMetadataFlags::BREATHING, $this->breathing);
987 $properties->setGenericFlag(EntityMetadataFlags::SNEAKING, $this->sneaking);
988 $properties->setGenericFlag(EntityMetadataFlags::SPRINTING, $this->sprinting);
989 $properties->setGenericFlag(EntityMetadataFlags::GLIDING, $this->gliding);
990 $properties->setGenericFlag(EntityMetadataFlags::SWIMMING, $this->swimming);
991 }
992
993 protected function onDispose() : void{
994 $this->armorInventory->removeAllWindows();
995 $this->effectManager->getEffectAddHooks()->clear();
996 $this->effectManager->getEffectRemoveHooks()->clear();
997 parent::onDispose();
998 }
999
1000 protected function destroyCycles() : void{
1001 unset(
1002 $this->effectManager
1003 );
1004 parent::destroyCycles();
1005 }
1006}
applyPostDamageEffects(EntityDamageEvent $source)
Definition Living.php:483
sendSpawnPacket(Player $player)
Definition Living.php:949
setMaxAirSupplyTicks(int $ticks)
Definition Living.php:845
setBreathing(bool $value=true)
Definition Living.php:814
lookAt(Vector3 $target)
Definition Living.php:933
onDeathUpdate(int $tickDiff)
Definition Living.php:650
damageArmor(float $damage)
Definition Living.php:523
setHealth(float $amount)
Definition Living.php:220
getLineOfSight(int $maxDistance, int $maxLength=0, array $transparent=[])
Definition Living.php:879
const DEFAULT_KNOCKBACK_VERTICAL_LIMIT
Definition Living.php:99
applyDamageModifiers(EntityDamageEvent $source)
Definition Living.php:446
getTargetBlock(int $maxDistance, array $transparent=[])
Definition Living.php:920
consumeObject(Consumable $consumable)
Definition Living.php:344
applyConsumptionResults(Consumable $consumable)
Definition Living.php:353
setAirSupplyTicks(int $ticks)
Definition Living.php:830
doAirSupplyTick(int $tickDiff)
Definition Living.php:764
getHighestArmorEnchantmentLevel(Enchantment $enchantment)
Definition Living.php:425
setTag(string $name, Tag $tag)
setFloat(string $name, float $value)
setShort(string $name, int $value)
getEnchantmentLevel(Enchantment $enchantment)