Wolf AI improoves

This commit is contained in:
NSDeathman 2026-03-15 21:10:32 +03:00
parent f4db2495e8
commit 791f592cef
16 changed files with 692 additions and 23 deletions

View file

@ -2445,6 +2445,7 @@
<ClInclude Include="BiomeOverrideLayer.h" /> <ClInclude Include="BiomeOverrideLayer.h" />
<ClInclude Include="BiomeSource.h" /> <ClInclude Include="BiomeSource.h" />
<ClInclude Include="BirchFeature.h" /> <ClInclude Include="BirchFeature.h" />
<ClInclude Include="BirchForestBiome.h" />
<ClInclude Include="Blaze.h" /> <ClInclude Include="Blaze.h" />
<ClInclude Include="BlockDestructionProgress.h" /> <ClInclude Include="BlockDestructionProgress.h" />
<ClInclude Include="BlockGenMethods.h" /> <ClInclude Include="BlockGenMethods.h" />
@ -2468,6 +2469,8 @@
<ClInclude Include="ComparatorTileEntity.h" /> <ClInclude Include="ComparatorTileEntity.h" />
<ClInclude Include="DaylightDetectorTile.h" /> <ClInclude Include="DaylightDetectorTile.h" />
<ClInclude Include="DaylightDetectorTileEntity.h" /> <ClInclude Include="DaylightDetectorTileEntity.h" />
<ClInclude Include="PrioritizeOwnerThreatGoal.h" />
<ClInclude Include="TameAnimalDefendGoal.h" />
<ClInclude Include="DropperTile.h" /> <ClInclude Include="DropperTile.h" />
<ClInclude Include="DropperTileEntity.h" /> <ClInclude Include="DropperTileEntity.h" />
<ClInclude Include="DummyCriteria.h" /> <ClInclude Include="DummyCriteria.h" />
@ -2847,6 +2850,8 @@
<ClInclude Include="Witch.h" /> <ClInclude Include="Witch.h" />
<ClInclude Include="WitherBoss.h" /> <ClInclude Include="WitherBoss.h" />
<ClInclude Include="WitherSkull.h" /> <ClInclude Include="WitherSkull.h" />
<ClInclude Include="WolfPackDistributedTargetGoal.h" />
<ClInclude Include="WolfRetreatGoal.h" />
<ClInclude Include="WoodButtonTile.h" /> <ClInclude Include="WoodButtonTile.h" />
<ClInclude Include="WoodSlabTile.h" /> <ClInclude Include="WoodSlabTile.h" />
<ClInclude Include="WoodTile.h" /> <ClInclude Include="WoodTile.h" />
@ -3519,6 +3524,7 @@
<ClCompile Include="BiomeOverrideLayer.cpp" /> <ClCompile Include="BiomeOverrideLayer.cpp" />
<ClCompile Include="BiomeSource.cpp" /> <ClCompile Include="BiomeSource.cpp" />
<ClCompile Include="BirchFeature.cpp" /> <ClCompile Include="BirchFeature.cpp" />
<ClCompile Include="BirchForestBiome.cpp" />
<ClCompile Include="Blaze.cpp" /> <ClCompile Include="Blaze.cpp" />
<ClCompile Include="BlockDestructionProgress.cpp" /> <ClCompile Include="BlockDestructionProgress.cpp" />
<ClCompile Include="BlockGenMethods.cpp" /> <ClCompile Include="BlockGenMethods.cpp" />
@ -3540,6 +3546,8 @@
<ClCompile Include="ComparatorTileEntity.cpp" /> <ClCompile Include="ComparatorTileEntity.cpp" />
<ClCompile Include="DaylightDetectorTile.cpp" /> <ClCompile Include="DaylightDetectorTile.cpp" />
<ClCompile Include="DaylightDetectorTileEntity.cpp" /> <ClCompile Include="DaylightDetectorTileEntity.cpp" />
<ClCompile Include="PrioritizeOwnerThreatGoal.cpp" />
<ClCompile Include="TameAnimalDefendGoal.cpp" />
<ClCompile Include="DropperTile.cpp" /> <ClCompile Include="DropperTile.cpp" />
<ClCompile Include="DropperTileEntity.cpp" /> <ClCompile Include="DropperTileEntity.cpp" />
<ClCompile Include="DummyCriteria.cpp" /> <ClCompile Include="DummyCriteria.cpp" />
@ -3649,6 +3657,8 @@
<ClCompile Include="StructureFeatureSavedData.cpp" /> <ClCompile Include="StructureFeatureSavedData.cpp" />
<ClCompile Include="Team.cpp" /> <ClCompile Include="Team.cpp" />
<ClCompile Include="WeightedPressurePlateTile.cpp" /> <ClCompile Include="WeightedPressurePlateTile.cpp" />
<ClCompile Include="WolfPackDistributedTargetGoal.cpp" />
<ClCompile Include="WolfRetreatGoal.cpp" />
<ClCompile Include="WoodButtonTile.cpp" /> <ClCompile Include="WoodButtonTile.cpp" />
<ClCompile Include="WoolTileItem.cpp" /> <ClCompile Include="WoolTileItem.cpp" />
<ClCompile Include="CoalItem.cpp" /> <ClCompile Include="CoalItem.cpp" />

View file

@ -3203,6 +3203,21 @@
<ClInclude Include="LightSourceTile.h"> <ClInclude Include="LightSourceTile.h">
<Filter>net\minecraft\world\level\tile</Filter> <Filter>net\minecraft\world\level\tile</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="TameAnimalDefendGoal.h">
<Filter>net\minecraft\world\entity\ai\goal\target</Filter>
</ClInclude>
<ClInclude Include="WolfRetreatGoal.h">
<Filter>net\minecraft\world\entity\ai\goal\target</Filter>
</ClInclude>
<ClInclude Include="PrioritizeOwnerThreatGoal.h">
<Filter>net\minecraft\world\entity\ai\goal\target</Filter>
</ClInclude>
<ClInclude Include="WolfPackDistributedTargetGoal.h">
<Filter>net\minecraft\world\entity\ai\goal\target</Filter>
</ClInclude>
<ClInclude Include="BirchForestBiome.h">
<Filter>net\minecraft\world\level\biome</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="stdafx.cpp"> <ClCompile Include="stdafx.cpp">
@ -5638,5 +5653,20 @@
<ClCompile Include="LightSourceTile.cpp"> <ClCompile Include="LightSourceTile.cpp">
<Filter>net\minecraft\world\level\tile</Filter> <Filter>net\minecraft\world\level\tile</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="TameAnimalDefendGoal.cpp">
<Filter>net\minecraft\world\entity\ai\goal\target</Filter>
</ClCompile>
<ClCompile Include="WolfRetreatGoal.cpp">
<Filter>net\minecraft\world\entity\ai\goal\target</Filter>
</ClCompile>
<ClCompile Include="PrioritizeOwnerThreatGoal.cpp">
<Filter>net\minecraft\world\entity\ai\goal\target</Filter>
</ClCompile>
<ClCompile Include="WolfPackDistributedTargetGoal.cpp">
<Filter>net\minecraft\world\entity\ai\goal\target</Filter>
</ClCompile>
<ClCompile Include="BirchForestBiome.cpp">
<Filter>net\minecraft\world\level\biome</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -32,5 +32,13 @@ void OwnerHurtByTargetGoal::start()
timestamp = owner->getLastHurtByMobTimestamp(); timestamp = owner->getLastHurtByMobTimestamp();
} }
tameAnimal->onTameAggroStarted(owner->getLastHurtByMob());
TargetGoal::start(); TargetGoal::start();
} }
void OwnerHurtByTargetGoal::stop()
{
tameAnimal->onTameAggroStopped();
TargetGoal::stop();
}

View file

@ -16,4 +16,5 @@ public:
bool canUse(); bool canUse();
void start(); void start();
void stop();
}; };

View file

@ -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<LivingEntity> currentTarget = mob->getTarget();
if (currentTarget == nullptr || !currentTarget->isAlive()) return false;
// Нужен живой хозяин
shared_ptr<LivingEntity> owner =
dynamic_pointer_cast<LivingEntity>(tameAnimal->getOwner());
if (owner == nullptr || !owner->isAlive()) return false;
// Кто последний ударил хозяина?
shared_ptr<LivingEntity> 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<LivingEntity>(ownerAttacker);
return true;
}
// ---------------------------------------------------------------------------
void PrioritizeOwnerThreatGoal::start()
{
shared_ptr<LivingEntity> target = newThreat.lock();
mob->setTarget(target);
tameAnimal->onTameAggroStarted(target); // Wolf: setAngry(true)
// Запомнить timestamp чтобы не обрабатывать это событие повторно
shared_ptr<LivingEntity> owner =
dynamic_pointer_cast<LivingEntity>(tameAnimal->getOwner());
if (owner != nullptr)
timestamp = owner->getLastHurtByMobTimestamp();
TargetGoal::start();
}

View file

@ -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<LivingEntity> 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;
};

View file

@ -36,6 +36,8 @@ public:
virtual void setOwnerUUID(const wstring &name); virtual void setOwnerUUID(const wstring &name);
virtual shared_ptr<Entity> getOwner(); virtual shared_ptr<Entity> getOwner();
virtual SitGoal *getSitGoal(); virtual SitGoal *getSitGoal();
virtual void onTameAggroStarted(shared_ptr<LivingEntity> target) {}
virtual void onTameAggroStopped() {}
bool wantsToAttack(shared_ptr<LivingEntity> target, shared_ptr<LivingEntity> owner); bool wantsToAttack(shared_ptr<LivingEntity> target, shared_ptr<LivingEntity> owner);
Team *getTeam(); Team *getTeam();
bool isAlliedTo(shared_ptr<LivingEntity> other); bool isAlliedTo(shared_ptr<LivingEntity> other);

View file

@ -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<LivingEntity> owner =
dynamic_pointer_cast<LivingEntity>(tameAnimal->getOwner());
// owner ìîæåò áûòü nullptr (îôôëàéí-èãðîê) — äîïóñêàåì, íî wantsToAttack
// íèæå îáðàáîòàåò ýòî êîððåêòíî
double range = getFollowDistance();
// Èùåì âñå Monster-ñóùíîñòè â ðàäèóñå
vector<shared_ptr<Entity>>* 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<LivingEntity> best = nullptr;
double bestSq = DBL_MAX;
for (auto& e : *candidates)
{
shared_ptr<LivingEntity> le = dynamic_pointer_cast<LivingEntity>(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<LivingEntity>(best);
return true;
}
void TameAnimalDefendGoal::start()
{
shared_ptr<LivingEntity> target = nearestHostile.lock();
mob->setTarget(target);
tameAnimal->onTameAggroStarted(target);
TargetGoal::start();
}
void TameAnimalDefendGoal::stop()
{
tameAnimal->onTameAggroStopped();
TargetGoal::stop();
}

View file

@ -0,0 +1,22 @@
#pragma once
#include "TargetGoal.h"
class TamableAnimal;
// Ïðèðó÷åííûé ïèòîìåö ïðîàêòèâíî àòàêóåò áëèæàéøåãî âðàæäåáíîãî ìîáà â ðàäèóñå
class TameAnimalDefendGoal : public TargetGoal
{
private:
TamableAnimal* tameAnimal;
weak_ptr<LivingEntity> nearestHostile;
public:
// mustSee=true: âèäåòü öåëü ïåðåä àòàêîé
// scanRadius: åñëè 0, èñïîëüçóåòñÿ FOLLOW_RANGE àòðèáóò
TameAnimalDefendGoal(TamableAnimal* tameAnimal, bool mustSee);
bool canUse() override;
void start() override;
void stop() override;
};

View file

@ -20,6 +20,7 @@
#include "Wolf.h" #include "Wolf.h"
#include "..\Minecraft.Client\Textures.h" #include "..\Minecraft.Client\Textures.h"
#include "SoundTypes.h" #include "SoundTypes.h"
#include "PrimedTnt.h"
@ -39,20 +40,28 @@ Wolf::Wolf(Level *level) : TamableAnimal( level )
getNavigation()->setAvoidWater(true); getNavigation()->setAvoidWater(true);
goalSelector.addGoal(1, new FloatGoal(this)); goalSelector.addGoal(1, new FloatGoal(this));
goalSelector.addGoal(2, sitGoal, false); goalSelector.addGoal(2, new WolfRetreatGoal(this, 0.25f, 1.5f));
goalSelector.addGoal(3, new LeapAtTargetGoal(this, 0.4)); goalSelector.addGoal(3, new AvoidPlayerGoal(this, typeid(Creeper), 12, 1.25, 1.25));
goalSelector.addGoal(4, new MeleeAttackGoal(this, 1.0, true)); goalSelector.addGoal(4, new AvoidPlayerGoal(this, typeid(PrimedTnt), 12, 1.25, 1.25));
goalSelector.addGoal(5, new FollowOwnerGoal(this, 1.0, 10, 2)); goalSelector.addGoal(5, sitGoal, false);
goalSelector.addGoal(6, new BreedGoal(this, 1.0)); goalSelector.addGoal(6, new LeapAtTargetGoal(this, 0.4));
goalSelector.addGoal(7, new RandomStrollGoal(this, 1.0)); goalSelector.addGoal(7, new MeleeAttackGoal(this, 1.0, true));
goalSelector.addGoal(8, new BegGoal(this, 8)); goalSelector.addGoal(8, new FollowOwnerGoal(this, 1.0, 10, 2));
goalSelector.addGoal(9, new LookAtPlayerGoal(this, typeid(Player), 8)); goalSelector.addGoal(9, new BreedGoal(this, 1.0));
goalSelector.addGoal(9, new RandomLookAroundGoal(this)); 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(1, new HurtByTargetGoal(this, true));
targetSelector.addGoal(2, new OwnerHurtTargetGoal(this)); targetSelector.addGoal(2, new OwnerHurtByTargetGoal(this));
targetSelector.addGoal(3, new HurtByTargetGoal(this, true)); targetSelector.addGoal(3, new PrioritizeOwnerThreatGoal(this));
targetSelector.addGoal(4, new NonTameRandomTargetGoal(this, typeid(Sheep), 200, false)); 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 setTame(false); // Initialize health
} }
@ -81,14 +90,20 @@ bool Wolf::useNewAi()
void Wolf::setTarget(shared_ptr<LivingEntity> target) void Wolf::setTarget(shared_ptr<LivingEntity> target)
{ {
TamableAnimal::setTarget(target); TamableAnimal::setTarget(target);
if ( target == nullptr ) if (target == nullptr)
{
setAngry(false); setAngry(false);
} else
else if(!isTame())
{
setAngry(true); setAngry(true);
} }
void Wolf::onTameAggroStarted(shared_ptr<LivingEntity> target)
{
setAngry(true);
}
void Wolf::onTameAggroStopped()
{
setAngry(false);
} }
void Wolf::serverAiMobStep() void Wolf::serverAiMobStep()
@ -474,8 +489,7 @@ bool Wolf::isFood(shared_ptr<ItemInstance> item)
int Wolf::getMaxSpawnClusterSize() 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 8;
return 4;
} }
bool Wolf::isAngry() bool Wolf::isAngry()

View file

@ -76,6 +76,9 @@ public:
// For tooltips // For tooltips
int GetSynchedHealth(); int GetSynchedHealth();
virtual void onTameAggroStarted(shared_ptr<LivingEntity> target) override;
virtual void onTameAggroStopped() override;
protected: protected:
virtual shared_ptr<AgableMob> getBreedOffspring(shared_ptr<AgableMob> target); virtual shared_ptr<AgableMob> getBreedOffspring(shared_ptr<AgableMob> target);

View file

@ -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<shared_ptr<LivingEntity>> PackDistributedTargetGoal::getPackmates(
double range)
{
vector<shared_ptr<LivingEntity>> result;
wstring ownerUUID = tameAnimal->getOwnerUUID();
if (ownerUUID.empty()) return result;
// Ищем всех волков в радиусе
vector<shared_ptr<Entity>>* 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> wolf = dynamic_pointer_cast<Wolf>(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<shared_ptr<LivingEntity>>& packmates,
shared_ptr<LivingEntity> target)
{
if (target == nullptr) return 0;
int count = 0;
for (auto& packmate : packmates)
{
shared_ptr<LivingEntity> packTarget =
dynamic_pointer_cast<LivingEntity>(
dynamic_pointer_cast<Wolf>(packmate) != nullptr
? dynamic_pointer_cast<Wolf>(packmate)->getTarget()
: nullptr);
if (packTarget != nullptr && packTarget.get() == target.get())
count++;
}
return count;
}
// ---------------------------------------------------------------------------
shared_ptr<LivingEntity> PackDistributedTargetGoal::pickBestTarget(
const vector<shared_ptr<Entity>>* candidates,
const vector<shared_ptr<LivingEntity>>& packmates,
bool respectLimit)
{
shared_ptr<LivingEntity> best = nullptr;
double bestScore = DBL_MAX;
shared_ptr<LivingEntity> owner =
dynamic_pointer_cast<LivingEntity>(tameAnimal->getOwner());
for (auto& e : *candidates)
{
shared_ptr<LivingEntity> le = dynamic_pointer_cast<LivingEntity>(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<shared_ptr<Entity>>* candidates = mob->level->getEntitiesOfClass(
typeid(Monster),
mob->bb->grow(range, 4.0, range),
nullptr);
if (candidates == nullptr || candidates->empty())
{
delete candidates;
return false;
}
vector<shared_ptr<LivingEntity>> packmates = getPackmates(range);
// Фаза 1: ищем цель с учётом лимита MAX_WOLVES_PER_TARGET
shared_ptr<LivingEntity> target = pickBestTarget(candidates, packmates, true);
// Фаза 2: если все цели заняты сверх лимита — игнорируем лимит,
// чтобы волк не простаивал
if (target == nullptr)
target = pickBestTarget(candidates, packmates, false);
delete candidates;
if (target == nullptr) return false;
assignedTarget = weak_ptr<LivingEntity>(target);
return true;
}
// ---------------------------------------------------------------------------
bool PackDistributedTargetGoal::canContinueToUse()
{
// Базовые проверки TargetGoal (цель жива, в радиусе follow_range и т.д.)
if (!TargetGoal::canContinueToUse()) return false;
// Не удаляться от хозяина дальше поводка
shared_ptr<Entity> owner = tameAnimal->getOwner();
if (owner == nullptr) return false;
return mob->distanceToSqr(owner) <=
maxChaseDistFromOwner * maxChaseDistFromOwner;
}
// ---------------------------------------------------------------------------
void PackDistributedTargetGoal::start()
{
shared_ptr<LivingEntity> target = assignedTarget.lock();
mob->setTarget(target);
tameAnimal->onTameAggroStarted(target);
TargetGoal::start();
}

View file

@ -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<LivingEntity> 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<shared_ptr<LivingEntity>> getPackmates(double range);
// Посчитать сколько волков из packmates уже атакуют target
int countAssignments(
const vector<shared_ptr<LivingEntity>>& packmates,
shared_ptr<LivingEntity> target);
// Выбрать лучшую цель из candidates с учётом распределения.
// respectLimit: если true — не назначать на цели с >= MAX_WOLVES_PER_TARGET
shared_ptr<LivingEntity> pickBestTarget(
const vector<shared_ptr<Entity>>* candidates,
const vector<shared_ptr<LivingEntity>>& packmates,
bool respectLimit);
};

View file

@ -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<Entity> owner = tameAnimal->getOwner();
if (owner == nullptr) return false;
shared_ptr<LivingEntity> ownerLiving =
dynamic_pointer_cast<LivingEntity>(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<Entity> owner = tameAnimal->getOwner();
if (owner == nullptr) return false;
shared_ptr<LivingEntity> ownerLiving =
dynamic_pointer_cast<LivingEntity>(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<Entity> 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<Entity> owner = tameAnimal->getOwner();
if (owner != nullptr)
{
tameAnimal->getNavigation()->moveTo(owner, runSpeedMultiplier);
}
}
}

View file

@ -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;
};

View file

@ -7,3 +7,7 @@
#include "NonTameRandomTargetGoal.h" #include "NonTameRandomTargetGoal.h"
#include "OwnerHurtByTargetGoal.h" #include "OwnerHurtByTargetGoal.h"
#include "OwnerHurtTargetGoal.h" #include "OwnerHurtTargetGoal.h"
#include "TameAnimalDefendGoal.h"
#include "WolfRetreatGoal.h"
#include "WolfPackDistributedTargetGoal.h"
#include "PrioritizeOwnerThreatGoal.h"