#include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules #include "IncludeAllMonsters.h" #include "LineBlockTracer.h" #include "../BlockInfo.h" #include "../Root.h" #include "../Server.h" #include "../ClientHandle.h" #include "../Items/ItemHandler.h" #include "../World.h" #include "../EffectID.h" #include "../Entities/Player.h" #include "../Entities/ExpOrb.h" #include "../MonsterConfig.h" #include "../BoundingBox.h" #include "Items/ItemSpawnEgg.h" #include "../Chunk.h" #include "../FastRandom.h" #include "PathFinder.h" #include "../Entities/LeashKnot.h" /** Map for eType <-> string Needs to be alpha-sorted by the strings, because binary search is used in StringToMobType() The strings need to be lowercase (for more efficient comparisons in StringToMobType()) m_VanillaName is the name that vanilla use for this mob. */ static const struct { eMonsterType m_Type; const char * m_lcName; const char * m_VanillaName; const char * m_VanillaNameNBT; } g_MobTypeNames[] = { {mtBat, "bat", "Bat", "bat"}, {mtBlaze, "blaze", "Blaze", "blaze"}, {mtCaveSpider, "cavespider", "CaveSpider", "cave_spider"}, {mtChicken, "chicken", "Chicken", "chicken"}, {mtCow, "cow", "Cow", "cow"}, {mtCreeper, "creeper", "Creeper", "creeper"}, {mtEnderman, "enderman", "Enderman", "enderman"}, {mtEndermite, "endermite", "Endermite", "endermite"}, {mtEnderDragon, "enderdragon", "EnderDragon", "ender_dragon"}, {mtGhast, "ghast", "Ghast", "ghast"}, {mtGiant, "giant", "Giant", "giant"}, {mtGuardian, "guardian", "Guardian", "guardian"}, {mtHorse, "horse", "EntityHorse", "horse"}, {mtIronGolem, "irongolem", "VillagerGolem", "iron_golem"}, {mtMagmaCube, "magmacube", "LavaSlime", "magma_cube"}, {mtMooshroom, "mooshroom", "MushroomCow", "mooshroom"}, {mtOcelot, "ocelot", "Ozelot", "ocelot"}, {mtPig, "pig", "Pig", "pig"}, {mtRabbit, "rabbit", "Rabbit", "rabbit"}, {mtSheep, "sheep", "Sheep", "sheep"}, {mtSilverfish, "silverfish", "Silverfish", "silverfish"}, {mtSkeleton, "skeleton", "Skeleton", "skeleton"}, {mtSlime, "slime", "Slime", "slime"}, {mtSnowGolem, "snowgolem", "SnowMan", "snow_golem"}, {mtSpider, "spider", "Spider", "spider"}, {mtSquid, "squid", "Squid", "squid"}, {mtVillager, "villager", "Villager", "villager"}, {mtWitch, "witch", "Witch", "witch"}, {mtWither, "wither", "WitherBoss", "wither"}, {mtWitherSkeleton, "witherskeleton", "WitherSkeleton", "wither_skeleton"}, {mtWolf, "wolf", "Wolf", "wolf"}, {mtZombie, "zombie", "Zombie", "zombie"}, {mtZombiePigman, "zombiepigman", "PigZombie", "zombie_pigman"}, {mtZombieVillager, "zombievillager", "ZombieVillager", "zombie_villager"}, }; //////////////////////////////////////////////////////////////////////////////// // cMonster: cMonster::cMonster( const AString & a_ConfigName, eMonsterType a_MobType, const AString & a_SoundHurt, const AString & a_SoundDeath, const AString & a_SoundAmbient, float a_Width, float a_Height ) : Super(etMonster, a_Width, a_Height), m_EMState(IDLE), m_EMPersonality(AGGRESSIVE), m_PathFinder(a_Width, a_Height), m_PathfinderActivated(false), m_JumpCoolDown(0), m_IdleInterval(0), m_DestroyTimer(0), m_MobType(a_MobType), m_CustomName(), m_CustomNameAlwaysVisible(false), m_SoundHurt(a_SoundHurt), m_SoundDeath(a_SoundDeath), m_SoundAmbient(a_SoundAmbient), m_AttackRate(3), m_AttackDamage(1), m_AttackRange(1), m_AttackCoolDownTicksLeft(0), m_SightDistance(25), m_DropChanceWeapon(0.085f), m_DropChanceHelmet(0.085f), m_DropChanceChestplate(0.085f), m_DropChanceLeggings(0.085f), m_DropChanceBoots(0.085f), m_CanPickUpLoot(true), m_TicksSinceLastDamaged(100), m_BurnsInDaylight(false), m_RelativeWalkSpeed(1), m_Age(1), m_AgingTimer(TPS * 60 * 20) // about 20 minutes , m_WasLastTargetAPlayer(false), m_LeashedTo(nullptr), m_LeashToPos(nullptr), m_IsLeashActionJustDone(false), m_CanBeLeashed(GetMobFamily() == eFamily::mfPassive), m_LovePartner(nullptr), m_LoveTimer(0), m_LoveCooldown(0), m_MatingTimer(0), m_Target(nullptr) { if (!a_ConfigName.empty()) { GetMonsterConfig(a_ConfigName); } // Prevent mobs spawning at the same time from making sounds simultaneously m_AmbientSoundTimer = GetRandomProvider().RandInt(0, 100); } void cMonster::OnRemoveFromWorld(cWorld & a_World) { SetTarget(nullptr); // Tell them we're no longer targeting them. if (m_LovePartner != nullptr) { m_LovePartner->ResetLoveMode(); } if (IsLeashed()) { cEntity * LeashedTo = GetLeashedTo(); Unleash(false, true); // Remove leash knot if there are no more mobs leashed to if (!LeashedTo->HasAnyMobLeashed() && LeashedTo->IsLeashKnot()) { LeashedTo->Destroy(); } } Super::OnRemoveFromWorld(a_World); } void cMonster::SpawnOn(cClientHandle & a_Client) { a_Client.SendSpawnMob(*this); if (IsLeashed()) { a_Client.SendLeashEntity(*this, *this->GetLeashedTo()); } } void cMonster::MoveToWayPoint(cChunk & a_Chunk) { if ((m_NextWayPointPosition - GetPosition()).SqrLength() < WAYPOINT_RADIUS * WAYPOINT_RADIUS) { return; } if (m_JumpCoolDown <= 0) { if (DoesPosYRequireJump(FloorC(m_NextWayPointPosition.y))) { if ((IsOnGround() && (GetSpeed().SqrLength() <= 0.5) ) || // If walking on the ground, we need to slow down first, otherwise we miss the jump IsInWater()) { m_bOnGround = false; m_JumpCoolDown = 20; AddPosY(1.6); // Jump!! SetSpeedY(1); SetSpeedX(3.2 * (m_NextWayPointPosition.x - GetPosition().x)); // Move forward in a preset speed. SetSpeedZ( 3.2 * (m_NextWayPointPosition.z - GetPosition().z) ); // The numbers were picked based on trial and error } } } else { --m_JumpCoolDown; } Vector3d Distance = m_NextWayPointPosition - GetPosition(); if ((std::abs(Distance.x) > 0.05) || (std::abs(Distance.z) > 0.05)) { Distance.y = 0; Distance.Normalize(); if (m_bOnGround) { Distance *= 2.5f; } else if (IsInWater()) { Distance *= 1.3f; } else { // Don't let the mob move too much if he's falling. Distance *= 0.25f; } // Apply walk speed: Distance *= m_RelativeWalkSpeed; /* Reduced default speed. Close to Vanilla, easier for mobs to follow m_NextWayPointPositions, hence better pathfinding. */ Distance *= 0.5; AddSpeedX(Distance.x); AddSpeedZ(Distance.z); } } void cMonster::MoveToPosition(const Vector3d & a_Position) { m_FinalDestination = a_Position; m_PathfinderActivated = true; } void cMonster::StopMovingToPosition() { m_PathfinderActivated = false; } void cMonster::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) { Super::Tick(a_Dt, a_Chunk); if (!IsTicking()) { // The base class tick destroyed us return; } GET_AND_VERIFY_CURRENT_CHUNK(Chunk, POSX_TOINT, POSZ_TOINT); ASSERT((GetTarget() == nullptr) || (GetTarget()->IsPawn() && (GetTarget()->GetWorld() == GetWorld()))); if (m_AttackCoolDownTicksLeft > 0) { m_AttackCoolDownTicksLeft -= 1; } if (m_Health <= 0) { // The mob is dead, but we're still animating the "puff" they leave when they die m_DestroyTimer += a_Dt; if (m_DestroyTimer > std::chrono::seconds(1)) { Destroy(); } return; } if (m_TicksSinceLastDamaged < 100) { ++m_TicksSinceLastDamaged; } // Process the undead burning in daylight. HandleDaylightBurning(*Chunk, WouldBurnAt(GetPosition(), *Chunk)); bool a_IsFollowingPath = false; if (m_PathfinderActivated && (GetMobType() != mtGhast )) // Pathfinder is currently disabled for ghasts, which have their own flying mechanism { if (ReachedFinalDestination() || (m_LeashToPos != nullptr)) { StopMovingToPosition(); // Simply sets m_PathfinderActivated to false. } else { // Note that m_NextWayPointPosition is actually returned by GetNextWayPoint) switch (m_PathFinder.GetNextWayPoint( *Chunk, GetPosition(), &m_FinalDestination, &m_NextWayPointPosition, m_EMState == IDLE )) { case ePathFinderStatus::PATH_FOUND: { /* If I burn in daylight, and I won't burn where I'm standing, and I'll burn in my next position, and at least one of those is true: 1. I am idle 2. I was not hurt by a player recently. Then STOP. */ if (m_BurnsInDaylight && ((m_TicksSinceLastDamaged >= 100) || (m_EMState == IDLE)) && WouldBurnAt(m_NextWayPointPosition, *Chunk) && !WouldBurnAt(GetPosition(), *Chunk)) { // If we burn in daylight, and we would burn at the next step, and we won't burn where we are // right now, and we weren't provoked recently: StopMovingToPosition(); } else { a_IsFollowingPath = true; // Used for proper body / head orientation only. MoveToWayPoint(*Chunk); } break; } case ePathFinderStatus::PATH_NOT_FOUND: { StopMovingToPosition(); break; } default: { } } } } SetPitchAndYawFromDestination(a_IsFollowingPath); switch (m_EMState) { case IDLE: { // If enemy passive we ignore checks for player visibility. InStateIdle(a_Dt, a_Chunk); break; } case CHASING: { // If we do not see a player anymore skip chasing action. InStateChasing(a_Dt, a_Chunk); break; } case ESCAPING: { InStateEscaping(a_Dt, a_Chunk); break; } case ATTACKING: break; } // switch (m_EMState) // Leash calculations CalcLeashActions(a_Dt); BroadcastMovementUpdate(); // Ambient mob sounds if (!m_SoundAmbient.empty() && (--m_AmbientSoundTimer <= 0)) { auto & Random = GetRandomProvider(); auto ShouldPlaySound = Random.RandBool(); if (ShouldPlaySound) { auto SoundPitchMultiplier = 1.0f + (Random.RandReal(1.0f) - Random.RandReal(1.0f)) * 0.2f; m_World->BroadcastSoundEffect(m_SoundAmbient, GetPosition(), 1.0f, SoundPitchMultiplier * 1.0f); } m_AmbientSoundTimer = 100; } if (m_AgingTimer > 0) { m_AgingTimer--; if ((m_AgingTimer <= 0) && IsBaby()) { SetAge(1); m_World->BroadcastEntityMetadata(*this); } } } void cMonster::CalcLeashActions(std::chrono::milliseconds a_Dt) { // This mob just spotted in the world and [m_LeashToPos not null] shows that should be leashed to a leash knot at // m_LeashToPos. This keeps trying until knot is found. Leash knot may be in a different chunk that needn't or can't // be loaded yet. if (!IsLeashed() && (m_LeashToPos != nullptr)) { auto LeashKnot = cLeashKnot::FindKnotAtPos( *m_World, {FloorC(m_LeashToPos->x), FloorC(m_LeashToPos->y), FloorC(m_LeashToPos->z)} ); if (LeashKnot != nullptr) { LeashTo(*LeashKnot); SetLeashToPos(nullptr); } } if (!IsLeashed()) { return; } static const double CloseFollowDistance = 1.8; // The closest the mob will path towards the leashed to entity static const double LeashNaturalLength = 5.0; // The closest the mob is actively pulled towards the entity static const double LeashMaximumLength = 10.0; // Length where the leash breaks static const double LeashSpringConstant = 20.0; // How stiff the leash is const auto LeashedToPos = m_LeashedTo->GetPosition(); const auto Displacement = LeashedToPos - GetPosition(); const auto Distance = Displacement.Length(); const auto Direction = Displacement.NormalizeCopy(); // If the leash is over-extended, break the leash: if (Distance > LeashMaximumLength) { LOGD("Leash broken (distance)"); Unleash(false); return; } // If the mob isn't following close enough, pull the mob towards the leashed to entity: if (Distance > LeashNaturalLength) { // Accelerate monster towards the leashed to entity: const auto Extension = Distance - LeashNaturalLength; auto Acceleration = Direction * (Extension * LeashSpringConstant); // Stop mobs from floating up when on the ground if (IsOnGround() && (Acceleration.y < std::abs(GetGravity()))) { Acceleration.y = 0.0; } // Apply the acceleration using namespace std::chrono; AddSpeed(Acceleration * duration_cast>(a_Dt).count()); } // Passively follow the leashed to entity: if (Distance > CloseFollowDistance) { const Vector3d TargetBlock((LeashedToPos - Direction * CloseFollowDistance).Floor()); // Move to centre of target block face MoveToPosition(TargetBlock + Vector3d {0.5, 0.0, 0.5}); } } void cMonster::SetPitchAndYawFromDestination(bool a_IsFollowingPath) { Vector3d BodyDistance; if (!a_IsFollowingPath) { if (GetTarget() == nullptr) { // Avoid dirtying head position when nothing will change // Thus avoids creating unnecessary network traffic return; } BodyDistance = GetTarget()->GetPosition() - GetPosition(); } else { BodyDistance = m_NextWayPointPosition - GetPosition(); } double BodyRotation, BodyPitch; BodyDistance.Normalize(); VectorToEuler(BodyDistance.x, BodyDistance.y, BodyDistance.z, BodyRotation, BodyPitch); SetYaw(BodyRotation); Vector3d HeadDistance; if (GetTarget() != nullptr) { if (GetTarget()->IsPlayer()) // Look at a player { HeadDistance = GetTarget()->GetPosition() - GetPosition(); } else // Look at some other entity { HeadDistance = GetTarget()->GetPosition() - GetPosition(); // HeadDistance.y = GetTarget()->GetPosY() + GetHeight(); } } else // Look straight { HeadDistance = BodyDistance; HeadDistance.y = 0; } double HeadRotation, HeadPitch; HeadDistance.Normalize(); VectorToEuler(HeadDistance.x, HeadDistance.y, HeadDistance.z, HeadRotation, HeadPitch); if ((std::abs(BodyRotation - HeadRotation) < 70) && (std::abs(HeadPitch) < 60)) { SetHeadYaw(HeadRotation); SetPitch(-HeadPitch); } else { SetHeadYaw(BodyRotation); SetPitch(0); } } void cMonster::HandleFalling() { m_bTouchGround = IsOnGround(); Super::HandleFalling(); } int cMonster::FindFirstNonAirBlockPosition(double a_PosX, double a_PosZ) { auto Position = GetPosition().Floor(); Position.y = Clamp(Position.y, 0, cChunkDef::Height); if (!cBlockInfo::IsSolid(m_World->GetBlock(Position))) { while (!cBlockInfo::IsSolid(m_World->GetBlock(Position)) && (Position.y > 0)) { Position.y--; } return Position.y + 1; } else { while ((Position.y < cChunkDef::Height) && cBlockInfo::IsSolid(m_World->GetBlock(Position))) { Position.y++; } return Position.y; } } bool cMonster::DoTakeDamage(TakeDamageInfo & a_TDI) { if (!Super::DoTakeDamage(a_TDI)) { return false; } if (!m_SoundHurt.empty() && (m_Health > 0)) { m_World->BroadcastSoundEffect(m_SoundHurt, GetPosition(), 1.0f, 0.8f); } if ((a_TDI.Attacker != nullptr) && a_TDI.Attacker->IsPawn()) { if ((!a_TDI.Attacker->IsPlayer()) || (static_cast(a_TDI.Attacker)->CanMobsTarget())) { SetTarget(static_cast(a_TDI.Attacker)); } m_TicksSinceLastDamaged = 0; } return true; } void cMonster::KilledBy(TakeDamageInfo & a_TDI) { Super::KilledBy(a_TDI); if (m_SoundHurt != "") { m_World->BroadcastSoundEffect(m_SoundDeath, GetPosition(), 1.0f, 0.8f); } if (IsTame()) { if ((m_MobType == mtWolf) || (m_MobType == mtOcelot) || (m_MobType == mtCat) || (m_MobType == mtParrot)) { BroadcastDeathMessage(a_TDI); } } int Reward; switch (m_MobType) { // Animals case mtChicken: case mtCow: case mtHorse: case mtPig: case mtRabbit: case mtSheep: case mtSquid: case mtMooshroom: case mtOcelot: case mtWolf: { Reward = GetRandomProvider().RandInt(1, 3); break; } // Monsters case mtCaveSpider: case mtCreeper: case mtEnderman: case mtGhast: case mtGuardian: case mtSilverfish: case mtSkeleton: case mtSpider: case mtWitch: case mtWitherSkeleton: case mtZombie: case mtZombiePigman: case mtZombieVillager: case mtSlime: case mtMagmaCube: { Reward = GetRandomProvider().RandInt(6, 8); break; } case mtEndermite: { Reward = 3; break; } case mtBlaze: { Reward = 10; break; } // Bosses case mtEnderDragon: { Reward = 12000; break; } case mtWither: { Reward = 50; break; } default: { Reward = 0; break; } } if ((a_TDI.Attacker != nullptr) && (!IsBaby())) { m_World->SpawnSplitExperienceOrbs(GetPosX(), GetPosY(), GetPosZ(), Reward); } m_DestroyTimer = std::chrono::milliseconds(0); } void cMonster::OnRightClicked(cPlayer & a_Player) { Super::OnRightClicked(a_Player); const cItem & EquippedItem = a_Player.GetEquippedItem(); if ((EquippedItem.m_ItemType == E_ITEM_NAME_TAG) && !EquippedItem.m_CustomName.empty()) { SetCustomName(EquippedItem.m_CustomName); if (!a_Player.IsGameModeCreative()) { a_Player.GetInventory().RemoveOneEquippedItem(); } } // Using leashes m_IsLeashActionJustDone = false; if (IsLeashed() && (GetLeashedTo() == &a_Player)) // a player can only unleash a mob leashed to him { Unleash(!a_Player.IsGameModeCreative()); } else if (IsLeashed()) { // Mob is already leashed but client anticipates the server action and draws a leash link, so we need to send // current leash to cancel it m_World->BroadcastLeashEntity(*this, *this->GetLeashedTo()); } else if (CanBeLeashed() && (EquippedItem.m_ItemType == E_ITEM_LEASH)) { if (!a_Player.IsGameModeCreative()) { a_Player.GetInventory().RemoveOneEquippedItem(); } LeashTo(a_Player); } } // Checks to see if EventSeePlayer should be fired // monster sez: Do I see the player void cMonster::CheckEventSeePlayer(cChunk & a_Chunk) { if (GetTarget() != nullptr) { return; } cPlayer * TargetPlayer = nullptr; double ClosestDistance = m_SightDistance * m_SightDistance; const auto MyHeadPosition = GetPosition().addedY(GetHeight()); // Enumerate all players within sight: m_World->ForEachPlayer( [this, &TargetPlayer, &ClosestDistance, MyHeadPosition](cPlayer & a_Player) { if (!a_Player.CanMobsTarget()) { return false; } const auto TargetHeadPosition = a_Player.GetPosition().addedY(a_Player.GetHeight()); const auto TargetDistance = (TargetHeadPosition - MyHeadPosition).SqrLength(); // TODO: Currently all mobs see through lava, but only Nether-native mobs should be able to. if ((TargetDistance < ClosestDistance) && cLineBlockTracer::LineOfSightTrace( *GetWorld(), MyHeadPosition, TargetHeadPosition, cLineBlockTracer::losAirWaterLava )) { TargetPlayer = &a_Player; ClosestDistance = TargetDistance; } return false; } ); // Target him if suitable player found: if (TargetPlayer != nullptr) { EventSeePlayer(TargetPlayer, a_Chunk); } } void cMonster::CheckEventLostPlayer(const std::chrono::milliseconds a_Dt) { const auto Target = GetTarget(); if (Target == nullptr) { return; } // Check if the player died, is in creative mode, etc: if (Target->IsPlayer() && !static_cast(Target)->CanMobsTarget()) { EventLosePlayer(); return; } // Check if the target is too far away: if (!Target->GetBoundingBox().DoesIntersect({GetPosition(), m_SightDistance * 2.0})) { EventLosePlayer(); return; } const auto MyHeadPosition = GetPosition().addedY(GetHeight()); const auto TargetHeadPosition = Target->GetPosition().addedY(Target->GetHeight()); if (!cLineBlockTracer::LineOfSightTrace( *GetWorld(), MyHeadPosition, TargetHeadPosition, cLineBlockTracer::losAirWaterLava )) { if ((m_LoseSightAbandonTargetTimer += a_Dt) > std::chrono::seconds(4)) { EventLosePlayer(); } } else { // Subtract the amount of time we "handled" instead of setting to zero, so we don't ignore a large a_Dt of say, // 8s: m_LoseSightAbandonTargetTimer -= std::min(std::chrono::milliseconds(4000), m_LoseSightAbandonTargetTimer); } } // What to do if player is seen // default to change state to chasing void cMonster::EventSeePlayer(cPlayer * a_SeenPlayer, cChunk & a_Chunk) { UNUSED(a_Chunk); SetTarget(a_SeenPlayer); } void cMonster::EventLosePlayer(void) { SetTarget(nullptr); m_EMState = IDLE; m_LoseSightAbandonTargetTimer = std::chrono::seconds::zero(); } void cMonster::InStateIdle(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) { if (m_PathfinderActivated) { return; // Still getting there } m_IdleInterval += a_Dt; if (m_IdleInterval > std::chrono::seconds(1)) { auto & Random = GetRandomProvider(); // At this interval the results are predictable int rem = Random.RandInt(1, 7); m_IdleInterval -= std::chrono::seconds(1); // So nothing gets dropped when the server hangs for a few seconds Vector3d Dist; Dist.x = static_cast(Random.RandInt(-5, 5)); Dist.z = static_cast(Random.RandInt(-5, 5)); if ((Dist.SqrLength() > 2) && (rem >= 3)) { Vector3d Destination(GetPosX() + Dist.x, GetPosition().y, GetPosZ() + Dist.z); cChunk * Chunk = a_Chunk.GetNeighborChunk(static_cast(Destination.x), static_cast(Destination.z)); if ((Chunk == nullptr) || !Chunk->IsValid()) { return; } BLOCKTYPE BlockType; NIBBLETYPE BlockMeta; int RelX = static_cast(Destination.x) - Chunk->GetPosX() * cChunkDef::Width; int RelZ = static_cast(Destination.z) - Chunk->GetPosZ() * cChunkDef::Width; int YBelowUs = static_cast(Destination.y) - 1; if (YBelowUs >= 0) { Chunk->GetBlockTypeMeta(RelX, YBelowUs, RelZ, BlockType, BlockMeta); if (BlockType != E_BLOCK_STATIONARY_WATER) // Idle mobs shouldn't enter water on purpose { MoveToPosition(Destination); } } } } } // What to do if in Chasing State // This state should always be defined in each child class void cMonster::InStateChasing(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) { UNUSED(a_Dt); } // What to do if in Escaping State void cMonster::InStateEscaping(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) { UNUSED(a_Dt); if (GetTarget() != nullptr) { Vector3d newloc = GetPosition(); newloc.x = (GetTarget()->GetPosition().x < newloc.x) ? (newloc.x + m_SightDistance) : (newloc.x - m_SightDistance); newloc.z = (GetTarget()->GetPosition().z < newloc.z) ? (newloc.z + m_SightDistance) : (newloc.z - m_SightDistance); MoveToPosition(newloc); } else { m_EMState = IDLE; // This shouldnt be required but just to be safe } } void cMonster::ResetAttackCooldown() { m_AttackCoolDownTicksLeft = static_cast(TPS * m_AttackRate); // A second has 20 ticks, an attack rate of 1 means 1 hit every second } void cMonster::SetCustomName(const AString & a_CustomName) { m_CustomName = a_CustomName; // The maximal length is 64 if (a_CustomName.length() > 64) { m_CustomName = a_CustomName.substr(0, 64); } if (m_World != nullptr) { m_World->BroadcastEntityMetadata(*this); } } void cMonster::SetCustomNameAlwaysVisible(bool a_CustomNameAlwaysVisible) { m_CustomNameAlwaysVisible = a_CustomNameAlwaysVisible; if (m_World != nullptr) { m_World->BroadcastEntityMetadata(*this); } } void cMonster::GetMonsterConfig(const AString & a_Name) { cRoot::Get()->GetMonsterConfig()->AssignAttributes(this, a_Name); } bool cMonster::IsNetherNative(void) { return false; } bool cMonster::IsUndead(void) { return false; } AString cMonster::MobTypeToString(eMonsterType a_MobType) { // Mob types aren't sorted, so we need to search linearly: for (size_t i = 0; i < ARRAYCOUNT(g_MobTypeNames); i++) { if (g_MobTypeNames[i].m_Type == a_MobType) { return g_MobTypeNames[i].m_lcName; } } // Not found: return ""; } AString cMonster::MobTypeToVanillaName(eMonsterType a_MobType) { // Mob types aren't sorted, so we need to search linearly: for (size_t i = 0; i < ARRAYCOUNT(g_MobTypeNames); i++) { if (g_MobTypeNames[i].m_Type == a_MobType) { return g_MobTypeNames[i].m_VanillaName; } } // Not found: return ""; } AString cMonster::MobTypeToVanillaNBT(eMonsterType a_MobType) { // Mob types aren't sorted, so we need to search linearly: for (size_t i = 0; i < ARRAYCOUNT(g_MobTypeNames); i++) { if (g_MobTypeNames[i].m_Type == a_MobType) { return g_MobTypeNames[i].m_VanillaNameNBT; } } // Not found: return ""; } eMonsterType cMonster::StringToMobType(const AString & a_Name) { AString lcName = StrToLower(a_Name); // Search Cuberite name: for (size_t i = 0; i < ARRAYCOUNT(g_MobTypeNames); i++) { if (strcmp(g_MobTypeNames[i].m_lcName, lcName.c_str()) == 0) { return g_MobTypeNames[i].m_Type; } } // Not found. Search Vanilla name: for (size_t i = 0; i < ARRAYCOUNT(g_MobTypeNames); i++) { if (strcmp(StrToLower(g_MobTypeNames[i].m_VanillaName).c_str(), lcName.c_str()) == 0) { return g_MobTypeNames[i].m_Type; } } // Search in NBT name for (size_t i = 0; i < ARRAYCOUNT(g_MobTypeNames); i++) { if (strcmp(StrToLower(g_MobTypeNames[i].m_VanillaNameNBT).c_str(), lcName.c_str()) == 0) { return g_MobTypeNames[i].m_Type; } } // Not found: return mtInvalidType; } cMonster::eFamily cMonster::FamilyFromType(eMonsterType a_Type) { // Passive-agressive mobs are counted in mob spawning code as passive switch (a_Type) { case mtBat: return mfAmbient; case mtBlaze: return mfHostile; case mtCat: return mfPassive; case mtCaveSpider: return mfHostile; case mtChicken: return mfPassive; case mtCod: return mfWater; case mtCow: return mfPassive; case mtCreeper: return mfHostile; case mtDolphin: return mfWater; case mtDonkey: return mfPassive; case mtDrowned: return mfHostile; case mtElderGuardian: return mfHostile; case mtEnderDragon: return mfNoSpawn; case mtEnderman: return mfHostile; case mtEndermite: return mfHostile; case mtEvoker: return mfHostile; case mtFox: return mfPassive; case mtGhast: return mfHostile; case mtGiant: return mfNoSpawn; case mtGuardian: return mfWater; // Just because they have special spawning conditions. TODO: If Watertemples have been // added, this needs to be edited! case mtHoglin: return mfHostile; case mtHorse: return mfPassive; case mtHusk: return mfHostile; case mtIllusioner: return mfHostile; case mtIronGolem: return mfPassive; case mtLlama: return mfPassive; case mtMagmaCube: return mfHostile; case mtMooshroom: return mfPassive; case mtMule: return mfPassive; case mtOcelot: return mfPassive; case mtPanda: return mfPassive; case mtParrot: return mfPassive; case mtPhantom: return mfHostile; case mtPig: return mfPassive; case mtPiglin: return mfHostile; case mtPiglinBrute: return mfHostile; case mtPillager: return mfHostile; case mtPolarBear: return mfPassive; case mtPufferfish: return mfWater; case mtRabbit: return mfPassive; case mtRavager: return mfHostile; case mtSalmon: return mfWater; case mtSheep: return mfPassive; case mtShulker: return mfHostile; case mtSilverfish: return mfHostile; case mtSkeleton: return mfHostile; case mtSkeletonHorse: return mfPassive; case mtSlime: return mfHostile; case mtSnowGolem: return mfNoSpawn; case mtSpider: return mfHostile; case mtSquid: return mfWater; case mtStray: return mfHostile; case mtStrider: return mfHostile; case mtTraderLlama: return mfPassive; case mtTropicalFish: return mfWater; case mtTurtle: return mfWater; // I'm not quite sure case mtVex: return mfHostile; case mtVindicator: return mfHostile; case mtVillager: return mfPassive; case mtWanderingTrader: return mfPassive; case mtWitch: return mfHostile; case mtWither: return mfNoSpawn; case mtWitherSkeleton: return mfHostile; case mtWolf: return mfPassive; case mtZoglin: return mfHostile; case mtZombie: return mfHostile; case mtZombieHorse: return mfPassive; case mtZombiePigman: return mfHostile; case mtZombieVillager: return mfHostile; case mtInvalidType: break; } UNREACHABLE("Unhandled mob type"); } cTickTime cMonster::GetSpawnDelay(cMonster::eFamily a_MobFamily) { switch (a_MobFamily) { case mfHostile: return 40_tick; case mfPassive: return 40_tick; case mfAmbient: return 40_tick; case mfWater: return 400_tick; case mfNoSpawn: return -1_tick; } UNREACHABLE("Unhandled mob family"); } /** Sets the target. */ void cMonster::SetTarget(cPawn * a_NewTarget) { ASSERT((a_NewTarget == nullptr) || (IsTicking())); if (m_Target == a_NewTarget) { return; } cPawn * OldTarget = m_Target; m_Target = a_NewTarget; if (OldTarget != nullptr) { // Notify the old target that we are no longer targeting it. OldTarget->NoLongerTargetingMe(this); } if (a_NewTarget != nullptr) { ASSERT(a_NewTarget->IsTicking()); // Notify the new target that we are now targeting it. m_Target->TargetingMe(this); m_WasLastTargetAPlayer = m_Target->IsPlayer(); } } void cMonster::UnsafeUnsetTarget() { m_Target = nullptr; } cPawn * cMonster::GetTarget() { return m_Target; } std::unique_ptr cMonster::NewMonsterFromType(eMonsterType a_MobType) { auto & Random = GetRandomProvider(); // Create the mob entity switch (a_MobType) { case mtMagmaCube: { return std::make_unique(1 << Random.RandInt(2)); // Size 1, 2 or 4 } case mtSlime: { return std::make_unique(1 << Random.RandInt(2)); // Size 1, 2 or 4 } case mtVillager: return std::make_unique(cVillager::GetRandomProfession()); case mtHorse: { // Horses take a type (species), a colour, and a style (dots, stripes, etc.) int HorseType = Random.RandInt(7); int HorseColor = Random.RandInt(6); int HorseStyle = Random.RandInt(4); int HorseTameTimes = Random.RandInt(1, 6); if ((HorseType == 5) || (HorseType == 6) || (HorseType == 7)) { // Increase chances of normal horse (zero) HorseType = 0; } return std::make_unique(HorseType, HorseColor, HorseStyle, HorseTameTimes); } case mtZombieVillager: { return std::make_unique(cVillager::GetRandomProfession()); } case mtBat: return std::make_unique(); case mtBlaze: return std::make_unique(); case mtCaveSpider: return std::make_unique(); case mtChicken: return std::make_unique(); case mtCow: return std::make_unique(); case mtCreeper: return std::make_unique(); case mtEnderDragon: return std::make_unique(); case mtEndermite: return std::make_unique(); case mtEnderman: return std::make_unique(); case mtGhast: return std::make_unique(); case mtGiant: return std::make_unique(); case mtGuardian: return std::make_unique(); case mtIronGolem: return std::make_unique(); case mtMooshroom: return std::make_unique(); case mtOcelot: return std::make_unique(); case mtPig: return std::make_unique(); case mtRabbit: return std::make_unique(); case mtSheep: return std::make_unique(); case mtSilverfish: return std::make_unique(); case mtSkeleton: return std::make_unique(); case mtSnowGolem: return std::make_unique(); case mtSpider: return std::make_unique(); case mtSquid: return std::make_unique(); case mtWitch: return std::make_unique(); case mtWither: return std::make_unique(); case mtWitherSkeleton: return std::make_unique(); case mtWolf: return std::make_unique(); case mtZombie: return std::make_unique(); case mtZombiePigman: return std::make_unique(); default: { ASSERT(!"Unhandled mob type whilst trying to spawn mob!"); return nullptr; } } } void cMonster::EngageLoveMode(cMonster * a_Partner) { m_LovePartner = a_Partner; m_MatingTimer = 50; // about 3 seconds of mating } void cMonster::ResetLoveMode() { m_LovePartner = nullptr; m_LoveTimer = 0; m_MatingTimer = 0; m_LoveCooldown = TPS * 60 * 5; // 5 minutes // when an animal is in love mode, the client only stops sending the hearts if we let them know it's in cooldown, // which is done with the "age" metadata m_World->BroadcastEntityMetadata(*this); } void cMonster::LoveTick(void) { // if we have a partner, mate if (m_LovePartner != nullptr) { if (m_MatingTimer > 0) { // If we should still mate, keep bumping into them until baby is made Vector3d Pos = m_LovePartner->GetPosition(); MoveToPosition(Pos); } else { // Mating finished. Spawn baby Vector3f Pos = (GetPosition() + m_LovePartner->GetPosition()) * 0.5; UInt32 BabyID = m_World->SpawnMob(Pos.x, Pos.y, Pos.z, GetMobType(), true); cMonster * Baby = nullptr; m_World->DoWithEntityByID( BabyID, [&](cEntity & a_Entity) { Baby = static_cast(&a_Entity); return true; } ); if (Baby != nullptr) { Baby->InheritFromParents(this, m_LovePartner); } m_World->SpawnExperienceOrb(Pos.x, Pos.y, Pos.z, GetRandomProvider().RandInt(1, 6)); m_World->DoWithPlayerByUUID( m_Feeder, [&](cPlayer & a_Player) { a_Player.GetStatistics().Custom[CustomStatistic::AnimalsBred]++; if (GetMobType() == eMonsterType::mtCow) { a_Player.AwardAchievement(CustomStatistic::AchBreedCow); } return true; } ); m_LovePartner->ResetLoveMode(); ResetLoveMode(); } } else { // We have no partner, so we just chase the player if they have our breeding item cItems FollowedItems; GetFollowedItems(FollowedItems); if (FollowedItems.Size() > 0) { m_World->DoWithNearestPlayer( GetPosition(), static_cast(m_SightDistance), [&](cPlayer & a_Player) -> bool { const cItem & EquippedItem = a_Player.GetEquippedItem(); if (FollowedItems.ContainsType(EquippedItem)) { Vector3d PlayerPos = a_Player.GetPosition(); MoveToPosition(PlayerPos); } return true; } ); } } // If we are in love mode but we have no partner, search for a partner neabry if (m_LoveTimer > 0) { if (m_LovePartner == nullptr) { m_World->ForEachEntityInBox( cBoundingBox(GetPosition(), 8, 8), [=](cEntity & a_Entity) { // If the entity is not a monster, don't breed with it // Also, do not self-breed if ((a_Entity.GetEntityType() != etMonster) || (&a_Entity == this)) { return false; } auto & Me = static_cast(*this); auto & PotentialPartner = static_cast(a_Entity); // If the potential partner is not of the same species, don't breed with it if (PotentialPartner.GetMobType() != Me.GetMobType()) { return false; } // If the potential partner is not in love // Or they already have a mate, do not breed with them if ((!PotentialPartner.IsInLove()) || (PotentialPartner.GetPartner() != nullptr)) { return false; } // All conditions met, let's breed! PotentialPartner.EngageLoveMode(&Me); Me.EngageLoveMode(&PotentialPartner); return true; } ); } m_LoveTimer--; } if (m_MatingTimer > 0) { m_MatingTimer--; } if (m_LoveCooldown > 0) { m_LoveCooldown--; } } void cMonster::RightClickFeed(cPlayer & a_Player) { const cItem & EquippedItem = a_Player.GetEquippedItem(); // If a player holding breeding items right-clicked me, go into love mode if ((m_LoveCooldown == 0) && !IsInLove() && !IsBaby()) { cItems Items; GetBreedingItems(Items); if (Items.ContainsType(EquippedItem.m_ItemType)) { if (!a_Player.IsGameModeCreative()) { a_Player.GetInventory().RemoveOneEquippedItem(); } m_LoveTimer = TPS * 30; // half a minute m_World->BroadcastEntityAnimation(*this, EntityAnimation::AnimalFallsInLove); } } // If a player holding my spawn egg right-clicked me, spawn a new baby if (EquippedItem.m_ItemType == E_ITEM_SPAWN_EGG) { eMonsterType MonsterType = cItemSpawnEggHandler::ItemDamageToMonsterType(EquippedItem.m_ItemDamage); if ((MonsterType == m_MobType) && (m_World->SpawnMob(GetPosX(), GetPosY(), GetPosZ(), m_MobType, true) != cEntity::INVALID_ID ) // Spawning succeeded ) { if (!a_Player.IsGameModeCreative()) { // The mob was spawned, "use" the item: a_Player.GetInventory().RemoveOneEquippedItem(); } } } // Stores feeder UUID for statistic tracking m_Feeder = a_Player.GetUUID(); } void cMonster::AddRandomDropItem( cItems & a_Drops, unsigned int a_Min, unsigned int a_Max, short a_Item, short a_ItemHealth ) { auto Count = GetRandomProvider().RandInt(a_Min, a_Max); auto MaxStackSize = static_cast(cItem(a_Item).GetMaxStackSize()); while (Count > MaxStackSize) { a_Drops.emplace_back(a_Item, MaxStackSize, a_ItemHealth); Count -= MaxStackSize; } if (Count > 0) { a_Drops.emplace_back(a_Item, Count, a_ItemHealth); } } void cMonster::AddRandomUncommonDropItem(cItems & a_Drops, float a_Chance, short a_Item, short a_ItemHealth) { if (GetRandomProvider().RandBool(a_Chance / 100.0)) { a_Drops.emplace_back(a_Item, static_cast(1), a_ItemHealth); } } void cMonster::AddRandomRareDropItem(cItems & a_Drops, cItems & a_Items, unsigned int a_LootingLevel) { auto & r1 = GetRandomProvider(); if (r1.RandBool((5 + a_LootingLevel) / 200.0)) { size_t Rare = r1.RandInt(a_Items.Size() - 1); a_Drops.push_back(a_Items.at(Rare)); } } void cMonster::AddRandomArmorDropItem(cItems & a_Drops, unsigned int a_LootingLevel) { auto & r1 = GetRandomProvider(); double LootingBonus = a_LootingLevel / 100.0; if (r1.RandBool(m_DropChanceHelmet + LootingBonus)) { if (!GetEquippedHelmet().IsEmpty()) { a_Drops.push_back(GetEquippedHelmet()); } } if (r1.RandBool(m_DropChanceChestplate + LootingBonus)) { if (!GetEquippedChestplate().IsEmpty()) { a_Drops.push_back(GetEquippedChestplate()); } } if (r1.RandBool(m_DropChanceLeggings + LootingBonus)) { if (!GetEquippedLeggings().IsEmpty()) { a_Drops.push_back(GetEquippedLeggings()); } } if (r1.RandBool(m_DropChanceBoots + LootingBonus)) { if (!GetEquippedBoots().IsEmpty()) { a_Drops.push_back(GetEquippedBoots()); } } } void cMonster::AddRandomWeaponDropItem(cItems & a_Drops, unsigned int a_LootingLevel) { if (GetRandomProvider().RandBool(m_DropChanceWeapon + (a_LootingLevel / 100.0))) { if (!GetEquippedWeapon().IsEmpty()) { a_Drops.push_back(GetEquippedWeapon()); } } } void cMonster::HandleDaylightBurning(cChunk & a_Chunk, bool WouldBurn) { if (!m_BurnsInDaylight) { return; } int RelY = POSY_TOINT; if ((RelY < 0) || (RelY >= cChunkDef::Height)) { // Outside the world return; } if (!a_Chunk.IsLightValid()) { m_World->QueueLightChunk(GetChunkX(), GetChunkZ()); return; } if (!IsOnFire() && WouldBurn) { // Burn for 100 ticks, then decide again StartBurning(100); } } bool cMonster::WouldBurnAt(Vector3d a_Location, cChunk & a_Chunk) { // If the Y coord is out of range, return the most logical result without considering anything else: int RelY = FloorC(a_Location.y); if (RelY >= cChunkDef::Height) { // Always burn above the world return true; } if (RelY <= 0) { // The mob is about to die, no point in burning return false; } PREPARE_REL_AND_CHUNK(a_Location, a_Chunk); if (!RelSuccess) { return false; } if ((Chunk->GetBlock(Rel) != E_BLOCK_SOULSAND) && // Not on soulsand (GetWorld()->GetTimeOfDay() < 13000_tick) && // Daytime Chunk->IsWeatherSunnyAt(Rel.x, Rel.z) && // Not raining !IsInWater() // Isn't swimming ) { int MobHeight = CeilC(a_Location.y + GetHeight()) - 1; // The block Y coord of the mob's head if (MobHeight >= cChunkDef::Height) { return true; } // Start with the highest block and scan down to just above the mob's head. // If a non transparent is found, return false (do not burn). Otherwise return true. // Note that this loop is not a performance concern as transparent blocks are rare and the loop almost always // bailes out instantly.(An exception is e.g. standing under a long column of glass). int CurrentBlock = Chunk->GetHeight(Rel.x, Rel.z); while (CurrentBlock > MobHeight) { BLOCKTYPE Block = Chunk->GetBlock(Rel.x, CurrentBlock, Rel.z); if ( // Do not burn if a block above us meets one of the following conditions: (!cBlockInfo::IsTransparent(Block)) || (Block == E_BLOCK_LEAVES) || (Block == E_BLOCK_NEW_LEAVES) || (IsBlockWater(Block)) ) { return false; } --CurrentBlock; } return true; } return false; } cMonster::eFamily cMonster::GetMobFamily(void) const { return FamilyFromType(m_MobType); } void cMonster::LeashTo(cEntity & a_Entity, bool a_ShouldBroadcast) { // Do nothing if already leashed if (m_LeashedTo != nullptr) { return; } m_LeashedTo = &a_Entity; a_Entity.AddLeashedMob(this); if (a_ShouldBroadcast) { m_World->BroadcastLeashEntity(*this, a_Entity); } m_IsLeashActionJustDone = true; } void cMonster::Unleash(bool a_ShouldDropLeashPickup, bool a_ShouldBroadcast) { // Do nothing if not leashed if (m_LeashedTo == nullptr) { return; } m_LeashedTo->RemoveLeashedMob(this); m_LeashedTo = nullptr; if (a_ShouldDropLeashPickup) { cItems Pickups; Pickups.Add(cItem(E_ITEM_LEASH, 1, 0)); GetWorld()->SpawnItemPickups(Pickups, GetPosX() + 0.5, GetPosY() + 0.5, GetPosZ() + 0.5); } if (a_ShouldBroadcast) { m_World->BroadcastUnleashEntity(*this); } m_IsLeashActionJustDone = true; } void cMonster::Unleash(bool a_ShouldDropLeashPickup) { Unleash(a_ShouldDropLeashPickup, true); }