Implemented Paintings (#2073)

This supports vanilla placement of paintings, with overlap and collision checking.
Paintings are removed when a block is placed inside them or if any of their supporting blocks are removed.

As per vanilla, a random painting is chosen from the largest subset that will fit into the given space.
This commit is contained in:
Dylan K. Taylor 2018-03-07 09:03:30 +00:00 committed by GitHub
parent c5336776a5
commit c7f8796136
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 538 additions and 95 deletions

View File

@ -30,6 +30,8 @@ use pocketmine\block\Block;
use pocketmine\block\BlockFactory;
use pocketmine\block\Water;
use pocketmine\entity\object\ExperienceOrb;
use pocketmine\entity\object\Painting;
use pocketmine\entity\object\PaintingMotive;
use pocketmine\entity\projectile\Arrow;
use pocketmine\entity\projectile\Egg;
use pocketmine\entity\projectile\Snowball;
@ -231,6 +233,7 @@ abstract class Entity extends Location implements Metadatable, EntityIds{
Entity::registerEntity(ExperienceOrb::class, false, ['XPOrb', 'minecraft:xp_orb']);
Entity::registerEntity(FallingSand::class, false, ['FallingSand', 'minecraft:falling_block']);
Entity::registerEntity(Item::class, false, ['Item', 'minecraft:item']);
Entity::registerEntity(Painting::class, false, ['Painting', 'minecraft:painting']);
Entity::registerEntity(PrimedTNT::class, false, ['PrimedTnt', 'PrimedTNT', 'minecraft:tnt']);
Entity::registerEntity(Snowball::class, false, ['Snowball', 'minecraft:snowball']);
Entity::registerEntity(Squid::class, false, ['Squid', 'minecraft:squid']);
@ -241,6 +244,7 @@ abstract class Entity extends Location implements Metadatable, EntityIds{
Attribute::init();
Effect::init();
PaintingMotive::init();
}

View File

@ -0,0 +1,301 @@
<?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\object;
use pocketmine\entity\Entity;
use pocketmine\event\entity\EntityDamageByEntityEvent;
use pocketmine\item\Item;
use pocketmine\item\ItemFactory;
use pocketmine\level\Level;
use pocketmine\level\particle\DestroyParticle;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\ByteTag;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\network\mcpe\protocol\AddPaintingPacket;
use pocketmine\Player;
class Painting extends Entity{
public const NETWORK_ID = self::PAINTING;
/** @var float */
protected $gravity = 0.0;
/** @var float */
protected $drag = 1.0;
/** @var Vector3 */
protected $blockIn;
/** @var int */
protected $direction = 0;
/** @var string */
protected $motive;
/** @var int */
protected $checkDestroyedTicker = 0;
public function __construct(Level $level, CompoundTag $nbt){
$this->motive = $nbt->getString("Motive");
$this->blockIn = new Vector3($nbt->getInt("TileX"), $nbt->getInt("TileY"), $nbt->getInt("TileZ"));
if($nbt->hasTag("Direction", ByteTag::class)){
$this->direction = $nbt->getByte("Direction");
}elseif($nbt->hasTag("Facing", ByteTag::class)){
$this->direction = $nbt->getByte("Facing");
}
parent::__construct($level, $nbt);
}
protected function initEntity(){
$this->setMaxHealth(1);
$this->setHealth(1);
parent::initEntity();
}
public function saveNBT(){
parent::saveNBT();
$this->namedtag->setInt("TileX", (int) $this->blockIn->x);
$this->namedtag->setInt("TileY", (int) $this->blockIn->y);
$this->namedtag->setInt("TileZ", (int) $this->blockIn->z);
$this->namedtag->setByte("Facing", (int) $this->direction);
$this->namedtag->setByte("Direction", (int) $this->direction); //Save both for full compatibility
}
public function entityBaseTick(int $tickDiff = 1) : bool{
static $directions = [
0 => Vector3::SIDE_SOUTH,
1 => Vector3::SIDE_WEST,
2 => Vector3::SIDE_NORTH,
3 => Vector3::SIDE_EAST
];
$hasUpdate = parent::entityBaseTick($tickDiff);
if($this->checkDestroyedTicker++ > 10){
/*
* we don't have a way to only update on local block updates yet! since random chunk ticking always updates
* all the things
* ugly hack, but vanilla uses 100 ticks so on there it looks even worse
*/
$this->checkDestroyedTicker = 0;
$face = $directions[$this->direction];
if(!self::canFit($this->level, $this->blockIn->getSide($face), $face, false, $this->getMotive())){
$this->kill();
$hasUpdate = true;
}
}
return $hasUpdate; //doesn't need to be ticked always
}
public function kill(){
parent::kill();
$drops = true;
if($this->lastDamageCause instanceof EntityDamageByEntityEvent){
$killer = $this->lastDamageCause->getDamager();
if($killer instanceof Player and $killer->isCreative()){
$drops = false;
}
}
if($drops){
//non-living entities don't have a way to create drops generically yet
$this->level->dropItem($this, ItemFactory::get(Item::PAINTING));
}
$this->level->addParticle(new DestroyParticle($this->add(0.5, 0.5, 0.5), Item::PAINTING));
}
protected function recalculateBoundingBox() : void{
static $directions = [
0 => Vector3::SIDE_SOUTH,
1 => Vector3::SIDE_WEST,
2 => Vector3::SIDE_NORTH,
3 => Vector3::SIDE_EAST
];
$facing = $directions[$this->direction];
$this->boundingBox->setBB(self::getPaintingBB($this->blockIn->getSide($facing), $facing, $this->getMotive()));
}
protected function tryChangeMovement(){
$this->motionX = $this->motionY = $this->motionZ = 0;
}
protected function updateMovement(bool $teleport = false){
}
public function canBeCollidedWith() : bool{
return false;
}
protected function sendSpawnPacket(Player $player) : void{
$pk = new AddPaintingPacket();
$pk->entityRuntimeId = $this->getId();
$pk->x = $this->blockIn->x;
$pk->y = $this->blockIn->y;
$pk->z = $this->blockIn->z;
$pk->direction = $this->direction;
$pk->title = $this->motive;
$player->dataPacket($pk);
}
/**
* Returns the painting motive (which image is displayed on the painting)
* @return PaintingMotive
*/
public function getMotive() : PaintingMotive{
return PaintingMotive::getMotiveByName($this->motive);
}
public function getDirection() : int{
return $this->direction;
}
/**
* Returns the bounding-box a painting with the specified motive would have at the given position and direction.
*
* @param Vector3 $blockIn
* @param int $facing
* @param PaintingMotive $motive
*
* @return AxisAlignedBB
*/
private static function getPaintingBB(Vector3 $blockIn, int $facing, PaintingMotive $motive) : AxisAlignedBB{
$width = $motive->getWidth();
$height = $motive->getHeight();
$horizontalStart = (int) (ceil($width / 2) - 1);
$verticalStart = (int) (ceil($height / 2) - 1);
$thickness = 1 / 16;
$minX = $maxX = 0;
$minZ = $maxZ = 0;
$minY = -$verticalStart;
$maxY = $minY + $height;
switch($facing){
case Vector3::SIDE_NORTH:
$minZ = 1 - $thickness;
$maxZ = 1;
$maxX = $horizontalStart + 1;
$minX = $maxX - $width;
break;
case Vector3::SIDE_SOUTH:
$minZ = 0;
$maxZ = $thickness;
$minX = -$horizontalStart;
$maxX = $minX + $width;
break;
case Vector3::SIDE_WEST:
$minX = 1 - $thickness;
$maxX = 1;
$minZ = -$horizontalStart;
$maxZ = $minZ + $width;
break;
case Vector3::SIDE_EAST:
$minX = 0;
$maxX = $thickness;
$maxZ = $horizontalStart + 1;
$minZ = $maxZ - $width;
break;
}
return new AxisAlignedBB(
$blockIn->x + $minX,
$blockIn->y + $minY,
$blockIn->z + $minZ,
$blockIn->x + $maxX,
$blockIn->y + $maxY,
$blockIn->z + $maxZ
);
}
/**
* Returns whether a painting with the specified motive can be placed at the given position.
*
* @param Level $level
* @param Vector3 $blockIn
* @param int $facing
* @param bool $checkOverlap
* @param PaintingMotive $motive
*
* @return bool
*/
public static function canFit(Level $level, Vector3 $blockIn, int $facing, bool $checkOverlap, PaintingMotive $motive) : bool{
$width = $motive->getWidth();
$height = $motive->getHeight();
$horizontalStart = (int) (ceil($width / 2) - 1);
$verticalStart = (int) (ceil($height / 2) - 1);
switch($facing){
case Vector3::SIDE_NORTH:
$rotatedFace = Vector3::SIDE_WEST;
break;
case Vector3::SIDE_WEST:
$rotatedFace = Vector3::SIDE_SOUTH;
break;
case Vector3::SIDE_SOUTH:
$rotatedFace = Vector3::SIDE_EAST;
break;
case Vector3::SIDE_EAST:
$rotatedFace = Vector3::SIDE_NORTH;
break;
default:
return false;
}
$oppositeSide = Vector3::getOppositeSide($facing);
$startPos = $blockIn->asVector3()->getSide(Vector3::getOppositeSide($rotatedFace), $horizontalStart)->getSide(Vector3::SIDE_DOWN, $verticalStart);
for($w = 0; $w < $width; ++$w){
for($h = 0; $h < $height; ++$h){
$pos = $startPos->getSide($rotatedFace, $w)->getSide(Vector3::SIDE_UP, $h);
$block = $level->getBlockAt($pos->x, $pos->y, $pos->z);
if($block->isSolid() or !$block->getSide($oppositeSide)->isSolid()){
return false;
}
}
}
if($checkOverlap){
$bb = self::getPaintingBB($blockIn, $facing, $motive);
foreach($level->getNearbyEntities($bb) as $entity){
if($entity instanceof self){
return false;
}
}
}
return true;
}
}

View File

@ -0,0 +1,127 @@
<?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\object;
class PaintingMotive{
/** @var PaintingMotive[] */
protected static $motives = [];
public static function init() : void{
foreach([
new PaintingMotive(1, 1, "Alban"),
new PaintingMotive(1, 1, "Aztec"),
new PaintingMotive(1, 1, "Aztec2"),
new PaintingMotive(1, 1, "Bomb"),
new PaintingMotive(1, 1, "Kebab"),
new PaintingMotive(1, 1, "Plant"),
new PaintingMotive(1, 1, "Wasteland"),
new PaintingMotive(1, 2, "Graham"),
new PaintingMotive(1, 2, "Wanderer"),
new PaintingMotive(2, 1, "Courbet"),
new PaintingMotive(2, 1, "Creebet"),
new PaintingMotive(2, 1, "Pool"),
new PaintingMotive(2, 1, "Sea"),
new PaintingMotive(2, 1, "Sunset"),
new PaintingMotive(2, 2, "Bust"),
new PaintingMotive(2, 2, "Earth"),
new PaintingMotive(2, 2, "Fire"),
new PaintingMotive(2, 2, "Match"),
new PaintingMotive(2, 2, "SkullAndRoses"),
new PaintingMotive(2, 2, "Stage"),
new PaintingMotive(2, 2, "Void"),
new PaintingMotive(2, 2, "Water"),
new PaintingMotive(2, 2, "Wind"),
new PaintingMotive(2, 2, "Wither"),
new PaintingMotive(4, 2, "Fighters"),
new PaintingMotive(4, 3, "DonkeyKong"),
new PaintingMotive(4, 3, "Skeleton"),
new PaintingMotive(4, 4, "BurningSkull"),
new PaintingMotive(4, 4, "Pigscene"),
new PaintingMotive(4, 4, "Pointer")
] as $motive){
self::registerMotive($motive);
}
}
/**
* @param PaintingMotive $motive
*/
public static function registerMotive(PaintingMotive $motive) : void{
self::$motives[$motive->getName()] = $motive;
}
/**
* @param string $name
* @return PaintingMotive|null
*/
public static function getMotiveByName(string $name) : ?PaintingMotive{
return self::$motives[$name] ?? null;
}
/**
* @return PaintingMotive[]
*/
public static function getAll() : array{
return self::$motives;
}
/** @var string */
protected $name;
/** @var int */
protected $width;
/** @var int */
protected $height;
public function __construct(int $width, int $height, string $name){
$this->name = $name;
$this->width = $width;
$this->height = $height;
}
/**
* @return string
*/
public function getName() : string{
return $this->name;
}
/**
* @return int
*/
public function getWidth() : int{
return $this->width;
}
/**
* @return int
*/
public function getHeight() : int{
return $this->height;
}
public function __toString() : string{
return "PaintingMotive(name: " . $this->getName() . ", height: " . $this->getHeight() . ", width: " . $this->getWidth() . ")";
}
}

View File

@ -105,7 +105,7 @@ class ItemFactory{
self::registerItem(new Item(Item::FLINT, 0, "Flint"));
self::registerItem(new RawPorkchop());
self::registerItem(new CookedPorkchop());
self::registerItem(new Painting());
self::registerItem(new PaintingItem());
self::registerItem(new GoldenApple());
self::registerItem(new Sign());
self::registerItem(new ItemBlock(Block::OAK_DOOR_BLOCK, 0, Item::OAK_DOOR));

View File

@ -1,94 +0,0 @@
<?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\math\Vector3;
use pocketmine\Player;
class Painting extends Item{
public function __construct(int $meta = 0){
parent::__construct(self::PAINTING, $meta, "Painting");
}
public function onActivate(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector) : bool{
if($blockClicked->isTransparent() === false and $face > 1 and $blockReplace->isSolid() === false){
$faces = [
2 => 1,
3 => 3,
4 => 0,
5 => 2,
];
$motives = [
// Motive Width Height
["Kebab", 1, 1],
["Aztec", 1, 1],
["Alban", 1, 1],
["Aztec2", 1, 1],
["Bomb", 1, 1],
["Plant", 1, 1],
["Wasteland", 1, 1],
["Wanderer", 1, 2],
["Graham", 1, 2],
["Pool", 2, 1],
["Courbet", 2, 1],
["Sunset", 2, 1],
["Sea", 2, 1],
["Creebet", 2, 1],
["Match", 2, 2],
["Bust", 2, 2],
["Stage", 2, 2],
["Void", 2, 2],
["SkullAndRoses", 2, 2],
//array("Wither", 2, 2),
["Fighters", 4, 2],
["Skeleton", 4, 3],
["DonkeyKong", 4, 3],
["Pointer", 4, 4],
["Pigscene", 4, 4],
["Flaming Skull", 4, 4],
];
$motive = $motives[mt_rand(0, count($motives) - 1)];
$data = [
"x" => $blockClicked->x,
"y" => $blockClicked->y,
"z" => $blockClicked->z,
"yaw" => $faces[$face] * 90,
"Motive" => $motive[0],
];
//TODO
//$e = $server->api->entity->add($level, ENTITY_OBJECT, OBJECT_PAINTING, $data);
//$e->spawnToAll();
/*if(($player->gamemode & 0x01) === 0x00){
$player->removeItem(Item::get($this->getId(), $this->getDamage(), 1));
}*/
return true;
}
return false;
}
}

View File

@ -0,0 +1,105 @@
<?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\object\Painting;
use pocketmine\entity\object\PaintingMotive;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
use pocketmine\Player;
class PaintingItem extends Item{
public function __construct(int $meta = 0){
parent::__construct(self::PAINTING, $meta, "Painting");
}
public function onActivate(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector) : bool{
if(!$blockClicked->isTransparent() and $face > 1 and !$blockReplace->isSolid()){
/** @var PaintingMotive[] $motives */
$motives = [];
$totalDimension = 0;
foreach(PaintingMotive::getAll() as $motive){
$currentTotalDimension = $motive->getHeight() + $motive->getWidth();
if($currentTotalDimension < $totalDimension){
continue;
}
if(Painting::canFit($player->level, $blockReplace, $face, true, $motive)){
if($currentTotalDimension > $totalDimension){
$totalDimension = $currentTotalDimension;
/*
* This drops all motive possibilities smaller than this
* We use the total of height + width to allow equal chance of horizontal/vertical paintings
* when there is an L-shape of space available.
*/
$motives = [];
}
$motives[] = $motive;
}
}
if(empty($motives)){ //No space available
return false;
}
/** @var PaintingMotive $motive */
$motive = $motives[array_rand($motives)];
static $directions = [
Vector3::SIDE_SOUTH => 0,
Vector3::SIDE_WEST => 1,
Vector3::SIDE_NORTH => 2,
Vector3::SIDE_EAST => 3
];
$direction = $directions[$face] ?? -1;
if($direction === -1){
return false;
}
$nbt = Entity::createBaseNBT($blockReplace, null, $direction * 90, 0);
$nbt->setByte("Direction", $direction);
$nbt->setString("Motive", $motive->getName());
$nbt->setInt("TileX", $blockClicked->getFloorX());
$nbt->setInt("TileY", $blockClicked->getFloorY());
$nbt->setInt("TileZ", $blockClicked->getFloorZ());
$entity = Entity::createEntity("Painting", $blockReplace->getLevel(), $nbt);
if($entity instanceof Entity){
--$this->count;
$entity->spawnToAll();
$player->getLevel()->broadcastLevelEvent($blockReplace->add(0.5, 0.5, 0.5), LevelEventPacket::EVENT_SOUND_ITEMFRAME_PLACE); //item frame and painting have the same sound
return true;
}
}
return false;
}
}