diff --git a/src/data/bedrock/FireworkRocketTypeIdMap.php b/src/data/bedrock/FireworkRocketTypeIdMap.php new file mode 100644 index 00000000000..4358c207301 --- /dev/null +++ b/src/data/bedrock/FireworkRocketTypeIdMap.php @@ -0,0 +1,45 @@ + */ + use IntSaveIdMapTrait; + + private function __construct(){ + foreach(FireworkRocketType::cases() as $case){ + $this->register(match($case){ + FireworkRocketType::SMALL_BALL => FireworkRocketTypeIds::SMALL_BALL, + FireworkRocketType::LARGE_BALL => FireworkRocketTypeIds::LARGE_BALL, + FireworkRocketType::STAR => FireworkRocketTypeIds::STAR, + FireworkRocketType::CREEPER => FireworkRocketTypeIds::CREEPER, + FireworkRocketType::BURST => FireworkRocketTypeIds::BURST, + }, $case); + } + } +} diff --git a/src/data/bedrock/FireworkRocketTypeIds.php b/src/data/bedrock/FireworkRocketTypeIds.php new file mode 100644 index 00000000000..7ddf0e6b9a9 --- /dev/null +++ b/src/data/bedrock/FireworkRocketTypeIds.php @@ -0,0 +1,32 @@ +map1to1Item(Ids::EYE_ARMOR_TRIM_SMITHING_TEMPLATE, Items::EYE_ARMOR_TRIM_SMITHING_TEMPLATE()); $this->map1to1Item(Ids::FEATHER, Items::FEATHER()); $this->map1to1Item(Ids::FERMENTED_SPIDER_EYE, Items::FERMENTED_SPIDER_EYE()); + $this->map1to1Item(Ids::FIREWORK_ROCKET, Items::FIREWORK_ROCKET()); $this->map1to1Item(Ids::FIRE_CHARGE, Items::FIRE_CHARGE()); $this->map1to1Item(Ids::FISHING_ROD, Items::FISHING_ROD()); $this->map1to1Item(Ids::FLINT, Items::FLINT()); @@ -488,6 +490,14 @@ function(Banner $item, int $meta) : void{ }, fn(Banner $item) => DyeColorIdMap::getInstance()->toInvertedId($item->getColor()) ); + $this->map1to1ItemWithMeta( + Ids::FIREWORK_STAR, + Items::FIREWORK_STAR(), + function(FireworkStar $item, int $meta) : void{ + // Colors will be defined by CompoundTag deserialization. + }, + fn(FireworkStar $item) => DyeColorIdMap::getInstance()->toInvertedId($item->getExplosion()->getFlashColor()) + ); $this->map1to1ItemWithMeta( Ids::GOAT_HORN, Items::GOAT_HORN(), diff --git a/src/entity/animation/FireworkParticlesAnimation.php b/src/entity/animation/FireworkParticlesAnimation.php new file mode 100644 index 00000000000..cdeb44f03bb --- /dev/null +++ b/src/entity/animation/FireworkParticlesAnimation.php @@ -0,0 +1,41 @@ +entity->getId(), ActorEvent::FIREWORK_PARTICLES, 0) + ]; + } +} diff --git a/src/entity/object/FireworkRocket.php b/src/entity/object/FireworkRocket.php new file mode 100644 index 00000000000..87db02c27c7 --- /dev/null +++ b/src/entity/object/FireworkRocket.php @@ -0,0 +1,201 @@ +lifeTicks = $lifeTicks; + $this->setExplosions($explosions); + + parent::__construct($location, $nbt); + } + + protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.25, 0.25); } + + protected function getInitialDragMultiplier() : float{ return 0.0; } + + protected function getInitialGravity() : float{ return 0.0; } + + /** + * Returns maximum number of ticks this will live for. + */ + public function getLifeTicks() : int{ + return $this->lifeTicks; + } + + /** + * Sets maximum number of ticks this will live for. + * + * @return $this + */ + public function setLifeTicks(int $lifeTicks) : self{ + if ($lifeTicks < 0) { + throw new \InvalidArgumentException("Life ticks cannot be negative"); + } + $this->lifeTicks = $lifeTicks; + return $this; + } + + /** + * @return FireworkRocketExplosion[] + */ + public function getExplosions() : array{ + return $this->explosions; + } + + /** + * @param FireworkRocketExplosion[] $explosions + * + * @return $this + */ + public function setExplosions(array $explosions) : self{ + Utils::validateArrayValueType($explosions, function(FireworkRocketExplosion $_) : void{}); + $this->explosions = $explosions; + return $this; + } + + /** + * TODO: The entity should be saved and loaded, but this is not possible. + * @see https://bugs.mojang.com/browse/MCPE-165230 + */ + public function canSaveWithChunk() : bool{ + return false; + } + + protected function onFirstUpdate(int $currentTick) : void{ + parent::onFirstUpdate($currentTick); + + $this->broadcastSound(new FireworkLaunchSound()); + } + + protected function entityBaseTick(int $tickDiff = 1) : bool{ + $hasUpdate = parent::entityBaseTick($tickDiff); + + if(!$this->isFlaggedForDespawn()){ + $this->addMotion($this->motion->x * 0.15, 0.04, $this->motion->z * 0.15); + + if($this->ticksLived >= $this->lifeTicks){ + $this->flagForDespawn(); + $this->explode(); + } + } + + return $hasUpdate; + } + + public function explode() : void{ + if(($expCount = count($this->explosions)) !== 0){ + $this->broadcastAnimation(new FireworkParticlesAnimation($this)); + foreach($this->explosions as $explosion){ + $this->broadcastSound($explosion->getType()->getExplosionSound()); + if($explosion->willTwinkle()){ + $this->broadcastSound(new FireworkCrackleSound()); + } + } + + $force = ($expCount * 2) + 5; + foreach($this->getWorld()->getCollidingEntities($this->getBoundingBox()->expandedCopy(5, 5, 5), $this) as $entity){ + if(!$entity instanceof Living){ + continue; + } + + $position = $entity->getEyePos(); + $distance = $position->distance($this->location); + if($distance > 5){ + continue; + } + + $world = $this->getWorld(); + + //check for obstructing blocks + foreach(VoxelRayTrace::betweenPoints($this->location, $position) as $pos){ + if($world->getBlockAt((int) $pos->x, (int) $pos->y, (int) $pos->z)->isSolid()){ + continue 2; + } + } + + $ev = new EntityDamageByEntityEvent($this, $entity, EntityDamageEvent::CAUSE_ENTITY_EXPLOSION, $force * sqrt((5 - $distance) / 5)); + $entity->attack($ev); + } + } + } + + public function canBeCollidedWith() : bool{ + return false; + } + + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ + parent::syncNetworkData($properties); + + $explosions = new ListTag(); + foreach($this->explosions as $explosion){ + $explosions->push($explosion->toCompoundTag()); + } + $fireworksData = CompoundTag::create() + ->setTag(FireworkItem::TAG_FIREWORK_DATA, CompoundTag::create() + ->setTag(FireworkItem::TAG_EXPLOSIONS, $explosions) + ) + ; + + $properties->setCompoundTag(EntityMetadataProperties::FIREWORK_ITEM, new CacheableNbt($fireworksData)); + } +} diff --git a/src/event/player/PlayerDeathEvent.php b/src/event/player/PlayerDeathEvent.php index aacff3438c7..ccbec13cac6 100644 --- a/src/event/player/PlayerDeathEvent.php +++ b/src/event/player/PlayerDeathEvent.php @@ -26,6 +26,7 @@ use pocketmine\block\BlockTypeIds; use pocketmine\entity\Living; use pocketmine\entity\object\FallingBlock; +use pocketmine\entity\object\FireworkRocket; use pocketmine\event\entity\EntityDamageByBlockEvent; use pocketmine\event\entity\EntityDamageByEntityEvent; use pocketmine\event\entity\EntityDamageEvent; @@ -157,7 +158,9 @@ public static function deriveMessage(string $name, ?EntityDamageEvent $deathCaus case EntityDamageEvent::CAUSE_ENTITY_EXPLOSION: if($deathCause instanceof EntityDamageByEntityEvent){ $e = $deathCause->getDamager(); - if($e instanceof Living){ + if($e instanceof FireworkRocket){ + return KnownTranslationFactory::death_attack_fireworks($name); + }elseif($e instanceof Living){ return KnownTranslationFactory::death_attack_explosion_player($name, $e->getDisplayName()); } } diff --git a/src/item/FireworkRocket.php b/src/item/FireworkRocket.php new file mode 100644 index 00000000000..4faeb55e4e9 --- /dev/null +++ b/src/item/FireworkRocket.php @@ -0,0 +1,154 @@ +flightDurationMultiplier; + } + + /** + * Sets the value that will be used to calculate a randomized flight duration + * for the firework. + * + * The higher this value, the longer the flight duration. + * + * @return $this + */ + public function setFlightDurationMultiplier(int $duration) : self{ + if($duration < 1 || $duration > 127){ + throw new \InvalidArgumentException("Flight duration must be in range 1-127"); + } + $this->flightDurationMultiplier = $duration; + + return $this; + } + + /** + * @return FireworkRocketExplosion[] + */ + public function getExplosions() : array{ + return $this->explosions; + } + + /** + * @param FireworkRocketExplosion[] $explosions + * + * @return $this + */ + public function setExplosions(array $explosions) : self{ + Utils::validateArrayValueType($explosions, function(FireworkRocketExplosion $_) : void{}); + $this->explosions = $explosions; + + return $this; + } + + public function onInteractBlock(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, array &$returnedItems) : ItemUseResult{ + $correction = 0.15; + $position = $blockClicked->getPosition()->addVector($clickVector); + $position = match($face){ + Facing::DOWN => $position->add(0, -$correction, 0), + Facing::UP => $position->add(0, $correction, 0), + Facing::NORTH => $position->add(0, 0, -$correction), + Facing::SOUTH => $position->add(0, 0, $correction), + Facing::WEST => $position->add(-$correction, 0, 0), + Facing::EAST => $position->add($correction, 0, 0), + default => throw new AssumptionFailedError("Invalid facing $face") + }; + + $randomDuration = (($this->flightDurationMultiplier + 1) * 10) + mt_rand(0, 12); + + $entity = new FireworkEntity(Location::fromObject($position, $player->getWorld(), lcg_value() * 360, 90), $randomDuration, $this->explosions); + $entity->setOwningEntity($player); + $entity->setMotion(new Vector3(lcg_value() * 0.001, 0.05, lcg_value() * 0.001)); + $entity->spawnToAll(); + + $this->pop(); + + return ItemUseResult::SUCCESS; + } + + protected function deserializeCompoundTag(CompoundTag $tag) : void{ + parent::deserializeCompoundTag($tag); + + $fireworkData = $tag->getCompoundTag(self::TAG_FIREWORK_DATA); + if($fireworkData === null){ + throw new SavedDataLoadingException("Missing firework data"); + } + + $this->setFlightDurationMultiplier($fireworkData->getByte(self::TAG_FLIGHT_DURATION, 1)); + + if(($explosions = $fireworkData->getListTag(self::TAG_EXPLOSIONS)) instanceof ListTag){ + /** @var CompoundTag $explosion */ + foreach($explosions as $explosion){ + $this->explosions[] = FireworkRocketExplosion::fromCompoundTag($explosion); + } + } + } + + protected function serializeCompoundTag(CompoundTag $tag) : void{ + parent::serializeCompoundTag($tag); + + $fireworkData = CompoundTag::create(); + $fireworkData->setByte(self::TAG_FLIGHT_DURATION, $this->flightDurationMultiplier); + + $explosions = new ListTag(); + foreach($this->explosions as $explosion){ + $explosions->push($explosion->toCompoundTag()); + } + $fireworkData->setTag(self::TAG_EXPLOSIONS, $explosions); + + $tag->setTag(self::TAG_FIREWORK_DATA, $fireworkData); + } +} diff --git a/src/item/FireworkRocketExplosion.php b/src/item/FireworkRocketExplosion.php new file mode 100644 index 00000000000..9553411234f --- /dev/null +++ b/src/item/FireworkRocketExplosion.php @@ -0,0 +1,186 @@ +getByteArray(self::TAG_COLORS)); + if(count($colors) === 0){ + throw new SavedDataLoadingException("Colors list cannot be empty"); + } + + return new self( + FireworkRocketTypeIdMap::getInstance()->fromId($tag->getByte(self::TAG_TYPE)) ?? throw new SavedDataLoadingException("Invalid firework type"), + $colors, + self::decodeColors($tag->getByteArray(self::TAG_FADE_COLORS)), + $tag->getByte(self::TAG_TWINKLE, 0) !== 0, + $tag->getByte(self::TAG_TRAIL, 0) !== 0 + ); + } + + /** + * @return DyeColor[] + * @phpstan-return list + */ + protected static function decodeColors(string $colorsBytes) : array{ + $colors = []; + + $dyeColorIdMap = DyeColorIdMap::getInstance(); + for($i = 0; $i < strlen($colorsBytes); $i++){ + $colorByte = Binary::readByte($colorsBytes[$i]); + $color = $dyeColorIdMap->fromInvertedId($colorByte); + if($color !== null){ + $colors[] = $color; + }else{ + throw new SavedDataLoadingException("Unknown color $colorByte"); + } + } + + return $colors; + } + + /** + * @param DyeColor[] $colors + */ + protected static function encodeColors(array $colors) : string{ + $colorsBytes = ""; + + $dyeColorIdMap = DyeColorIdMap::getInstance(); + foreach($colors as $color){ + $colorsBytes .= Binary::writeByte($dyeColorIdMap->toInvertedId($color)); + } + + return $colorsBytes; + } + + /** + * @param DyeColor[] $colors + * @param DyeColor[] $fadeColors + * @phpstan-param non-empty-list $colors + * @phpstan-param list $fadeColors + */ + public function __construct( + protected FireworkRocketType $type, + protected array $colors, + protected array $fadeColors = [], + protected bool $twinkle = false, + protected bool $trail = false + ){ + if(count($colors) === 0){ + throw new \InvalidArgumentException("Colors list cannot be empty"); + } + + $colorsValidator = function(DyeColor $_) : void{}; + + Utils::validateArrayValueType($colors, $colorsValidator); + Utils::validateArrayValueType($fadeColors, $colorsValidator); + } + + public function getType() : FireworkRocketType{ + return $this->type; + } + + /** + * Returns the colors of the particles. + * + * @return DyeColor[] + * @phpstan-return non-empty-list + */ + public function getColors() : array{ + return $this->colors; + } + + /** + * Returns the flash color of the explosion. + */ + public function getFlashColor() : DyeColor{ + return $this->colors[array_key_first($this->colors)]; + } + + /** + * Returns the mixure of colors from {@link FireworkRocketExplosion::getColors()}) + */ + public function getColorMix() : Color{ + /** @var Color[] $colors */ + $colors = []; + foreach ($this->colors as $dyeColor) { + $colors[] = $dyeColor->getRgbValue(); + } + return Color::mix(...$colors); + } + + /** + * Returns the colors to which the particles will change their color after a few seconds. + * If it is empty, there will be no color change in the particles. + * + * @return DyeColor[] + * @phpstan-return list + */ + public function getFadeColors() : array{ + return $this->fadeColors; + } + + /** + * Returns whether the explosion has a flickering effect. + */ + public function willTwinkle() : bool{ + return $this->twinkle; + } + + /** + * Returns whether the particles has a trail effect. + */ + public function getTrail() : bool{ + return $this->trail; + } + + public function toCompoundTag() : CompoundTag{ + return CompoundTag::create() + ->setByte(self::TAG_TYPE, FireworkRocketTypeIdMap::getInstance()->toId($this->type)) + ->setByteArray(self::TAG_COLORS, self::encodeColors($this->colors)) + ->setByteArray(self::TAG_FADE_COLORS, self::encodeColors($this->fadeColors)) + ->setByte(self::TAG_TWINKLE, $this->twinkle ? 1 : 0) + ->setByte(self::TAG_TRAIL, $this->trail ? 1 : 0) + ; + } +} diff --git a/src/item/FireworkRocketType.php b/src/item/FireworkRocketType.php new file mode 100644 index 00000000000..6aa20e6d9f2 --- /dev/null +++ b/src/item/FireworkRocketType.php @@ -0,0 +1,46 @@ + new FireworkExplosionSound(), + self::LARGE_BALL => new FireworkLargeExplosionSound(), + }; + } +} diff --git a/src/item/FireworkStar.php b/src/item/FireworkStar.php new file mode 100644 index 00000000000..0eed6963bdd --- /dev/null +++ b/src/item/FireworkStar.php @@ -0,0 +1,106 @@ +explosion = new FireworkRocketExplosion(FireworkRocketType::SMALL_BALL, [DyeColor::BLACK], [], false, false); + } + + public function getExplosion() : FireworkRocketExplosion{ + return $this->explosion; + } + + /** @return $this */ + public function setExplosion(FireworkRocketExplosion $explosion) : self{ + $this->explosion = $explosion; + return $this; + } + + /** + * Returns the displayed color of the item. + * The mixture of explosion colors, or the custom color if it is set. + */ + public function getColor() : Color{ + return $this->customColor ?? $this->explosion->getColorMix(); + } + + /** + * Returns the displayed custom color of the item that overrides + * the mixture of explosion colors, or null is it is not set. + */ + public function getCustomColor() : ?Color{ + return $this->customColor; + } + + /** + * Sets the displayed custom color of the item that overrides + * the mixture of explosion colors, or removes if $color is null. + * + * @return $this + */ + public function setCustomColor(?Color $color) : self{ + $this->customColor = $color; + return $this; + } + + protected function deserializeCompoundTag(CompoundTag $tag) : void{ + parent::deserializeCompoundTag($tag); + + $explosionTag = $tag->getTag(self::TAG_EXPLOSION); + if(!$explosionTag instanceof CompoundTag){ + throw new SavedDataLoadingException("Missing explosion data"); + } + $this->explosion = FireworkRocketExplosion::fromCompoundTag($explosionTag); + + $customColor = Color::fromARGB(Binary::unsignInt($tag->getInt(self::TAG_CUSTOM_COLOR))); + $color = $this->explosion->getColorMix(); + if(!$customColor->equals($color)){ //check that $customColor is actually custom. + $this->customColor = $customColor; + } + } + + protected function serializeCompoundTag(CompoundTag $tag) : void{ + parent::serializeCompoundTag($tag); + + $tag->setTag(self::TAG_EXPLOSION, $this->explosion->toCompoundTag()); + $tag->setInt(self::TAG_CUSTOM_COLOR, Binary::signInt($this->getColor()->toARGB())); + } +} diff --git a/src/item/ItemTypeIds.php b/src/item/ItemTypeIds.php index c93c23e817e..2ff3d901b5c 100644 --- a/src/item/ItemTypeIds.php +++ b/src/item/ItemTypeIds.php @@ -328,8 +328,10 @@ private function __construct(){ public const END_CRYSTAL = 20289; public const ICE_BOMB = 20290; public const RECOVERY_COMPASS = 20291; + public const FIREWORK_ROCKET = 20292; + public const FIREWORK_STAR = 20293; - public const FIRST_UNUSED_ITEM_ID = 20292; + public const FIRST_UNUSED_ITEM_ID = 20294; private static int $nextDynamicId = self::FIRST_UNUSED_ITEM_ID; diff --git a/src/item/StringToItemParser.php b/src/item/StringToItemParser.php index 09c93d5d925..3dd96602162 100644 --- a/src/item/StringToItemParser.php +++ b/src/item/StringToItemParser.php @@ -1337,6 +1337,9 @@ private static function registerItems(self $result) : void{ $result->register("eye_drops", fn() => Items::MEDICINE()->setType(MedicineType::EYE_DROPS)); $result->register("feather", fn() => Items::FEATHER()); $result->register("fermented_spider_eye", fn() => Items::FERMENTED_SPIDER_EYE()); + $result->register("firework_rocket", fn() => Items::FIREWORK_ROCKET()); + $result->register("firework_star", fn() => Items::FIREWORK_STAR()); + $result->register("fireworks", fn() => Items::FIREWORK_ROCKET()); $result->register("fire_charge", fn() => Items::FIRE_CHARGE()); $result->register("fish", fn() => Items::RAW_FISH()); $result->register("fishing_rod", fn() => Items::FISHING_ROD()); diff --git a/src/item/VanillaItems.php b/src/item/VanillaItems.php index 6768ed8f07e..57d39bd1369 100644 --- a/src/item/VanillaItems.php +++ b/src/item/VanillaItems.php @@ -163,6 +163,8 @@ * @method static Item EYE_ARMOR_TRIM_SMITHING_TEMPLATE() * @method static Item FEATHER() * @method static Item FERMENTED_SPIDER_EYE() + * @method static FireworkRocket FIREWORK_ROCKET() + * @method static FireworkStar FIREWORK_STAR() * @method static FireCharge FIRE_CHARGE() * @method static FishingRod FISHING_ROD() * @method static Item FLINT() @@ -487,6 +489,8 @@ protected static function setup() : void{ self::register("experience_bottle", fn(IID $id) => new ExperienceBottle($id, "Bottle o' Enchanting")); self::register("feather", fn(IID $id) => new Item($id, "Feather")); self::register("fermented_spider_eye", fn(IID $id) => new Item($id, "Fermented Spider Eye")); + self::register("firework_rocket", fn(IID $id) => new FireworkRocket($id, "Firework Rocket")); + self::register("firework_star", fn(IID $id) => new FireworkStar($id, "Firework Star")); self::register("fire_charge", fn(IID $id) => new FireCharge($id, "Fire Charge")); self::register("fishing_rod", fn(IID $id) => new FishingRod($id, "Fishing Rod", [EnchantmentTags::FISHING_ROD])); self::register("flint", fn(IID $id) => new Item($id, "Flint")); diff --git a/src/world/sound/FireworkCrackleSound.php b/src/world/sound/FireworkCrackleSound.php new file mode 100644 index 00000000000..c0e897d70b7 --- /dev/null +++ b/src/world/sound/FireworkCrackleSound.php @@ -0,0 +1,35 @@ +