Moved the whole logic to a new file (TooltipManager.cpp), and rewrote most of the logic to profit from object oriented programming. The side-effect is that the changes are scattered across entities, tiles and items because moving everything to OOP requires to add the tooltip to every .cpp file for every tile/item/entities (or their class, on plants for example which all use the tooltip IDS_TOOLTIPS_PLANT)
The issue with the default tooltips loop written by 4J is that:
Its running in the main Minecraft::tick loop and is more than a thousand lines long, polluting the function.
Not using C++'s full potential with object oriented programming, 4J decided to use a big switch case (probably because it could run better on hardware without branch prediction like the WiiU or Xbox 360 ?)
And seems to be better off in its own file than in the main Minecraft.cpp file
810 lines
26 KiB
C++
810 lines
26 KiB
C++
#include "stdafx.h"
|
|
#include "com.mojang.nbt.h"
|
|
#include "net.minecraft.world.entity.ai.attributes.h"
|
|
#include "net.minecraft.world.entity.ai.goal.h"
|
|
#include "net.minecraft.world.entity.ai.navigation.h"
|
|
#include "net.minecraft.world.entity.ai.village.h"
|
|
#include "net.minecraft.world.entity.monster.h"
|
|
#include "net.minecraft.world.entity.player.h"
|
|
#include "net.minecraft.world.effect.h"
|
|
#include "net.minecraft.world.entity.h"
|
|
#include "net.minecraft.world.damagesource.h"
|
|
#include "net.minecraft.world.item.h"
|
|
#include "net.minecraft.world.item.enchantment.h"
|
|
#include "net.minecraft.world.item.trading.h"
|
|
#include "net.minecraft.world.level.tile.h"
|
|
#include "net.minecraft.world.level.h"
|
|
#include "../Minecraft.Client/Textures.h"
|
|
#include "Villager.h"
|
|
#include "AbstractContainerMenu.h"
|
|
|
|
unordered_map<int, pair<int,int> > Villager::MIN_MAX_VALUES;
|
|
unordered_map<int, pair<int,int> > Villager::MIN_MAX_PRICES;
|
|
|
|
void Villager::_init(int profession)
|
|
{
|
|
// 4J Stu - This function call had to be moved here from the Entity ctor to ensure that
|
|
// the derived version of the function is called
|
|
this->defineSynchedData();
|
|
registerAttributes();
|
|
setHealth(getMaxHealth());
|
|
|
|
setProfession(profession);
|
|
setSize(.6f, 1.8f);
|
|
|
|
villageUpdateInterval = 0;
|
|
inLove = false;
|
|
chasing = false;
|
|
village = weak_ptr<Village>();
|
|
|
|
tradingPlayer = weak_ptr<Player>();
|
|
offers = nullptr;
|
|
updateMerchantTimer = 0;
|
|
addRecipeOnUpdate = false;
|
|
riches = 0;
|
|
lastPlayerTradeName = L"";
|
|
rewardPlayersOnFirstVillage = false;
|
|
baseRecipeChanceMod = 0.0f;
|
|
|
|
getNavigation()->setCanOpenDoors(true);
|
|
getNavigation()->setAvoidWater(true);
|
|
|
|
goalSelector.addGoal(0, new FloatGoal(this));
|
|
goalSelector.addGoal(1, new AvoidPlayerGoal(this, typeid(Zombie), 8, 0.6, 0.6));
|
|
goalSelector.addGoal(1, new TradeWithPlayerGoal(this));
|
|
goalSelector.addGoal(1, new LookAtTradingPlayerGoal(this));
|
|
goalSelector.addGoal(2, new MoveIndoorsGoal(this));
|
|
goalSelector.addGoal(3, new RestrictOpenDoorGoal(this));
|
|
goalSelector.addGoal(4, new OpenDoorGoal(this, true));
|
|
goalSelector.addGoal(5, new MoveTowardsRestrictionGoal(this, 0.6));
|
|
goalSelector.addGoal(6, new MakeLoveGoal(this));
|
|
goalSelector.addGoal(7, new TakeFlowerGoal(this));
|
|
goalSelector.addGoal(8, new PlayGoal(this, 0.32));
|
|
goalSelector.addGoal(9, new InteractGoal(this, typeid(Player), 3, 1.f));
|
|
goalSelector.addGoal(9, new InteractGoal(this, typeid(Villager), 5, 0.02f));
|
|
goalSelector.addGoal(9, new RandomStrollGoal(this, 0.6));
|
|
goalSelector.addGoal(10, new LookAtPlayerGoal(this, typeid(Mob), 8));
|
|
}
|
|
|
|
Villager::Villager(Level *level) : AgableMob(level)
|
|
{
|
|
_init(0);
|
|
}
|
|
|
|
Villager::Villager(Level *level, int profession) : AgableMob(level)
|
|
{
|
|
_init(profession);
|
|
}
|
|
|
|
Villager::~Villager()
|
|
{
|
|
delete offers;
|
|
}
|
|
|
|
void Villager::registerAttributes()
|
|
{
|
|
AgableMob::registerAttributes();
|
|
|
|
getAttribute(SharedMonsterAttributes::MOVEMENT_SPEED)->setBaseValue(0.5f);
|
|
}
|
|
|
|
bool Villager::useNewAi()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
void Villager::serverAiMobStep()
|
|
{
|
|
if (--villageUpdateInterval <= 0)
|
|
{
|
|
level->villages->queryUpdateAround(Mth::floor(x), Mth::floor(y), Mth::floor(z));
|
|
villageUpdateInterval = 70 + random->nextInt(50);
|
|
|
|
shared_ptr<Village> _village = level->villages->getClosestVillage(Mth::floor(x), Mth::floor(y), Mth::floor(z), Villages::MaxDoorDist);
|
|
village = _village;
|
|
if (_village == nullptr) clearRestriction();
|
|
else
|
|
{
|
|
Pos *center = _village->getCenter();
|
|
restrictTo(center->x, center->y, center->z, static_cast<int>((float)_village->getRadius() * 0.6f));
|
|
if (rewardPlayersOnFirstVillage)
|
|
{
|
|
rewardPlayersOnFirstVillage = false;
|
|
_village->rewardAllPlayers(5);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isTrading() && updateMerchantTimer > 0)
|
|
{
|
|
updateMerchantTimer--;
|
|
if (updateMerchantTimer <= 0)
|
|
{
|
|
if (addRecipeOnUpdate)
|
|
{
|
|
// improve max uses for all obsolete recipes
|
|
if (offers->size() > 0)
|
|
{
|
|
//for (MerchantRecipe recipe : offers)
|
|
for(auto& recipe : *offers)
|
|
{
|
|
if (recipe->isDeprecated())
|
|
{
|
|
recipe->increaseMaxUses(random->nextInt(6) + random->nextInt(6) + 2);
|
|
}
|
|
}
|
|
}
|
|
addOffers(1);
|
|
addRecipeOnUpdate = false;
|
|
|
|
if (village.lock() != nullptr && !lastPlayerTradeName.empty())
|
|
{
|
|
level->broadcastEntityEvent(shared_from_this(), EntityEvent::VILLAGER_HAPPY);
|
|
village.lock()->modifyStanding(lastPlayerTradeName, 1);
|
|
}
|
|
}
|
|
addEffect(new MobEffectInstance(MobEffect::regeneration->id, SharedConstants::TICKS_PER_SECOND * 10, 0));
|
|
}
|
|
}
|
|
|
|
AgableMob::serverAiMobStep();
|
|
}
|
|
|
|
bool Villager::mobInteract(shared_ptr<Player> player)
|
|
{
|
|
// [EB]: Truly dislike this code but I don't see another easy way
|
|
shared_ptr<ItemInstance> item = player->inventory->getSelected();
|
|
bool holdingSpawnEgg = item != nullptr && item->id == Item::spawn_egg_Id;
|
|
|
|
if (!player->isSneaking() && !holdingSpawnEgg && isAlive() && !isTrading() && !isBaby())
|
|
{
|
|
if (!level->isClientSide)
|
|
{
|
|
// note: stop() logic is controlled by trading ai goal
|
|
setTradingPlayer(player);
|
|
|
|
// 4J-JEV: Villagers in PC game don't display professions.
|
|
player->openTrading(dynamic_pointer_cast<Merchant>(shared_from_this()), getDisplayName() );
|
|
}
|
|
return true;
|
|
}
|
|
return AgableMob::mobInteract(player);
|
|
}
|
|
|
|
void Villager::defineSynchedData()
|
|
{
|
|
AgableMob::defineSynchedData();
|
|
entityData->define(DATA_PROFESSION_ID, 0);
|
|
}
|
|
|
|
void Villager::addAdditonalSaveData(CompoundTag *tag)
|
|
{
|
|
AgableMob::addAdditonalSaveData(tag);
|
|
tag->putInt(L"Profession", getProfession());
|
|
tag->putInt(L"Riches", riches);
|
|
if (offers != nullptr)
|
|
{
|
|
tag->putCompound(L"Offers", offers->createTag());
|
|
}
|
|
}
|
|
|
|
void Villager::readAdditionalSaveData(CompoundTag *tag)
|
|
{
|
|
AgableMob::readAdditionalSaveData(tag);
|
|
setProfession(tag->getInt(L"Profession"));
|
|
riches = tag->getInt(L"Riches");
|
|
if (tag->contains(L"Offers"))
|
|
{
|
|
CompoundTag *compound = tag->getCompound(L"Offers");
|
|
delete offers;
|
|
offers = new MerchantRecipeList(compound);
|
|
}
|
|
}
|
|
|
|
bool Villager::removeWhenFarAway()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int Villager::getAmbientSound()
|
|
{
|
|
if(isTrading())
|
|
{
|
|
return eSoundType_MOB_VILLAGER_HAGGLE;
|
|
}
|
|
return eSoundType_MOB_VILLAGER_IDLE;
|
|
}
|
|
|
|
int Villager::getHurtSound()
|
|
{
|
|
return eSoundType_MOB_VILLAGER_HIT;
|
|
}
|
|
|
|
int Villager::getDeathSound()
|
|
{
|
|
return eSoundType_MOB_VILLAGER_DEATH;
|
|
}
|
|
|
|
void Villager::setProfession(int profession)
|
|
{
|
|
entityData->set(DATA_PROFESSION_ID, profession);
|
|
}
|
|
|
|
int Villager::getProfession()
|
|
{
|
|
return entityData->getInteger(DATA_PROFESSION_ID);
|
|
}
|
|
|
|
bool Villager::isInLove()
|
|
{
|
|
return inLove;
|
|
}
|
|
|
|
void Villager::setInLove(bool inLove)
|
|
{
|
|
this->inLove = inLove;
|
|
}
|
|
|
|
void Villager::setChasing(bool chasing)
|
|
{
|
|
this->chasing = chasing;
|
|
}
|
|
|
|
bool Villager::isChasing()
|
|
{
|
|
return chasing;
|
|
}
|
|
|
|
void Villager::setLastHurtByMob(shared_ptr<LivingEntity> mob)
|
|
{
|
|
AgableMob::setLastHurtByMob(mob);
|
|
shared_ptr<Village> _village = village.lock();
|
|
if (_village != nullptr && mob != nullptr)
|
|
{
|
|
_village->addAggressor(mob);
|
|
|
|
if ( mob->instanceof(eTYPE_PLAYER) )
|
|
{
|
|
int amount = -1;
|
|
if (isBaby())
|
|
{
|
|
amount = -3;
|
|
}
|
|
_village->modifyStanding( dynamic_pointer_cast<Player>(mob)->getName(), amount );
|
|
if (isAlive())
|
|
{
|
|
level->broadcastEntityEvent(shared_from_this(), EntityEvent::VILLAGER_ANGRY);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Villager::die(DamageSource *source)
|
|
{
|
|
shared_ptr<Village> _village = village.lock();
|
|
if (_village != nullptr)
|
|
{
|
|
shared_ptr<Entity> sourceEntity = source->getEntity();
|
|
if (sourceEntity != nullptr)
|
|
{
|
|
if ( sourceEntity->instanceof(eTYPE_PLAYER) )
|
|
{
|
|
_village->modifyStanding( dynamic_pointer_cast<Player>(sourceEntity)->getName(), -2 );
|
|
}
|
|
else if ( sourceEntity->instanceof(eTYPE_ENEMY) )
|
|
{
|
|
_village->resetNoBreedTimer();
|
|
}
|
|
}
|
|
else if (sourceEntity == nullptr)
|
|
{
|
|
// if the villager was killed by the world (such as lava or falling), blame
|
|
// the nearest player by not reproducing for a while
|
|
shared_ptr<Player> nearestPlayer = level->getNearestPlayer(shared_from_this(), 16.0f);
|
|
if (nearestPlayer != nullptr)
|
|
{
|
|
_village->resetNoBreedTimer();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Make the gui close if the villager die while trading
|
|
if (auto currentTrader = tradingPlayer.lock())
|
|
{
|
|
if (currentTrader->containerMenu != nullptr)
|
|
{
|
|
auto menu = currentTrader->containerMenu;
|
|
menu->removed(currentTrader);
|
|
}
|
|
|
|
tradingPlayer.reset();
|
|
}
|
|
|
|
AgableMob::die(source);
|
|
}
|
|
|
|
void Villager::setTradingPlayer(shared_ptr<Player> player)
|
|
{
|
|
tradingPlayer = weak_ptr<Player>(player);
|
|
}
|
|
|
|
shared_ptr<Player> Villager::getTradingPlayer()
|
|
{
|
|
return tradingPlayer.lock();
|
|
}
|
|
|
|
bool Villager::isTrading()
|
|
{
|
|
return tradingPlayer.lock() != nullptr;
|
|
}
|
|
|
|
void Villager::notifyTrade(MerchantRecipe *activeRecipe)
|
|
{
|
|
activeRecipe->increaseUses();
|
|
ambientSoundTime = -getAmbientSoundInterval();
|
|
playSound(eSoundType_MOB_VILLAGER_YES, getSoundVolume(), getVoicePitch());
|
|
|
|
// when the player buys the latest item, we improve the merchant a little while later
|
|
if (activeRecipe->isSame(offers->at(offers->size() - 1)))
|
|
{
|
|
updateMerchantTimer = SharedConstants::TICKS_PER_SECOND * 2;
|
|
addRecipeOnUpdate = true;
|
|
if (tradingPlayer.lock() != nullptr)
|
|
{
|
|
lastPlayerTradeName = tradingPlayer.lock()->getName();
|
|
}
|
|
else
|
|
{
|
|
lastPlayerTradeName = L"";
|
|
}
|
|
}
|
|
|
|
if (activeRecipe->getBuyAItem()->id == Item::emerald_Id)
|
|
{
|
|
riches += activeRecipe->getBuyAItem()->count;
|
|
}
|
|
}
|
|
|
|
void Villager::notifyTradeUpdated(shared_ptr<ItemInstance> item)
|
|
{
|
|
if (!level->isClientSide && (ambientSoundTime > (-getAmbientSoundInterval() + SharedConstants::TICKS_PER_SECOND)))
|
|
{
|
|
ambientSoundTime = -getAmbientSoundInterval();
|
|
if (item != nullptr)
|
|
{
|
|
playSound(eSoundType_MOB_VILLAGER_YES, getSoundVolume(), getVoicePitch());
|
|
}
|
|
else
|
|
{
|
|
playSound(eSoundType_MOB_VILLAGER_NO, getSoundVolume(), getVoicePitch());
|
|
}
|
|
}
|
|
}
|
|
|
|
MerchantRecipeList *Villager::getOffers(shared_ptr<Player> forPlayer)
|
|
{
|
|
if (offers == nullptr)
|
|
{
|
|
addOffers(1);
|
|
}
|
|
return offers;
|
|
}
|
|
|
|
float Villager::getRecipeChance(float baseChance)
|
|
{
|
|
float newChance = baseChance + baseRecipeChanceMod;
|
|
if (newChance > .9f)
|
|
{
|
|
return .9f - (newChance - .9f);
|
|
}
|
|
return newChance;
|
|
}
|
|
|
|
void Villager::addOffers(int addCount)
|
|
{
|
|
MerchantRecipeList *newOffers = new MerchantRecipeList();
|
|
switch (getProfession())
|
|
{
|
|
case PROFESSION_FARMER:
|
|
addItemForTradeIn(newOffers, Item::wheat_Id, random, getRecipeChance(.9f));
|
|
addItemForTradeIn(newOffers, Tile::wool_Id, random, getRecipeChance(.5f));
|
|
addItemForTradeIn(newOffers, Item::chicken_Id, random, getRecipeChance(.5f));
|
|
addItemForTradeIn(newOffers, Item::cooked_fish_Id, random, getRecipeChance(.4f));
|
|
addItemForPurchase(newOffers, Item::bread_Id, random, getRecipeChance(.9f));
|
|
addItemForPurchase(newOffers, Item::melon_block_Id, random, getRecipeChance(.3f));
|
|
addItemForPurchase(newOffers, Item::apple_Id, random, getRecipeChance(.3f));
|
|
addItemForPurchase(newOffers, Item::cookie_Id, random, getRecipeChance(.3f));
|
|
addItemForPurchase(newOffers, Item::shears_Id, random, getRecipeChance(.3f));
|
|
addItemForPurchase(newOffers, Item::flint_and_steel_Id, random, getRecipeChance(.3f));
|
|
addItemForPurchase(newOffers, Item::cooked_chicken_Id, random, getRecipeChance(.3f));
|
|
addItemForPurchase(newOffers, Item::arrow_Id, random, getRecipeChance(.5f));
|
|
if (random->nextFloat() < getRecipeChance(.5f))
|
|
{
|
|
newOffers->push_back(new MerchantRecipe(std::make_shared<ItemInstance>(Tile::gravel, 10), std::make_shared<ItemInstance>(Item::emerald), std::make_shared<ItemInstance>(Item::flint_Id, 4 + random->nextInt(2), 0)));
|
|
}
|
|
break;
|
|
case PROFESSION_BUTCHER:
|
|
addItemForTradeIn(newOffers, Item::coal_Id, random, getRecipeChance(.7f));
|
|
addItemForTradeIn(newOffers, Item::porkchop_Id, random, getRecipeChance(.5f));
|
|
addItemForTradeIn(newOffers, Item::beef_Id, random, getRecipeChance(.5f));
|
|
addItemForPurchase(newOffers, Item::saddle_Id, random, getRecipeChance(.1f));
|
|
addItemForPurchase(newOffers, Item::leather_chestplate_Id, random, getRecipeChance(.3f));
|
|
addItemForPurchase(newOffers, Item::leather_boots_Id, random, getRecipeChance(.3f));
|
|
addItemForPurchase(newOffers, Item::leather_helmet_Id, random, getRecipeChance(.3f));
|
|
addItemForPurchase(newOffers, Item::leather_leggings_Id, random, getRecipeChance(.3f));
|
|
addItemForPurchase(newOffers, Item::cooked_porkchop_Id, random, getRecipeChance(.3f));
|
|
addItemForPurchase(newOffers, Item::cooked_beef_Id, random, getRecipeChance(.3f));
|
|
break;
|
|
case PROFESSION_SMITH:
|
|
addItemForTradeIn(newOffers, Item::coal_Id, random, getRecipeChance(.7f));
|
|
addItemForTradeIn(newOffers, Item::iron_ingot_Id, random, getRecipeChance(.5f));
|
|
addItemForTradeIn(newOffers, Item::gold_ingot_Id, random, getRecipeChance(.5f));
|
|
addItemForTradeIn(newOffers, Item::diamond_Id, random, getRecipeChance(.5f));
|
|
|
|
addItemForPurchase(newOffers, Item::iron_sword_Id, random, getRecipeChance(.5f));
|
|
addItemForPurchase(newOffers, Item::diamond_sword_Id, random, getRecipeChance(.5f));
|
|
addItemForPurchase(newOffers, Item::iron_axe_Id, random, getRecipeChance(.3f));
|
|
addItemForPurchase(newOffers, Item::diamond_axe_Id, random, getRecipeChance(.3f));
|
|
addItemForPurchase(newOffers, Item::iron_pickaxe_Id, random, getRecipeChance(.5f));
|
|
addItemForPurchase(newOffers, Item::diamond_pickaxe_Id, random, getRecipeChance(.5f));
|
|
addItemForPurchase(newOffers, Item::iron_shovel_Id, random, getRecipeChance(.2f));
|
|
addItemForPurchase(newOffers, Item::diamond_shovel_Id, random, getRecipeChance(.2f));
|
|
addItemForPurchase(newOffers, Item::iron_hoe_Id, random, getRecipeChance(.2f));
|
|
addItemForPurchase(newOffers, Item::diamond_hoe_Id, random, getRecipeChance(.2f));
|
|
addItemForPurchase(newOffers, Item::iron_boots_Id, random, getRecipeChance(.2f));
|
|
addItemForPurchase(newOffers, Item::diamond_boots_Id, random, getRecipeChance(.2f));
|
|
addItemForPurchase(newOffers, Item::iron_helmet_Id, random, getRecipeChance(.2f));
|
|
addItemForPurchase(newOffers, Item::diamond_helmet_Id, random, getRecipeChance(.2f));
|
|
addItemForPurchase(newOffers, Item::iron_chestplate_Id, random, getRecipeChance(.2f));
|
|
addItemForPurchase(newOffers, Item::diamond_chestplate_Id, random, getRecipeChance(.2f));
|
|
addItemForPurchase(newOffers, Item::iron_leggings_Id, random, getRecipeChance(.2f));
|
|
addItemForPurchase(newOffers, Item::diamond_leggings_Id, random, getRecipeChance(.2f));
|
|
addItemForPurchase(newOffers, Item::chainmail_boots_Id, random, getRecipeChance(.1f));
|
|
addItemForPurchase(newOffers, Item::chainmail_helmet_Id, random, getRecipeChance(.1f));
|
|
addItemForPurchase(newOffers, Item::chainmail_chestplate_Id, random, getRecipeChance(.1f));
|
|
addItemForPurchase(newOffers, Item::chainmail_leggings_Id, random, getRecipeChance(.1f));
|
|
break;
|
|
case PROFESSION_LIBRARIAN:
|
|
addItemForTradeIn(newOffers, Item::paper_Id, random, getRecipeChance(.8f));
|
|
addItemForTradeIn(newOffers, Item::book_Id, random, getRecipeChance(.8f));
|
|
//addItemForTradeIn(newOffers, Item::written_book_Id, random, getRecipeChance(0.3f));
|
|
addItemForPurchase(newOffers, Tile::bookshelf_Id, random, getRecipeChance(.8f));
|
|
addItemForPurchase(newOffers, Tile::glass_Id, random, getRecipeChance(.2f));
|
|
addItemForPurchase(newOffers, Item::compass_Id, random, getRecipeChance(.2f));
|
|
addItemForPurchase(newOffers, Item::clock_Id, random, getRecipeChance(.2f));
|
|
addItemForPurchase(newOffers, Item::name_tag_Id, random, getRecipeChance(.2f));
|
|
|
|
if (random->nextFloat() < getRecipeChance(0.07f))
|
|
{
|
|
Enchantment *enchantment = Enchantment::validEnchantments[random->nextInt(Enchantment::validEnchantments.size())];
|
|
int level = Mth::nextInt(random, enchantment->getMinLevel(), enchantment->getMaxLevel());
|
|
shared_ptr<ItemInstance> book = Item::enchanted_book->createForEnchantment(new EnchantmentInstance(enchantment, level));
|
|
int cost = 2 + random->nextInt(5 + (level * 10)) + 3 * level;
|
|
|
|
newOffers->push_back(new MerchantRecipe(std::make_shared<ItemInstance>(Item::book), std::make_shared<ItemInstance>(Item::emerald, cost), book));
|
|
}
|
|
break;
|
|
case PROFESSION_PRIEST:
|
|
addItemForPurchase(newOffers, Item::eye_of_ender_Id, random, getRecipeChance(.3f));
|
|
addItemForPurchase(newOffers, Item::experience_bottle_Id, random, getRecipeChance(.2f));
|
|
addItemForPurchase(newOffers, Item::redstone_Id, random, getRecipeChance(.4f));
|
|
addItemForPurchase(newOffers, Tile::glowstone_Id, random, getRecipeChance(.3f));
|
|
{
|
|
int enchantItems[] = {
|
|
Item::iron_sword_Id, Item::diamond_sword_Id, Item::iron_chestplate_Id, Item::diamond_chestplate_Id, Item::iron_axe_Id, Item::diamond_axe_Id, Item::iron_pickaxe_Id,
|
|
Item::diamond_pickaxe_Id
|
|
};
|
|
for (unsigned int i = 0; i < 8; ++i)
|
|
{
|
|
int id = enchantItems[i];
|
|
if (random->nextFloat() < getRecipeChance(.05f))
|
|
{
|
|
newOffers->push_back(new MerchantRecipe(std::make_shared<ItemInstance>(id, 1, 0),
|
|
std::make_shared<ItemInstance>(Item::emerald, 2 + random->nextInt(3), 0),
|
|
EnchantmentHelper::enchantItem(random, std::make_shared<ItemInstance>(id, 1, 0), 5 + random->nextInt(15))));
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (newOffers->empty())
|
|
{
|
|
addItemForTradeIn(newOffers, Item::gold_ingot_Id, random, 1.0f);
|
|
}
|
|
|
|
// shuffle the list to make it more interesting
|
|
std::shuffle(newOffers->begin(), newOffers->end(), std::mt19937{std::random_device{}()});
|
|
|
|
if (offers == nullptr)
|
|
{
|
|
offers = new MerchantRecipeList();
|
|
}
|
|
for (int i = 0; i < addCount && i < newOffers->size(); i++)
|
|
{
|
|
if( offers->addIfNewOrBetter(newOffers->at(i)))
|
|
{
|
|
// 4J Added so we can delete newOffers
|
|
newOffers->erase(newOffers->begin() + i);
|
|
}
|
|
}
|
|
delete newOffers;
|
|
}
|
|
|
|
void Villager::overrideOffers(MerchantRecipeList *recipeList)
|
|
{
|
|
}
|
|
|
|
|
|
void Villager::staticCtor()
|
|
{
|
|
MIN_MAX_VALUES[Item::coal_Id] = pair<int,int>(16, 24);
|
|
MIN_MAX_VALUES[Item::iron_ingot_Id] = pair<int,int>(8, 10);
|
|
MIN_MAX_VALUES[Item::gold_ingot_Id] = pair<int,int>(8, 10);
|
|
MIN_MAX_VALUES[Item::diamond_Id] = pair<int,int>(4, 6);
|
|
MIN_MAX_VALUES[Item::paper_Id] = pair<int,int>(24, 36);
|
|
MIN_MAX_VALUES[Item::book_Id] = pair<int,int>(11, 13);
|
|
//MIN_MAX_VALUES.insert(Item::written_book_Id, pair<int,int>(1, 1));
|
|
MIN_MAX_VALUES[Item::ender_pearl_Id] = pair<int,int>(3, 4);
|
|
MIN_MAX_VALUES[Item::eye_of_ender_Id] = pair<int,int>(2, 3);
|
|
MIN_MAX_VALUES[Item::porkchop_Id] = pair<int,int>(14, 18);
|
|
MIN_MAX_VALUES[Item::beef_Id] = pair<int,int>(14, 18);
|
|
MIN_MAX_VALUES[Item::chicken_Id] = pair<int,int>(14, 18);
|
|
MIN_MAX_VALUES[Item::cooked_fish_Id] = pair<int,int>(9, 13);
|
|
MIN_MAX_VALUES[Item::wheat_seeds_Id] = pair<int,int>(34, 48);
|
|
MIN_MAX_VALUES[Item::melon_seeds_Id] = pair<int,int>(30, 38);
|
|
MIN_MAX_VALUES[Item::pumpkin_seeds_Id] = pair<int,int>(30, 38);
|
|
MIN_MAX_VALUES[Item::wheat_Id] = pair<int,int>(18, 22);
|
|
MIN_MAX_VALUES[Tile::wool_Id] = pair<int,int>(14, 22);
|
|
MIN_MAX_VALUES[Item::rotten_flesh_Id] = pair<int,int>(36, 64);
|
|
|
|
MIN_MAX_PRICES[Item::flint_and_steel_Id] = pair<int,int>(3, 4);
|
|
MIN_MAX_PRICES[Item::shears_Id] = pair<int,int>(3, 4);
|
|
MIN_MAX_PRICES[Item::iron_sword_Id] = pair<int,int>(7, 11);
|
|
MIN_MAX_PRICES[Item::diamond_sword_Id] = pair<int,int>(12, 14);
|
|
MIN_MAX_PRICES[Item::iron_axe_Id] = pair<int,int>(6, 8);
|
|
MIN_MAX_PRICES[Item::diamond_axe_Id] = pair<int,int>(9, 12);
|
|
MIN_MAX_PRICES[Item::iron_pickaxe_Id] = pair<int,int>(7, 9);
|
|
MIN_MAX_PRICES[Item::diamond_pickaxe_Id] = pair<int,int>(10, 12);
|
|
MIN_MAX_PRICES[Item::iron_shovel_Id] = pair<int,int>(4, 6);
|
|
MIN_MAX_PRICES[Item::diamond_shovel_Id] = pair<int,int>(7, 8);
|
|
MIN_MAX_PRICES[Item::iron_hoe_Id] = pair<int,int>(4, 6);
|
|
MIN_MAX_PRICES[Item::diamond_hoe_Id] = pair<int,int>(7, 8);
|
|
MIN_MAX_PRICES[Item::iron_boots_Id] = pair<int,int>(4, 6);
|
|
MIN_MAX_PRICES[Item::diamond_boots_Id] = pair<int,int>(7, 8);
|
|
MIN_MAX_PRICES[Item::iron_helmet_Id] = pair<int,int>(4, 6);
|
|
MIN_MAX_PRICES[Item::diamond_helmet_Id] = pair<int,int>(7, 8);
|
|
MIN_MAX_PRICES[Item::iron_chestplate_Id] = pair<int,int>(10, 14);
|
|
MIN_MAX_PRICES[Item::diamond_chestplate_Id] = pair<int,int>(16, 19);
|
|
MIN_MAX_PRICES[Item::iron_leggings_Id] = pair<int,int>(8, 10);
|
|
MIN_MAX_PRICES[Item::diamond_leggings_Id] = pair<int,int>(11, 14);
|
|
MIN_MAX_PRICES[Item::chainmail_boots_Id] = pair<int,int>(5, 7);
|
|
MIN_MAX_PRICES[Item::chainmail_helmet_Id] = pair<int,int>(5, 7);
|
|
MIN_MAX_PRICES[Item::chainmail_chestplate_Id] = pair<int,int>(11, 15);
|
|
MIN_MAX_PRICES[Item::chainmail_leggings_Id] = pair<int,int>(9, 11);
|
|
MIN_MAX_PRICES[Item::bread_Id] = pair<int,int>(-4, -2);
|
|
MIN_MAX_PRICES[Item::melon_block_Id] = pair<int,int>(-8, -4);
|
|
MIN_MAX_PRICES[Item::apple_Id] = pair<int,int>(-8, -4);
|
|
MIN_MAX_PRICES[Item::cookie_Id] = pair<int,int>(-10, -7);
|
|
MIN_MAX_PRICES[Tile::glass_Id] = pair<int,int>(-5, -3);
|
|
MIN_MAX_PRICES[Tile::bookshelf_Id] = pair<int,int>(3, 4);
|
|
MIN_MAX_PRICES[Item::leather_chestplate_Id] = pair<int,int>(4, 5);
|
|
MIN_MAX_PRICES[Item::leather_boots_Id] = pair<int,int>(2, 4);
|
|
MIN_MAX_PRICES[Item::leather_helmet_Id] = pair<int,int>(2, 4);
|
|
MIN_MAX_PRICES[Item::leather_leggings_Id] = pair<int,int>(2, 4);
|
|
MIN_MAX_PRICES[Item::saddle_Id] = pair<int,int>(6, 8);
|
|
MIN_MAX_PRICES[Item::experience_bottle_Id] = pair<int,int>(-4, -1);
|
|
MIN_MAX_PRICES[Item::redstone_Id] = pair<int,int>(-4, -1);
|
|
MIN_MAX_PRICES[Item::compass_Id] = pair<int,int>(10, 12);
|
|
MIN_MAX_PRICES[Item::clock_Id] = pair<int,int>(10, 12);
|
|
MIN_MAX_PRICES[Tile::glowstone_Id] = pair<int,int>(-3, -1);
|
|
MIN_MAX_PRICES[Item::cooked_porkchop_Id] = pair<int,int>(-7, -5);
|
|
MIN_MAX_PRICES[Item::cooked_beef_Id] = pair<int,int>(-7, -5);
|
|
MIN_MAX_PRICES[Item::cooked_chicken_Id] = pair<int,int>(-8, -6);
|
|
MIN_MAX_PRICES[Item::eye_of_ender_Id] = pair<int,int>(7, 11);
|
|
MIN_MAX_PRICES[Item::arrow_Id] = pair<int,int>(-12, -8);
|
|
}
|
|
|
|
/**
|
|
* Adds a merchant recipe that trades items for a single ruby.
|
|
*
|
|
* @param list
|
|
* @param itemId
|
|
* @param random
|
|
* @param likelyHood
|
|
*/
|
|
void Villager::addItemForTradeIn(MerchantRecipeList *list, int itemId, Random *random, float likelyHood)
|
|
{
|
|
if (random->nextFloat() < likelyHood)
|
|
{
|
|
list->push_back(new MerchantRecipe(getItemTradeInValue(itemId, random), Item::emerald));
|
|
}
|
|
}
|
|
|
|
shared_ptr<ItemInstance> Villager::getItemTradeInValue(int itemId, Random *random)
|
|
{
|
|
return std::make_shared<ItemInstance>(itemId, getTradeInValue(itemId, random), 0);
|
|
}
|
|
|
|
int Villager::getTradeInValue(int itemId, Random *random)
|
|
{
|
|
auto it = MIN_MAX_VALUES.find(itemId);
|
|
if (it == MIN_MAX_VALUES.end())
|
|
{
|
|
return 1;
|
|
}
|
|
pair<int, int> minMax = it->second;
|
|
if (minMax.first >= minMax.second)
|
|
{
|
|
return minMax.first;
|
|
}
|
|
return minMax.first + random->nextInt(minMax.second - minMax.first);
|
|
}
|
|
|
|
/**
|
|
* Adds a merchant recipe that trades rubies for an item. If the cost is
|
|
* negative, one ruby will give several of that item.
|
|
*
|
|
* @param list
|
|
* @param itemId
|
|
* @param random
|
|
* @param likelyHood
|
|
*/
|
|
void Villager::addItemForPurchase(MerchantRecipeList *list, int itemId, Random *random, float likelyHood)
|
|
{
|
|
if (random->nextFloat() < likelyHood)
|
|
{
|
|
int purchaseCost = getPurchaseCost(itemId, random);
|
|
shared_ptr<ItemInstance> rubyItem;
|
|
shared_ptr<ItemInstance> resultItem;
|
|
if (purchaseCost < 0)
|
|
{
|
|
rubyItem = std::make_shared<ItemInstance>(Item::emerald_Id, 1, 0);
|
|
resultItem = std::make_shared<ItemInstance>(itemId, -purchaseCost, 0);
|
|
}
|
|
else
|
|
{
|
|
rubyItem = std::make_shared<ItemInstance>(Item::emerald_Id, purchaseCost, 0);
|
|
resultItem = std::make_shared<ItemInstance>(itemId, 1, 0);
|
|
}
|
|
list->push_back(new MerchantRecipe(rubyItem, resultItem));
|
|
}
|
|
}
|
|
|
|
int Villager::getPurchaseCost(int itemId, Random *random)
|
|
{
|
|
auto it = MIN_MAX_PRICES.find(itemId);
|
|
if (it == MIN_MAX_PRICES.end())
|
|
{
|
|
return 1;
|
|
}
|
|
pair<int, int> minMax = it->second;
|
|
if (minMax.first >= minMax.second)
|
|
{
|
|
return minMax.first;
|
|
}
|
|
return minMax.first + random->nextInt(minMax.second - minMax.first);
|
|
}
|
|
|
|
void Villager::handleEntityEvent(byte id)
|
|
{
|
|
if (id == EntityEvent::LOVE_HEARTS)
|
|
{
|
|
addParticlesAroundSelf(eParticleType_heart);
|
|
}
|
|
else if (id == EntityEvent::VILLAGER_ANGRY)
|
|
{
|
|
addParticlesAroundSelf(eParticleType_angryVillager);
|
|
}
|
|
else if (id == EntityEvent::VILLAGER_HAPPY)
|
|
{
|
|
addParticlesAroundSelf(eParticleType_happyVillager);
|
|
}
|
|
else
|
|
{
|
|
AgableMob::handleEntityEvent(id);
|
|
}
|
|
}
|
|
|
|
void Villager::addParticlesAroundSelf(ePARTICLE_TYPE particle)
|
|
{
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
double xa = random->nextGaussian() * 0.02;
|
|
double ya = random->nextGaussian() * 0.02;
|
|
double za = random->nextGaussian() * 0.02;
|
|
level->addParticle(particle, x + random->nextFloat() * bbWidth * 2 - bbWidth, y + 1.0f + random->nextFloat() * bbHeight, z + random->nextFloat() * bbWidth * 2 - bbWidth, xa, ya, za);
|
|
}
|
|
}
|
|
|
|
MobGroupData *Villager::finalizeMobSpawn(MobGroupData *groupData, int extraData /*= 0*/) // 4J Added extraData param
|
|
{
|
|
groupData = AgableMob::finalizeMobSpawn(groupData);
|
|
|
|
setProfession(level->random->nextInt(PROFESSION_MAX));
|
|
|
|
return groupData;
|
|
}
|
|
|
|
void Villager::setRewardPlayersInVillage()
|
|
{
|
|
rewardPlayersOnFirstVillage = true;
|
|
}
|
|
|
|
shared_ptr<AgableMob> Villager::getBreedOffspring(shared_ptr<AgableMob> target)
|
|
{
|
|
// 4J - added limit to villagers that can be bred
|
|
if(level->canCreateMore(GetType(), Level::eSpawnType_Breed) )
|
|
{
|
|
shared_ptr<Villager> villager = std::make_shared<Villager>(level);
|
|
villager->finalizeMobSpawn(nullptr);
|
|
return villager;
|
|
}
|
|
else
|
|
{
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
bool Villager::canBeLeashed()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
wstring Villager::getDisplayName()
|
|
{
|
|
if (hasCustomName()) return getCustomName();
|
|
|
|
int name = IDS_VILLAGER;
|
|
switch(getProfession())
|
|
{
|
|
case PROFESSION_FARMER:
|
|
name = IDS_VILLAGER_FARMER;
|
|
break;
|
|
case PROFESSION_LIBRARIAN:
|
|
name = IDS_VILLAGER_LIBRARIAN;
|
|
break;
|
|
case PROFESSION_PRIEST:
|
|
name = IDS_VILLAGER_PRIEST;
|
|
break;
|
|
case PROFESSION_SMITH:
|
|
name = IDS_VILLAGER_SMITH;
|
|
break;
|
|
case PROFESSION_BUTCHER:
|
|
name = IDS_VILLAGER_BUTCHER;
|
|
break;
|
|
};
|
|
return app.GetString(name);
|
|
}
|
|
|
|
void Villager::thunderHit(const LightningBolt *lightningBolt)
|
|
{
|
|
if (level->isClientSide) return;
|
|
shared_ptr<Witch> witch = std::make_shared<Witch>(level);
|
|
witch->moveTo(x, y, z, yRot, xRot);
|
|
|
|
if (this->hasCustomName())
|
|
witch->setCustomName(this->getCustomName());
|
|
|
|
if (this->isPersistenceRequired())
|
|
witch->setPersistenceRequired();
|
|
|
|
level->addEntity(witch);
|
|
remove();
|
|
}
|
|
|
|
int Villager::getAttackTooltipId(std::shared_ptr<Player> player)
|
|
{
|
|
return IDS_TOOLTIPS_HIT;
|
|
}
|
|
|
|
int Villager::getInteractTooltipId(Level *level,
|
|
std::shared_ptr<Player> player,
|
|
std::shared_ptr<ItemInstance> heldItem)
|
|
{
|
|
if (!this->isBaby())
|
|
return IDS_TOOLTIPS_TRADE;
|
|
|
|
return 0;
|
|
}
|