diff --git a/Minecraft.World/Minecraft.World.vcxproj b/Minecraft.World/Minecraft.World.vcxproj
index bae4d2733..464d40378 100644
--- a/Minecraft.World/Minecraft.World.vcxproj
+++ b/Minecraft.World/Minecraft.World.vcxproj
@@ -2445,6 +2445,7 @@
+
@@ -2468,6 +2469,8 @@
+
+
@@ -2847,6 +2850,8 @@
+
+
@@ -3519,6 +3524,7 @@
+
@@ -3540,6 +3546,8 @@
+
+
@@ -3649,6 +3657,8 @@
+
+
diff --git a/Minecraft.World/Minecraft.World.vcxproj.filters b/Minecraft.World/Minecraft.World.vcxproj.filters
index ec5b3cb68..101c163f0 100644
--- a/Minecraft.World/Minecraft.World.vcxproj.filters
+++ b/Minecraft.World/Minecraft.World.vcxproj.filters
@@ -3203,6 +3203,21 @@
net\minecraft\world\level\tile
+
+ net\minecraft\world\entity\ai\goal\target
+
+
+ net\minecraft\world\entity\ai\goal\target
+
+
+ net\minecraft\world\entity\ai\goal\target
+
+
+ net\minecraft\world\entity\ai\goal\target
+
+
+ net\minecraft\world\level\biome
+
@@ -5638,5 +5653,20 @@
net\minecraft\world\level\tile
+
+ net\minecraft\world\entity\ai\goal\target
+
+
+ net\minecraft\world\entity\ai\goal\target
+
+
+ net\minecraft\world\entity\ai\goal\target
+
+
+ net\minecraft\world\entity\ai\goal\target
+
+
+ net\minecraft\world\level\biome
+
\ No newline at end of file
diff --git a/Minecraft.World/OwnerHurtByTargetGoal.cpp b/Minecraft.World/OwnerHurtByTargetGoal.cpp
index 787eb2aec..129680e09 100644
--- a/Minecraft.World/OwnerHurtByTargetGoal.cpp
+++ b/Minecraft.World/OwnerHurtByTargetGoal.cpp
@@ -32,5 +32,13 @@ void OwnerHurtByTargetGoal::start()
timestamp = owner->getLastHurtByMobTimestamp();
}
+ tameAnimal->onTameAggroStarted(owner->getLastHurtByMob());
+
TargetGoal::start();
-}
\ No newline at end of file
+}
+
+void OwnerHurtByTargetGoal::stop()
+{
+ tameAnimal->onTameAggroStopped();
+ TargetGoal::stop();
+}
diff --git a/Minecraft.World/OwnerHurtByTargetGoal.h b/Minecraft.World/OwnerHurtByTargetGoal.h
index 2cdc93781..15d7b822c 100644
--- a/Minecraft.World/OwnerHurtByTargetGoal.h
+++ b/Minecraft.World/OwnerHurtByTargetGoal.h
@@ -16,4 +16,5 @@ public:
bool canUse();
void start();
+ void stop();
};
\ No newline at end of file
diff --git a/Minecraft.World/PrioritizeOwnerThreatGoal.cpp b/Minecraft.World/PrioritizeOwnerThreatGoal.cpp
new file mode 100644
index 000000000..4aa43295b
--- /dev/null
+++ b/Minecraft.World/PrioritizeOwnerThreatGoal.cpp
@@ -0,0 +1,75 @@
+#include "stdafx.h"
+#include "net.minecraft.world.entity.h"
+#include "net.minecraft.world.entity.animal.h"
+#include "PrioritizeOwnerThreatGoal.h"
+
+PrioritizeOwnerThreatGoal::PrioritizeOwnerThreatGoal(TamableAnimal* tameAnimal)
+ : TargetGoal(tameAnimal, false)
+{
+ this->tameAnimal = tameAnimal;
+ this->timestamp = 0;
+ setRequiredControlFlags(TargetGoal::TargetFlag);
+}
+
+// ---------------------------------------------------------------------------
+bool PrioritizeOwnerThreatGoal::canUse()
+{
+ // Только прирученный и не сидящий
+ if (!tameAnimal->isTame()) return false;
+ if (tameAnimal->isSitting()) return false;
+
+ // Этот goal — именно ПЕРЕКЛЮЧАТЕЛЬ, а не первичный выбор цели.
+ // Если текущей цели нет — пусть этим занимаются
+ // HurtByTargetGoal / TameAnimalDefendGoal
+ shared_ptr currentTarget = mob->getTarget();
+ if (currentTarget == nullptr || !currentTarget->isAlive()) return false;
+
+ // Нужен живой хозяин
+ shared_ptr owner =
+ dynamic_pointer_cast(tameAnimal->getOwner());
+ if (owner == nullptr || !owner->isAlive()) return false;
+
+ // Кто последний ударил хозяина?
+ shared_ptr ownerAttacker = owner->getLastHurtByMob();
+ int ts = owner->getLastHurtByMobTimestamp();
+
+ // Ничего нового с момента последней обработки
+ if (ts == timestamp) return false;
+
+ // Уже атакуем именно эту угрозу — незачем переключаться
+ if (ownerAttacker == currentTarget) return false;
+
+ // Стандартные проверки: цель жива, не союзник, видна и т.д.
+ if (!canAttack(ownerAttacker, false)) return false;
+
+ // Питомец-специфичный фильтр (Wolf: не атаковать Creeper,
+ // питомцев того же хозяина, PvP-флаг и т.д.)
+ if (!tameAnimal->wantsToAttack(ownerAttacker, owner)) return false;
+
+ // Переключаемся только если новая угроза ощутимо ближе к хозяину,
+ // чем текущая цель — иначе не стоит бросать начатое
+ double distToOwnerSq = owner->distanceToSqr(ownerAttacker);
+ double currentDistToOwnerSq = owner->distanceToSqr(currentTarget);
+
+ if (distToOwnerSq >= currentDistToOwnerSq - SWITCH_DIST_ADVANTAGE_SQ)
+ return false;
+
+ newThreat = weak_ptr(ownerAttacker);
+ return true;
+}
+
+// ---------------------------------------------------------------------------
+void PrioritizeOwnerThreatGoal::start()
+{
+ shared_ptr target = newThreat.lock();
+ mob->setTarget(target);
+ tameAnimal->onTameAggroStarted(target); // Wolf: setAngry(true)
+
+ // Запомнить timestamp чтобы не обрабатывать это событие повторно
+ shared_ptr owner =
+ dynamic_pointer_cast(tameAnimal->getOwner());
+ if (owner != nullptr)
+ timestamp = owner->getLastHurtByMobTimestamp();
+
+ TargetGoal::start();
+}
diff --git a/Minecraft.World/PrioritizeOwnerThreatGoal.h b/Minecraft.World/PrioritizeOwnerThreatGoal.h
new file mode 100644
index 000000000..9569f3844
--- /dev/null
+++ b/Minecraft.World/PrioritizeOwnerThreatGoal.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "TargetGoal.h"
+
+class TamableAnimal;
+
+// Если волк уже дерётся с мобом A, но хозяина начал бить моб B —
+// переключиться на B. Работает только при наличии активной цели,
+// иначе уступает TameAnimalDefendGoal и HurtByTargetGoal.
+class PrioritizeOwnerThreatGoal : public TargetGoal
+{
+private:
+ TamableAnimal* tameAnimal;
+ weak_ptr newThreat;
+
+ // Запоминаем timestamp последней обработанной атаки на хозяина,
+ // чтобы не переключаться на одну и ту же угрозу дважды
+ int timestamp;
+
+ // Минимальная разница дистанций (в блоках²), при которой
+ // новая угроза считается "ближе" и стоит переключиться.
+ // Защищает от дёрганья между двумя равноудалёнными целями.
+ static constexpr double SWITCH_DIST_ADVANTAGE_SQ = 4.0 * 4.0;
+
+public:
+ PrioritizeOwnerThreatGoal(TamableAnimal* tameAnimal);
+
+ bool canUse() override;
+ void start() override;
+};
+
diff --git a/Minecraft.World/TamableAnimal.h b/Minecraft.World/TamableAnimal.h
index 7d17a43c4..5c456cef9 100644
--- a/Minecraft.World/TamableAnimal.h
+++ b/Minecraft.World/TamableAnimal.h
@@ -36,6 +36,8 @@ public:
virtual void setOwnerUUID(const wstring &name);
virtual shared_ptr getOwner();
virtual SitGoal *getSitGoal();
+ virtual void onTameAggroStarted(shared_ptr target) {}
+ virtual void onTameAggroStopped() {}
bool wantsToAttack(shared_ptr target, shared_ptr owner);
Team *getTeam();
bool isAlliedTo(shared_ptr other);
diff --git a/Minecraft.World/TameAnimalDefendGoal.cpp b/Minecraft.World/TameAnimalDefendGoal.cpp
new file mode 100644
index 000000000..547190cb6
--- /dev/null
+++ b/Minecraft.World/TameAnimalDefendGoal.cpp
@@ -0,0 +1,88 @@
+#include "stdafx.h"
+#include "net.minecraft.world.entity.h"
+#include "net.minecraft.world.entity.animal.h"
+#include "net.minecraft.world.entity.monster.h"
+#include "net.minecraft.world.phys.h"
+#include "TameAnimalDefendGoal.h"
+#include "Level.h"
+
+TameAnimalDefendGoal::TameAnimalDefendGoal(TamableAnimal* tameAnimal, bool mustSee)
+ : TargetGoal(tameAnimal, mustSee)
+{
+ this->tameAnimal = tameAnimal;
+ setRequiredControlFlags(TargetGoal::TargetFlag);
+}
+
+bool TameAnimalDefendGoal::canUse()
+{
+ //
+ if (!tameAnimal->isTame()) return false;
+ if (tameAnimal->isSitting()) return false;
+
+ // ( )
+ if (mob->getTarget() != nullptr && mob->getTarget()->isAlive()) return false;
+
+ shared_ptr owner =
+ dynamic_pointer_cast(tameAnimal->getOwner());
+ // owner nullptr (-) , wantsToAttack
+ //
+
+ double range = getFollowDistance();
+
+ // Monster-
+ vector>* candidates = mob->level->getEntitiesOfClass(
+ typeid(Monster),
+ mob->bb->grow(range, 4.0, range),
+ nullptr // canAttack
+ );
+
+ if (candidates == nullptr || candidates->empty())
+ {
+ delete candidates;
+ return false;
+ }
+
+ shared_ptr best = nullptr;
+ double bestSq = DBL_MAX;
+
+ for (auto& e : *candidates)
+ {
+ shared_ptr le = dynamic_pointer_cast(e);
+ if (!le) continue;
+
+ // , ,
+ if (!canAttack(le, false)) continue;
+
+ //
+ // ( , ..)
+ if (owner != nullptr && !tameAnimal->wantsToAttack(le, owner)) continue;
+
+ double distSq = mob->distanceToSqr(le);
+ if (distSq < bestSq)
+ {
+ bestSq = distSq;
+ best = le;
+ }
+ }
+
+ delete candidates;
+
+ if (!best) return false;
+
+ nearestHostile = weak_ptr(best);
+ return true;
+}
+
+void TameAnimalDefendGoal::start()
+{
+ shared_ptr target = nearestHostile.lock();
+ mob->setTarget(target);
+ tameAnimal->onTameAggroStarted(target);
+ TargetGoal::start();
+}
+
+void TameAnimalDefendGoal::stop()
+{
+ tameAnimal->onTameAggroStopped();
+ TargetGoal::stop();
+}
diff --git a/Minecraft.World/TameAnimalDefendGoal.h b/Minecraft.World/TameAnimalDefendGoal.h
new file mode 100644
index 000000000..8f225071a
--- /dev/null
+++ b/Minecraft.World/TameAnimalDefendGoal.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "TargetGoal.h"
+
+class TamableAnimal;
+
+//
+class TameAnimalDefendGoal : public TargetGoal
+{
+private:
+ TamableAnimal* tameAnimal;
+ weak_ptr nearestHostile;
+
+public:
+ // mustSee=true:
+ // scanRadius: 0, FOLLOW_RANGE
+ TameAnimalDefendGoal(TamableAnimal* tameAnimal, bool mustSee);
+
+ bool canUse() override;
+ void start() override;
+ void stop() override;
+};
diff --git a/Minecraft.World/Wolf.cpp b/Minecraft.World/Wolf.cpp
index 7588a67b0..cbfa46a2b 100644
--- a/Minecraft.World/Wolf.cpp
+++ b/Minecraft.World/Wolf.cpp
@@ -20,6 +20,7 @@
#include "Wolf.h"
#include "..\Minecraft.Client\Textures.h"
#include "SoundTypes.h"
+#include "PrimedTnt.h"
@@ -39,20 +40,28 @@ Wolf::Wolf(Level *level) : TamableAnimal( level )
getNavigation()->setAvoidWater(true);
goalSelector.addGoal(1, new FloatGoal(this));
- goalSelector.addGoal(2, sitGoal, false);
- goalSelector.addGoal(3, new LeapAtTargetGoal(this, 0.4));
- goalSelector.addGoal(4, new MeleeAttackGoal(this, 1.0, true));
- goalSelector.addGoal(5, new FollowOwnerGoal(this, 1.0, 10, 2));
- goalSelector.addGoal(6, new BreedGoal(this, 1.0));
- goalSelector.addGoal(7, new RandomStrollGoal(this, 1.0));
- goalSelector.addGoal(8, new BegGoal(this, 8));
- goalSelector.addGoal(9, new LookAtPlayerGoal(this, typeid(Player), 8));
- goalSelector.addGoal(9, new RandomLookAroundGoal(this));
+ goalSelector.addGoal(2, new WolfRetreatGoal(this, 0.25f, 1.5f));
+ goalSelector.addGoal(3, new AvoidPlayerGoal(this, typeid(Creeper), 12, 1.25, 1.25));
+ goalSelector.addGoal(4, new AvoidPlayerGoal(this, typeid(PrimedTnt), 12, 1.25, 1.25));
+ goalSelector.addGoal(5, sitGoal, false);
+ goalSelector.addGoal(6, new LeapAtTargetGoal(this, 0.4));
+ goalSelector.addGoal(7, new MeleeAttackGoal(this, 1.0, true));
+ goalSelector.addGoal(8, new FollowOwnerGoal(this, 1.0, 10, 2));
+ goalSelector.addGoal(9, new BreedGoal(this, 1.0));
+ goalSelector.addGoal(10, new RandomStrollGoal(this, 1.0));
+ goalSelector.addGoal(11, new BegGoal(this, 8));
+ goalSelector.addGoal(12, new LookAtPlayerGoal(this, typeid(Player), 8));
+ goalSelector.addGoal(13, new LookAtPlayerGoal(this, typeid(Wolf), 8));
+ goalSelector.addGoal(14, new RandomLookAroundGoal(this));
- targetSelector.addGoal(1, new OwnerHurtByTargetGoal(this));
- targetSelector.addGoal(2, new OwnerHurtTargetGoal(this));
- targetSelector.addGoal(3, new HurtByTargetGoal(this, true));
- targetSelector.addGoal(4, new NonTameRandomTargetGoal(this, typeid(Sheep), 200, false));
+ targetSelector.addGoal(1, new HurtByTargetGoal(this, true));
+ targetSelector.addGoal(2, new OwnerHurtByTargetGoal(this));
+ targetSelector.addGoal(3, new PrioritizeOwnerThreatGoal(this));
+ targetSelector.addGoal(4, new OwnerHurtTargetGoal(this));
+ targetSelector.addGoal(5, new PackDistributedTargetGoal(this, true, 20.0f));
+ targetSelector.addGoal(6, new TameAnimalDefendGoal(this, true));
+ targetSelector.addGoal(7, new NonTameRandomTargetGoal(this, typeid(Sheep), 200, false));
+ targetSelector.addGoal(8, new NonTameRandomTargetGoal(this, typeid(Skeleton), 200, false));
setTame(false); // Initialize health
}
@@ -81,14 +90,20 @@ bool Wolf::useNewAi()
void Wolf::setTarget(shared_ptr target)
{
TamableAnimal::setTarget(target);
- if ( target == nullptr )
- {
+ if (target == nullptr)
setAngry(false);
- }
- else if(!isTame())
- {
+ else
setAngry(true);
- }
+}
+
+void Wolf::onTameAggroStarted(shared_ptr target)
+{
+ setAngry(true);
+}
+
+void Wolf::onTameAggroStopped()
+{
+ setAngry(false);
}
void Wolf::serverAiMobStep()
@@ -474,8 +489,7 @@ bool Wolf::isFood(shared_ptr item)
int Wolf::getMaxSpawnClusterSize()
{
- // 4J - changed - was 8 but we have a limit of only 8 wolves in the world so doesn't seem right potentially spawning them all in once cluster
- return 4;
+ return 8;
}
bool Wolf::isAngry()
diff --git a/Minecraft.World/Wolf.h b/Minecraft.World/Wolf.h
index edfd38146..823385e05 100644
--- a/Minecraft.World/Wolf.h
+++ b/Minecraft.World/Wolf.h
@@ -76,6 +76,9 @@ public:
// For tooltips
int GetSynchedHealth();
+ virtual void onTameAggroStarted(shared_ptr target) override;
+ virtual void onTameAggroStopped() override;
+
protected:
virtual shared_ptr getBreedOffspring(shared_ptr target);
diff --git a/Minecraft.World/WolfPackDistributedTargetGoal.cpp b/Minecraft.World/WolfPackDistributedTargetGoal.cpp
new file mode 100644
index 000000000..062812542
--- /dev/null
+++ b/Minecraft.World/WolfPackDistributedTargetGoal.cpp
@@ -0,0 +1,182 @@
+#include "stdafx.h"
+#include "net.minecraft.world.entity.h"
+#include "net.minecraft.world.entity.animal.h"
+#include "net.minecraft.world.entity.monster.h"
+#include "net.minecraft.world.phys.h"
+#include "Level.h"
+#include "Wolf.h"
+#include "WolfPackDistributedTargetGoal.h"
+
+// ---------------------------------------------------------------------------
+PackDistributedTargetGoal::PackDistributedTargetGoal(
+ TamableAnimal* tameAnimal,
+ bool mustSee,
+ float maxChaseDistFromOwner)
+ : TargetGoal(tameAnimal, mustSee)
+{
+ this->tameAnimal = tameAnimal;
+ this->maxChaseDistFromOwner = maxChaseDistFromOwner;
+ setRequiredControlFlags(TargetGoal::TargetFlag);
+}
+
+// ---------------------------------------------------------------------------
+vector> PackDistributedTargetGoal::getPackmates(
+ double range)
+{
+ vector> result;
+
+ wstring ownerUUID = tameAnimal->getOwnerUUID();
+ if (ownerUUID.empty()) return result;
+
+ // Ищем всех волков в радиусе
+ vector>* nearby = mob->level->getEntitiesOfClass(
+ typeid(Wolf),
+ mob->bb->grow(range, 4.0, range),
+ nullptr);
+
+ if (nearby == nullptr)
+ return result;
+
+ for (auto& e : *nearby)
+ {
+ // Пропустить самого себя
+ if (e.get() == mob) continue;
+
+ shared_ptr wolf = dynamic_pointer_cast(e);
+ if (wolf == nullptr) continue;
+ if (!wolf->isTame()) continue;
+
+ // Только волки того же хозяина
+ if (wolf->getOwnerUUID() != ownerUUID) continue;
+
+ result.push_back(wolf);
+ }
+
+ delete nearby;
+ return result;
+}
+
+// ---------------------------------------------------------------------------
+int PackDistributedTargetGoal::countAssignments(
+ const vector>& packmates,
+ shared_ptr target)
+{
+ if (target == nullptr) return 0;
+
+ int count = 0;
+ for (auto& packmate : packmates)
+ {
+ shared_ptr packTarget =
+ dynamic_pointer_cast(
+ dynamic_pointer_cast(packmate) != nullptr
+ ? dynamic_pointer_cast(packmate)->getTarget()
+ : nullptr);
+
+ if (packTarget != nullptr && packTarget.get() == target.get())
+ count++;
+ }
+ return count;
+}
+
+// ---------------------------------------------------------------------------
+shared_ptr PackDistributedTargetGoal::pickBestTarget(
+ const vector>* candidates,
+ const vector>& packmates,
+ bool respectLimit)
+{
+ shared_ptr best = nullptr;
+ double bestScore = DBL_MAX;
+
+ shared_ptr owner =
+ dynamic_pointer_cast(tameAnimal->getOwner());
+
+ for (auto& e : *candidates)
+ {
+ shared_ptr le = dynamic_pointer_cast(e);
+ if (le == nullptr) continue;
+ if (!canAttack(le, false)) continue;
+ if (owner != nullptr && !tameAnimal->wantsToAttack(le, owner)) continue;
+
+ int assigned = countAssignments(packmates, le);
+
+ // Фаза 1: уважаем жёсткий лимит
+ if (respectLimit && assigned >= MAX_WOLVES_PER_TARGET)
+ continue;
+
+ // score = штраф за назначенных волков + дистанция²
+ // Это позволяет предпочесть менее занятую цель,
+ // но при равном числе назначений — выбрать ближайшую
+ double distSq = mob->distanceToSqr(le);
+ double score = assigned * ASSIGNMENT_PENALTY_SQ + distSq;
+
+ if (score < bestScore)
+ {
+ bestScore = score;
+ best = le;
+ }
+ }
+
+ return best;
+}
+
+// ---------------------------------------------------------------------------
+bool PackDistributedTargetGoal::canUse()
+{
+ if (!tameAnimal->isTame()) return false;
+ if (tameAnimal->isSitting()) return false;
+ if (mob->getTarget() != nullptr && mob->getTarget()->isAlive())
+ return false;
+
+ double range = getFollowDistance();
+
+ vector>* candidates = mob->level->getEntitiesOfClass(
+ typeid(Monster),
+ mob->bb->grow(range, 4.0, range),
+ nullptr);
+
+ if (candidates == nullptr || candidates->empty())
+ {
+ delete candidates;
+ return false;
+ }
+
+ vector> packmates = getPackmates(range);
+
+ // Фаза 1: ищем цель с учётом лимита MAX_WOLVES_PER_TARGET
+ shared_ptr target = pickBestTarget(candidates, packmates, true);
+
+ // Фаза 2: если все цели заняты сверх лимита — игнорируем лимит,
+ // чтобы волк не простаивал
+ if (target == nullptr)
+ target = pickBestTarget(candidates, packmates, false);
+
+ delete candidates;
+
+ if (target == nullptr) return false;
+
+ assignedTarget = weak_ptr(target);
+ return true;
+}
+
+// ---------------------------------------------------------------------------
+bool PackDistributedTargetGoal::canContinueToUse()
+{
+ // Базовые проверки TargetGoal (цель жива, в радиусе follow_range и т.д.)
+ if (!TargetGoal::canContinueToUse()) return false;
+
+ // Не удаляться от хозяина дальше поводка
+ shared_ptr owner = tameAnimal->getOwner();
+ if (owner == nullptr) return false;
+
+ return mob->distanceToSqr(owner) <=
+ maxChaseDistFromOwner * maxChaseDistFromOwner;
+}
+
+// ---------------------------------------------------------------------------
+void PackDistributedTargetGoal::start()
+{
+ shared_ptr target = assignedTarget.lock();
+ mob->setTarget(target);
+ tameAnimal->onTameAggroStarted(target);
+ TargetGoal::start();
+}
diff --git a/Minecraft.World/WolfPackDistributedTargetGoal.h b/Minecraft.World/WolfPackDistributedTargetGoal.h
new file mode 100644
index 000000000..0cd2bff0e
--- /dev/null
+++ b/Minecraft.World/WolfPackDistributedTargetGoal.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include "Goal.h"
+#include "TargetGoal.h"
+#include "LivingEntity.h"
+
+class TamableAnimal;
+
+// TameAnimalDefendGoal .
+// ,
+// "" :
+// score = assignedWolves * ASSIGNMENT_PENALTY_SQ + distanceSq
+// score = .
+class PackDistributedTargetGoal : public TargetGoal
+{
+private:
+ TamableAnimal* tameAnimal;
+ weak_ptr assignedTarget;
+ float maxChaseDistFromOwner;
+
+ // .
+ // " " 12 .
+ // .
+ // .
+ static constexpr double ASSIGNMENT_PENALTY_SQ = 12.0 * 12.0;
+
+ // Ƹ .
+ //
+ // ( ).
+ static constexpr int MAX_WOLVES_PER_TARGET = 2;
+
+public:
+ PackDistributedTargetGoal(TamableAnimal* tameAnimal,
+ bool mustSee,
+ float maxChaseDistFromOwner = 20.0f);
+
+ bool canUse() override;
+ bool canContinueToUse() override;
+ void start() override;
+
+private:
+ // range
+ vector> getPackmates(double range);
+
+ // packmates target
+ int countAssignments(
+ const vector>& packmates,
+ shared_ptr target);
+
+ // candidates .
+ // respectLimit: true >= MAX_WOLVES_PER_TARGET
+ shared_ptr pickBestTarget(
+ const vector>* candidates,
+ const vector>& packmates,
+ bool respectLimit);
+};
diff --git a/Minecraft.World/WolfRetreatGoal.cpp b/Minecraft.World/WolfRetreatGoal.cpp
new file mode 100644
index 000000000..9bac32d76
--- /dev/null
+++ b/Minecraft.World/WolfRetreatGoal.cpp
@@ -0,0 +1,102 @@
+#include "stdafx.h"
+#include "net.minecraft.world.entity.h"
+#include "net.minecraft.world.entity.animal.h"
+#include "net.minecraft.world.entity.ai.navigation.h"
+#include "WolfRetreatGoal.h"
+
+WolfRetreatGoal::WolfRetreatGoal(TamableAnimal* tameAnimal,
+ float retreatHealthFraction,
+ float runSpeedMultiplier)
+{
+ this->tameAnimal = tameAnimal;
+ this->retreatHealthFraction = retreatHealthFraction;
+ this->runSpeedMultiplier = runSpeedMultiplier;
+ this->recalcPathTicks = 0;
+
+ // FollowOwnerGoal,
+ // RandomStrollGoal ..
+ setRequiredControlFlags(0);
+}
+
+// ---------------------------------------------------------------------------
+bool WolfRetreatGoal::canUse()
+{
+ //
+ if (!tameAnimal->isTame()) return false;
+ if (tameAnimal->isSitting()) return false;
+
+ //
+ float hpFraction = tameAnimal->getHealth() / tameAnimal->getMaxHealth();
+ if (hpFraction > retreatHealthFraction) return false;
+
+ //
+ shared_ptr owner = tameAnimal->getOwner();
+ if (owner == nullptr) return false;
+
+ shared_ptr ownerLiving =
+ dynamic_pointer_cast(owner);
+ return ownerLiving != nullptr && ownerLiving->isAlive();
+}
+
+// ---------------------------------------------------------------------------
+bool WolfRetreatGoal::canContinueToUse()
+{
+ //
+ if (tameAnimal->isSitting()) return false;
+
+ // ( )
+ float hpFraction = tameAnimal->getHealth() / tameAnimal->getMaxHealth();
+ if (hpFraction >= retreatHealthFraction + HYSTERESIS) return false;
+
+ //
+ shared_ptr owner = tameAnimal->getOwner();
+ if (owner == nullptr) return false;
+ shared_ptr ownerLiving =
+ dynamic_pointer_cast(owner);
+ if (ownerLiving == nullptr || !ownerLiving->isAlive()) return false;
+
+ //
+ if (tameAnimal->distanceToSqr(owner) <= SAFE_DIST_SQ) return false;
+
+ return true;
+}
+
+// ---------------------------------------------------------------------------
+void WolfRetreatGoal::start()
+{
+ recalcPathTicks = 0;
+
+ //
+ tameAnimal->setTarget(nullptr);
+ tameAnimal->onTameAggroStopped(); // Wolf: setAngry(false)
+
+ //
+ shared_ptr owner = tameAnimal->getOwner();
+ if (owner != nullptr)
+ {
+ tameAnimal->getNavigation()->moveTo(owner, runSpeedMultiplier);
+ }
+}
+
+// ---------------------------------------------------------------------------
+void WolfRetreatGoal::stop()
+{
+ tameAnimal->getNavigation()->stop();
+}
+
+// ---------------------------------------------------------------------------
+void WolfRetreatGoal::tick()
+{
+ // ,
+ //
+ if (--recalcPathTicks <= 0)
+ {
+ recalcPathTicks = PATH_RECALC_INTERVAL;
+
+ shared_ptr owner = tameAnimal->getOwner();
+ if (owner != nullptr)
+ {
+ tameAnimal->getNavigation()->moveTo(owner, runSpeedMultiplier);
+ }
+ }
+}
diff --git a/Minecraft.World/WolfRetreatGoal.h b/Minecraft.World/WolfRetreatGoal.h
new file mode 100644
index 000000000..e070ad16b
--- /dev/null
+++ b/Minecraft.World/WolfRetreatGoal.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "Goal.h"
+
+class TamableAnimal;
+
+class WolfRetreatGoal : public Goal
+{
+private:
+ TamableAnimal* tameAnimal;
+
+ // HP (0.01.0),
+ float retreatHealthFraction;
+
+ //
+ float runSpeedMultiplier;
+
+ // c ( )
+ int recalcPathTicks;
+
+ // : ,
+ // -
+ static constexpr float HYSTERESIS = 0.15f;
+
+ // , ""
+ static constexpr float SAFE_DIST_SQ = 4.0f * 4.0f;
+
+ // N
+ static constexpr int PATH_RECALC_INTERVAL = 10;
+
+public:
+ WolfRetreatGoal(TamableAnimal* tameAnimal,
+ float retreatHealthFraction,
+ float runSpeedMultiplier = 1.5f);
+
+ bool canUse() override;
+ bool canContinueToUse() override;
+ void start() override;
+ void stop() override;
+ void tick() override;
+};
diff --git a/Minecraft.World/net.minecraft.world.entity.ai.goal.target.h b/Minecraft.World/net.minecraft.world.entity.ai.goal.target.h
index 98b96a16a..961360521 100644
--- a/Minecraft.World/net.minecraft.world.entity.ai.goal.target.h
+++ b/Minecraft.World/net.minecraft.world.entity.ai.goal.target.h
@@ -6,4 +6,8 @@
#include "NearestAttackableTargetGoal.h"
#include "NonTameRandomTargetGoal.h"
#include "OwnerHurtByTargetGoal.h"
-#include "OwnerHurtTargetGoal.h"
\ No newline at end of file
+#include "OwnerHurtTargetGoal.h"
+#include "TameAnimalDefendGoal.h"
+#include "WolfRetreatGoal.h"
+#include "WolfPackDistributedTargetGoal.h"
+#include "PrioritizeOwnerThreatGoal.h"