Implement Trident (#4547)

Co-authored-by: Dylan T. <dktapps@pmmp.io>
This commit is contained in:
IvanCraft623
2025-09-18 12:00:55 -06:00
committed by GitHub
parent fbf09d990e
commit 6d2d23a210
12 changed files with 429 additions and 5 deletions

View File

@@ -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());

View File

@@ -46,6 +46,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;
@@ -171,6 +172,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']);

View File

@@ -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;
if($motionBeforeOnHit->equals($this->motion)){
$this->motion = Vector3::zero();
}
}else{
$this->isCollided = $this->onGround = false;
$this->blockHit = null;
@@ -295,8 +298,10 @@ abstract class Projectile extends Entity{
}
}
if($this->despawnsOnEntityHit()){
$this->flagForDespawn();
}
}
/**
* Called when the projectile collides with a Block.
@@ -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;
}
}

View File

@@ -0,0 +1,183 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\entity\projectile;
use pocketmine\block\Block;
use pocketmine\entity\Entity;
use pocketmine\entity\EntitySizeInfo;
use pocketmine\entity\Location;
use pocketmine\event\entity\EntityItemPickupEvent;
use pocketmine\item\Item;
use pocketmine\math\RayTraceResult;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\network\mcpe\EntityEventBroadcaster;
use pocketmine\network\mcpe\NetworkBroadcastUtils;
use pocketmine\network\mcpe\protocol\types\entity\EntityIds;
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection;
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataFlags;
use pocketmine\player\Player;
use pocketmine\world\sound\TridentHitEntitySound;
use pocketmine\world\sound\TridentHitGroundSound;
class Trident extends Projectile{
public const TAG_ITEM = "Trident"; //TAG_Compound
protected const TAG_SPAWNED_IN_CREATIVE = "isCreative"; //TAG_Byte
public static function getNetworkTypeId() : string{ return EntityIds::THROWN_TRIDENT; }
protected Item $item;
protected float $damage = 8.0;
protected bool $canCollide = true;
protected bool $spawnedInCreative = false;
public function __construct(
Location $location,
Item $item,
?Entity $shootingEntity,
?CompoundTag $nbt = null
){
if($item->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());
}
}

View File

@@ -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,12 +115,17 @@ class PlayerDeathEvent extends EntityDeathEvent{
}
break;
case EntityDamageEvent::CAUSE_PROJECTILE:
if($deathCause instanceof EntityDamageByEntityEvent){
if($deathCause instanceof EntityDamageByChildEntityEvent){
$e = $deathCause->getDamager();
if($e instanceof Living){
$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;
case EntityDamageEvent::CAUSE_SUICIDE:
return KnownTranslationFactory::death_attack_generic($name);

View File

@@ -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;

View File

@@ -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());

93
src/item/Trident.php Normal file
View File

@@ -0,0 +1,93 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\item;
use pocketmine\block\Block;
use pocketmine\entity\Entity;
use pocketmine\entity\Location;
use pocketmine\entity\projectile\Trident as TridentEntity;
use pocketmine\event\entity\ProjectileLaunchEvent;
use pocketmine\player\Player;
use pocketmine\world\sound\TridentThrowSound;
use function min;
class Trident extends Tool implements Releasable{
public function getMaxDurability() : int{
return 251;
}
public function onReleaseUsing(Player $player, array &$returnedItems) : ItemUseResult{
$location = $player->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;
}
}

View File

@@ -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()));

View File

@@ -0,0 +1,35 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class TridentHitEntitySound implements Sound{
public function encode(Vector3 $pos) : array{
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::ITEM_TRIDENT_HIT, $pos, false)];
}
}

View File

@@ -0,0 +1,35 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class TridentHitGroundSound implements Sound{
public function encode(Vector3 $pos) : array{
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::ITEM_TRIDENT_HIT_GROUND, $pos, false)];
}
}

View File

@@ -0,0 +1,35 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class TridentThrowSound implements Sound{
public function encode(Vector3 $pos) : array{
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::ITEM_TRIDENT_THROW, $pos, false)];
}
}