diff --git a/src/block/Cauldron.php b/src/block/Cauldron.php index 772583a5af9..1d275e137fc 100644 --- a/src/block/Cauldron.php +++ b/src/block/Cauldron.php @@ -82,7 +82,7 @@ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $this->fill(FillableCauldron::MAX_FILL_LEVEL, VanillaBlocks::LAVA_CAULDRON(), $item, VanillaItems::BUCKET(), $returnedItems); }elseif($item->getTypeId() === ItemTypeIds::POWDER_SNOW_BUCKET){ //TODO: powder snow cauldron - }elseif($item instanceof Potion || $item instanceof SplashPotion){ //TODO: lingering potion + }elseif($item instanceof Potion || $item instanceof SplashPotion){ if($item->getType() === PotionType::WATER){ $this->fill(WaterCauldron::WATER_BOTTLE_FILL_AMOUNT, VanillaBlocks::WATER_CAULDRON(), $item, VanillaItems::GLASS_BOTTLE(), $returnedItems); }else{ diff --git a/src/block/WaterCauldron.php b/src/block/WaterCauldron.php index e470aa6cb85..a620357fa4d 100644 --- a/src/block/WaterCauldron.php +++ b/src/block/WaterCauldron.php @@ -122,7 +122,7 @@ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $world->addSound($this->position->add(0.5, 0.5, 0.5), new CauldronAddDyeSound()); $item->pop(); - }elseif($item instanceof Potion || $item instanceof SplashPotion){ //TODO: lingering potion + }elseif($item instanceof Potion || $item instanceof SplashPotion){ if($item->getType() === PotionType::WATER){ $this->setCustomWaterColor(null)->addFillLevels(self::WATER_BOTTLE_FILL_AMOUNT, $item, VanillaItems::GLASS_BOTTLE(), $returnedItems); }else{ diff --git a/src/block/tile/Cauldron.php b/src/block/tile/Cauldron.php index d10f97e1494..53d2ae3892d 100644 --- a/src/block/tile/Cauldron.php +++ b/src/block/tile/Cauldron.php @@ -76,7 +76,6 @@ protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ default => throw new AssumptionFailedError("Unexpected potion item type") }); - //TODO: lingering potion $type = $this->potionItem instanceof Potion || $this->potionItem instanceof SplashPotion ? $this->potionItem->getType() : null; $nbt->setShort(self::TAG_POTION_ID, $type === null ? self::POTION_ID_NONE : PotionTypeIdMap::getInstance()->toId($type)); @@ -96,7 +95,7 @@ public function readSaveData(CompoundTag $nbt) : void{ $this->potionItem = match($containerType){ self::POTION_CONTAINER_TYPE_NORMAL => VanillaItems::POTION()->setType($potionType), self::POTION_CONTAINER_TYPE_SPLASH => VanillaItems::SPLASH_POTION()->setType($potionType), - self::POTION_CONTAINER_TYPE_LINGERING => throw new SavedDataLoadingException("Not implemented"), + self::POTION_CONTAINER_TYPE_LINGERING => VanillaItems::LINGERING_POTION()->setType($potionType), default => throw new SavedDataLoadingException("Invalid potion container type ID $containerType") }; }else{ @@ -115,7 +114,6 @@ protected function writeSaveData(CompoundTag $nbt) : void{ default => throw new AssumptionFailedError("Unexpected potion item type") }); - //TODO: lingering potion $type = $this->potionItem instanceof Potion || $this->potionItem instanceof SplashPotion ? $this->potionItem->getType() : null; $nbt->setShort(self::TAG_POTION_ID, $type === null ? self::POTION_ID_NONE : PotionTypeIdMap::getInstance()->toId($type)); diff --git a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php index 771154d462b..56d513c2f44 100644 --- a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php +++ b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php @@ -503,6 +503,14 @@ function(GoatHorn $item, int $meta) : void{ }, fn(GoatHorn $item) => GoatHornTypeIdMap::getInstance()->toId($item->getHornType()) ); + $this->map1to1ItemWithMeta( + Ids::LINGERING_POTION, + Items::LINGERING_POTION(), + function(SplashPotion $item, int $meta) : void{ + $item->setType(PotionTypeIdMap::getInstance()->fromId($meta) ?? throw new ItemTypeDeserializeException("Unknown potion type ID $meta")); + }, + fn(SplashPotion $item) => PotionTypeIdMap::getInstance()->toId($item->getType()) + ); $this->map1to1ItemWithMeta( Ids::MEDICINE, Items::MEDICINE(), diff --git a/src/entity/EntityFactory.php b/src/entity/EntityFactory.php index 03d9c03e6be..fd337991c55 100644 --- a/src/entity/EntityFactory.php +++ b/src/entity/EntityFactory.php @@ -32,6 +32,7 @@ use pocketmine\data\bedrock\PotionTypeIds; use pocketmine\data\SavedDataLoadingException; use pocketmine\entity\EntityDataHelper as Helper; +use pocketmine\entity\object\AreaEffectCloud; use pocketmine\entity\object\EndCrystal; use pocketmine\entity\object\ExperienceOrb; use pocketmine\entity\object\FallingBlock; @@ -86,6 +87,18 @@ public function __construct(){ //define legacy save IDs first - use them for saving for maximum compatibility with Minecraft PC //TODO: index them by version to allow proper multi-save compatibility + $this->register(AreaEffectCloud::class, function(World $world, CompoundTag $nbt) : AreaEffectCloud{ + $potionType = PotionTypeIdMap::getInstance()->fromId($nbt->getShort(AreaEffectCloud::TAG_POTION_ID, PotionTypeIds::WATER)); + if($potionType === null){ + throw new SavedDataLoadingException("No such potion type"); + } + return new AreaEffectCloud( + Helper::parseLocation($nbt, $world), + $potionType, + $nbt + ); + }, ['AreaEffectCloud', 'minecraft:area_effect_cloud']); + $this->register(Arrow::class, function(World $world, CompoundTag $nbt) : Arrow{ return new Arrow(Helper::parseLocation($nbt, $world), null, $nbt->getByte(Arrow::TAG_CRIT, 0) === 1, $nbt); }, ['Arrow', 'minecraft:arrow']); diff --git a/src/entity/effect/EffectContainer.php b/src/entity/effect/EffectContainer.php new file mode 100644 index 00000000000..5a50db0a139 --- /dev/null +++ b/src/entity/effect/EffectContainer.php @@ -0,0 +1,229 @@ + + */ + protected ObjectSet $effectAddHooks; + + /** + * @var \Closure[]|ObjectSet + * @phpstan-var ObjectSet<\Closure(EffectInstance) : void> + */ + protected ObjectSet $effectRemoveHooks; + + protected Color $bubbleColor; + + protected bool $onlyAmbientEffects = false; + + /** + * Validates whether an effect will be used for bubbles color calculation. + * + * @phpstan-var \Closure(EffectInstance) : bool + */ + protected \Closure $effectFilterForBubbles; + + public function __construct(){ + $this->bubbleColor = new Color(0, 0, 0, 0); + $this->effectAddHooks = new ObjectSet(); + $this->effectRemoveHooks = new ObjectSet(); + + $this->setEffectFilterForBubbles(function(EffectInstance $effect) : bool{ + return $effect->isVisible() && $effect->getType()->hasBubbles(); + }); + } + + /** + * Returns an array of Effects currently active. + * @return EffectInstance[] + */ + public function all() : array{ + return $this->effects; + } + + /** + * Removes all effects. + */ + public function clear() : void{ + foreach($this->effects as $effect){ + $this->remove($effect->getType()); + } + } + + /** + * Removes the effect with the specified ID. + */ + public function remove(Effect $effectType) : void{ + $index = spl_object_id($effectType); + if(isset($this->effects[$index])){ + $effect = $this->effects[$index]; + + unset($this->effects[$index]); + foreach($this->effectRemoveHooks as $hook){ + $hook($effect); + } + + $this->recalculateEffectColor(); + } + } + + /** + * Returns the effect instance active with the specified ID, or null if does not have the + * effect. + */ + public function get(Effect $effect) : ?EffectInstance{ + return $this->effects[spl_object_id($effect)] ?? null; + } + + /** + * Returns whether the specified effect is active. + */ + public function has(Effect $effect) : bool{ + return isset($this->effects[spl_object_id($effect)]); + } + + /** + * In the following cases it will return true: + * - if the effect type is not already applied + * - if an existing effect of the same type can be replaced (due to shorter duration or lower level) + */ + public function canAdd(EffectInstance $effect) : bool{ + $index = spl_object_id($effect->getType()); + if(isset($this->effects[$index])){ + $oldEffect = $this->effects[$index]; + if( + abs($effect->getAmplifier()) < $oldEffect->getAmplifier() + || (abs($effect->getAmplifier()) === abs($oldEffect->getAmplifier()) && $effect->getDuration() < $oldEffect->getDuration()) + ){ + return false; + } + } + return true; + } + + /** + * Adds an effect. + * If {@link EffectContainer::canAdd()} conditions are met. + * + * @return bool whether the effect has been successfully applied. + */ + public function add(EffectInstance $effect) : bool{ + if($this->canAdd($effect)){ + $index = spl_object_id($effect->getType()); + $replacesOldEffect = isset($this->effects[$index]); + + $this->effects[$index] = $effect; + foreach($this->effectAddHooks as $hook){ + $hook($effect, $replacesOldEffect); + } + + $this->recalculateEffectColor(); + return true; + } + + return false; + } + + /** + * Sets the filter that determines which effects will be displayed in the bubbles. + * + * @phpstan-param \Closure(EffectInstance) : bool $effectValidator + */ + public function setEffectFilterForBubbles(\Closure $effectValidator) : void{ + Utils::validateCallableSignature(new CallbackType(new ReturnType(BuiltInTypes::BOOL), new ParameterType("effect", EffectInstance::class)), $effectValidator); + $this->effectFilterForBubbles = $effectValidator; + } + + /** + * Recalculates the potion bubbles colour based on the active effects. + */ + protected function recalculateEffectColor() : void{ + /** @var Color[] $colors */ + $colors = []; + $ambient = true; + foreach($this->effects as $effect){ + if(($this->effectFilterForBubbles)($effect)){ + $level = $effect->getEffectLevel(); + $color = $effect->getColor(); + for($i = 0; $i < $level; ++$i){ + $colors[] = $color; + } + + if(!$effect->isAmbient()){ + $ambient = false; + } + } + } + + if(count($colors) > 0){ + $this->bubbleColor = Color::mix(...$colors); + $this->onlyAmbientEffects = $ambient; + }else{ + $this->bubbleColor = new Color(0, 0, 0, 0); + $this->onlyAmbientEffects = false; + } + } + + public function getBubbleColor() : Color{ + return $this->bubbleColor; + } + + public function hasOnlyAmbientEffects() : bool{ + return $this->onlyAmbientEffects; + } + + /** + * @return \Closure[]|ObjectSet + * @phpstan-return ObjectSet<\Closure(EffectInstance, bool $replacesOldEffect) : void> + */ + public function getEffectAddHooks() : ObjectSet{ + return $this->effectAddHooks; + } + + /** + * @return \Closure[]|ObjectSet + * @phpstan-return ObjectSet<\Closure(EffectInstance) : void> + */ + public function getEffectRemoveHooks() : ObjectSet{ + return $this->effectRemoveHooks; + } +} diff --git a/src/entity/effect/EffectManager.php b/src/entity/effect/EffectManager.php index 747662dc713..b7fd9396169 100644 --- a/src/entity/effect/EffectManager.php +++ b/src/entity/effect/EffectManager.php @@ -23,56 +23,18 @@ namespace pocketmine\entity\effect; -use pocketmine\color\Color; use pocketmine\entity\Living; use pocketmine\event\entity\EntityEffectAddEvent; use pocketmine\event\entity\EntityEffectRemoveEvent; -use pocketmine\utils\ObjectSet; -use function abs; use function count; use function spl_object_id; -class EffectManager{ - /** @var EffectInstance[] */ - protected array $effects = []; - - protected Color $bubbleColor; - protected bool $onlyAmbientEffects = false; - - /** - * @var \Closure[]|ObjectSet - * @phpstan-var ObjectSet<\Closure(EffectInstance, bool $replacesOldEffect) : void> - */ - protected ObjectSet $effectAddHooks; - /** - * @var \Closure[]|ObjectSet - * @phpstan-var ObjectSet<\Closure(EffectInstance) : void> - */ - protected ObjectSet $effectRemoveHooks; +class EffectManager extends EffectContainer{ public function __construct( private Living $entity ){ - $this->bubbleColor = new Color(0, 0, 0, 0); - $this->effectAddHooks = new ObjectSet(); - $this->effectRemoveHooks = new ObjectSet(); - } - - /** - * Returns an array of Effects currently active on the mob. - * @return EffectInstance[] - */ - public function all() : array{ - return $this->effects; - } - - /** - * Removes all effects from the mob. - */ - public function clear() : void{ - foreach($this->effects as $effect){ - $this->remove($effect->getType()); - } + parent::__construct(); } /** @@ -91,55 +53,17 @@ public function remove(Effect $effectType) : void{ return; } - unset($this->effects[$index]); $effect->getType()->remove($this->entity, $effect); - foreach($this->effectRemoveHooks as $hook){ - $hook($effect); - } - - $this->recalculateEffectColor(); + parent::remove($effectType); } } - /** - * Returns the effect instance active on this entity with the specified ID, or null if the mob does not have the - * effect. - */ - public function get(Effect $effect) : ?EffectInstance{ - return $this->effects[spl_object_id($effect)] ?? null; - } - - /** - * Returns whether the specified effect is active on the mob. - */ - public function has(Effect $effect) : bool{ - return isset($this->effects[spl_object_id($effect)]); - } - - /** - * Adds an effect to the mob. - * If a weaker effect of the same type is already applied, it will be replaced. - * If a weaker or equal-strength effect is already applied but has a shorter duration, it will be replaced. - * - * @return bool whether the effect has been successfully applied. - */ public function add(EffectInstance $effect) : bool{ - $oldEffect = null; - $cancelled = false; - $index = spl_object_id($effect->getType()); - if(isset($this->effects[$index])){ - $oldEffect = $this->effects[$index]; - if( - abs($effect->getAmplifier()) < $oldEffect->getAmplifier() - || (abs($effect->getAmplifier()) === abs($oldEffect->getAmplifier()) && $effect->getDuration() < $oldEffect->getDuration()) - ){ - $cancelled = true; - } - } + $oldEffect = $this->effects[$index] ?? null; $ev = new EntityEffectAddEvent($this->entity, $effect, $oldEffect); - if($cancelled){ + if(!$this->canAdd($effect)){ $ev->cancel(); } @@ -153,53 +77,8 @@ public function add(EffectInstance $effect) : bool{ } $effect->getType()->add($this->entity, $effect); - foreach($this->effectAddHooks as $hook){ - $hook($effect, $oldEffect !== null); - } - - $this->effects[$index] = $effect; - - $this->recalculateEffectColor(); - - return true; - } - - /** - * Recalculates the mob's potion bubbles colour based on the active effects. - */ - protected function recalculateEffectColor() : void{ - /** @var Color[] $colors */ - $colors = []; - $ambient = true; - foreach($this->effects as $effect){ - if($effect->isVisible() && $effect->getType()->hasBubbles()){ - $level = $effect->getEffectLevel(); - $color = $effect->getColor(); - for($i = 0; $i < $level; ++$i){ - $colors[] = $color; - } - - if(!$effect->isAmbient()){ - $ambient = false; - } - } - } - - if(count($colors) > 0){ - $this->bubbleColor = Color::mix(...$colors); - $this->onlyAmbientEffects = $ambient; - }else{ - $this->bubbleColor = new Color(0, 0, 0, 0); - $this->onlyAmbientEffects = false; - } - } - public function getBubbleColor() : Color{ - return $this->bubbleColor; - } - - public function hasOnlyAmbientEffects() : bool{ - return $this->onlyAmbientEffects; + return parent::add($effect); } public function tick(int $tickDiff = 1) : bool{ @@ -216,20 +95,4 @@ public function tick(int $tickDiff = 1) : bool{ return count($this->effects) > 0; } - - /** - * @return \Closure[]|ObjectSet - * @phpstan-return ObjectSet<\Closure(EffectInstance, bool $replacesOldEffect) : void> - */ - public function getEffectAddHooks() : ObjectSet{ - return $this->effectAddHooks; - } - - /** - * @return \Closure[]|ObjectSet - * @phpstan-return ObjectSet<\Closure(EffectInstance) : void> - */ - public function getEffectRemoveHooks() : ObjectSet{ - return $this->effectRemoveHooks; - } } diff --git a/src/entity/object/AreaEffectCloud.php b/src/entity/object/AreaEffectCloud.php new file mode 100644 index 00000000000..6ea7a4790fd --- /dev/null +++ b/src/entity/object/AreaEffectCloud.php @@ -0,0 +1,438 @@ + entity ID => expiration */ + protected array $victims = []; + + protected int $maxAge = self::DEFAULT_DURATION; + protected int $maxAgeChangeOnUse = self::DEFAULT_DURATION_CHANGE_ON_USE; + + protected int $reapplicationDelay = self::REAPPLICATION_DELAY; + + protected int $pickupCount = 0; + protected float $radiusChangeOnPickup = self::DEFAULT_RADIUS_CHANGE_ON_PICKUP; + + protected float $initialRadius = self::DEFAULT_RADIUS; + protected float $radius = self::DEFAULT_RADIUS; + protected float $radiusChangeOnUse = self::DEFAULT_RADIUS_CHANGE_ON_USE; + protected float $radiusChangePerTick = self::DEFAULT_RADIUS_CHANGE_PER_TICK; + + public function __construct( + Location $location, + protected PotionType $potionType, + ?CompoundTag $nbt = null + ){ + parent::__construct($location, $nbt); + } + + protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.5, $this->radius * 2); } + + protected function getInitialDragMultiplier() : float{ return 0.0; } + + protected function getInitialGravity() : float{ return 0.0; } + + protected function initEntity(CompoundTag $nbt) : void{ + parent::initEntity($nbt); + + $this->effectContainer = new EffectContainer(); + $this->effectContainer->getEffectAddHooks()->add(function() : void{ $this->networkPropertiesDirty = true; }); + $this->effectContainer->getEffectRemoveHooks()->add(function() : void{ $this->networkPropertiesDirty = true; }); + $this->effectContainer->setEffectFilterForBubbles(function(EffectInstance $effect) : bool{ + return $effect->isVisible(); + }); + + $worldTime = $this->getWorld()->getTime(); + $this->age = max($worldTime - $nbt->getLong(self::TAG_SPAWN_TICK, $worldTime), 0); + $this->maxAge = $nbt->getInt(self::TAG_DURATION, self::DEFAULT_DURATION); + $this->maxAgeChangeOnUse = $nbt->getInt(self::TAG_DURATION_ON_USE, self::DEFAULT_DURATION_CHANGE_ON_USE); + $this->pickupCount = $nbt->getInt(self::TAG_PICKUP_COUNT, 0); + $this->reapplicationDelay = $nbt->getInt(self::TAG_REAPPLICATION_DELAY, self::REAPPLICATION_DELAY); + + $this->initialRadius = $nbt->getFloat(self::TAG_INITIAL_RADIUS, self::DEFAULT_RADIUS); + $this->setRadius($nbt->getFloat(self::TAG_RADIUS, $this->initialRadius)); + $this->radiusChangeOnPickup = $nbt->getFloat(self::TAG_RADIUS_CHANGE_ON_PICKUP, self::DEFAULT_RADIUS_CHANGE_ON_PICKUP); + $this->radiusChangeOnUse = $nbt->getFloat(self::TAG_RADIUS_ON_USE, self::DEFAULT_RADIUS_CHANGE_ON_USE); + $this->radiusChangePerTick = $nbt->getFloat(self::TAG_RADIUS_PER_TICK, self::DEFAULT_RADIUS_CHANGE_PER_TICK); + + /** @var CompoundTag[]|ListTag|null $effectsTag */ + $effectsTag = $nbt->getListTag(self::TAG_EFFECTS); + if($effectsTag !== null){ + foreach($effectsTag as $e){ + $effect = EffectIdMap::getInstance()->fromId($e->getByte("Id")); + if($effect === null){ + continue; + } + + $this->effectContainer->add(new EffectInstance( + $effect, + $e->getInt("Duration"), + Binary::unsignByte($e->getByte("Amplifier")), + $e->getByte("ShowParticles", 1) !== 0, + $e->getByte("Ambient", 0) !== 0 + )); + } + }else{ + foreach($this->potionType->getEffects() as $effect){ + $this->effectContainer->add($effect); + if($effect->getType() instanceof InstantEffect){ + $this->setReapplicationDelay(0); + } + } + } + } + + public function saveNBT() : CompoundTag{ + $nbt = parent::saveNBT(); + + $nbt->setLong(self::TAG_SPAWN_TICK, $this->getWorld()->getTime() - $this->age); + $nbt->setShort(self::TAG_POTION_ID, PotionTypeIdMap::getInstance()->toId($this->potionType)); + $nbt->setInt(self::TAG_DURATION, $this->maxAge); + $nbt->setInt(self::TAG_DURATION_ON_USE, $this->maxAgeChangeOnUse); + $nbt->setInt(self::TAG_PICKUP_COUNT, $this->pickupCount); + $nbt->setInt(self::TAG_REAPPLICATION_DELAY, $this->reapplicationDelay); + $nbt->setFloat(self::TAG_INITIAL_RADIUS, $this->initialRadius); + $nbt->setFloat(self::TAG_RADIUS, $this->radius); + $nbt->setFloat(self::TAG_RADIUS_CHANGE_ON_PICKUP, $this->radiusChangeOnPickup); + $nbt->setFloat(self::TAG_RADIUS_ON_USE, $this->radiusChangeOnUse); + $nbt->setFloat(self::TAG_RADIUS_PER_TICK, $this->radiusChangePerTick); + + if(count($this->effectContainer->all()) > 0){ + $effects = []; + foreach($this->effectContainer->all() as $effect){ + $effects[] = CompoundTag::create() + ->setByte("Id", EffectIdMap::getInstance()->toId($effect->getType())) + ->setByte("Amplifier", Binary::signByte($effect->getAmplifier())) + ->setInt("Duration", $effect->getDuration()) + ->setByte("Ambient", $effect->isAmbient() ? 1 : 0) + ->setByte("ShowParticles", $effect->isVisible() ? 1 : 0); + } + $nbt->setTag(self::TAG_EFFECTS, new ListTag($effects)); + } + + return $nbt; + } + + public function isFireProof() : bool{ + return true; + } + + public function canBeCollidedWith() : bool{ + return false; + } + + /** + * Returns the current age of the cloud (in ticks). + */ + public function getAge() : int{ + return $this->age; + } + + public function getPotionType() : PotionType{ + return $this->potionType; + } + + public function getEffects() : EffectContainer{ + return $this->effectContainer; + } + + /** + * Returns the initial radius (in blocks). + */ + public function getInitialRadius() : float{ + return $this->initialRadius; + } + + /** + * Returns the current radius (in blocks). + */ + public function getRadius() : float{ + return $this->radius; + } + + /** + * Sets the current radius (in blocks). + */ + protected function setRadius(float $radius) : void{ + $this->radius = $radius; + $this->setSize($this->getInitialSizeInfo()); + $this->networkPropertiesDirty = true; + } + + /** + * Returns the amount that the radius of this cloud will add by when it is + * picked up (in blocks). Usually negative resulting in a radius reduction. + * + * Applied when getting dragon breath bottle. + */ + public function getRadiusChangeOnPickup() : float{ + return $this->radiusChangeOnPickup; + } + + /** + * Sets the amount that the radius of this cloud will add by when it is + * picked up (in blocks). Usually negative resulting in a radius reduction. + * + * Applied when getting dragon breath bottle. + */ + public function setRadiusChangeOnPickup(float $radiusChangeOnPickup) : void{ + $this->radiusChangeOnPickup = $radiusChangeOnPickup; + } + + /** + * Returns the amount that the radius of this cloud will add by when it + * applies an effect to an entity (in blocks). Usually negative resulting in a radius reduction. + */ + public function getRadiusChangeOnUse() : float{ + return $this->radiusChangeOnUse; + } + + /** + * Sets the amount that the radius of this cloud will add by when it + * applies an effect to an entity (in blocks). + */ + public function setRadiusChangeOnUse(float $radiusChangeOnUse) : void{ + $this->radiusChangeOnUse = $radiusChangeOnUse; + } + + /** + * Returns the amount that the radius of this cloud will add by when an update + * is performed (in blocks). Usually negative resulting in a radius reduction. + */ + public function getRadiusChangePerTick() : float{ + return $this->radiusChangePerTick; + } + + /** + * Sets the amount that the radius of this cloud will add by when an update is performed (in blocks). + */ + public function setRadiusChangePerTick(float $radiusChangePerTick) : void{ + $this->radiusChangePerTick = $radiusChangePerTick; + } + + /** + * Returns the age at which the cloud will despawn. + */ + public function getMaxAge() : int{ + return $this->maxAge; + } + + /** + * Sets the age at which the cloud will despawn. + */ + public function setMaxAge(int $maxAge) : void{ + $this->maxAge = $maxAge; + } + + /** + * Returns the amount that the max age of this cloud will change by when it + * applies an effect to an entity (in ticks). + */ + public function getMaxAgeChangeOnUse() : int{ + return $this->maxAgeChangeOnUse; + } + + /** + * Sets the amount that the max age of this cloud will change by when it + * applies an effect to an entity (in ticks). + */ + public function setMaxAgeChangeOnUse(int $maxAgeChangeOnUse) : void{ + $this->maxAgeChangeOnUse = $maxAgeChangeOnUse; + } + + /** + * Returns the time that an entity will be immune from subsequent exposure (in ticks). + */ + public function getReapplicationDelay() : int{ + return $this->reapplicationDelay; + } + + /** + * Sets the time that an entity will be immune from subsequent exposure (in ticks). + */ + public function setReapplicationDelay(int $delay) : void{ + $this->reapplicationDelay = $delay; + } + + protected function entityBaseTick(int $tickDiff = 1) : bool{ + $hasUpdate = parent::entityBaseTick($tickDiff); + + $this->age += $tickDiff; + $radius = $this->radius + ($this->radiusChangePerTick * $tickDiff); + if($radius < 0.5){ + $this->flagForDespawn(); + return true; + } + $this->setRadius($radius); + if($this->age >= self::UPDATE_DELAY && ($this->age % self::UPDATE_DELAY) === 0){ + if($this->age > $this->maxAge){ + $this->flagForDespawn(); + return true; + } + + foreach($this->victims as $entityId => $expiration){ + if($this->age >= $expiration){ + unset($this->victims[$entityId]); + } + } + + /** @var Living[] $entities */ + $entities = []; + $radiusChange = 0.0; + $maxAgeChange = 0; + foreach($this->getWorld()->getCollidingEntities($this->getBoundingBox(), $this) as $entity){ + if(!$entity instanceof Living || isset($this->victims[$entity->getId()])){ + continue; + } + $entityPosition = $entity->getPosition(); + $xDiff = $entityPosition->getX() - $this->location->getX(); + $zDiff = $entityPosition->getZ() - $this->location->getZ(); + if(($xDiff ** 2 + $zDiff ** 2) > $this->radius ** 2){ + continue; + } + $entities[] = $entity; + if($this->radiusChangeOnUse !== 0.0){ + $radiusChange += $this->radiusChangeOnUse; + if($this->radius + $radiusChange <= 0){ + break; + } + } + if($this->maxAgeChangeOnUse !== 0){ + $maxAgeChange += $this->maxAgeChangeOnUse; + if($this->maxAge + $maxAgeChange <= 0){ + break; + } + } + } + if(count($entities) === 0){ + return $hasUpdate; + } + + $ev = new AreaEffectCloudApplyEvent($this, $entities); + $ev->call(); + if($ev->isCancelled()){ + return $hasUpdate; + } + + foreach($ev->getAffectedEntities() as $entity){ + foreach($this->effectContainer->all() as $effect){ + $effect = clone $effect; //avoid accidental modification + if($effect->getType() instanceof InstantEffect){ + $effect->getType()->applyEffect($entity, $effect, 0.5, $this); + }else{ + $entity->getEffects()->add($effect->setDuration((int) round($effect->getDuration() / 4))); + } + } + if($this->reapplicationDelay !== 0){ + $this->victims[$entity->getId()] = $this->age + $this->reapplicationDelay; + } + } + + $radius = $this->radius + $radiusChange; + $maxAge = $this->maxAge + $maxAgeChange; + if($radius <= 0 || $maxAge <= 0){ + $this->flagForDespawn(); + return true; + } + $this->setRadius($radius); + $this->setMaxAge($maxAge); + $hasUpdate = true; + } + + return $hasUpdate; + } + + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ + parent::syncNetworkData($properties); + + //visual properties + $properties->setFloat(EntityMetadataProperties::AREA_EFFECT_CLOUD_RADIUS, $this->radius); + $properties->setInt(EntityMetadataProperties::POTION_COLOR, Binary::signInt(( + count($this->effectContainer->all()) === 0 ? PotionSplashParticle::DEFAULT_COLOR() : $this->effectContainer->getBubbleColor() + )->toARGB())); + + //these are properties the client expects, and are used for client-sided logic, which we don't want + $properties->setByte(EntityMetadataProperties::POTION_AMBIENT, 0); + $properties->setInt(EntityMetadataProperties::AREA_EFFECT_CLOUD_DURATION, -1); + $properties->setFloat(EntityMetadataProperties::AREA_EFFECT_CLOUD_RADIUS_CHANGE_ON_PICKUP, 0); + $properties->setFloat(EntityMetadataProperties::AREA_EFFECT_CLOUD_RADIUS_PER_TICK, 0); + $properties->setInt(EntityMetadataProperties::AREA_EFFECT_CLOUD_SPAWN_TIME, 0); + $properties->setFloat(EntityMetadataProperties::AREA_EFFECT_CLOUD_PICKUP_COUNT, 0); + $properties->setInt(EntityMetadataProperties::AREA_EFFECT_CLOUD_WAITING, 0); + } +} diff --git a/src/entity/projectile/SplashPotion.php b/src/entity/projectile/SplashPotion.php index f4635cad77c..65a8204013b 100644 --- a/src/entity/projectile/SplashPotion.php +++ b/src/entity/projectile/SplashPotion.php @@ -32,6 +32,7 @@ use pocketmine\entity\Entity; use pocketmine\entity\Living; use pocketmine\entity\Location; +use pocketmine\entity\object\AreaEffectCloud; use pocketmine\event\entity\ProjectileHitBlockEvent; use pocketmine\event\entity\ProjectileHitEntityEvent; use pocketmine\event\entity\ProjectileHitEvent; @@ -96,8 +97,8 @@ protected function onHit(ProjectileHitEvent $event) : void{ $this->getWorld()->addParticle($this->location, $particle); $this->broadcastSound(new PotionSplashSound()); - if($hasEffects){ - if(!$this->willLinger()){ + if(!$this->willLinger()){ + if($hasEffects){ foreach($this->getWorld()->getCollidingEntities($this->boundingBox->expandedCopy(4.125, 2.125, 4.125), $this) as $entity){ if($entity instanceof Living){ $distanceSquared = $entity->getEyePos()->distanceSquared($this->location); @@ -126,10 +127,18 @@ protected function onHit(ProjectileHitEvent $event) : void{ } } } - }else{ - //TODO: lingering potions } - }elseif($event instanceof ProjectileHitBlockEvent && $this->getPotionType() === PotionType::WATER){ + }else{ + $entity = new AreaEffectCloud( + Location::fromObject($this->location->floor()->add(0.5, 0.5, 0.5), $this->getWorld()), + $this->potionType + ); + if(($owner = $this->getOwningEntity()) !== null && !$owner->isClosed()){ + $entity->setOwningEntity($owner); + } + $entity->spawnToAll(); + } + if(!$hasEffects && $event instanceof ProjectileHitBlockEvent && $this->getPotionType() === PotionType::WATER){ $blockIn = $event->getBlockHit()->getSide($event->getRayTraceResult()->getHitFace()); if($blockIn->hasTypeTag(BlockTypeTags::FIRE)){ diff --git a/src/event/entity/AreaEffectCloudApplyEvent.php b/src/event/entity/AreaEffectCloudApplyEvent.php new file mode 100644 index 00000000000..c6246a339c4 --- /dev/null +++ b/src/event/entity/AreaEffectCloudApplyEvent.php @@ -0,0 +1,64 @@ + + */ +class AreaEffectCloudApplyEvent extends EntityEvent implements Cancellable{ + use CancellableTrait; + + /** + * @param Living[] $affectedEntities + */ + public function __construct( + AreaEffectCloud $entity, + protected array $affectedEntities + ){ + $this->entity = $entity; + } + + /** + * @return AreaEffectCloud + */ + public function getEntity(){ + return $this->entity; + } + + /** + * Returns the affected entities. + * + * @return Living[] + */ + public function getAffectedEntities() : array{ + return $this->affectedEntities; + } +} diff --git a/src/item/SplashPotion.php b/src/item/SplashPotion.php index e1c9167ac34..947b1b4bed0 100644 --- a/src/item/SplashPotion.php +++ b/src/item/SplashPotion.php @@ -33,6 +33,16 @@ class SplashPotion extends ProjectileItem{ private PotionType $potionType = PotionType::WATER; + public function __construct( + ItemIdentifier $identifier, + string $name = "Splash Potion", + array $enchantmentTags = [], + private bool $linger = false + ){ + //TODO: remove unnecessary default parameters in PM6, they remain because backward compatibility + parent::__construct($identifier, $name, $enchantmentTags); + } + protected function describeState(RuntimeDataDescriber $w) : void{ $w->enum($this->potionType); } @@ -52,10 +62,19 @@ public function getMaxStackSize() : int{ } protected function createEntity(Location $location, Player $thrower) : Throwable{ - return new SplashPotionEntity($location, $thrower, $this->potionType); + $projectile = new SplashPotionEntity($location, $thrower, $this->potionType); + $projectile->setLinger($this->linger); + return $projectile; } public function getThrowForce() : float{ return 0.5; } + + /** + * Returns whether this splash potion will create an area-effect cloud on impact. + */ + public function willLinger() : bool{ + return $this->linger; + } } diff --git a/src/item/StringToItemParser.php b/src/item/StringToItemParser.php index a3bd7b87272..50aa398ee16 100644 --- a/src/item/StringToItemParser.php +++ b/src/item/StringToItemParser.php @@ -1225,6 +1225,7 @@ private static function registerDynamicItems(self $result) : void{ $result->register($prefix("potion"), fn() => Items::POTION()->setType($potionType)); $result->register($prefix("splash_potion"), fn() => Items::SPLASH_POTION()->setType($potionType)); + $result->register($prefix("lingering_potion"), fn() => Items::LINGERING_POTION()->setType($potionType)); } } diff --git a/src/item/VanillaItems.php b/src/item/VanillaItems.php index f76cf369f4e..32ef357c13a 100644 --- a/src/item/VanillaItems.php +++ b/src/item/VanillaItems.php @@ -214,6 +214,7 @@ * @method static Armor LEATHER_CAP() * @method static Armor LEATHER_PANTS() * @method static Armor LEATHER_TUNIC() + * @method static SplashPotion LINGERING_POTION() * @method static Item MAGMA_CREAM() * @method static Boat MANGROVE_BOAT() * @method static ItemBlockWallOrFloor MANGROVE_SIGN() @@ -520,6 +521,7 @@ protected static function setup() : void{ self::register("lapis_lazuli", fn(IID $id) => new Item($id, "Lapis Lazuli")); self::register("lava_bucket", fn(IID $id) => new LiquidBucket($id, "Lava Bucket", Blocks::LAVA())); self::register("leather", fn(IID $id) => new Item($id, "Leather")); + self::register("lingering_potion", fn(IID $id) => new SplashPotion($id, "Lingering Potion", linger: true)); self::register("magma_cream", fn(IID $id) => new Item($id, "Magma Cream")); self::register("mangrove_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::MANGROVE_SIGN(), Blocks::MANGROVE_WALL_SIGN())); self::register("medicine", fn(IID $id) => new Medicine($id, "Medicine"));