PocketMine-MP 5.14.2 git-50e2c469a547a16a23b2dc691e70a51d34e29395
ExperienceManager.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
33use function array_rand;
34use function ceil;
35use function count;
36use function max;
37use function min;
38
40
41 private Attribute $levelAttr;
42 private Attribute $progressAttr;
43
44 private int $totalXp = 0;
45
46 private bool $canAttractXpOrbs = true;
47
48 private int $xpCooldown = 0;
49
50 public function __construct(
51 private Human $entity
52 ){
53 $this->levelAttr = self::fetchAttribute($entity, Attribute::EXPERIENCE_LEVEL);
54 $this->progressAttr = self::fetchAttribute($entity, Attribute::EXPERIENCE);
55 }
56
57 private static function fetchAttribute(Entity $entity, string $attributeId) : Attribute{
58 $attribute = AttributeFactory::getInstance()->mustGet($attributeId);
59 $entity->getAttributeMap()->add($attribute);
60 return $attribute;
61 }
62
66 public function getXpLevel() : int{
67 return (int) $this->levelAttr->getValue();
68 }
69
73 public function setXpLevel(int $level) : bool{
74 return $this->setXpAndProgress($level, null);
75 }
76
80 public function addXpLevels(int $amount, bool $playSound = true) : bool{
81 $oldLevel = $this->getXpLevel();
82 if($this->setXpLevel($oldLevel + $amount)){
83 if($playSound){
84 $newLevel = $this->getXpLevel();
85 if((int) ($newLevel / 5) > (int) ($oldLevel / 5)){
86 $this->entity->broadcastSound(new XpLevelUpSound($newLevel));
87 }
88 }
89
90 return true;
91 }
92
93 return false;
94 }
95
99 public function subtractXpLevels(int $amount) : bool{
100 return $this->addXpLevels(-$amount);
101 }
102
106 public function getXpProgress() : float{
107 return $this->progressAttr->getValue();
108 }
109
113 public function setXpProgress(float $progress) : bool{
114 return $this->setXpAndProgress(null, $progress);
115 }
116
120 public function getRemainderXp() : int{
121 return (int) (ExperienceUtils::getXpToCompleteLevel($this->getXpLevel()) * $this->getXpProgress());
122 }
123
129 public function getCurrentTotalXp() : int{
130 return ExperienceUtils::getXpToReachLevel($this->getXpLevel()) + $this->getRemainderXp();
131 }
132
137 public function setCurrentTotalXp(int $amount) : bool{
138 $newLevel = ExperienceUtils::getLevelFromXp($amount);
139
140 $xpLevel = (int) $newLevel;
141 $xpProgress = $newLevel - (int) $newLevel;
142 return $this->setXpAndProgress($xpLevel, $xpProgress);
143 }
144
151 public function addXp(int $amount, bool $playSound = true) : bool{
152 $amount = min($amount, Limits::INT32_MAX - $this->totalXp);
153 $oldLevel = $this->getXpLevel();
154 $oldTotal = $this->getCurrentTotalXp();
155
156 if($this->setCurrentTotalXp($oldTotal + $amount)){
157 if($amount > 0){
158 $this->totalXp += $amount;
159 }
160 if($playSound){
161 $newLevel = $this->getXpLevel();
162 if((int) ($newLevel / 5) > (int) ($oldLevel / 5)){
163 $this->entity->broadcastSound(new XpLevelUpSound($newLevel));
164 }elseif($this->getCurrentTotalXp() > $oldTotal){
165 $this->entity->broadcastSound(new XpCollectSound());
166 }
167 }
168
169 return true;
170 }
171
172 return false;
173 }
174
178 public function subtractXp(int $amount) : bool{
179 return $this->addXp(-$amount);
180 }
181
182 public function setXpAndProgress(?int $level, ?float $progress) : bool{
183 $ev = new PlayerExperienceChangeEvent($this->entity, $this->getXpLevel(), $this->getXpProgress(), $level, $progress);
184 $ev->call();
185
186 if($ev->isCancelled()){
187 return false;
188 }
189
190 $level = $ev->getNewLevel();
191 $progress = $ev->getNewProgress();
192
193 if($level !== null){
194 $this->levelAttr->setValue($level);
195 }
196
197 if($progress !== null){
198 $this->progressAttr->setValue($progress);
199 }
200
201 return true;
202 }
203
207 public function setXpAndProgressNoEvent(int $level, float $progress) : void{
208 $this->levelAttr->setValue($level);
209 $this->progressAttr->setValue($progress);
210 }
211
216 public function getLifetimeTotalXp() : int{
217 return $this->totalXp;
218 }
219
224 public function setLifetimeTotalXp(int $amount) : void{
225 if($amount < 0 || $amount > Limits::INT32_MAX){
226 throw new \InvalidArgumentException("XP must be greater than 0 and less than " . Limits::INT32_MAX);
227 }
228
229 $this->totalXp = $amount;
230 }
231
235 public function canPickupXp() : bool{
236 return $this->xpCooldown === 0;
237 }
238
239 public function onPickupXp(int $xpValue) : void{
240 $mainHandIndex = -1;
241 $offHandIndex = -2;
242
243 //TODO: replace this with a more generic equipment getting/setting interface
244 $equipment = [];
245
246 if(($item = $this->entity->getInventory()->getItemInHand()) instanceof Durable && $item->hasEnchantment(VanillaEnchantments::MENDING())){
247 $equipment[$mainHandIndex] = $item;
248 }
249 if(($item = $this->entity->getOffHandInventory()->getItem(0)) instanceof Durable && $item->hasEnchantment(VanillaEnchantments::MENDING())){
250 $equipment[$offHandIndex] = $item;
251 }
252 foreach($this->entity->getArmorInventory()->getContents() as $k => $armorItem){
253 if($armorItem instanceof Durable && $armorItem->hasEnchantment(VanillaEnchantments::MENDING())){
254 $equipment[$k] = $armorItem;
255 }
256 }
257
258 if(count($equipment) > 0){
259 $repairItem = $equipment[$k = array_rand($equipment)];
260 if($repairItem->getDamage() > 0){
261 $repairAmount = min($repairItem->getDamage(), $xpValue * 2);
262 $repairItem->setDamage($repairItem->getDamage() - $repairAmount);
263 $xpValue -= (int) ceil($repairAmount / 2);
264
265 if($k === $mainHandIndex){
266 $this->entity->getInventory()->setItemInHand($repairItem);
267 }elseif($k === $offHandIndex){
268 $this->entity->getOffHandInventory()->setItem(0, $repairItem);
269 }else{
270 $this->entity->getArmorInventory()->setItem($k, $repairItem);
271 }
272 }
273 }
274
275 $this->addXp($xpValue); //this will still get fired even if the value is 0 due to mending, to play sounds
276 $this->resetXpCooldown();
277 }
278
282 public function resetXpCooldown(int $value = 2) : void{
283 $this->xpCooldown = $value;
284 }
285
286 public function tick(int $tickDiff = 1) : void{
287 if($this->xpCooldown > 0){
288 $this->xpCooldown = max(0, $this->xpCooldown - $tickDiff);
289 }
290 }
291
292 public function canAttractXpOrbs() : bool{
293 return $this->canAttractXpOrbs;
294 }
295
296 public function setCanAttractXpOrbs(bool $v = true) : void{
297 $this->canAttractXpOrbs = $v;
298 }
299}
addXp(int $amount, bool $playSound=true)
addXpLevels(int $amount, bool $playSound=true)