22declare(strict_types=1);
66use
function array_shift;
71use
function lcg_value;
74use
function mt_getrandmax;
81 protected const DEFAULT_BREATH_TICKS = 300;
94 private const TAG_LEGACY_HEALTH =
"HealF";
95 private const TAG_HEALTH =
"Health";
96 private const TAG_BREATH_TICKS =
"Air";
97 private const TAG_ACTIVE_EFFECTS =
"ActiveEffects";
98 private const TAG_EFFECT_ID =
"Id";
99 private const TAG_EFFECT_DURATION =
"Duration";
100 private const TAG_EFFECT_AMPLIFIER =
"Amplifier";
101 private const TAG_EFFECT_SHOW_PARTICLES =
"ShowParticles";
102 private const TAG_EFFECT_AMBIENT =
"Ambient";
104 protected int $attackTime = 0;
106 public int $deadTicks = 0;
107 protected int $maxDeadTicks = 25;
109 protected float $jumpVelocity = 0.42;
115 protected bool $breathing =
true;
116 protected int $breathTicks = self::DEFAULT_BREATH_TICKS;
117 protected int $maxBreathTicks = self::DEFAULT_BREATH_TICKS;
121 protected Attribute $knockbackResistanceAttr;
124 protected bool $sprinting =
false;
125 protected bool $sneaking =
false;
126 protected bool $gliding =
false;
127 protected bool $swimming =
false;
133 abstract public function getName() : string;
139 protected function initEntity(
CompoundTag $nbt) : void{
140 parent::initEntity($nbt);
143 $this->effectManager->getEffectAddHooks()->add(
function() :
void{ $this->networkPropertiesDirty =
true; });
144 $this->effectManager->getEffectRemoveHooks()->add(
function() :
void{ $this->networkPropertiesDirty =
true; });
146 $this->armorInventory =
new ArmorInventory($this);
148 $this->armorInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
150 fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobArmorChange($recipients, $this)
153 $health = $this->getMaxHealth();
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();
159 }elseif($healthTag instanceof FloatTag){
160 $health = $healthTag->getValue();
163 $this->setHealth($health);
165 $this->setAirSupplyTicks($nbt->getShort(self::TAG_BREATH_TICKS, self::DEFAULT_BREATH_TICKS));
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){
176 $this->effectManager->add(
new EffectInstance(
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
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));
200 return $this->nameTag !==
"" ? $this->nameTag : $this->getName();
204 $wasAlive = $this->isAlive();
205 parent::setHealth($amount);
206 $this->healthAttr->setValue(ceil($this->getHealth()),
true);
207 if($this->isAlive() && !$wasAlive){
212 public function getMaxHealth() : int{
213 return (int) $this->healthAttr->getMaxValue();
216 public function setMaxHealth(
int $amount) : void{
217 $this->healthAttr->setMaxValue($amount)->setDefaultValue($amount);
220 public function getAbsorption() : float{
221 return $this->absorptionAttr->getValue();
224 public function setAbsorption(
float $absorption) : void{
225 $this->absorptionAttr->setValue($absorption);
228 public function isSneaking() : bool{
229 return $this->sneaking;
232 public function setSneaking(
bool $value =
true) : void{
233 $this->sneaking = $value;
234 $this->networkPropertiesDirty =
true;
235 $this->recalculateSize();
238 public function isSprinting() : bool{
239 return $this->sprinting;
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);
252 public function isGliding() : bool{
253 return $this->gliding;
256 public function setGliding(
bool $value =
true) : void{
257 $this->gliding = $value;
258 $this->networkPropertiesDirty =
true;
259 $this->recalculateSize();
262 public function isSwimming() : bool{
263 return $this->swimming;
266 public function setSwimming(
bool $value =
true) : void{
267 $this->swimming = $value;
268 $this->networkPropertiesDirty =
true;
269 $this->recalculateSize();
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()));
280 $this->setSize($size->scale($this->getScale()));
284 public function getMovementSpeed() : float{
285 return $this->moveSpeedAttr->getValue();
288 public function setMovementSpeed(
float $v,
bool $fit =
false) : void{
289 $this->moveSpeedAttr->setValue($v, $fit);
292 public function saveNBT() : CompoundTag{
293 $nbt = parent::saveNBT();
294 $nbt->
setFloat(self::TAG_HEALTH, $this->getHealth());
296 $nbt->
setShort(self::TAG_BREATH_TICKS, $this->getAirSupplyTicks());
298 if(count($this->effectManager->all()) > 0){
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);
309 $nbt->
setTag(self::TAG_ACTIVE_EFFECTS,
new ListTag($effects));
316 return $this->effectManager;
324 $this->applyConsumptionResults($consumable);
333 foreach($consumable->getAdditionalEffects() as $effect){
334 $this->effectManager->add($effect);
340 $consumable->onConsume($this);
347 return $this->jumpVelocity + ((($jumpBoost = $this->effectManager->get(
VanillaEffects::JUMP_BOOST())) !== null ? $jumpBoost->getEffectLevel() : 0) / 10);
355 $this->motion = $this->motion->withComponents(
null, $this->getJumpVelocity(),
null);
359 protected function calculateFallDamage(
float $fallDistance) : float{
360 return ceil($fallDistance - 3 - (($jumpBoost = $this->effectManager->get(VanillaEffects::JUMP_BOOST())) !== null ? $jumpBoost->getEffectLevel() : 0));
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);
370 $newVerticalVelocity = $fallBlock->onEntityLand($this);
372 $damage = $this->calculateFallDamage($this->fallDistance);
377 $this->broadcastSound($damage > 4 ?
381 }elseif($fallBlock->getTypeId() !== BlockTypeIds::AIR){
382 $this->broadcastSound(
new EntityLandSound($this, $fallBlock));
384 return $newVerticalVelocity;
394 foreach($this->armorInventory->getContents() as $item){
395 $total += $item->getDefensePoints();
406 foreach($this->armorInventory->getContents() as $item){
407 $result = max($result, $item->getEnchantmentLevel($enchantment));
413 public function getArmorInventory() : ArmorInventory{
414 return $this->armorInventory;
417 public function setOnFire(
int $seconds) : void{
418 parent::setOnFire($seconds - (int) min($seconds, $seconds * $this->getHighestArmorEnchantmentLevel(VanillaEnchantments::FIRE_PROTECTION()) * 0.15));
426 if($this->lastDamageCause !== null && $this->attackTime > 0){
427 if($this->lastDamageCause->getBaseDamage() >= $source->
getBaseDamage()){
430 $source->setModifier(-$this->lastDamageCause->getBaseDamage(), EntityDamageEvent::MODIFIER_PREVIOUS_DAMAGE_COOLDOWN);
432 if($source->canBeReducedByArmor()){
434 $source->setModifier(-$source->getFinalDamage() * $this->getArmorPoints() * 0.04, EntityDamageEvent::MODIFIER_ARMOR);
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);
443 foreach($this->armorInventory->getContents() as $item){
444 if($item instanceof Armor){
445 $totalEpf += $item->getEnchantmentProtectionFactor($source);
448 $source->setModifier(-$source->getFinalDamage() * min(ceil(min($totalEpf, 25) * (mt_rand(50, 100) / 100)), 20) * 0.04, EntityDamageEvent::MODIFIER_ARMOR_ENCHANTMENTS);
450 $source->setModifier(-min($this->getAbsorption(), $source->getFinalDamage()), EntityDamageEvent::MODIFIER_ABSORPTION);
452 if($cause === EntityDamageEvent::CAUSE_FALLING_BLOCK && $this->armorInventory->getHelmet() instanceof Armor){
453 $source->setModifier(-($source->getFinalDamage() / 4), EntityDamageEvent::MODIFIER_ARMOR_HELMET);
463 $this->setAbsorption(max(0, $this->getAbsorption() + $source->getModifier(
EntityDamageEvent::MODIFIER_ABSORPTION)));
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));
476 $this->damageItem($item, 1);
479 $this->armorInventory->setItem($k, $item);
484 $attacker->attack(
new EntityDamageByEntityEvent($this, $attacker, EntityDamageEvent::CAUSE_MAGIC, $damage));
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);
503 $durabilityRemoved = (int) max(floor($damage / 4), 1);
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);
517 private function damageItem(Durable $item,
int $durabilityRemoved) : void{
518 $item->applyDamage($durabilityRemoved);
519 if($item->isBroken()){
520 $this->broadcastSound(
new ItemBreakSound());
524 public function attack(EntityDamageEvent $source) : void{
525 if($this->noDamageTicks > 0 && $source->getCause() !== EntityDamageEvent::CAUSE_SUICIDE){
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
538 if($source->getCause() !== EntityDamageEvent::CAUSE_SUICIDE){
539 $this->applyDamageModifiers($source);
542 if($source instanceof EntityDamageByEntityEvent && (
543 $source->getCause() === EntityDamageEvent::CAUSE_BLOCK_EXPLOSION ||
544 $source->getCause() === EntityDamageEvent::CAUSE_ENTITY_EXPLOSION)
548 $base = $source->getKnockBack();
549 $source->setKnockBack($base - min($base, $base * $this->getHighestArmorEnchantmentLevel(VanillaEnchantments::BLAST_PROTECTION()) * 0.15));
552 parent::attack($source);
554 if($source->isCancelled()){
558 if($this->attackTime <= 0){
561 $this->attackTime = $source->getAttackCooldown();
563 if($source instanceof EntityDamageByChildEntityEvent){
564 $e = $source->getChild();
566 $motion = $e->getMotion();
567 $this->knockBack($motion->x, $motion->z, $source->getKnockBack(), $source->getVerticalKnockBackLimit());
569 }elseif($source instanceof EntityDamageByEntityEvent){
570 $e = $source->getDamager();
572 $deltaX = $this->location->x - $e->location->x;
573 $deltaZ = $this->location->z - $e->location->z;
574 $this->knockBack($deltaX, $deltaZ, $source->getKnockBack(), $source->getVerticalKnockBackLimit());
578 if($this->isAlive()){
579 $this->doHitAnimation();
583 if($this->isAlive()){
584 $this->applyPostDamageEffects($source);
588 protected function doHitAnimation() : void{
589 $this->broadcastAnimation(new HurtAnimation($this));
592 public function knockBack(
float $x,
float $z,
float $force = self::DEFAULT_KNOCKBACK_FORCE, ?
float $verticalLimit = self::DEFAULT_KNOCKBACK_VERTICAL_LIMIT) : void{
593 $f = sqrt($x * $x + $z * $z);
597 if(mt_rand() / mt_getrandmax() > $this->knockbackResistanceAttr->getValue()){
600 $motionX = $this->motion->x / 2;
601 $motionY = $this->motion->y / 2;
602 $motionZ = $this->motion->z / 2;
603 $motionX += $x * $f * $force;
605 $motionZ += $z * $f * $force;
607 $verticalLimit ??= $force;
608 if($motionY > $verticalLimit){
609 $motionY = $verticalLimit;
612 $this->setMotion(
new Vector3($motionX, $motionY, $motionZ));
617 $ev = new
EntityDeathEvent($this, $this->getDrops(), $this->getXpDropAmount());
619 foreach($ev->getDrops() as $item){
620 $this->getWorld()->dropItem($this->location, $item);
624 $this->getWorld()->dropExperience($this->location, $ev->getXpDropAmount());
626 $this->startDeathAnimation();
630 if($this->deadTicks < $this->maxDeadTicks){
631 $this->deadTicks += $tickDiff;
632 if($this->deadTicks >= $this->maxDeadTicks){
633 $this->endDeathAnimation();
637 return $this->deadTicks >= $this->maxDeadTicks;
640 protected function startDeathAnimation() : void{
641 $this->broadcastAnimation(new DeathAnimation($this));
644 protected function endDeathAnimation() : void{
645 $this->despawnFromAll();
648 protected function entityBaseTick(
int $tickDiff = 1) : bool{
649 Timings::$livingEntityBaseTick->startTiming();
651 $hasUpdate = parent::entityBaseTick($tickDiff);
653 if($this->isAlive()){
654 if($this->effectManager->tick($tickDiff)){
658 if($this->isInsideOfSolid()){
660 $ev =
new EntityDamageEvent($this, EntityDamageEvent::CAUSE_SUFFOCATION, 1);
664 if($this->doAirSupplyTick($tickDiff)){
668 foreach($this->armorInventory->getContents() as $index => $item){
669 $oldItem = clone $item;
670 if($item->onTickWorn($this)){
672 if(!$item->equalsExact($oldItem)){
673 $this->armorInventory->setItem($index, $item);
679 if($this->attackTime > 0){
680 $this->attackTime -= $tickDiff;
683 Timings::$livingEntityBaseTick->stopTiming();
692 $ticks = $this->getAirSupplyTicks();
694 if(!$this->canBreathe()){
695 $this->setBreathing(
false);
697 if(($respirationLevel = $this->armorInventory->getHelmet()->getEnchantmentLevel(VanillaEnchantments::RESPIRATION())) <= 0 ||
698 lcg_value() <= (1 / ($respirationLevel + 1))
703 $this->onAirExpired();
706 }elseif(!$this->isBreathing()){
707 if($ticks < ($max = $this->getMaxAirSupplyTicks())){
708 $ticks += $tickDiff * 5;
712 $this->setBreathing(
true);
716 if($ticks !== $oldTicks){
717 $this->setAirSupplyTicks($ticks);
720 return $ticks !== $oldTicks;
727 return $this->effectManager->has(
VanillaEffects::WATER_BREATHING()) || $this->effectManager->has(
VanillaEffects::CONDUIT_POWER()) || !$this->isUnderwater();
734 return $this->breathing;
742 $this->breathing = $value;
743 $this->networkPropertiesDirty =
true;
751 return $this->breathTicks;
758 $this->breathTicks = $ticks;
759 $this->networkPropertiesDirty =
true;
766 return $this->maxBreathTicks;
773 $this->maxBreathTicks = $ticks;
774 $this->networkPropertiesDirty =
true;
806 public function getLineOfSight(
int $maxDistance,
int $maxLength = 0, array $transparent = []) : array{
807 if($maxDistance > 120){
811 if(count($transparent) === 0){
818 foreach(VoxelRayTrace::inDirection($this->location->add(0, $this->size->getEyeHeight(), 0), $this->getDirectionVector(), $maxDistance) as $vector3){
819 $block = $this->getWorld()->getBlockAt($vector3->x, $vector3->y, $vector3->z);
820 $blocks[$nextIndex++] = $block;
822 if($maxLength !== 0 && count($blocks) > $maxLength){
823 array_shift($blocks);
827 $id = $block->getTypeId();
829 if($transparent ===
null){
830 if($id !== BlockTypeIds::AIR){
834 if(!isset($transparent[$id])){
848 $line = $this->getLineOfSight($maxDistance, 1, $transparent);
849 if(count($line) > 0){
850 return array_shift($line);
861 $horizontal = sqrt(($target->x - $this->location->x) ** 2 + ($target->z - $this->location->z) ** 2);
862 $vertical = $target->y - ($this->location->y + $this->getEyeHeight());
863 $pitch = -atan2($vertical, $horizontal) / M_PI * 180;
865 $xDist = $target->x - $this->location->x;
866 $zDist = $target->z - $this->location->z;
868 $yaw = atan2($zDist, $xDist) / M_PI * 180 - 90;
873 $this->setRotation($yaw, $pitch);
877 parent::sendSpawnPacket($player);
879 $networkSession = $player->getNetworkSession();
880 $networkSession->getEntityEventBroadcaster()->onMobArmorChange([$networkSession], $this);
884 parent::syncNetworkData($properties);
886 $properties->setByte(EntityMetadataProperties::POTION_AMBIENT, $this->effectManager->hasOnlyAmbientEffects() ? 1 : 0);
887 $properties->setInt(EntityMetadataProperties::POTION_COLOR, Binary::signInt($this->effectManager->getBubbleColor()->toARGB()));
888 $properties->setShort(EntityMetadataProperties::AIR, $this->breathTicks);
889 $properties->setShort(EntityMetadataProperties::MAX_AIR, $this->maxBreathTicks);
891 $properties->setGenericFlag(EntityMetadataFlags::BREATHING, $this->breathing);
892 $properties->setGenericFlag(EntityMetadataFlags::SNEAKING, $this->sneaking);
893 $properties->setGenericFlag(EntityMetadataFlags::SPRINTING, $this->sprinting);
894 $properties->setGenericFlag(EntityMetadataFlags::GLIDING, $this->gliding);
895 $properties->setGenericFlag(EntityMetadataFlags::SWIMMING, $this->swimming);
899 $this->armorInventory->removeAllViewers();
900 $this->effectManager->getEffectAddHooks()->clear();
901 $this->effectManager->getEffectRemoveHooks()->clear();
907 $this->armorInventory,
910 parent::destroyCycles();
applyPostDamageEffects(EntityDamageEvent $source)
sendSpawnPacket(Player $player)
setMaxAirSupplyTicks(int $ticks)
setBreathing(bool $value=true)
const DEFAULT_KNOCKBACK_FORCE
onDeathUpdate(int $tickDiff)
damageArmor(float $damage)
getLineOfSight(int $maxDistance, int $maxLength=0, array $transparent=[])
const DEFAULT_KNOCKBACK_VERTICAL_LIMIT
applyDamageModifiers(EntityDamageEvent $source)
getTargetBlock(int $maxDistance, array $transparent=[])
consumeObject(Consumable $consumable)
applyConsumptionResults(Consumable $consumable)
setAirSupplyTicks(int $ticks)
getInitialDragMultiplier()
doAirSupplyTick(int $tickDiff)
getHighestArmorEnchantmentLevel(Enchantment $enchantment)
setTag(string $name, Tag $tag)
setFloat(string $name, float $value)
setShort(string $name, int $value)