ИИ питомцев переписан, добавлен Дух крысы, пороги разнесены по шкале
Работа по указаниям 2026-09-18. В релиз пока не выходит. НОВЫЙ ПИТОМЕЦ "Дух крысы" (necroRatSpirit), Некромантия 3 (60 убийств): модель зомбоволка в масштабе 0.28, урон 5, кроличьи звуки. Сам не нападает никогда, держится справа-сзади в двух блоках, упёршись в препятствие проходит СКВОЗЬ него и блоков не ломает. Укус замедляет, оставляет метку духа (видна на компасе, +25% получаемого урона) и рвёт жилу. Обгрызая труп, лечится. СВОЯ ЗАДАЧА ИИ. Задачи "иди за сущностью" в игре нет вовсе - проверены все 32 типа EAI*. Написана NecroFollowOwnerTask по образцу EAIApproachSpot: сектор "3-6 часов" от хозяина, FindPath с canBreak:false, проход сквозь препятствие через Entity.IsStuck. Вешается в рантайме, минуя Type.GetType. ПРИКАЗ АТАКОВАТЬ. Повторное применение свитка при живом питомце натравливает его на цель под прицелом: EntityPlayerLocal.HitInfo + ItemActionAttack. GetEntityFromHit. Торговцы и игроки отсеяны. Нет цели - "Нет цели для атаки". ЗОМБОЖИВОТНЫЕ ПРИВЕДЕНЫ К ТОМУ ЖЕ ОБРАЗЦУ. Сняты BreakBlock, Territorial, ApproachSpot, Wander, BlockingTargetTask и поедание трупов; цель они больше не выбирают сами. Лестница урона 20/35/45/60 плюс расчленение у Пса, Медведя и Волка. Кровотечение всем, метка и ослабление - только у крысы. УБИЙСТВА ПИТОМЦЕМ ЗАСЧИТЫВАЮТСЯ ВЛАДЕЛЬЦУ, включая добивание кровотечением. Префикс на AwardKillXPServer подменяет убийцу владельцем; для смерти от баффа заведена память укусов, потому что в DamageSource от баффа нет того, кто его наложил. Зомби под Камнем духов это не задело - решение от 17.09 в силе. ГРИФ откачен на летающую ветку EntityVulture и переименован в Могильного стервятника: модель наконец соответствует имени. Держится у игрока сам, через собственный механизм "дома" (setHomeArea), на время погони дом отвязывается. Попытка натянуть птичий префаб на наземный класс провалилась и записана - так делать нельзя. ПРОЧЕЕ: призрачный вид распространён с торговцев на питомцев (у Пса, Медведя и Волка выключен по указанию), у Пса светятся фиолетовые глаза, белая иконка книги снята со всех свитков призыва, "Жуки Властелина" переименованы в "Рой фараона" на всех 13 языках. ПОРОГИ: крыса 60, стервятник 500, пёс 1300, медведь 2000, волк 4000. ИСПРАВЛЕНО ПО ХОДУ ИГРОВЫХ ПРОВЕРОК: питомцы не призывались обычным кликом (AnimWait требовал удержания), отзыв срабатывал не с первого раза (автомат состояний ItemActionSpawnEntity), крыса проваливалась сквозь мир (IsStuck отключает и пол), питомец подбрасывал хозяина (коллайдеры разводились до появления модели). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
15c59dd37a
commit
d97f83e972
+349
-56
@@ -85,6 +85,54 @@ namespace NecromancerTome
|
||||
/// fix the earlier "dog died, no way to get the book back" complaint: with nothing
|
||||
/// ever taken, there's nothing to lose when the dog dies off-screen.</summary>
|
||||
public bool ConsumesBook;
|
||||
|
||||
/// <summary>Указание 2026-09-18, пока только для Духа крысы: "если крыса уже
|
||||
/// призвана, то призыв становится атакой". Для остальных питомцев Action0 при живом
|
||||
/// питомце по-прежнему просто говорит "уже призван" (AlreadyActiveKey), как и было.
|
||||
/// Разбор прицела - в PetCommandPatch.cs.</summary>
|
||||
public bool CommandsAttack;
|
||||
|
||||
/// <summary>Что показать, когда команда атаки отдана, но под прицелом некого
|
||||
/// атаковать. Читается только при CommandsAttack.</summary>
|
||||
public string NoTargetKey;
|
||||
|
||||
/// <summary>Радиус "дома" для ЛЕТАЮЩИХ питомцев, в блоках. Ноль - выключено.
|
||||
///
|
||||
/// Указание 2026-09-18: "гриф улетает слишком далеко от игрока и срабатывает поводок.
|
||||
/// Можем сделать ему следование, которое возвратит в радиус игрока, чтобы поводок
|
||||
/// вообще не использовался?" Можем, и писать для этого ничего не пришлось: у
|
||||
/// EntityVulture слежение за "домом" уже встроено. Каждые 60 тиков он проверяет
|
||||
/// isWithinHomeDistanceCurrentPosition(), и если вышел за радиус - сам переходит в
|
||||
/// State.Home и летит обратно СВОИМ полётом:
|
||||
///
|
||||
/// if (state != State.Home && --homeCheckDelay <= 0) {
|
||||
/// homeCheckDelay = 60;
|
||||
/// if (!isWithinHomeDistanceCurrentPosition()) SetState(State.AttackStop);
|
||||
/// }
|
||||
/// ... StartHome(getHomePosition().position.ToVector3());
|
||||
///
|
||||
/// Достаточно каждую секунду переставлять этот дом на позицию хозяина
|
||||
/// (EntityAlive.setHomeArea), и получается настоящее следование за игроком, без
|
||||
/// единого телепорта и без нашего пафайндинга.</summary>
|
||||
public int FlyingHomeRadius;
|
||||
|
||||
/// <summary>Зажигать ли этому питомцу глаза (PetEyeGlowPatch.cs). Только у Пса и
|
||||
/// Волка: у Медведя и Грифа модель - один материал на всё, и глаза там запечены в
|
||||
/// текстуру шкуры, доставать их нечем.</summary>
|
||||
public bool LitEyes;
|
||||
|
||||
/// <summary>За сколько блоков от хозяина держится этот питомец. Ноль означает "как
|
||||
/// у всех" (NecroFollowOwnerTask.DefaultSlotDistance, два блока). Медведю и Волку
|
||||
/// задан блок дальше - указание 2026-09-18, "они мощнее": сектор тот же, радиус
|
||||
/// больше, чтобы туша не наступала хозяину на пятки.</summary>
|
||||
public float FollowDistance;
|
||||
|
||||
/// <summary>Вешать ли этому питомцу собственную задачу следования
|
||||
/// (NecroFollowOwnerTask): держаться справа-сзади, не бродить, не грызть блоки,
|
||||
/// проходить сквозь препятствия. Старый телепорт-поводок из PetFollowPatch.cs для
|
||||
/// таких питомцев выключается - две системы на одно и то же дёргали бы питомца в
|
||||
/// разные стороны.</summary>
|
||||
public bool UsesFollowTask;
|
||||
}
|
||||
|
||||
public static readonly Dictionary<string, PetInfo> LimitedPets = new Dictionary<string, PetInfo>
|
||||
@@ -98,6 +146,14 @@ namespace NecromancerTome
|
||||
NothingToRecallKey = "necroZombieDogNothingToRecall",
|
||||
SummonItemName = "bookSummonZombieDog",
|
||||
ConsumesBook = false,
|
||||
LitEyes = true,
|
||||
// 2026-09-18: "поведение других призванных животных зомби тоже подгони под
|
||||
// крысу". Те же три флага, что у неё: повторный Action0 - приказ атаковать
|
||||
// цель под прицелом, своя задача следования вместо телепорт-поводка.
|
||||
// AlreadyActiveKey у всех четверых теперь не читается никогда.
|
||||
CommandsAttack = true,
|
||||
NoTargetKey = "necroPetNoTarget",
|
||||
UsesFollowTask = true,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -131,6 +187,18 @@ namespace NecromancerTome
|
||||
NothingToRecallKey = "necroZombieGriffinNothingToRecall",
|
||||
SummonItemName = "bookSummonZombieGriffin",
|
||||
ConsumesBook = false,
|
||||
// 15 блоков: заметно меньше 32-метрового поводка, чтобы тот вообще не
|
||||
// понадобился, и достаточно, чтобы Гриф не висел у игрока над головой.
|
||||
FlyingHomeRadius = 15,
|
||||
// UsesFollowTask у Грифа НЕТ с 18.09: он снова летающий (EntityVulture), а
|
||||
// задача следования наземная - она гоняла бы его пафайндером по земле и
|
||||
// дралась бы с его собственным полётом. Приказ атаковать оставлен.
|
||||
// 2026-09-18: "поведение других призванных животных зомби тоже подгони под
|
||||
// крысу". Те же три флага, что у неё: повторный Action0 - приказ атаковать
|
||||
// цель под прицелом, своя задача следования вместо телепорт-поводка.
|
||||
// AlreadyActiveKey у всех четверых теперь не читается никогда.
|
||||
CommandsAttack = true,
|
||||
NoTargetKey = "necroPetNoTarget",
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -142,6 +210,14 @@ namespace NecromancerTome
|
||||
NothingToRecallKey = "necroZombieBearNothingToRecall",
|
||||
SummonItemName = "bookSummonZombieBear",
|
||||
ConsumesBook = false,
|
||||
FollowDistance = 3f,
|
||||
// 2026-09-18: "поведение других призванных животных зомби тоже подгони под
|
||||
// крысу". Те же три флага, что у неё: повторный Action0 - приказ атаковать
|
||||
// цель под прицелом, своя задача следования вместо телепорт-поводка.
|
||||
// AlreadyActiveKey у всех четверых теперь не читается никогда.
|
||||
CommandsAttack = true,
|
||||
NoTargetKey = "necroPetNoTarget",
|
||||
UsesFollowTask = true,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -153,6 +229,34 @@ namespace NecromancerTome
|
||||
NothingToRecallKey = "necroZombieWolfNothingToRecall",
|
||||
SummonItemName = "bookSummonZombieWolf",
|
||||
ConsumesBook = false,
|
||||
FollowDistance = 3f,
|
||||
// 2026-09-18: "поведение других призванных животных зомби тоже подгони под
|
||||
// крысу". Те же три флага, что у неё: повторный Action0 - приказ атаковать
|
||||
// цель под прицелом, своя задача следования вместо телепорт-поводка.
|
||||
// AlreadyActiveKey у всех четверых теперь не читается никогда.
|
||||
CommandsAttack = true,
|
||||
NoTargetKey = "necroPetNoTarget",
|
||||
UsesFollowTask = true,
|
||||
}
|
||||
},
|
||||
// "Дух крысы", указание 2026-09-18 - питомец начального уровня (Некромантия 3) и
|
||||
// первый, у кого поведение задано намеренно, а не унаследовано от зомбопса. На нём
|
||||
// отлаживается ИИ питомцев вообще, поэтому он единственный, у кого стоят оба новых
|
||||
// флага. AlreadyActiveKey у него НЕ ЧИТАЕТСЯ НИКОГДА: повторный Action0 - это не
|
||||
// отказ "уже призван", а команда атаковать (CommandsAttack), так что вместо него
|
||||
// показывается либо ничего (приказ принят), либо NoTargetKey.
|
||||
{
|
||||
"necroRatSpirit",
|
||||
new PetInfo
|
||||
{
|
||||
AlreadyActiveKey = null,
|
||||
RecalledKey = "necroRatSpiritRecalled",
|
||||
NothingToRecallKey = "necroRatSpiritNothingToRecall",
|
||||
SummonItemName = "bookSummonRatSpirit",
|
||||
ConsumesBook = false,
|
||||
CommandsAttack = true,
|
||||
NoTargetKey = "necroPetNoTarget",
|
||||
UsesFollowTask = true,
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -161,8 +265,17 @@ namespace NecromancerTome
|
||||
/// summon books now declare Action1 with the same Class="SpawnEntity"/Entity as Action0.</summary>
|
||||
public const int RecallActionIndex = 1;
|
||||
|
||||
/// <summary>Правда ровно между "префикс разрешил призыв" и концом этого же вызова Spawn.
|
||||
/// Нужна постфиксу ниже, чтобы отличить НАСТОЯЩИЙ призыв от команды атаковать: обе идут
|
||||
/// через один и тот же Action0, постфикс Harmony отрабатывает в обоих случаях (возврат
|
||||
/// false из префикса его не отменяет), а делать им надо противоположное - в одном случае
|
||||
/// снять цель со свежего питомца, в другом ни в коем случае её не трогать, её только что
|
||||
/// поставил игрок.</summary>
|
||||
public static bool summonAllowedThisCall;
|
||||
|
||||
public static bool Prefix(ItemActionSpawnEntity __instance, ItemActionData _actionData)
|
||||
{
|
||||
summonAllowedThisCall = false;
|
||||
if (!LimitedPets.TryGetValue(__instance.entityToSpawn, out PetInfo tooltips))
|
||||
{
|
||||
return true;
|
||||
@@ -207,15 +320,73 @@ namespace NecromancerTome
|
||||
|
||||
if (owned.Count > 0)
|
||||
{
|
||||
// Action0 при уже живом питомце. Для всех, кроме Духа крысы, это тупик "уже
|
||||
// призван". Для него - команда атаковать то, на что смотрит игрок: см.
|
||||
// PetCommandPatch.cs, там же и весь отсев (торговцы, игроки, мёртвые).
|
||||
if (tooltips.CommandsAttack)
|
||||
{
|
||||
EntityAlive pet = holdingEntity.world != null
|
||||
? holdingEntity.world.GetEntity(owned[0].Id) as EntityAlive
|
||||
: null;
|
||||
if (!PetAttackCommand.TryOrderAttack(holdingEntity, pet) && holdingEntity.world != null)
|
||||
{
|
||||
GameManager.ShowTooltip(GameManager.Instance.World.GetPrimaryPlayer(), tooltips.NoTargetKey);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (holdingEntity.world != null)
|
||||
{
|
||||
GameManager.ShowTooltip(GameManager.Instance.World.GetPrimaryPlayer(), tooltips.AlreadyActiveKey);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
summonAllowedThisCall = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>ДОБАВЛЕНО 2026-09-18, после чтения ItemActionSpawnEntity.Spawn целиком.
|
||||
/// Последней строкой ваниль делает вот что:
|
||||
///
|
||||
/// entityAlive.SetAttackTarget(holdingEntity.GetAttackTarget(), 600);
|
||||
///
|
||||
/// то есть свежепризванный питомец НАСЛЕДУЕТ ЦЕЛЬ ХОЗЯИНА. Для зомбопса это задумано и
|
||||
/// полезно, а для Духа крысы прямо противоречит его единственному правилу - "сама не
|
||||
/// атакует никогда". Призови её в драке, и она бросилась бы в бой сама, без приказа.
|
||||
///
|
||||
/// Почистить это в постфиксе на EntityFactory.CreateEntity нельзя: тот отрабатывает
|
||||
/// ВНУТРИ Spawn, ещё до этой строки. Поэтому чистка здесь, после всего.
|
||||
///
|
||||
/// Флаг summonAllowedThisCall обязателен: без него постфикс сбрасывал бы цель и после
|
||||
/// команды атаковать - то есть приказ гасил бы сам себя.</summary>
|
||||
public static void Postfix(ItemActionSpawnEntity __instance)
|
||||
{
|
||||
if (!summonAllowedThisCall)
|
||||
{
|
||||
return;
|
||||
}
|
||||
summonAllowedThisCall = false;
|
||||
if (!LimitedPets.TryGetValue(__instance.entityToSpawn, out PetInfo petInfo) || !petInfo.UsesFollowTask)
|
||||
{
|
||||
return;
|
||||
}
|
||||
World world = GameManager.Instance != null ? GameManager.Instance.World : null;
|
||||
if (world == null || world.EntityAlives == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int petClassId = EntityClass.GetId(__instance.entityToSpawn);
|
||||
for (int i = world.EntityAlives.Count - 1; i >= 0; i--)
|
||||
{
|
||||
EntityAlive candidate = world.EntityAlives[i];
|
||||
if (candidate != null && candidate.entityClass == petClassId && candidate.GetAttackTarget() != null)
|
||||
{
|
||||
candidate.SetAttackTarget(null, 0);
|
||||
Debug.Log("[NecromancerTome] SummonPatch: cleared the target vanilla Spawn handed to fresh pet " +
|
||||
candidate.entityId + " (this species never attacks on its own)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>summonItemName is null when this species doesn't consume its book on summon
|
||||
/// (see PetInfo.ConsumesBook) - nothing was taken, so nothing is given back.</summary>
|
||||
public static void RecallPet(EntityAlive owner, int petEntityId, string recalledTooltipKey, string summonItemName)
|
||||
@@ -267,6 +438,140 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ОТЗЫВ СРАБАТЫВАЕТ СРАЗУ ПО НАЖАТИЮ - вторая правка того же бага, 2026-09-18. Первая
|
||||
/// (Patch_..._IsActionRunning_FreeTheRecall ниже) сняла блокировку вторичного действия
|
||||
/// основным, и этого оказалось мало: "отозвать собаку получилось далеко не с первого раза.
|
||||
/// Это проблема всех питомцев".
|
||||
///
|
||||
/// ОСТАВШАЯСЯ ПРИЧИНА - ТА ЖЕ САМАЯ СЕМЬЯ, что и у призыва, только в другом слоте. Отзыв идёт
|
||||
/// через ItemActionSpawnEntity.Spawn, а до него надо ДОЖИТЬ через автомат состояний:
|
||||
///
|
||||
/// OnHoldingUpdate: stateTime += 0.05
|
||||
/// Anim: если stateTime >= animWait -> state = Spawn
|
||||
/// Spawn: Spawn(data); state = End
|
||||
/// ExecuteAction(_bReleased: true): state = None // отпустил - всё сбросилось
|
||||
///
|
||||
/// При animWait = 0.1 это ТРИ тика: 0.05 (мало), 0.10 (переход в Spawn), и только третий
|
||||
/// вызывает сам Spawn. То есть кнопку надо продержать около 0.15 секунды. У призыва это
|
||||
/// лечилось уменьшением animWait до 0.05, но там и оставалось два тика; меньше двух автомат
|
||||
/// не умеет в принципе - один переход и одно исполнение.
|
||||
///
|
||||
/// Поэтому отзыв вынут из автомата совсем: он делается прямо на НАЖАТИИ, в префиксе
|
||||
/// ExecuteAction, и оригинал не запускается вовсе. Никакого удержания, никакого ожидания.
|
||||
///
|
||||
/// ЗАЩЁЛКА ОБЯЗАТЕЛЬНА. ExecuteAction(_bReleased: false) зовётся не один раз за клик, а
|
||||
/// КАЖДЫЙ КАДР, пока кнопка нажата (PlayerMoveController проверяет Secondary.IsPressed, а не
|
||||
/// WasPressed), и вся защита от повторов у ванили держится на её же state == None, который мы
|
||||
/// теперь не выставляем. Без задержки ниже одно удержание правой кнопки отозвало бы питомца и
|
||||
/// следом залило экран надписью "не призван" по разу в кадр.
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(ItemActionSpawnEntity), "ExecuteAction")]
|
||||
public static class Patch_ItemActionSpawnEntity_ExecuteAction_InstantRecall
|
||||
{
|
||||
/// <summary>Секунды между двумя срабатываниями отзыва. Полсекунды заведомо больше любого
|
||||
/// клика и заведомо меньше осмысленного повторного нажатия.</summary>
|
||||
public const float RepeatGuardSeconds = 0.5f;
|
||||
|
||||
public static float lastRecallTime;
|
||||
|
||||
public static bool Prefix(ItemActionSpawnEntity __instance, ItemActionData _actionData, bool _bReleased)
|
||||
{
|
||||
if (_bReleased || _actionData == null ||
|
||||
_actionData.indexInEntityOfAction != Patch_ItemActionSpawnEntity_Spawn_PetLimit.RecallActionIndex)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!Patch_ItemActionSpawnEntity_Spawn_PetLimit.LimitedPets.TryGetValue(
|
||||
__instance.entityToSpawn, out Patch_ItemActionSpawnEntity_Spawn_PetLimit.PetInfo petInfo))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
EntityAlive holdingEntity = _actionData.invData != null ? _actionData.invData.holdingEntity : null;
|
||||
if (holdingEntity == null || holdingEntity.world == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (Time.time - lastRecallTime < RepeatGuardSeconds)
|
||||
{
|
||||
// Кнопка всё ещё зажата с прошлого кадра - молча проглатываем.
|
||||
return false;
|
||||
}
|
||||
int petClassId = EntityClass.GetId(__instance.entityToSpawn);
|
||||
if (petClassId == -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
lastRecallTime = Time.time;
|
||||
|
||||
List<OwnedEntityData> owned = holdingEntity.GetOwnedEntities(petClassId);
|
||||
if (owned.Count > 0)
|
||||
{
|
||||
Patch_ItemActionSpawnEntity_Spawn_PetLimit.RecallPet(holdingEntity, owned[0].Id,
|
||||
petInfo.RecalledKey, petInfo.ConsumesBook ? petInfo.SummonItemName : null);
|
||||
holdingEntity.PlayOneShot(__instance.soundWarn);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameManager.ShowTooltip(GameManager.Instance.World.GetPrimaryPlayer(), petInfo.NothingToRecallKey);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// "ОТЗЫВАЕТСЯ НЕ С ПЕРВОГО РАЗА" - баг-репорт пользователя 2026-09-18: "если прожал атаку
|
||||
/// несколько раз, то при попытке отозвать крысу, она не отзывается с первого раза".
|
||||
///
|
||||
/// Причина не в нашем коде и не в питомце, а в гейте ввода. PlayerMoveController решает,
|
||||
/// пускать ли вторичное действие, так:
|
||||
///
|
||||
/// bool flag16 = Actions[0].AllowConcurrentActions() || Actions[1].AllowConcurrentActions();
|
||||
/// bool flag18 = Actions[0].IsActionRunning(actionData[0]); // основное ещё "идёт"
|
||||
/// ...
|
||||
/// if (flag12 && flag15 && (flag16 || !flag18)) // flag15 = Secondary.IsPressed
|
||||
/// inventory.Execute(1, _bReleased: false, ...); // ВОТ ЭТО - отзыв
|
||||
///
|
||||
/// То есть пока основное действие считается идущим, вторичное просто не запускается.
|
||||
/// А "идущим" оно считается вот по чему (ItemActionSpawnEntity):
|
||||
///
|
||||
/// public override bool IsActionRunning(ItemActionData _actionData)
|
||||
/// => ((ItemActionDataSpawnEntity)_actionData).state != State.None;
|
||||
///
|
||||
/// Состояние проходит None -> Anim -> Spawn -> **End**, и в None возвращается ТОЛЬКО из
|
||||
/// ExecuteAction(_bReleased: true), то есть по отпусканию кнопки. End - это уже
|
||||
/// отработавшее, законченное действие, но формально всё ещё "не None". Чем чаще игрок
|
||||
/// щёлкает приказом атаковать, тем выше шанс, что в момент нажатия правой кнопки Action0
|
||||
/// висит именно в End, - и первый отзыв уходит в никуда.
|
||||
///
|
||||
/// ЧИНИМ РОВНО ЭТО И НИЧЕГО БОЛЬШЕ: для наших книг призыва End больше не считается "идёт".
|
||||
/// Соблазн был переписывать сам state в None - так делать НЕЛЬЗЯ: ExecuteAction стартует
|
||||
/// новое действие как раз по условию state == None, и при зажатой кнопке призыв/приказ пошёл
|
||||
/// бы на повтор каждые два тика, вместе с воплем кролика на каждый. Здесь же меняется только
|
||||
/// ОТВЕТ на вопрос "идёт ли действие": вторичное разблокировано, а повторный старт
|
||||
/// основного по-прежнему заперт настоящим полем state, которое так и осталось End.
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(ItemActionSpawnEntity), "IsActionRunning")]
|
||||
public static class Patch_ItemActionSpawnEntity_IsActionRunning_FreeTheRecall
|
||||
{
|
||||
public static void Postfix(ItemActionSpawnEntity __instance, ItemActionData _actionData, ref bool __result)
|
||||
{
|
||||
if (!__result || _actionData == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!Patch_ItemActionSpawnEntity_Spawn_PetLimit.LimitedPets.ContainsKey(__instance.entityToSpawn))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_actionData is ItemActionSpawnEntity.ItemActionDataSpawnEntity data
|
||||
&& data.state == ItemActionSpawnEntity.ItemActionDataSpawnEntity.State.End)
|
||||
{
|
||||
__result = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(EntityFactory), "CreateEntity", new System.Type[] { typeof(int), typeof(Vector3), typeof(Vector3) })]
|
||||
public static class Patch_EntityFactory_CreateEntity_PetOwnership
|
||||
{
|
||||
@@ -323,69 +628,57 @@ namespace NecromancerTome
|
||||
{
|
||||
owner.inventory.DecHoldingItem(1);
|
||||
}
|
||||
PetFollowPatch.Register(owner, __result);
|
||||
IgnoreCollisionWithOwner(owner, __result);
|
||||
ApplyGhostlyTransparency(__result);
|
||||
// Первая попытка развести коллайдеры - здесь же, чтобы в удачном случае не ждать
|
||||
// секунду до тика. Удачной она, скорее всего, НЕ БУДЕТ: модель питомца на этом шаге
|
||||
// ещё не собрана и коллайдеров у него нет (разбор - в TryIgnoreCollisionWithOwner).
|
||||
// Поэтому результат передаётся в Register, и тик повторяет попытку до успеха.
|
||||
bool collisionIgnored = PetFollowPatch.TryIgnoreCollisionWithOwner(owner, __result);
|
||||
PetFollowPatch.Register(owner, __result, petInfo.UsesFollowTask, collisionIgnored, petInfo.LitEyes,
|
||||
petInfo.FlyingHomeRadius);
|
||||
AttachFollowTask(owner, __result, petInfo);
|
||||
// Полупрозрачность больше НЕ делается здесь. Бывший ApplyGhostlyTransparency ставил
|
||||
// альфу в material.color и не работал ни разу: шейдеры моделей мобов альфу не
|
||||
// поддерживают (на Рое это выяснилось ещё 28.08 - он только засорял лог). Призрачный
|
||||
// вид всем питомцам теперь даёт GhostTraderPatch.cs - та же система, что у торговцев,
|
||||
// с клонированием материала, обесцвечиванием и подбором blend-шейдера.
|
||||
Debug.Log("[NecromancerTome] SummonPatch: owner=" + owner.entityId + " now owns pet " + __result.entityId);
|
||||
}
|
||||
|
||||
/// <summary>User request 2026-08-28 ("нематериальными") - makes the pet pass through its
|
||||
/// own owner specifically, via Physics.IgnoreCollision on every collider pair between the
|
||||
/// two, rather than stripping the pet's PhysicsBody entirely the way vanilla's own
|
||||
/// animalInsectSwarm does for its "no physics body at all" look (confirmed by checking
|
||||
/// entityclasses.xml). That works for a flying swarm; a ground-walking dog with no
|
||||
/// collider at all would fall through the terrain. This keeps it solid against ground and
|
||||
/// zombies - just not its owner - which also directly closes the last piece of the
|
||||
/// spawn-launch bug documented in items.xml/PetFollowPatch.cs (nothing left to shove the
|
||||
/// player if the two colliders can't touch in the first place).</summary>
|
||||
public static void IgnoreCollisionWithOwner(EntityPlayer owner, Entity pet)
|
||||
{
|
||||
Collider[] ownerColliders = owner.GetComponentsInChildren<Collider>();
|
||||
Collider[] petColliders = pet.GetComponentsInChildren<Collider>();
|
||||
foreach (Collider oc in ownerColliders)
|
||||
{
|
||||
if (oc == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach (Collider pc in petColliders)
|
||||
{
|
||||
if (pc == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Physics.IgnoreCollision(oc, pc, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>User request 2026-08-28 ("слегка прозрачными") - best effort only. Directly
|
||||
/// sets renderer.material.color's alpha, the same technique ItemActionSpawnTurret uses for
|
||||
/// its own placement-preview tint (confirmed by decompiling it), but that only visibly
|
||||
/// shows up if the model's actual shader supports alpha blending - most opaque mob
|
||||
/// shaders in this game don't, and there's no reliable XML/reflection-only way to swap a
|
||||
/// live renderer's shader to a transparent variant without risking breaking how it's lit.
|
||||
/// <summary>Вешает питомцу собственную задачу следования - указание 2026-09-18, пока
|
||||
/// только Духу крысы (PetInfo.UsesFollowTask).
|
||||
///
|
||||
/// BUG FIXED 2026-08-28: the Insect Swarm's renderers use a particle shader
|
||||
/// ("Game Particles/surfaceShader_masked_particleEnhanced") that has no "_Color" property
|
||||
/// at all - setting .color on it doesn't throw, but Unity logs "doesn't have a color
|
||||
/// property '_Color'" on every single access, once per renderer per spawn (confirmed in
|
||||
/// output_log - this is what the user saw as "an error about colors"). HasProperty check
|
||||
/// added so this silently skips any renderer whose shader doesn't support it instead of
|
||||
/// spamming the log - the visual effect was never going to work on those anyway.</summary>
|
||||
public static void ApplyGhostlyTransparency(Entity pet)
|
||||
/// ЧЕРЕЗ КОД, А НЕ ЧЕРЕЗ XML - см. шапку PetFollowTask.cs: XML-путь существует, но упирается
|
||||
/// в Type.GetType с именем сборки, а этот не упирается ни во что и заодно позволяет отдать
|
||||
/// владельца прямо в поле задачи, без поиска по ownedEntities на каждом тике.
|
||||
///
|
||||
/// Приоритет 3: у крысы в entityclasses.xml объявлены AITask-1 (ApproachAndAttackTarget) и
|
||||
/// AITask-2 (Look), а AITask-3 пустой - то есть следование встаёт ровно туда, где в XML
|
||||
/// кончился список, и ниже погони за целью. Разводить их приоритетом при этом всё равно
|
||||
/// недостаточно: задача следования сама отказывается работать, пока у питомца есть цель
|
||||
/// (NecroFollowOwnerTask.CanExecute), и несёт те же MutexBits=3, что ванильный
|
||||
/// EAIApproachSpot.</summary>
|
||||
public static void AttachFollowTask(EntityPlayer owner, Entity pet, Patch_ItemActionSpawnEntity_Spawn_PetLimit.PetInfo petInfo)
|
||||
{
|
||||
Renderer[] renderers = pet.GetComponentsInChildren<Renderer>();
|
||||
foreach (Renderer renderer in renderers)
|
||||
if (!petInfo.UsesFollowTask)
|
||||
{
|
||||
if (renderer == null || renderer.material == null || !renderer.material.HasProperty("_Color"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Color color = renderer.material.color;
|
||||
color.a = 0.55f;
|
||||
renderer.material.color = color;
|
||||
return;
|
||||
}
|
||||
EntityAlive alive = pet as EntityAlive;
|
||||
if (alive == null || alive.aiManager == null || alive.aiManager.tasks == null)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] SummonPatch: pet " + pet.entityId +
|
||||
" has no aiManager - follow task NOT attached");
|
||||
return;
|
||||
}
|
||||
NecroFollowOwnerTask task = new NecroFollowOwnerTask { OwnerEntityId = owner.entityId };
|
||||
if (petInfo.FollowDistance > 0f)
|
||||
{
|
||||
task.SlotDistance = petInfo.FollowDistance;
|
||||
}
|
||||
task.Init(alive);
|
||||
alive.aiManager.tasks.AddTask(3, task);
|
||||
Debug.Log("[NecromancerTome] SummonPatch: follow task attached to pet " + pet.entityId +
|
||||
" (owner " + owner.entityId + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user