Extract a HungerManager unit from Human

This commit is contained in:
Dylan K. Taylor 2019-07-06 18:50:34 +01:00
parent 119cb083bf
commit da0358529a
6 changed files with 300 additions and 201 deletions

View File

@ -150,6 +150,7 @@ This version features substantial changes to the network system, improving coher
- The following API methods have been added:
- `ItemEntity->getDespawnDelay()`
- `ItemEntity->setDespawnDelay()`
- `Human->getHungerManager()`
- The following methods have signature changes:
- `Entity->entityBaseTick()` is now `protected`.
- `Entity->move()` is now `protected`.
@ -157,6 +158,7 @@ This version features substantial changes to the network system, improving coher
- `Living->getEffects()` now returns `EffectManager` instead of `Effect[]`.
- The following classes have been added:
- `effect\EffectManager`: contains effect-management functionality extracted from `Living`
- `HungerManager`: contains hunger-management functionality extracted from `Human`
- The following API methods have been moved / renamed:
- `Living->removeAllEffects()` -> `EffectManager->clear()`
- `Living->removeEffect()` -> `EffectManager->remove()`
@ -165,6 +167,17 @@ This version features substantial changes to the network system, improving coher
- `Living->hasEffect()` -> `EffectManager->has()`
- `Living->hasEffects()` -> `EffectManager->hasEffects()`
- `Living->getEffects()` -> `EffectManager->all()`
- `Human->getFood()` -> `HungerManager->getFood()`
- `Human->setFood()` -> `HungerManager->setFood()`
- `Human->getMaxFood()` -> `HungerManager->getMaxFood()`
- `Human->addFood()` -> `HungerManager->addFood()`
- `Human->isHungry()` -> `HungerManager->isHungry()`
- `Human->getSaturation()` -> `HungerManager->getSaturation()`
- `Human->setSaturation()` -> `HungerManager->setSaturation()`
- `Human->addSaturation()` -> `HungerManager->addSaturation()`
- `Human->getExhaustion()` -> `HungerManager->getExhaustion()`
- `Human->setExhaustion()` -> `HungerManager->setExhaustion()`
- `Human->exhaust()` -> `HungerManager->exhaust()`
- The following API methods have been removed:
- `Human->getRawUniqueId()`: use `Human->getUniqueId()->toBinary()` instead
- The following classes have been removed:

View File

@ -28,7 +28,6 @@ use pocketmine\entity\effect\EffectInstance;
use pocketmine\entity\projectile\ProjectileSource;
use pocketmine\entity\utils\ExperienceUtils;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\event\entity\EntityRegainHealthEvent;
use pocketmine\event\player\PlayerExhaustEvent;
use pocketmine\event\player\PlayerExperienceChangeEvent;
use pocketmine\inventory\EnderChestInventory;
@ -65,7 +64,6 @@ use function array_rand;
use function array_values;
use function ceil;
use function in_array;
use function max;
use function min;
use function random_int;
use function strlen;
@ -90,7 +88,8 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
/** @var Skin */
protected $skin;
protected $foodTickTimer = 0;
/** @var HungerManager */
protected $hungerManager;
protected $totalXp = 0;
protected $xpSeed;
@ -170,139 +169,27 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
public function jump() : void{
parent::jump();
if($this->isSprinting()){
$this->exhaust(0.8, PlayerExhaustEvent::CAUSE_SPRINT_JUMPING);
$this->hungerManager->exhaust(0.8, PlayerExhaustEvent::CAUSE_SPRINT_JUMPING);
}else{
$this->exhaust(0.2, PlayerExhaustEvent::CAUSE_JUMPING);
$this->hungerManager->exhaust(0.2, PlayerExhaustEvent::CAUSE_JUMPING);
}
}
public function getFood() : float{
return $this->attributeMap->getAttribute(Attribute::HUNGER)->getValue();
}
/**
* WARNING: This method does not check if full and may throw an exception if out of bounds.
* Use {@link Human::addFood()} for this purpose
*
* @param float $new
*
* @throws \InvalidArgumentException
* @return HungerManager
*/
public function setFood(float $new) : void{
$attr = $this->attributeMap->getAttribute(Attribute::HUNGER);
$old = $attr->getValue();
$attr->setValue($new);
// ranges: 18-20 (regen), 7-17 (none), 1-6 (no sprint), 0 (health depletion)
foreach([17, 6, 0] as $bound){
if(($old > $bound) !== ($new > $bound)){
$this->foodTickTimer = 0;
break;
}
}
}
public function getMaxFood() : float{
return $this->attributeMap->getAttribute(Attribute::HUNGER)->getMaxValue();
}
public function addFood(float $amount) : void{
$attr = $this->attributeMap->getAttribute(Attribute::HUNGER);
$amount += $attr->getValue();
$amount = max(min($amount, $attr->getMaxValue()), $attr->getMinValue());
$this->setFood($amount);
}
/**
* Returns whether this Human may consume objects requiring hunger.
*
* @return bool
*/
public function isHungry() : bool{
return $this->getFood() < $this->getMaxFood();
}
public function getSaturation() : float{
return $this->attributeMap->getAttribute(Attribute::SATURATION)->getValue();
}
/**
* WARNING: This method does not check if saturated and may throw an exception if out of bounds.
* Use {@link Human::addSaturation()} for this purpose
*
* @param float $saturation
*
* @throws \InvalidArgumentException
*/
public function setSaturation(float $saturation) : void{
$this->attributeMap->getAttribute(Attribute::SATURATION)->setValue($saturation);
}
public function addSaturation(float $amount) : void{
$attr = $this->attributeMap->getAttribute(Attribute::SATURATION);
$attr->setValue($attr->getValue() + $amount, true);
}
public function getExhaustion() : float{
return $this->attributeMap->getAttribute(Attribute::EXHAUSTION)->getValue();
}
/**
* WARNING: This method does not check if exhausted and does not consume saturation/food.
* Use {@link Human::exhaust()} for this purpose.
*
* @param float $exhaustion
*/
public function setExhaustion(float $exhaustion) : void{
$this->attributeMap->getAttribute(Attribute::EXHAUSTION)->setValue($exhaustion);
}
/**
* Increases a human's exhaustion level.
*
* @param float $amount
* @param int $cause
*
* @return float the amount of exhaustion level increased
*/
public function exhaust(float $amount, int $cause = PlayerExhaustEvent::CAUSE_CUSTOM) : float{
$ev = new PlayerExhaustEvent($this, $amount, $cause);
$ev->call();
if($ev->isCancelled()){
return 0.0;
}
$exhaustion = $this->getExhaustion();
$exhaustion += $ev->getAmount();
while($exhaustion >= 4.0){
$exhaustion -= 4.0;
$saturation = $this->getSaturation();
if($saturation > 0){
$saturation = max(0, $saturation - 1.0);
$this->setSaturation($saturation);
}else{
$food = $this->getFood();
if($food > 0){
$food--;
$this->setFood(max($food, 0));
}
}
}
$this->setExhaustion($exhaustion);
return $ev->getAmount();
public function getHungerManager() : HungerManager{
return $this->hungerManager;
}
public function consumeObject(Consumable $consumable) : bool{
if($consumable instanceof FoodSource){
if($consumable->requiresHunger() and !$this->isHungry()){
if($consumable->requiresHunger() and !$this->hungerManager->isHungry()){
return false;
}
$this->addFood($consumable->getFoodRestore());
$this->addSaturation($consumable->getSaturationRestore());
$this->hungerManager->addFood($consumable->getFoodRestore());
$this->hungerManager->addSaturation($consumable->getSaturationRestore());
}
return parent::consumeObject($consumable);
@ -587,6 +474,8 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
protected function initEntity(CompoundTag $nbt) : void{
parent::initEntity($nbt);
$this->hungerManager = new HungerManager($this);
$this->setPlayerFlag(PlayerMetadataFlags::SLEEP, false);
$this->propertyManager->setBlockPos(EntityMetadataProperties::PLAYER_BED_POSITION, null);
@ -624,10 +513,10 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
$this->inventory->setHeldItemIndex($nbt->getInt("SelectedInventorySlot", 0), false);
$this->setFood((float) $nbt->getInt("foodLevel", (int) $this->getFood(), true));
$this->setExhaustion($nbt->getFloat("foodExhaustionLevel", $this->getExhaustion(), true));
$this->setSaturation($nbt->getFloat("foodSaturationLevel", $this->getSaturation(), true));
$this->foodTickTimer = $nbt->getInt("foodTickTimer", $this->foodTickTimer, true);
$this->hungerManager->setFood((float) $nbt->getInt("foodLevel", (int) $this->hungerManager->getFood(), true));
$this->hungerManager->setExhaustion($nbt->getFloat("foodExhaustionLevel", $this->hungerManager->getExhaustion(), true));
$this->hungerManager->setSaturation($nbt->getFloat("foodSaturationLevel", $this->hungerManager->getSaturation(), true));
$this->hungerManager->setFoodTickTimer($nbt->getInt("foodTickTimer", $this->hungerManager->getFoodTickTimer(), true));
$this->setXpLevel($nbt->getInt("XpLevel", $this->getXpLevel(), true));
$this->setXpProgress($nbt->getFloat("XpP", $this->getXpProgress(), true));
@ -643,9 +532,6 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
protected function addAttributes() : void{
parent::addAttributes();
$this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::SATURATION));
$this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::EXHAUSTION));
$this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::HUNGER));
$this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::EXPERIENCE_LEVEL));
$this->attributeMap->addAttribute(Attribute::getAttribute(Attribute::EXPERIENCE));
}
@ -653,7 +539,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
protected function entityBaseTick(int $tickDiff = 1) : bool{
$hasUpdate = parent::entityBaseTick($tickDiff);
$this->doFoodTick($tickDiff);
$this->hungerManager->tick($tickDiff);
if($this->xpCooldown > 0){
$this->xpCooldown--;
@ -662,46 +548,6 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
return $hasUpdate;
}
protected function doFoodTick(int $tickDiff = 1) : void{
if($this->isAlive()){
$food = $this->getFood();
$health = $this->getHealth();
$difficulty = $this->world->getDifficulty();
$this->foodTickTimer += $tickDiff;
if($this->foodTickTimer >= 80){
$this->foodTickTimer = 0;
}
if($difficulty === World::DIFFICULTY_PEACEFUL and $this->foodTickTimer % 10 === 0){
if($food < $this->getMaxFood()){
$this->addFood(1.0);
$food = $this->getFood();
}
if($this->foodTickTimer % 20 === 0 and $health < $this->getMaxHealth()){
$this->heal(new EntityRegainHealthEvent($this, 1, EntityRegainHealthEvent::CAUSE_SATURATION));
}
}
if($this->foodTickTimer === 0){
if($food >= 18){
if($health < $this->getMaxHealth()){
$this->heal(new EntityRegainHealthEvent($this, 1, EntityRegainHealthEvent::CAUSE_SATURATION));
$this->exhaust(3.0, PlayerExhaustEvent::CAUSE_HEALTH_REGEN);
}
}elseif($food <= 0){
if(($difficulty === World::DIFFICULTY_EASY and $health > 10) or ($difficulty === World::DIFFICULTY_NORMAL and $health > 1) or $difficulty === World::DIFFICULTY_HARD){
$this->attack(new EntityDamageEvent($this, EntityDamageEvent::CAUSE_STARVATION, 1));
}
}
}
if($food <= 6){
$this->setSprinting(false);
}
}
}
public function getName() : string{
return $this->getNameTag();
}
@ -751,10 +597,10 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
public function saveNBT() : CompoundTag{
$nbt = parent::saveNBT();
$nbt->setInt("foodLevel", (int) $this->getFood());
$nbt->setFloat("foodExhaustionLevel", $this->getExhaustion());
$nbt->setFloat("foodSaturationLevel", $this->getSaturation());
$nbt->setInt("foodTickTimer", $this->foodTickTimer);
$nbt->setInt("foodLevel", (int) $this->hungerManager->getFood());
$nbt->setFloat("foodExhaustionLevel", $this->hungerManager->getExhaustion());
$nbt->setFloat("foodSaturationLevel", $this->hungerManager->getSaturation());
$nbt->setInt("foodTickTimer", $this->hungerManager->getFoodTickTimer());
$nbt->setInt("XpLevel", $this->getXpLevel());
$nbt->setFloat("XpP", $this->getXpProgress());
@ -855,6 +701,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
protected function destroyCycles() : void{
$this->inventory = null;
$this->enderChestInventory = null;
$this->hungerManager = null;
parent::destroyCycles();
}

View File

@ -0,0 +1,254 @@
<?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;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\event\entity\EntityRegainHealthEvent;
use pocketmine\event\player\PlayerExhaustEvent;
use pocketmine\world\World;
use function max;
use function min;
class HungerManager{
/** @var Human */
private $entity;
/** @var Attribute */
private $hungerAttr;
/** @var Attribute */
private $saturationAttr;
/** @var Attribute */
private $exhaustionAttr;
/** @var int */
private $foodTickTimer = 0;
/** @var bool */
private $enabled = true;
public function __construct(Human $entity){
$this->entity = $entity;
$this->hungerAttr = self::fetchAttribute($entity, Attribute::HUNGER);
$this->saturationAttr = self::fetchAttribute($entity, Attribute::SATURATION);
$this->exhaustionAttr = self::fetchAttribute($entity, Attribute::EXHAUSTION);
}
private static function fetchAttribute(Entity $entity, string $attributeId) : Attribute{
$entity->getAttributeMap()->addAttribute(Attribute::getAttribute($attributeId));
return $entity->getAttributeMap()->getAttribute($attributeId);
}
public function getFood() : float{
return $this->hungerAttr->getValue();
}
/**
* WARNING: This method does not check if full and may throw an exception if out of bounds.
* @see HungerManager::addFood()
*
* @param float $new
*
* @throws \InvalidArgumentException
*/
public function setFood(float $new) : void{
$old = $this->hungerAttr->getValue();
$this->hungerAttr->setValue($new);
// ranges: 18-20 (regen), 7-17 (none), 1-6 (no sprint), 0 (health depletion)
foreach([17, 6, 0] as $bound){
if(($old > $bound) !== ($new > $bound)){
$this->foodTickTimer = 0;
break;
}
}
}
public function getMaxFood() : float{
return $this->hungerAttr->getMaxValue();
}
public function addFood(float $amount) : void{
$amount += $this->hungerAttr->getValue();
$amount = max(min($amount, $this->hungerAttr->getMaxValue()), $this->hungerAttr->getMinValue());
$this->setFood($amount);
}
/**
* Returns whether this Human may consume objects requiring hunger.
*
* @return bool
*/
public function isHungry() : bool{
return $this->getFood() < $this->getMaxFood();
}
public function getSaturation() : float{
return $this->saturationAttr->getValue();
}
/**
* WARNING: This method does not check if saturated and may throw an exception if out of bounds.
* @see HungerManager::addSaturation()
*
* @param float $saturation
*
* @throws \InvalidArgumentException
*/
public function setSaturation(float $saturation) : void{
$this->saturationAttr->setValue($saturation);
}
public function addSaturation(float $amount) : void{
$this->saturationAttr->setValue($this->saturationAttr->getValue() + $amount, true);
}
public function getExhaustion() : float{
return $this->exhaustionAttr->getValue();
}
/**
* WARNING: This method does not check if exhausted and does not consume saturation/food.
* @see HungerManager::exhaust()
*
* @param float $exhaustion
*/
public function setExhaustion(float $exhaustion) : void{
$this->exhaustionAttr->setValue($exhaustion);
}
/**
* Increases exhaustion level.
*
* @param float $amount
* @param int $cause
*
* @return float the amount of exhaustion level increased
*/
public function exhaust(float $amount, int $cause = PlayerExhaustEvent::CAUSE_CUSTOM) : float{
if(!$this->enabled){
return 0;
}
$ev = new PlayerExhaustEvent($this->entity, $amount, $cause);
$ev->call();
if($ev->isCancelled()){
return 0.0;
}
$exhaustion = $this->getExhaustion();
$exhaustion += $ev->getAmount();
while($exhaustion >= 4.0){
$exhaustion -= 4.0;
$saturation = $this->getSaturation();
if($saturation > 0){
$saturation = max(0, $saturation - 1.0);
$this->setSaturation($saturation);
}else{
$food = $this->getFood();
if($food > 0){
$food--;
$this->setFood(max($food, 0));
}
}
}
$this->setExhaustion($exhaustion);
return $ev->getAmount();
}
/**
* @return int
*/
public function getFoodTickTimer() : int{
return $this->foodTickTimer;
}
/**
* @param int $foodTickTimer
*/
public function setFoodTickTimer(int $foodTickTimer) : void{
if($foodTickTimer < 0){
throw new \InvalidArgumentException("Expected a non-negative value");
}
$this->foodTickTimer = $foodTickTimer;
}
public function tick(int $tickDiff = 1) : void{
if(!$this->entity->isAlive() or !$this->enabled){
return;
}
$food = $this->getFood();
$health = $this->entity->getHealth();
$difficulty = $this->entity->getWorld()->getDifficulty();
$this->foodTickTimer += $tickDiff;
if($this->foodTickTimer >= 80){
$this->foodTickTimer = 0;
}
if($difficulty === World::DIFFICULTY_PEACEFUL and $this->foodTickTimer % 10 === 0){
if($food < $this->getMaxFood()){
$this->addFood(1.0);
$food = $this->getFood();
}
if($this->foodTickTimer % 20 === 0 and $health < $this->entity->getMaxHealth()){
$this->entity->heal(new EntityRegainHealthEvent($this->entity, 1, EntityRegainHealthEvent::CAUSE_SATURATION));
}
}
if($this->foodTickTimer === 0){
if($food >= 18){
if($health < $this->entity->getMaxHealth()){
$this->entity->heal(new EntityRegainHealthEvent($this->entity, 1, EntityRegainHealthEvent::CAUSE_SATURATION));
$this->exhaust(3.0, PlayerExhaustEvent::CAUSE_HEALTH_REGEN);
}
}elseif($food <= 0){
if(($difficulty === World::DIFFICULTY_EASY and $health > 10) or ($difficulty === World::DIFFICULTY_NORMAL and $health > 1) or $difficulty === World::DIFFICULTY_HARD){
$this->entity->attack(new EntityDamageEvent($this->entity, EntityDamageEvent::CAUSE_STARVATION, 1));
}
}
}
if($food <= 6){
$this->entity->setSprinting(false);
}
}
/**
* @return bool
*/
public function isEnabled() : bool{
return $this->enabled;
}
/**
* @param bool $enabled
*/
public function setEnabled(bool $enabled) : void{
$this->enabled = $enabled;
}
}

View File

@ -36,7 +36,7 @@ class HungerEffect extends Effect{
public function applyEffect(Living $entity, EffectInstance $instance, float $potency = 1.0, ?Entity $source = null) : void{
if($entity instanceof Human){
$entity->exhaust(0.025 * $instance->getEffectLevel(), PlayerExhaustEvent::CAUSE_POTION);
$entity->getHungerManager()->exhaust(0.025 * $instance->getEffectLevel(), PlayerExhaustEvent::CAUSE_POTION);
}
}
}

View File

@ -31,8 +31,9 @@ class SaturationEffect extends InstantEffect{
public function applyEffect(Living $entity, EffectInstance $instance, float $potency = 1.0, ?Entity $source = null) : void{
if($entity instanceof Human){
$entity->addFood($instance->getEffectLevel());
$entity->addSaturation($instance->getEffectLevel() * 2);
$manager = $entity->getHungerManager();
$manager->addFood($instance->getEffectLevel());
$manager->addSaturation($instance->getEffectLevel() * 2);
}
}
}

View File

@ -1198,6 +1198,8 @@ class Player extends Human implements CommandSender, ChunkLoader, ChunkListener,
$this->gamemode = $gm;
$this->allowFlight = $this->isCreative();
$this->hungerManager->setEnabled($this->isSurvival());
if($this->isSpectator()){
$this->setFlying(true);
$this->despawnFromAll();
@ -1414,9 +1416,9 @@ class Player extends Human implements CommandSender, ChunkLoader, ChunkListener,
$distance = sqrt((($from->x - $to->x) ** 2) + (($from->z - $to->z) ** 2));
//TODO: check swimming (adds 0.015 exhaustion in MCPE)
if($this->isSprinting()){
$this->exhaust(0.1 * $distance, PlayerExhaustEvent::CAUSE_SPRINTING);
$this->hungerManager->exhaust(0.1 * $distance, PlayerExhaustEvent::CAUSE_SPRINTING);
}else{
$this->exhaust(0.01 * $distance, PlayerExhaustEvent::CAUSE_WALKING);
$this->hungerManager->exhaust(0.01 * $distance, PlayerExhaustEvent::CAUSE_WALKING);
}
}
}
@ -1510,24 +1512,6 @@ class Player extends Human implements CommandSender, ChunkLoader, ChunkListener,
return true;
}
protected function doFoodTick(int $tickDiff = 1) : void{
if($this->isSurvival()){
parent::doFoodTick($tickDiff);
}
}
public function exhaust(float $amount, int $cause = PlayerExhaustEvent::CAUSE_CUSTOM) : float{
if($this->isSurvival()){
return parent::exhaust($amount, $cause);
}
return 0.0;
}
public function isHungry() : bool{
return $this->isSurvival() and parent::isHungry();
}
public function canBreathe() : bool{
return $this->isCreative() or parent::canBreathe();
}
@ -1828,7 +1812,7 @@ class Player extends Human implements CommandSender, ChunkLoader, ChunkListener,
if($this->hasFiniteResources() and !$item->equalsExact($oldItem)){
$this->inventory->setItemInHand($item);
}
$this->exhaust(0.025, PlayerExhaustEvent::CAUSE_MINING);
$this->hungerManager->exhaust(0.025, PlayerExhaustEvent::CAUSE_MINING);
return true;
}
}
@ -1928,7 +1912,7 @@ class Player extends Human implements CommandSender, ChunkLoader, ChunkListener,
$this->inventory->setItemInHand($heldItem);
}
$this->exhaust(0.3, PlayerExhaustEvent::CAUSE_ATTACK);
$this->hungerManager->exhaust(0.3, PlayerExhaustEvent::CAUSE_ATTACK);
}
return true;
@ -2431,7 +2415,7 @@ class Player extends Human implements CommandSender, ChunkLoader, ChunkListener,
protected function applyPostDamageEffects(EntityDamageEvent $source) : void{
parent::applyPostDamageEffects($source);
$this->exhaust(0.3, PlayerExhaustEvent::CAUSE_DAMAGE);
$this->hungerManager->exhaust(0.3, PlayerExhaustEvent::CAUSE_DAMAGE);
}
public function attack(EntityDamageEvent $source) : void{