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