diff --git a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php index f5c03dbeb..c7e5d7020 100644 --- a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php +++ b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php @@ -403,6 +403,7 @@ final class ItemSerializerDeserializerRegistrar{ $this->map1to1Item(Ids::TORCHFLOWER_SEEDS, Items::TORCHFLOWER_SEEDS()); $this->map1to1Item(Ids::TIDE_ARMOR_TRIM_SMITHING_TEMPLATE, Items::TIDE_ARMOR_TRIM_SMITHING_TEMPLATE()); $this->map1to1Item(Ids::TOTEM_OF_UNDYING, Items::TOTEM()); + $this->map1to1Item(Ids::TRIDENT, Items::TRIDENT()); $this->map1to1Item(Ids::TROPICAL_FISH, Items::CLOWNFISH()); $this->map1to1Item(Ids::TURTLE_HELMET, Items::TURTLE_HELMET()); $this->map1to1Item(Ids::VEX_ARMOR_TRIM_SMITHING_TEMPLATE, Items::VEX_ARMOR_TRIM_SMITHING_TEMPLATE()); diff --git a/src/entity/EntityFactory.php b/src/entity/EntityFactory.php index c41f76d64..a5acb0c61 100644 --- a/src/entity/EntityFactory.php +++ b/src/entity/EntityFactory.php @@ -43,6 +43,7 @@ use pocketmine\entity\projectile\ExperienceBottle; use pocketmine\entity\projectile\IceBomb; use pocketmine\entity\projectile\Snowball; use pocketmine\entity\projectile\SplashPotion; +use pocketmine\entity\projectile\Trident; use pocketmine\item\Item; use pocketmine\math\Facing; use pocketmine\math\Vector3; @@ -168,6 +169,24 @@ final class EntityFactory{ return new SplashPotion(Helper::parseLocation($nbt, $world), null, $potionType, $nbt); }, ['ThrownPotion', 'minecraft:potion', 'thrownpotion']); + $this->register(Trident::class, function(World $world, CompoundTag $nbt) : Trident{ + $itemTag = $nbt->getCompoundTag(Trident::TAG_ITEM); + if($itemTag === null){ + throw new SavedDataLoadingException("Expected \"" . Trident::TAG_ITEM . "\" NBT tag not found"); + } + + $item = Item::nbtDeserialize($itemTag); + if($item->isNull()){ + throw new SavedDataLoadingException("Trident item is invalid"); + } + return new Trident(Helper::parseLocation($nbt, $world), $item, null, $nbt); + }, [ + 'minecraft:trident', //java + 'minecraft:thrown_trident', //bedrock + 'Trident', //backwards compat for people who used #4547 before it was merged, since it was sitting around for 4 years... + 'ThrownTrident' //as above + ]); + $this->register(Squid::class, function(World $world, CompoundTag $nbt) : Squid{ return new Squid(Helper::parseLocation($nbt, $world), $nbt); }, ['Squid', 'minecraft:squid']); diff --git a/src/entity/projectile/Projectile.php b/src/entity/projectile/Projectile.php index a6735b3fe..cefc6b77d 100644 --- a/src/entity/projectile/Projectile.php +++ b/src/entity/projectile/Projectile.php @@ -227,12 +227,15 @@ abstract class Projectile extends Entity{ $specificHitFunc = fn() => $this->onHitBlock($objectHit, $rayTraceResult); } + $motionBeforeOnHit = clone $this->motion; $ev->call(); $this->onHit($ev); $specificHitFunc(); $this->isCollided = $this->onGround = true; - $this->motion = Vector3::zero(); + if($motionBeforeOnHit->equals($this->motion)){ + $this->motion = Vector3::zero(); + } }else{ $this->isCollided = $this->onGround = false; $this->blockHit = null; @@ -295,7 +298,9 @@ abstract class Projectile extends Entity{ } } - $this->flagForDespawn(); + if($this->despawnsOnEntityHit()){ + $this->flagForDespawn(); + } } /** @@ -305,4 +310,11 @@ abstract class Projectile extends Entity{ $this->blockHit = $blockHit->getPosition()->asVector3(); $blockHit->onProjectileHit($this, $hitResult); } + + /** + * @deprecated This will be dropped in favor of deciding whether to despawn within `onHitEntity()` method. + */ + protected function despawnsOnEntityHit() : bool{ + return true; + } } diff --git a/src/entity/projectile/Trident.php b/src/entity/projectile/Trident.php new file mode 100644 index 000000000..73b3880ac --- /dev/null +++ b/src/entity/projectile/Trident.php @@ -0,0 +1,183 @@ +isNull()){ + throw new \InvalidArgumentException("Trident must have a count of at least 1"); + } + $this->item = clone $item; + parent::__construct($location, $shootingEntity, $nbt); + } + + protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.35, 0.25); } + + protected function getInitialDragMultiplier() : float{ return 0.01; } + + protected function getInitialGravity() : float{ return 0.1; } + + protected function initEntity(CompoundTag $nbt) : void{ + parent::initEntity($nbt); + + $this->spawnedInCreative = $nbt->getByte(self::TAG_SPAWNED_IN_CREATIVE, 0) === 1; + } + + public function saveNBT() : CompoundTag{ + $nbt = parent::saveNBT(); + $nbt->setTag(self::TAG_ITEM, $this->item->nbtSerialize()); + $nbt->setByte(self::TAG_SPAWNED_IN_CREATIVE, $this->spawnedInCreative ? 1 : 0); + return $nbt; + } + + protected function onFirstUpdate(int $currentTick) : void{ + $owner = $this->getOwningEntity(); + $this->spawnedInCreative = $owner instanceof Player && $owner->isCreative(); + + parent::onFirstUpdate($currentTick); + } + + protected function entityBaseTick(int $tickDiff = 1) : bool{ + if($this->closed){ + return false; + } + //TODO: Loyalty enchantment. + + return parent::entityBaseTick($tickDiff); + } + + protected function despawnsOnEntityHit() : bool{ + return false; + } + + protected function onHitEntity(Entity $entityHit, RayTraceResult $hitResult) : void{ + parent::onHitEntity($entityHit, $hitResult); + + $this->canCollide = false; + $this->broadcastSound(new TridentHitEntitySound()); + $this->setMotion(new Vector3($this->motion->x * -0.01, $this->motion->y * -0.1, $this->motion->z * -0.01)); + } + + protected function onHitBlock(Block $blockHit, RayTraceResult $hitResult) : void{ + parent::onHitBlock($blockHit, $hitResult); + $this->canCollide = true; + $this->broadcastSound(new TridentHitGroundSound()); + } + + public function getItem() : Item{ + return clone $this->item; + } + + public function setItem(Item $item) : void{ + if($item->isNull()){ + throw new \InvalidArgumentException("Trident must have a count of at least 1"); + } + if($this->item->hasEnchantments() !== $item->hasEnchantments()){ + $this->networkPropertiesDirty = true; + } + $this->item = clone $item; + } + + public function canCollideWith(Entity $entity) : bool{ + return $this->canCollide && $entity->getId() !== $this->ownerId && parent::canCollideWith($entity); + } + + public function onCollideWithPlayer(Player $player) : void{ + if($this->blockHit !== null){ + $this->pickup($player); + } + } + + private function pickup(Player $player) : void{ + $shouldDespawn = false; + + $playerInventory = $player->getInventory(); + $ev = new EntityItemPickupEvent($player, $this, $this->getItem(), $playerInventory); + if($player->hasFiniteResources() && !$playerInventory->canAddItem($ev->getItem())){ + $ev->cancel(); + } + if($this->spawnedInCreative){ + $ev->cancel(); + $shouldDespawn = true; + } + + $ev->call(); + if(!$ev->isCancelled()){ + $ev->getInventory()?->addItem($ev->getItem()); + $shouldDespawn = true; + } + + if($shouldDespawn){ + //even if the item was not actually picked up, the animation must be displayed. + NetworkBroadcastUtils::broadcastEntityEvent( + $this->getViewers(), + fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onPickUpItem($recipients, $player, $this) + ); + $this->flagForDespawn(); + } + } + + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ + parent::syncNetworkData($properties); + + $properties->setGenericFlag(EntityMetadataFlags::ENCHANTED, $this->item->hasEnchantments()); + } +} diff --git a/src/event/player/PlayerDeathEvent.php b/src/event/player/PlayerDeathEvent.php index aacff3438..ca4b46564 100644 --- a/src/event/player/PlayerDeathEvent.php +++ b/src/event/player/PlayerDeathEvent.php @@ -26,7 +26,9 @@ namespace pocketmine\event\player; use pocketmine\block\BlockTypeIds; use pocketmine\entity\Living; use pocketmine\entity\object\FallingBlock; +use pocketmine\entity\projectile\Trident; use pocketmine\event\entity\EntityDamageByBlockEvent; +use pocketmine\event\entity\EntityDamageByChildEntityEvent; use pocketmine\event\entity\EntityDamageByEntityEvent; use pocketmine\event\entity\EntityDamageEvent; use pocketmine\event\entity\EntityDeathEvent; @@ -113,10 +115,15 @@ class PlayerDeathEvent extends EntityDeathEvent{ } break; case EntityDamageEvent::CAUSE_PROJECTILE: - if($deathCause instanceof EntityDamageByEntityEvent){ + if($deathCause instanceof EntityDamageByChildEntityEvent){ $e = $deathCause->getDamager(); if($e instanceof Living){ - return KnownTranslationFactory::death_attack_arrow($name, $e->getDisplayName()); + $child = $deathCause->getChild(); + if($child instanceof Trident){ + return KnownTranslationFactory::death_attack_trident($name, $e->getDisplayName()); + }else{ + return KnownTranslationFactory::death_attack_arrow($name, $e->getDisplayName()); + } } } break; diff --git a/src/item/ItemTypeIds.php b/src/item/ItemTypeIds.php index af32cbcc2..36fc2c65f 100644 --- a/src/item/ItemTypeIds.php +++ b/src/item/ItemTypeIds.php @@ -346,8 +346,9 @@ final class ItemTypeIds{ public const PALE_OAK_HANGING_SIGN = 20307; public const SPRUCE_HANGING_SIGN = 20308; public const WARPED_HANGING_SIGN = 20309; + public const TRIDENT = 20310; - public const FIRST_UNUSED_ITEM_ID = 20310; + public const FIRST_UNUSED_ITEM_ID = 20311; private static int $nextDynamicId = self::FIRST_UNUSED_ITEM_ID; diff --git a/src/item/StringToItemParser.php b/src/item/StringToItemParser.php index 2f316f66b..5e45ea25d 100644 --- a/src/item/StringToItemParser.php +++ b/src/item/StringToItemParser.php @@ -1560,6 +1560,7 @@ final class StringToItemParser extends StringToTParser{ $result->register("torchflower_seeds", fn() => Items::TORCHFLOWER_SEEDS()); $result->register("tide_armor_trim_smithing_template", fn() => Items::TIDE_ARMOR_TRIM_SMITHING_TEMPLATE()); $result->register("totem", fn() => Items::TOTEM()); + $result->register("trident", fn() => Items::TRIDENT()); $result->register("turtle_helmet", fn() => Items::TURTLE_HELMET()); $result->register("vex_armor_trim_smithing_template", fn() => Items::VEX_ARMOR_TRIM_SMITHING_TEMPLATE()); $result->register("turtle_shell_piece", fn() => Items::SCUTE()); diff --git a/src/item/Trident.php b/src/item/Trident.php new file mode 100644 index 000000000..991f45b1d --- /dev/null +++ b/src/item/Trident.php @@ -0,0 +1,93 @@ +getLocation(); + + $diff = $player->getItemUseDuration(); + if($diff < 14){ + return ItemUseResult::FAIL; + } + + $item = $this->pop(); + if($player->hasFiniteResources()){ + $item->applyDamage(1); + } + $entity = new TridentEntity(Location::fromObject( + $player->getEyePos(), + $player->getWorld(), + ($location->yaw > 180 ? 360 : 0) - $location->yaw, + -$location->pitch + ), $item, $player); + $p = $diff / 20; + $baseForce = min((($p ** 2) + $p * 2) / 3, 1) * 2.4; + $entity->setMotion($player->getDirectionVector()->multiply($baseForce)); + + $ev = new ProjectileLaunchEvent($entity); + $ev->call(); + if($ev->isCancelled()){ + $ev->getEntity()->flagForDespawn(); + return ItemUseResult::FAIL; + } + $ev->getEntity()->spawnToAll(); + $location->getWorld()->addSound($location, new TridentThrowSound()); + + return ItemUseResult::SUCCESS; + } + + public function getAttackPoints() : int{ + return 9; + } + + public function canStartUsingItem(Player $player) : bool{ + return $this->damage < $this->getMaxDurability(); + } + + public function onAttackEntity(Entity $victim, array &$returnedItems) : bool{ + return $this->applyDamage(1); + } + + public function onDestroyBlock(Block $block, array &$returnedItems) : bool{ + if(!$block->getBreakInfo()->breaksInstantly()){ + return $this->applyDamage(2); + } + return false; + } +} diff --git a/src/item/VanillaItems.php b/src/item/VanillaItems.php index e4eeffc1d..48ae95c32 100644 --- a/src/item/VanillaItems.php +++ b/src/item/VanillaItems.php @@ -335,6 +335,7 @@ use function strtolower; * @method static Item TIDE_ARMOR_TRIM_SMITHING_TEMPLATE() * @method static TorchflowerSeeds TORCHFLOWER_SEEDS() * @method static Totem TOTEM() + * @method static Trident TRIDENT() * @method static TurtleHelmet TURTLE_HELMET() * @method static Item VEX_ARMOR_TRIM_SMITHING_TEMPLATE() * @method static SpawnEgg VILLAGER_SPAWN_EGG() @@ -630,6 +631,7 @@ final class VanillaItems{ self::register("sweet_berries", fn(IID $id) => new SweetBerries($id, "Sweet Berries")); self::register("torchflower_seeds", fn(IID $id) => new TorchflowerSeeds($id, "Torchflower Seeds")); self::register("totem", fn(IID $id) => new Totem($id, "Totem of Undying")); + self::register("trident", fn(IID $id) => new Trident($id, "Trident")); self::register("warped_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::WARPED_SIGN(), Blocks::WARPED_WALL_SIGN())); self::register("warped_hanging_sign", fn(IID $id) => new HangingSign($id, "Warped Hanging Sign", Blocks::WARPED_CEILING_CENTER_HANGING_SIGN(), Blocks::WARPED_CEILING_EDGES_HANGING_SIGN(), Blocks::WARPED_WALL_HANGING_SIGN())); self::register("water_bucket", fn(IID $id) => new LiquidBucket($id, "Water Bucket", Blocks::WATER())); diff --git a/src/world/sound/TridentHitEntitySound.php b/src/world/sound/TridentHitEntitySound.php new file mode 100644 index 000000000..ea77a7404 --- /dev/null +++ b/src/world/sound/TridentHitEntitySound.php @@ -0,0 +1,35 @@ +