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