diff --git a/generated/item/VanillaItems.php b/generated/item/VanillaItems.php index aca3d0331..12bdf4dd5 100644 --- a/generated/item/VanillaItems.php +++ b/generated/item/VanillaItems.php @@ -153,6 +153,7 @@ final class VanillaItems{ private static BannerPattern $_mCREEPER_BANNER_PATTERN; private static HangingSign $_mCRIMSON_HANGING_SIGN; private static ItemBlockWallOrFloor $_mCRIMSON_SIGN; + private static Crossbow $_mCROSSBOW; private static Cushion $_mCUSHION; private static Boat $_mDARK_OAK_BOAT; private static HangingSign $_mDARK_OAK_HANGING_SIGN; @@ -549,6 +550,7 @@ private static function getInitAssigners() : array{ "creeper_banner_pattern" => fn(BannerPattern $v) => self::$_mCREEPER_BANNER_PATTERN = $v, "crimson_hanging_sign" => fn(HangingSign $v) => self::$_mCRIMSON_HANGING_SIGN = $v, "crimson_sign" => fn(ItemBlockWallOrFloor $v) => self::$_mCRIMSON_SIGN = $v, + "crossbow" => fn(Crossbow $v) => self::$_mCROSSBOW = $v, "cushion" => fn(Cushion $v) => self::$_mCUSHION = $v, "dark_oak_boat" => fn(Boat $v) => self::$_mDARK_OAK_BOAT = $v, "dark_oak_hanging_sign" => fn(HangingSign $v) => self::$_mDARK_OAK_HANGING_SIGN = $v, @@ -1401,6 +1403,11 @@ public static function CRIMSON_SIGN() : ItemBlockWallOrFloor{ return clone self::$_mCRIMSON_SIGN; } + public static function CROSSBOW() : Crossbow{ + if(!isset(self::$_mCROSSBOW)){ self::init(); } + return clone self::$_mCROSSBOW; + } + public static function CUSHION() : Cushion{ if(!isset(self::$_mCUSHION)){ self::init(); } return clone self::$_mCUSHION; diff --git a/generated/item/enchantment/VanillaEnchantments.php b/generated/item/enchantment/VanillaEnchantments.php index 263e27273..68c4873b9 100644 --- a/generated/item/enchantment/VanillaEnchantments.php +++ b/generated/item/enchantment/VanillaEnchantments.php @@ -53,10 +53,13 @@ final class VanillaEnchantments{ private static KnockbackEnchantment $_mKNOCKBACK; private static Enchantment $_mLUNGE; private static Enchantment $_mMENDING; + private static Enchantment $_mMULTISHOT; + private static Enchantment $_mPIERCING; private static Enchantment $_mPOWER; private static ProtectionEnchantment $_mPROJECTILE_PROTECTION; private static ProtectionEnchantment $_mPROTECTION; private static Enchantment $_mPUNCH; + private static Enchantment $_mQUICK_CHARGE; private static Enchantment $_mRESPIRATION; private static SharpnessEnchantment $_mSHARPNESS; private static Enchantment $_mSILK_TOUCH; @@ -114,10 +117,13 @@ private static function getInitAssigners() : array{ "KNOCKBACK" => fn(KnockbackEnchantment $v) => self::$_mKNOCKBACK = $v, "LUNGE" => fn(Enchantment $v) => self::$_mLUNGE = $v, "MENDING" => fn(Enchantment $v) => self::$_mMENDING = $v, + "MULTISHOT" => fn(Enchantment $v) => self::$_mMULTISHOT = $v, + "PIERCING" => fn(Enchantment $v) => self::$_mPIERCING = $v, "POWER" => fn(Enchantment $v) => self::$_mPOWER = $v, "PROJECTILE_PROTECTION" => fn(ProtectionEnchantment $v) => self::$_mPROJECTILE_PROTECTION = $v, "PROTECTION" => fn(ProtectionEnchantment $v) => self::$_mPROTECTION = $v, "PUNCH" => fn(Enchantment $v) => self::$_mPUNCH = $v, + "QUICK_CHARGE" => fn(Enchantment $v) => self::$_mQUICK_CHARGE = $v, "RESPIRATION" => fn(Enchantment $v) => self::$_mRESPIRATION = $v, "SHARPNESS" => fn(SharpnessEnchantment $v) => self::$_mSHARPNESS = $v, "SILK_TOUCH" => fn(Enchantment $v) => self::$_mSILK_TOUCH = $v, @@ -239,6 +245,16 @@ public static function MENDING() : Enchantment{ return self::$_mMENDING; } + public static function MULTISHOT() : Enchantment{ + if(!isset(self::$_mMULTISHOT)){ self::init(); } + return self::$_mMULTISHOT; + } + + public static function PIERCING() : Enchantment{ + if(!isset(self::$_mPIERCING)){ self::init(); } + return self::$_mPIERCING; + } + public static function POWER() : Enchantment{ if(!isset(self::$_mPOWER)){ self::init(); } return self::$_mPOWER; @@ -259,6 +275,11 @@ public static function PUNCH() : Enchantment{ return self::$_mPUNCH; } + public static function QUICK_CHARGE() : Enchantment{ + if(!isset(self::$_mQUICK_CHARGE)){ self::init(); } + return self::$_mQUICK_CHARGE; + } + public static function RESPIRATION() : Enchantment{ if(!isset(self::$_mRESPIRATION)){ self::init(); } return self::$_mRESPIRATION; diff --git a/src/data/bedrock/EnchantmentIdMap.php b/src/data/bedrock/EnchantmentIdMap.php index 0d9477988..7b8660266 100644 --- a/src/data/bedrock/EnchantmentIdMap.php +++ b/src/data/bedrock/EnchantmentIdMap.php @@ -67,6 +67,10 @@ private function __construct(){ $this->register(EnchantmentIds::VANISHING, VanillaEnchantments::VANISHING()); + $this->register(EnchantmentIds::MULTISHOT, VanillaEnchantments::MULTISHOT()); + $this->register(EnchantmentIds::PIERCING, VanillaEnchantments::PIERCING()); + $this->register(EnchantmentIds::QUICK_CHARGE, VanillaEnchantments::QUICK_CHARGE()); + $this->register(EnchantmentIds::SWIFT_SNEAK, VanillaEnchantments::SWIFT_SNEAK()); $this->register(EnchantmentIds::FROST_WALKER, VanillaEnchantments::FROST_WALKER()); diff --git a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php index 9fbfc46d9..558b8e092 100644 --- a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php +++ b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php @@ -253,6 +253,7 @@ private function register1to1ItemMappings() : void{ $this->map1to1Item(Ids::CRIMSON_HANGING_SIGN, Items::CRIMSON_HANGING_SIGN()); $this->map1to1Item(Ids::CRIMSON_SIGN, Items::CRIMSON_SIGN()); $this->map1to1Item(Ids::CHERRY_BOAT, Items::CHERRY_BOAT()); + $this->map1to1Item(Ids::CROSSBOW, Items::CROSSBOW()); $this->map1to1Item(Ids::DARK_OAK_BOAT, Items::DARK_OAK_BOAT()); $this->map1to1Item(Ids::DARK_OAK_HANGING_SIGN, Items::DARK_OAK_HANGING_SIGN()); $this->map1to1Item(Ids::DARK_OAK_SIGN, Items::DARK_OAK_SIGN()); diff --git a/src/entity/object/CrossbowFireworkRocket.php b/src/entity/object/CrossbowFireworkRocket.php new file mode 100644 index 000000000..5229a5f05 --- /dev/null +++ b/src/entity/object/CrossbowFireworkRocket.php @@ -0,0 +1,84 @@ +lastMoveStart = $this->location->asVector3(); + parent::move($dx, $dy, $dz); + } + + protected function entityBaseTick(int $tickDiff = 1) : bool{ + $hasUpdate = parent::entityBaseTick($tickDiff); + + if(!$this->isFlaggedForDespawn() && $this->hitEntity() !== null){ + $this->flagForDespawn(); + $this->explode(); + } + + return $hasUpdate; + } + + private function hitEntity() : ?Living{ + $owningEntityId = $this->getOwningEntityId(); + $end = $this->location->asVector3(); + $start = $this->lastMoveStart ?? $end; + $delta = $end->subtractVector($start); + $halfWidth = $this->getSize()->getWidth() / 2; + + foreach($this->getWorld()->getNearbyEntities($this->boundingBox->addCoord(-$delta->x, -$delta->y, -$delta->z), $this) as $entity){ + if(!$entity instanceof Living || !$entity->canBeCollidedWith()){ + continue; + } + + if($entity->getId() === $owningEntityId && $this->ticksLived <= self::OWNER_IMMUNITY_TICKS){ + continue; + } + + $entityBB = $entity->getBoundingBox()->expandedCopy($halfWidth, $halfWidth, $halfWidth); + if($entityBB->isVectorInside($end) || $entityBB->calculateIntercept($start, $end) !== null){ + return $entity; + } + } + + return null; + } +} diff --git a/src/entity/projectile/Arrow.php b/src/entity/projectile/Arrow.php index 3284ef5a9..c869d28b2 100644 --- a/src/entity/projectile/Arrow.php +++ b/src/entity/projectile/Arrow.php @@ -43,6 +43,7 @@ use pocketmine\player\Player; use pocketmine\world\sound\ArrowHitSound; use function ceil; +use function count; use function mt_rand; use function sqrt; @@ -54,6 +55,11 @@ public static function getNetworkTypeId() : string{ return EntityIds::ARROW; } public const PICKUP_ANY = 1; public const PICKUP_CREATIVE = 2; + /** + * Speed kept by a piercing arrow after going through an entity. + */ + private const PIERCING_SPEED_MULTIPLIER = 0.99; + private const TAG_PICKUP = "pickup"; //TAG_Byte public const TAG_CRIT = "crit"; //TAG_Byte private const TAG_LIFE = "life"; //TAG_Short @@ -63,6 +69,16 @@ public static function getNetworkTypeId() : string{ return EntityIds::ARROW; } protected float $punchKnockback = 0.0; protected int $collideTicks = 0; protected bool $critical = false; + protected int $piercing = 0; + private bool $piercedThisMove = false; + + /** + * Entities the arrow already went through, so that it doesn't damage them again on the next tick. + * + * @var true[] + * @phpstan-var array + */ + protected array $piercedEntities = []; public function __construct(Location $location, ?Entity $shootingEntity, bool $critical, ?CompoundTag $nbt = null){ parent::__construct($location, $shootingEntity, $nbt); @@ -147,8 +163,51 @@ protected function onHitBlock(Block $blockHit, RayTraceResult $hitResult) : void $this->broadcastAnimation(new ArrowShakeAnimation($this, 7)); } + /** + * Returns how many entities the arrow can go through before stopping. + */ + public function getPiercing() : int{ + return $this->piercing; + } + + public function setPiercing(int $piercing) : void{ + if($piercing < 0){ + throw new \InvalidArgumentException("Piercing must not be negative"); + } + $this->piercing = $piercing; + } + + public function canCollideWith(Entity $entity) : bool{ + return !isset($this->piercedEntities[$entity->getId()]) && parent::canCollideWith($entity); + } + + protected function despawnsOnEntityHit() : bool{ + return count($this->piercedEntities) > $this->piercing; + } + + /** + * A piercing arrow keeps flying after going through an entity, so it must not be left in the "stuck" state that + * {@link Projectile::move()} puts it in after a hit, otherwise it ignores every entity during its next step. + */ + protected function move(float $dx, float $dy, float $dz) : void{ + parent::move($dx, $dy, $dz); + if($this->piercedThisMove){ + $this->isCollided = $this->onGround = false; + $this->piercedThisMove = false; + } + } + protected function onHitEntity(Entity $entityHit, RayTraceResult $hitResult) : void{ + $this->piercedEntities[$entityHit->getId()] = true; + parent::onHitEntity($entityHit, $hitResult); + + if(!$this->isFlaggedForDespawn()){ + //without this the projectile code would zero the motion, stopping the arrow inside the entity it just went through + $this->motion = $this->motion->multiply(self::PIERCING_SPEED_MULTIPLIER); + $this->piercedThisMove = true; + } + if($this->punchKnockback > 0){ $horizontalSpeed = sqrt($this->motion->x ** 2 + $this->motion->z ** 2); if($horizontalSpeed > 0){ diff --git a/src/item/Crossbow.php b/src/item/Crossbow.php new file mode 100644 index 000000000..511ad4dea --- /dev/null +++ b/src/item/Crossbow.php @@ -0,0 +1,328 @@ +chargedItem !== null; + } + + /** + * Returns the ammo the crossbow is loaded with, or null if it isn't loaded. + */ + public function getChargedItem() : ?Item{ + return $this->chargedItem === null ? null : clone $this->chargedItem; + } + + /** @return $this */ + public function setChargedItem(?Item $item) : self{ + if($item !== null && !self::isAmmo($item)){ + throw new \InvalidArgumentException("A crossbow can only be charged with an arrow or a firework rocket"); + } + $this->chargedItem = $item === null ? null : (clone $item)->setCount(1); + return $this; + } + + public static function isAmmo(Item $item) : bool{ + return $item->getTypeId() === ItemTypeIds::ARROW || $item instanceof FireworkRocket; + } + + protected function deserializeCompoundTag(CompoundTag $tag) : void{ + parent::deserializeCompoundTag($tag); + + $this->chargedItem = null; + + $chargedItem = $tag->getCompoundTag(self::TAG_CHARGED_ITEM); + if($chargedItem === null || $chargedItem->getByte(self::TAG_CHARGED_ITEM_COUNT, 0) <= 0){ + return; + } + + $item = $chargedItem->getString(self::TAG_CHARGED_ITEM_NAME, ItemTypeNames::ARROW) === ItemTypeNames::FIREWORK_ROCKET ? + VanillaItems::FIREWORK_ROCKET() : + VanillaItems::ARROW(); + + $itemTag = $chargedItem->getCompoundTag(self::TAG_CHARGED_ITEM_TAG); + if($itemTag !== null){ + $item->setNamedTag($itemTag); + } + + $this->chargedItem = $item; + } + + protected function serializeCompoundTag(CompoundTag $tag) : void{ + parent::serializeCompoundTag($tag); + + if($this->chargedItem === null){ + $tag->removeTag(self::TAG_CHARGED_ITEM); + return; + } + + //the client renders the loaded crossbow from this tag + $chargedItem = CompoundTag::create() + ->setByte(self::TAG_CHARGED_ITEM_COUNT, 1) + ->setShort(self::TAG_CHARGED_ITEM_DAMAGE, 0) + ->setString(self::TAG_CHARGED_ITEM_NAME, $this->chargedItem instanceof FireworkRocket ? ItemTypeNames::FIREWORK_ROCKET : ItemTypeNames::ARROW); + + //keeps the explosions of a loaded firework rocket + $itemTag = $this->chargedItem->getNamedTag(); + if($itemTag->count() > 0){ + $chargedItem->setTag(self::TAG_CHARGED_ITEM_TAG, $itemTag); + } + + $tag->setTag(self::TAG_CHARGED_ITEM, $chargedItem); + } + + public function canStartUsingItem(Player $player) : bool{ + return !$this->isCharged() && ($this->findAmmo($player) !== null || !$player->hasFiniteResources()); + } + + public function getMinUseDuration() : int{ + return $this->getChargeDuration(); + } + + /** + * Returns how long the crossbow has to be held down before it becomes loaded, Quick Charge included. + */ + public function getChargeDuration() : int{ + $quickCharge = $this->getEnchantmentLevel(VanillaEnchantments::QUICK_CHARGE()); + + return max(0, self::CHARGE_DURATION - ($quickCharge * self::QUICK_CHARGE_REDUCTION)); + } + + public function onReleaseUsing(Player $player, array &$returnedItems) : ItemUseResult{ + if($this->isCharged() || $player->getItemUseDuration() < $this->getChargeDuration()){ + return ItemUseResult::NONE; + } + + $ammo = $this->findAmmo($player); + if(!$player->hasFiniteResources()){ + $this->setChargedItem($ammo === null ? VanillaItems::ARROW() : $ammo[1]); + }else{ + if($ammo === null){ + return ItemUseResult::FAIL; + } + + [$inventory, $item] = $ammo; + $this->setChargedItem($item); + $inventory->removeItem((clone $item)->setCount(1)); + } + + $player->getWorld()->addSound($player->getPosition(), new CrossbowLoadingEndSound()); + + return ItemUseResult::SUCCESS; + } + + public function onClickAir(Player $player, Vector3 $directionVector, array &$returnedItems) : ItemUseResult{ + $chargedItem = $this->chargedItem; + if($chargedItem === null){ + return ItemUseResult::NONE; + } + + $multishot = $this->hasEnchantment(VanillaEnchantments::MULTISHOT()); + $angles = $multishot ? [-self::MULTISHOT_ANGLE, 0.0, self::MULTISHOT_ANGLE] : [0.0]; + + $fired = 0; + foreach($angles as $angle){ + if($this->shoot($player, $chargedItem, $angle)){ + $fired++; + } + } + + if($fired === 0){ + return ItemUseResult::FAIL; + } + + $this->chargedItem = null; + if($player->hasFiniteResources()){ + $this->applyDamage(match(true){ + $chargedItem instanceof FireworkRocket => self::DURABILITY_COST_FIREWORK, + $multishot => self::DURABILITY_COST_MULTISHOT, + default => self::DURABILITY_COST + }); + } + + return ItemUseResult::SUCCESS; + } + + /** + * Fires a single projectile, rotated horizontally by the given angle in degrees. + */ + private function shoot(Player $player, Item $ammo, float $angle) : bool{ + $location = $player->getLocation(); + $yaw = $location->yaw + $angle; + $origin = Location::fromObject( + $player->getEyePos(), + $player->getWorld(), + ($yaw > 180 ? 360 : 0) - $yaw, + -$location->pitch + ); + $direction = $this->getDirectionVector($yaw, $location->pitch); + + if($ammo instanceof FireworkRocket){ + $entity = new CrossbowFireworkRocket($origin, self::FIREWORK_FLIGHT_TIME + mt_rand(0, self::FIREWORK_FLIGHT_TIME_RANDOM), $ammo->getExplosions()); + $entity->setOwningEntity($player); + $entity->setMotion($direction->multiply(self::FIREWORK_SHOOT_FORCE)); + $entity->spawnToAll(); + $player->getWorld()->addSound($location, new CrossbowShootSound()); + + return true; + } + + $entity = new ArrowEntity($origin, $player, true); + $entity->setMotion($direction); + $entity->setPiercing($this->getEnchantmentLevel(VanillaEnchantments::PIERCING())); + if($angle !== 0.0){ + $entity->setPickupMode(ArrowEntity::PICKUP_CREATIVE); + } + + $ev = new EntityShootBowEvent($player, $this, $entity, self::SHOOT_FORCE); + if($player->isSpectator()){ + $ev->cancel(); + } + $ev->call(); + + $entity = $ev->getProjectile(); //this might have been changed by plugins + + if($ev->isCancelled()){ + $entity->flagForDespawn(); + return false; + } + + $entity->setMotion($entity->getMotion()->multiply($ev->getForce())); + + if($entity instanceof Projectile){ + $projectileEv = new ProjectileLaunchEvent($entity); + $projectileEv->call(); + if($projectileEv->isCancelled()){ + $entity->flagForDespawn(); + return false; + } + } + + $entity->spawnToAll(); + $player->getWorld()->addSound($location, new CrossbowShootSound()); + + return true; + } + + private function getDirectionVector(float $yaw, float $pitch) : Vector3{ + $pitchRad = $pitch * (M_PI / 180); + $yawRad = $yaw * (M_PI / 180); + + return (new Vector3( + -sin($yawRad) * cos($pitchRad), + -sin($pitchRad), + cos($yawRad) * cos($pitchRad) + ))->normalize(); + } + + /** + * Returns the inventory holding the first usable ammo and the ammo itself, or null if the player has none. + * The offhand is searched first, like vanilla does. + * + * @phpstan-return array{Inventory, Item}|null + */ + private function findAmmo(Player $player) : ?array{ + foreach([$player->getOffHandInventory(), $player->getInventory()] as $inventory){ + foreach($inventory->getContents() as $item){ + if(self::isAmmo($item)){ + return [$inventory, $item]; + } + } + } + + return null; + } +} diff --git a/src/item/ItemTypeIds.php b/src/item/ItemTypeIds.php index 6dc7e7f83..845b62e4a 100644 --- a/src/item/ItemTypeIds.php +++ b/src/item/ItemTypeIds.php @@ -401,8 +401,9 @@ private function __construct(){ public const CUSHION = 20360; public const BOLT_ARMOR_TRIM_SMITHING_TEMPLATE = 20361; public const FLOW_ARMOR_TRIM_SMITHING_TEMPLATE = 20362; + public const CROSSBOW = 20363; - public const FIRST_UNUSED_ITEM_ID = 20363; + public const FIRST_UNUSED_ITEM_ID = 20364; private static int $nextDynamicId = self::FIRST_UNUSED_ITEM_ID; diff --git a/src/item/StringToItemParser.php b/src/item/StringToItemParser.php index 482780ab9..240c7a57e 100644 --- a/src/item/StringToItemParser.php +++ b/src/item/StringToItemParser.php @@ -1468,6 +1468,7 @@ private static function registerItems(self $result) : void{ $result->register("creeper_banner_pattern", fn() => Items::CREEPER_BANNER_PATTERN()); $result->register("crimson_hanging_sign", fn() => Items::CRIMSON_HANGING_SIGN()); $result->register("cherry_boat", fn() => Items::CHERRY_BOAT()); + $result->register("crossbow", fn() => Items::CROSSBOW()); $result->register("dark_oak_boat", fn() => Items::DARK_OAK_BOAT()); $result->register("dark_oak_hanging_sign", fn() => Items::DARK_OAK_HANGING_SIGN()); $result->register("diamond", fn() => Items::DIAMOND()); diff --git a/src/item/VanillaItemsInputs.php b/src/item/VanillaItemsInputs.php index 64c6efba6..56f542ed6 100644 --- a/src/item/VanillaItemsInputs.php +++ b/src/item/VanillaItemsInputs.php @@ -199,6 +199,7 @@ protected function setup() : void{ self::register("copper_ingot", fn(IID $id) => new Item($id, "Copper Ingot")); self::register("copper_nugget", fn(IID $id) => new Item($id, "Copper Nugget")); self::registerDelayed("coral_fan", fn(string $name) : CoralFan => new CoralFan(self::makeIID($name))); //uses VanillaBlocks in constructor :( + self::register("crossbow", fn(IID $id) => new Crossbow($id, "Crossbow", [EnchantmentTags::CROSSBOW])); self::registerDelayed("crimson_sign", fn(string $name) : ItemBlockWallOrFloor => new ItemBlockWallOrFloor(self::makeIID($name), Blocks::CRIMSON_SIGN(), Blocks::CRIMSON_WALL_SIGN())); self::registerDelayed("crimson_hanging_sign", fn(string $name) : HangingSign => new HangingSign(self::makeIID($name), "Crimson Hanging Sign", Blocks::CRIMSON_CEILING_CENTER_HANGING_SIGN(), Blocks::CRIMSON_CEILING_EDGES_HANGING_SIGN(), Blocks::CRIMSON_WALL_HANGING_SIGN())); self::registerDelayed("dark_oak_sign", fn(string $name) : ItemBlockWallOrFloor => new ItemBlockWallOrFloor(self::makeIID($name), Blocks::DARK_OAK_SIGN(), Blocks::DARK_OAK_WALL_SIGN())); diff --git a/src/item/enchantment/AvailableEnchantmentRegistry.php b/src/item/enchantment/AvailableEnchantmentRegistry.php index 28985d4dd..34fcabb05 100644 --- a/src/item/enchantment/AvailableEnchantmentRegistry.php +++ b/src/item/enchantment/AvailableEnchantmentRegistry.php @@ -76,6 +76,9 @@ private function __construct(){ [Tags::ARMOR, Tags::WEAPONS, Tags::FISHING_ROD], [Tags::SHEARS, Tags::FLINT_AND_STEEL, Tags::SHIELD, Tags::CARROT_ON_STICK, Tags::ELYTRA, Tags::BRUSH] ); + $this->register(Enchantments::MULTISHOT(), [Tags::CROSSBOW], []); + $this->register(Enchantments::PIERCING(), [Tags::CROSSBOW], []); + $this->register(Enchantments::QUICK_CHARGE(), [Tags::CROSSBOW], []); $this->register(Enchantments::POWER(), [Tags::BOW], []); $this->register(Enchantments::PUNCH(), [Tags::BOW], []); $this->register(Enchantments::FLAME(), [Tags::BOW], []); diff --git a/src/item/enchantment/IncompatibleEnchantmentGroups.php b/src/item/enchantment/IncompatibleEnchantmentGroups.php index c9b237b7a..2f1db048f 100644 --- a/src/item/enchantment/IncompatibleEnchantmentGroups.php +++ b/src/item/enchantment/IncompatibleEnchantmentGroups.php @@ -34,4 +34,5 @@ final class IncompatibleEnchantmentGroups{ public const BOW_INFINITE = "bow_infinite"; public const BLOCK_DROPS = "block_drops"; public const HEAVY_WEAPON_DAMAGE = "heavy_weapon_damage"; + public const CROSSBOW_PROJECTILE = "crossbow_projectile"; } diff --git a/src/item/enchantment/IncompatibleEnchantmentRegistry.php b/src/item/enchantment/IncompatibleEnchantmentRegistry.php index 09ef3e9ce..8ae78ce2d 100644 --- a/src/item/enchantment/IncompatibleEnchantmentRegistry.php +++ b/src/item/enchantment/IncompatibleEnchantmentRegistry.php @@ -50,6 +50,7 @@ private function __construct(){ $this->register(Groups::BOW_INFINITE, [Enchantments::INFINITY(), Enchantments::MENDING()]); $this->register(Groups::BLOCK_DROPS, [Enchantments::FORTUNE(), Enchantments::SILK_TOUCH()]); $this->register(Groups::HEAVY_WEAPON_DAMAGE, [Enchantments::DENSITY(), Enchantments::BREACH()]); + $this->register(Groups::CROSSBOW_PROJECTILE, [Enchantments::MULTISHOT(), Enchantments::PIERCING()]); } /** diff --git a/src/item/enchantment/StringToEnchantmentParser.php b/src/item/enchantment/StringToEnchantmentParser.php index 6ae4d8236..28ab2dc8b 100644 --- a/src/item/enchantment/StringToEnchantmentParser.php +++ b/src/item/enchantment/StringToEnchantmentParser.php @@ -53,7 +53,10 @@ private static function make() : self{ $result->register("knockback", fn() => VanillaEnchantments::KNOCKBACK()); $result->register("lunge", fn() => VanillaEnchantments::LUNGE()); $result->register("mending", fn() => VanillaEnchantments::MENDING()); + $result->register("multishot", fn() => VanillaEnchantments::MULTISHOT()); + $result->register("piercing", fn() => VanillaEnchantments::PIERCING()); $result->register("power", fn() => VanillaEnchantments::POWER()); + $result->register("quick_charge", fn() => VanillaEnchantments::QUICK_CHARGE()); $result->register("projectile_protection", fn() => VanillaEnchantments::PROJECTILE_PROTECTION()); $result->register("protection", fn() => VanillaEnchantments::PROTECTION()); $result->register("punch", fn() => VanillaEnchantments::PUNCH()); diff --git a/src/item/enchantment/VanillaEnchantmentsInputs.php b/src/item/enchantment/VanillaEnchantmentsInputs.php index 739dfa11f..aa51a1853 100644 --- a/src/item/enchantment/VanillaEnchantmentsInputs.php +++ b/src/item/enchantment/VanillaEnchantmentsInputs.php @@ -301,6 +301,36 @@ protected function setup() : void{ 20 )); + self::register("MULTISHOT", new Enchantment( + KnownTranslationFactory::enchantment_crossbowMultishot(), + Rarity::RARE, + 0, + 0, + 1, + fn(int $level) : int => 20, + 50 + )); + + self::register("PIERCING", new Enchantment( + KnownTranslationFactory::enchantment_crossbowPiercing(), + Rarity::COMMON, + 0, + 0, + 4, + fn(int $level) : int => 10 * ($level - 1) + 1, + 50 + )); + + self::register("QUICK_CHARGE", new Enchantment( + KnownTranslationFactory::enchantment_crossbowQuickCharge(), + Rarity::UNCOMMON, + 0, + 0, + 3, + fn(int $level) : int => 20 * ($level - 1) + 12, + 50 + )); + self::register("SWIFT_SNEAK", new Enchantment( KnownTranslationFactory::enchantment_swift_sneak(), Rarity::MYTHIC, diff --git a/src/world/sound/CrossbowLoadingEndSound.php b/src/world/sound/CrossbowLoadingEndSound.php new file mode 100644 index 000000000..ea4e912b7 --- /dev/null +++ b/src/world/sound/CrossbowLoadingEndSound.php @@ -0,0 +1,37 @@ +