PocketMine-MP 5.15.1 git-fb9a74e8799c71ed8292cfa53abe7a4c9204629d
ExperienceOrb.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\object;
25
35use function max;
36use function sqrt;
37
38class ExperienceOrb extends Entity{
39
40 public static function getNetworkTypeId() : string{ return EntityIds::XP_ORB; }
41
42 public const TAG_VALUE_PC = "Value"; //short
43 public const TAG_VALUE_PE = "experience value"; //int (WTF?)
44 private const TAG_AGE = "Age"; //TAG_Short
45
47 public const MAX_TARGET_DISTANCE = 8.0;
48
50 public const ORB_SPLIT_SIZES = [2477, 1237, 617, 307, 149, 73, 37, 17, 7, 3, 1]; //This is indexed biggest to smallest so that we can return as soon as we found the biggest value.
51
52 public const DEFAULT_DESPAWN_DELAY = 6000;
53 public const NEVER_DESPAWN = -1;
54 public const MAX_DESPAWN_DELAY = 32767 + self::DEFAULT_DESPAWN_DELAY; //max value storable by mojang NBT :(
55
60 public static function getMaxOrbSize(int $amount) : int{
61 foreach(self::ORB_SPLIT_SIZES as $split){
62 if($amount >= $split){
63 return $split;
64 }
65 }
66
67 return 1;
68 }
69
75 public static function splitIntoOrbSizes(int $amount) : array{
76 $result = [];
77
78 while($amount > 0){
79 $size = self::getMaxOrbSize($amount);
80 $result[] = $size;
81 $amount -= $size;
82 }
83
84 return $result;
85 }
86
88 protected int $lookForTargetTime = 0;
89
91 protected ?int $targetPlayerRuntimeId = null;
92
93 protected int $xpValue;
94
95 private int $despawnDelay = self::DEFAULT_DESPAWN_DELAY;
96
97 public function __construct(Location $location, int $xpValue, ?CompoundTag $nbt = null){
98 $this->xpValue = $xpValue;
99 parent::__construct($location, $nbt);
100 }
101
102 protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.25, 0.25); }
103
104 protected function getInitialDragMultiplier() : float{ return 0.02; }
105
106 protected function getInitialGravity() : float{ return 0.04; }
107
108 protected function initEntity(CompoundTag $nbt) : void{
109 parent::initEntity($nbt);
110
111 $age = $nbt->getShort(self::TAG_AGE, 0);
112 if($age === -32768){
113 $this->despawnDelay = self::NEVER_DESPAWN;
114 }else{
115 $this->despawnDelay = max(0, self::DEFAULT_DESPAWN_DELAY - $age);
116 }
117 }
118
119 public function saveNBT() : CompoundTag{
120 $nbt = parent::saveNBT();
121
122 if($this->despawnDelay === self::NEVER_DESPAWN){
123 $age = -32768;
124 }else{
125 $age = self::DEFAULT_DESPAWN_DELAY - $this->despawnDelay;
126 }
127 $nbt->setShort(self::TAG_AGE, $age);
128
129 $nbt->setShort(self::TAG_VALUE_PC, $this->getXpValue());
130 $nbt->setInt(self::TAG_VALUE_PE, $this->getXpValue());
131
132 return $nbt;
133 }
134
135 public function getDespawnDelay() : int{ return $this->despawnDelay; }
136
137 public function setDespawnDelay(int $despawnDelay) : void{
138 if(($despawnDelay < 0 || $despawnDelay > self::MAX_DESPAWN_DELAY) && $despawnDelay !== self::NEVER_DESPAWN){
139 throw new \InvalidArgumentException("Despawn ticker must be in range 0 ... " . self::MAX_DESPAWN_DELAY . " or " . self::NEVER_DESPAWN . ", got $despawnDelay");
140 }
141 $this->despawnDelay = $despawnDelay;
142 }
143
144 public function getXpValue() : int{
145 return $this->xpValue;
146 }
147
148 public function setXpValue(int $amount) : void{
149 if($amount <= 0){
150 throw new \InvalidArgumentException("XP amount must be greater than 0, got $amount");
151 }
152 $this->xpValue = $amount;
153 $this->networkPropertiesDirty = true;
154 }
155
156 public function hasTargetPlayer() : bool{
157 return $this->targetPlayerRuntimeId !== null;
158 }
159
160 public function getTargetPlayer() : ?Human{
161 if($this->targetPlayerRuntimeId === null){
162 return null;
163 }
164
165 $entity = $this->getWorld()->getEntity($this->targetPlayerRuntimeId);
166 if($entity instanceof Human){
167 return $entity;
168 }
169
170 return null;
171 }
172
173 public function setTargetPlayer(?Human $player) : void{
174 $this->targetPlayerRuntimeId = $player !== null ? $player->getId() : null;
175 }
176
177 protected function entityBaseTick(int $tickDiff = 1) : bool{
178 $hasUpdate = parent::entityBaseTick($tickDiff);
179
180 $this->despawnDelay -= $tickDiff;
181 if($this->despawnDelay <= 0){
182 $this->flagForDespawn();
183 return true;
184 }
185
186 $currentTarget = $this->getTargetPlayer();
187 if($currentTarget !== null && (!$currentTarget->isAlive() || !$currentTarget->getXpManager()->canAttractXpOrbs() || $currentTarget->location->distanceSquared($this->location) > self::MAX_TARGET_DISTANCE ** 2)){
188 $currentTarget = null;
189 }
190
191 if($this->lookForTargetTime >= 20){
192 if($currentTarget === null){
193 $newTarget = $this->getWorld()->getNearestEntity($this->location, self::MAX_TARGET_DISTANCE, Human::class);
194
195 if($newTarget instanceof Human && !($newTarget instanceof Player && $newTarget->isSpectator()) && $newTarget->getXpManager()->canAttractXpOrbs()){
196 $currentTarget = $newTarget;
197 }
198 }
199
200 $this->lookForTargetTime = 0;
201 }else{
202 $this->lookForTargetTime += $tickDiff;
203 }
204
205 $this->setTargetPlayer($currentTarget);
206
207 if($currentTarget !== null){
208 $vector = $currentTarget->getPosition()->add(0, $currentTarget->getEyeHeight() / 2, 0)->subtractVector($this->location)->divide(self::MAX_TARGET_DISTANCE);
209
210 $distance = $vector->lengthSquared();
211 if($distance < 1){
212 $this->motion = $this->motion->addVector($vector->normalize()->multiply(0.2 * (1 - sqrt($distance)) ** 2));
213 }
214
215 if($currentTarget->getXpManager()->canPickupXp() && $this->boundingBox->intersectsWith($currentTarget->getBoundingBox())){
216 $this->flagForDespawn();
217
218 $currentTarget->getXpManager()->onPickupXp($this->getXpValue());
219 }
220 }
221
222 return $hasUpdate;
223 }
224
225 protected function tryChangeMovement() : void{
226 $this->checkObstruction($this->location->x, $this->location->y, $this->location->z);
227 parent::tryChangeMovement();
228 }
229
230 public function canBeCollidedWith() : bool{
231 return false;
232 }
233
234 protected function syncNetworkData(EntityMetadataCollection $properties) : void{
235 parent::syncNetworkData($properties);
236
237 $properties->setInt(EntityMetadataProperties::EXPERIENCE_VALUE, $this->xpValue);
238 }
239}
setInt(string $name, int $value)
setShort(string $name, int $value)