diff --git a/src/pocketmine/entity/Entity.php b/src/pocketmine/entity/Entity.php index f39ccd97c..1d7b6e5a3 100644 --- a/src/pocketmine/entity/Entity.php +++ b/src/pocketmine/entity/Entity.php @@ -29,6 +29,7 @@ namespace pocketmine\entity; use pocketmine\block\Block; use pocketmine\block\BlockFactory; use pocketmine\block\Water; +use pocketmine\entity\object\ExperienceOrb; use pocketmine\entity\projectile\Arrow; use pocketmine\entity\projectile\Egg; use pocketmine\entity\projectile\Snowball; @@ -227,6 +228,7 @@ abstract class Entity extends Location implements Metadatable, EntityIds{ Entity::registerEntity(Arrow::class, false, ['Arrow', 'minecraft:arrow']); Entity::registerEntity(Egg::class, false, ['Egg', 'minecraft:egg']); + Entity::registerEntity(ExperienceOrb::class, false, ['XPOrb', 'minecraft:xp_orb']); Entity::registerEntity(FallingSand::class, false, ['FallingSand', 'minecraft:falling_block']); Entity::registerEntity(Item::class, false, ['Item', 'minecraft:item']); Entity::registerEntity(PrimedTNT::class, false, ['PrimedTnt', 'PrimedTNT', 'minecraft:tnt']); diff --git a/src/pocketmine/entity/Human.php b/src/pocketmine/entity/Human.php index 163571cd4..8f9e43f88 100644 --- a/src/pocketmine/entity/Human.php +++ b/src/pocketmine/entity/Human.php @@ -76,6 +76,7 @@ class Human extends Creature implements ProjectileSource, InventoryHolder{ protected $totalXp = 0; protected $xpSeed; + protected $xpCooldown = 0; protected $baseOffset = 1.62; @@ -410,6 +411,23 @@ class Human extends Creature implements ProjectileSource, InventoryHolder{ $this->totalXp = $amount; } + /** + * Returns whether the human can pickup XP orbs (checks cooldown time) + * @return bool + */ + public function canPickupXp() : bool{ + return $this->xpCooldown === 0; + } + + /** + * Sets the duration in ticks until the human can pick up another XP orb. + * @param int $value + */ + public function resetXpCooldown(int $value = 2){ + $this->xpCooldown = $value; + } + + public function getInventory(){ return $this->inventory; } @@ -505,6 +523,10 @@ class Human extends Creature implements ProjectileSource, InventoryHolder{ $this->doFoodTick($tickDiff); + if($this->xpCooldown > 0){ + $this->xpCooldown--; + } + return $hasUpdate; } diff --git a/src/pocketmine/entity/object/ExperienceOrb.php b/src/pocketmine/entity/object/ExperienceOrb.php new file mode 100644 index 000000000..6e87ab1a5 --- /dev/null +++ b/src/pocketmine/entity/object/ExperienceOrb.php @@ -0,0 +1,170 @@ +age = $this->namedtag->getShort("Age", 0); + + $value = 0; + if($this->namedtag->hasTag(self::TAG_VALUE_PC, ShortTag::class)){ //PC + $value = $this->namedtag->getShort(self::TAG_VALUE_PC); + }elseif($this->namedtag->hasTag(self::TAG_VALUE_PE, IntTag::class)){ //PE save format + $value = $this->namedtag->getInt(self::TAG_VALUE_PE); + } + + $this->setXpValue($value); + } + + public function saveNBT(){ + parent::saveNBT(); + + $this->namedtag->setShort("Age", $this->age); + + $this->namedtag->setShort(self::TAG_VALUE_PC, $this->getXpValue()); + $this->namedtag->setInt(self::TAG_VALUE_PE, $this->getXpValue()); + } + + public function getXpValue() : int{ + return $this->getDataProperty(self::DATA_EXPERIENCE_VALUE) ?? 0; + } + + public function setXpValue(int $amount) : void{ + if($amount <= 0){ + throw new \InvalidArgumentException("XP amount must be greater than 0, got $amount"); + } + $this->setDataProperty(self::DATA_EXPERIENCE_VALUE, self::DATA_TYPE_INT, $amount); + } + + public function hasTargetPlayer() : bool{ + return $this->targetPlayerRuntimeId !== null; + } + + public function getTargetPlayer() : ?Human{ + if($this->targetPlayerRuntimeId === null){ + return null; + } + + $entity = $this->server->findEntity($this->targetPlayerRuntimeId, $this->level); + if($entity instanceof Human){ + return $entity; + } + + return null; + } + + public function setTargetPlayer(?Human $player) : void{ + $this->targetPlayerRuntimeId = $player ? $player->getId() : null; + } + + public function entityBaseTick(int $tickDiff = 1) : bool{ + $hasUpdate = parent::entityBaseTick($tickDiff); + + if($this->age > 6000){ + $this->flagForDespawn(); + return true; + } + + $currentTarget = $this->getTargetPlayer(); + + if($this->lookForTargetTime >= 20){ + if($currentTarget === null or $currentTarget->distanceSquared($this) > self::MAX_TARGET_DISTANCE ** 2){ + $this->setTargetPlayer(null); + + $newTarget = $this->level->getNearestEntity($this, self::MAX_TARGET_DISTANCE, Human::class); + + if($newTarget instanceof Human and !($newTarget instanceof Player and $newTarget->isSpectator())){ + $currentTarget = $newTarget; + $this->setTargetPlayer($currentTarget); + } + } + + $this->lookForTargetTime = 0; + }else{ + $this->lookForTargetTime += $tickDiff; + } + + if($currentTarget !== null){ + $vector = $currentTarget->subtract($this)->add(0, $currentTarget->getEyeHeight() / 2, 0)->divide(self::MAX_TARGET_DISTANCE); + + $distance = $vector->length(); + $oneMinusDistance = (1 - $distance) ** 2; + + if($oneMinusDistance > 0){ + $this->motionX += $vector->x / $distance * $oneMinusDistance * 0.2; + $this->motionY += $vector->y / $distance * $oneMinusDistance * 0.2; + $this->motionZ += $vector->z / $distance * $oneMinusDistance * 0.2; + } + + if($currentTarget->canPickupXp() and $this->boundingBox->intersectsWith($currentTarget->getBoundingBox())){ + $this->flagForDespawn(); + + $currentTarget->addXp($this->getXpValue()); + $this->level->broadcastLevelEvent($this, LevelEventPacket::EVENT_SOUND_ORB, mt_rand()); + $currentTarget->resetXpCooldown(); + + //TODO: check Mending enchantment + } + } + + return $hasUpdate; + } +}