Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Monster Spawner functionality #6187

Open
wants to merge 25 commits into
base: minor-next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 104 additions & 1 deletion src/block/MonsterSpawner.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,24 @@

namespace pocketmine\block;

use pocketmine\block\tile\MonsterSpawner as TileSpawner;
use pocketmine\block\tile\SpawnerSpawnRangeRegistry;
use pocketmine\block\utils\SupportType;
use pocketmine\entity\Entity;
use pocketmine\entity\EntityFactory;
use pocketmine\entity\Location;
use pocketmine\event\block\SpawnerAttemptSpawnEvent;
use pocketmine\item\Item;
use pocketmine\item\SpawnEgg;
use pocketmine\item\SpawnEggEntityRegistry;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\DoubleTag;
use pocketmine\nbt\tag\FloatTag;
use pocketmine\nbt\tag\ListTag;
use pocketmine\player\Player;
use pocketmine\world\particle\MobSpawnParticle;
use function mt_rand;

class MonsterSpawner extends Transparent{
Expand All @@ -38,10 +54,97 @@ protected function getXpDropAmount() : int{
}

public function onScheduledUpdate() : void{
//TODO
if($this->onUpdate()){
$this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 1);
}
}

public function getSupportType(int $facing) : SupportType{
return SupportType::NONE;
}

public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($item instanceof SpawnEgg){
$entityId = SpawnEggEntityRegistry::getInstance()->getEntityId($item);
if($entityId === null){
return false;
}
$spawner = $this->position->getWorld()->getTile($this->position);
if($spawner instanceof TileSpawner){
$spawner->setEntityTypeId($entityId);
$this->position->getWorld()->setBlock($this->position, $this);
$this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 1);
return true;
}
}
return parent::onInteract($item, $face, $clickVector, $player, $returnedItems);
}

public function onUpdate() : bool{
$world = $this->position->getWorld();
$spawnerTile = $world->getTile($this->position);

if(!$spawnerTile instanceof TileSpawner || $spawnerTile->closed || $spawnerTile->getEntityTypeId() === TileSpawner::DEFAULT_ENTITY_TYPE_ID){
return false;
}
$spawnDelay = $spawnerTile->getSpawnDelay();
if($spawnDelay > 0){
$spawnerTile->setSpawnDelay($spawnDelay - 1);
return true;
}
$position = $spawnerTile->getPosition();
$world = $position->getWorld();
if($world->getNearestEntity($position, $spawnerTile->getRequiredPlayerRange(), Player::class) === null){
return true;
}
$entityTypeId = $spawnerTile->getEntityTypeId();
$spawnRange = $spawnerTile->getSpawnRange();
$count = 0;
$spawnRangeBB = SpawnerSpawnRangeRegistry::getInstance()->getSpawnRange($entityTypeId) ?? AxisAlignedBB::one()->expand($spawnRange * 2 + 1, 8, $spawnRange * 2 + 1);
$spawnRangeBB->offset($position->x, $position->y, $position->z);
foreach($world->getNearbyEntities($spawnRangeBB) as $entity){
if($entity::getNetworkTypeId() === $entityTypeId){
$count++;
if($count >= $spawnerTile->getMaxNearbyEntities()){
return true;
}
}
}
if(SpawnerAttemptSpawnEvent::hasHandlers()){
$ev = new SpawnerAttemptSpawnEvent($spawnerTile->getBlock(), $entityTypeId);
$ev->call();
if($ev->isCancelled()){
return true;
}
$entityTypeId = $ev->getEntityType();
}
// TODO: spawn condition check (light level etc.)
for($i = 0; $i < $spawnerTile->getSpawnPerAttempt(); $i++){
$spawnLocation = $position->add(mt_rand(-$spawnRange, $spawnRange), 0, mt_rand(-$spawnRange, $spawnRange));
$spawnLocation = Location::fromObject($spawnLocation, $world);
$nbt = CompoundTag::create()
->setString(EntityFactory::TAG_IDENTIFIER, $entityTypeId)
->setTag(Entity::TAG_POS, new ListTag([
new DoubleTag($spawnLocation->x),
new DoubleTag($spawnLocation->y),
new DoubleTag($spawnLocation->z)
]))
->setTag(Entity::TAG_ROTATION, new ListTag([
new FloatTag($spawnLocation->yaw),
new FloatTag($spawnLocation->pitch)
]));
// TODO: spawnData, spawnPotentials
$entity = EntityFactory::getInstance()->createFromData($world, $nbt);
if($entity !== null){
$entity->spawnToAll();
$world->addParticle($spawnLocation, new MobSpawnParticle((int) $entity->getSize()->getWidth(), (int) $entity->getSize()->getHeight()));
$count++;
if($count >= $spawnerTile->getMaxNearbyEntities()){
break;
}
}
}
$spawnerTile->setSpawnDelay(mt_rand($spawnerTile->getMinSpawnDelay(), $spawnerTile->getMaxSpawnDelay()));
return true;
}
}
76 changes: 73 additions & 3 deletions src/block/tile/MonsterSpawner.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,11 @@ class MonsterSpawner extends Spawnable{
public const DEFAULT_SPAWN_RANGE = 4; //blocks
public const DEFAULT_REQUIRED_PLAYER_RANGE = 16;

public const DEFAULT_ENTITY_TYPE_ID = "";
public const DEFAULT_LEGACY_ENTITY_TYPE_ID = ":";

/** TODO: replace this with a cached entity or something of that nature */
private string $entityTypeId = ":";
private string $entityTypeId = self::DEFAULT_ENTITY_TYPE_ID;
/** TODO: deserialize this properly and drop the NBT (PC and PE formats are different, just for fun) */
private ?ListTag $spawnPotentials = null;
/** TODO: deserialize this properly and drop the NBT (PC and PE formats are different, just for fun) */
Expand All @@ -78,11 +81,14 @@ class MonsterSpawner extends Spawnable{
public function readSaveData(CompoundTag $nbt) : void{
if(($legacyIdTag = $nbt->getTag(self::TAG_LEGACY_ENTITY_TYPE_ID)) instanceof IntTag){
//TODO: this will cause unexpected results when there's no mapping for the entity
$this->entityTypeId = LegacyEntityIdToStringIdMap::getInstance()->legacyToString($legacyIdTag->getValue()) ?? ":";
$this->entityTypeId = LegacyEntityIdToStringIdMap::getInstance()->legacyToString($legacyIdTag->getValue()) ?? self::DEFAULT_ENTITY_TYPE_ID;
}elseif(($idTag = $nbt->getTag(self::TAG_ENTITY_TYPE_ID)) instanceof StringTag){
$this->entityTypeId = $idTag->getValue();
if($this->entityTypeId === self::DEFAULT_LEGACY_ENTITY_TYPE_ID){
$this->entityTypeId = self::DEFAULT_ENTITY_TYPE_ID;
}
}else{
$this->entityTypeId = ":"; //default - TODO: replace this with a constant
$this->entityTypeId = self::DEFAULT_ENTITY_TYPE_ID;
}

$this->spawnData = $nbt->getCompoundTag(self::TAG_SPAWN_DATA);
Expand Down Expand Up @@ -130,4 +136,68 @@ protected function addAdditionalSpawnData(CompoundTag $nbt) : void{

$nbt->setFloat(self::TAG_ENTITY_SCALE, $this->displayEntityScale);
}

public function getEntityTypeId() : string{
return $this->entityTypeId;
}

public function setEntityTypeId(string $entityTypeId) : void{
$this->entityTypeId = $entityTypeId;
}

public function getSpawnDelay() : int{
return $this->spawnDelay;
}

public function setSpawnDelay(int $spawnDelay) : void{
$this->spawnDelay = $spawnDelay;
}

public function getMinSpawnDelay() : int{
return $this->minSpawnDelay;
}

public function setMinSpawnDelay(int $minSpawnDelay) : void{
$this->minSpawnDelay = $minSpawnDelay;
}

public function getMaxSpawnDelay() : int{
return $this->maxSpawnDelay;
}

public function setMaxSpawnDelay(int $maxSpawnDelay) : void{
$this->maxSpawnDelay = $maxSpawnDelay;
}

public function getRequiredPlayerRange() : int{
return $this->requiredPlayerRange;
}

public function setRequiredPlayerRange(int $requiredPlayerRange) : void{
$this->requiredPlayerRange = $requiredPlayerRange;
}

public function getSpawnRange() : int{
return $this->spawnRange;
}

public function setSpawnRange(int $spawnRange) : void{
$this->spawnRange = $spawnRange;
}

public function getSpawnPerAttempt() : int{
return $this->spawnPerAttempt;
}

public function setSpawnPerAttempt(int $spawnPerAttempt) : void{
$this->spawnPerAttempt = $spawnPerAttempt;
}

public function getMaxNearbyEntities() : int{
return $this->maxNearbyEntities;
}

public function setMaxNearbyEntities(int $maxNearbyEntities) : void{
$this->maxNearbyEntities = $maxNearbyEntities;
}
}
45 changes: 45 additions & 0 deletions src/block/tile/SpawnerSpawnRangeRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?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\block\tile;

use pocketmine\math\AxisAlignedBB;
use pocketmine\utils\SingletonTrait;

final class SpawnerSpawnRangeRegistry{
use SingletonTrait;

/**
* @phpstan-var array<string, AxisAlignedBB>
* @var AxisAlignedBB[]
*/
private array $spawnRange;
diamond-gold marked this conversation as resolved.
Show resolved Hide resolved

public function register(string $entitySaveId, AxisAlignedBB $spawnRange) : void{
$this->spawnRange[$entitySaveId] = $spawnRange;
}
diamond-gold marked this conversation as resolved.
Show resolved Hide resolved

public function getSpawnRange(string $entitySaveId) : ?AxisAlignedBB{
return (clone $this->spawnRange[$entitySaveId]) ?? null;
diamond-gold marked this conversation as resolved.
Show resolved Hide resolved
}
}
44 changes: 44 additions & 0 deletions src/event/block/SpawnerAttemptSpawnEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?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\event\block;

use pocketmine\block\Block;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;

class SpawnerAttemptSpawnEvent extends BlockEvent implements Cancellable{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be possible to change more values? The size, the number? Is it useful ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense to allow changing those in the event, those can be changed on the spawner block

use CancellableTrait;

public function __construct(Block $block, private string $entityType){
parent::__construct($block);
}

public function getEntityType() : string{
return $this->entityType;
}

public function setEntityType(string $entityType) : void{
$this->entityType = $entityType;
}
}
4 changes: 4 additions & 0 deletions src/item/SpawnEgg.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
namespace pocketmine\item;

use pocketmine\block\Block;
use pocketmine\block\MonsterSpawner;
use pocketmine\entity\Entity;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
Expand All @@ -35,6 +36,9 @@ abstract class SpawnEgg extends Item{
abstract protected function createEntity(World $world, Vector3 $pos, float $yaw, float $pitch) : Entity;

public function onInteractBlock(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, array &$returnedItems) : ItemUseResult{
if($blockClicked instanceof MonsterSpawner){
return ItemUseResult::NONE;
}
$entity = $this->createEntity($player->getWorld(), $blockReplace->getPosition()->add(0.5, 0, 0.5), lcg_value() * 360, 0);

if($this->hasCustomName()){
Expand Down
57 changes: 57 additions & 0 deletions src/item/SpawnEggEntityRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?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\network\mcpe\protocol\types\entity\EntityIds;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;

final class SpawnEggEntityRegistry{
use SingletonTrait;

/**
* @phpstan-var array<string, SpawnEgg>
* @var SpawnEgg[]
*/
private array $entityMap = [];

private function __construct(){
$this->register(EntityIds::SQUID, VanillaItems::SQUID_SPAWN_EGG());
$this->register(EntityIds::VILLAGER, VanillaItems::VILLAGER_SPAWN_EGG());
$this->register(EntityIds::ZOMBIE, VanillaItems::ZOMBIE_SPAWN_EGG());
}

public function register(string $entitySaveId, SpawnEgg $spawnEgg) : void{
$this->entityMap[$entitySaveId] = $spawnEgg;
}
diamond-gold marked this conversation as resolved.
Show resolved Hide resolved

public function getEntityId(SpawnEgg $item) : ?string{
foreach(Utils::stringifyKeys($this->entityMap) as $entitySaveId => $spawnEgg){
ShockedPlot7560 marked this conversation as resolved.
Show resolved Hide resolved
if($spawnEgg->equals($item, false, false)){
return $entitySaveId;
}
}
return null;
}
}