ИИ питомцев переписан, добавлен Дух крысы, пороги разнесены по шкале

Работа по указаниям 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:
AlexCube
2026-09-18 20:51:42 +03:00
co-authored by Claude Opus 5
parent 15c59dd37a
commit d97f83e972
21 changed files with 2501 additions and 179 deletions
+77 -7
View File
@@ -266,6 +266,61 @@ namespace NecromancerTome
timer = 0f;
}
/// <summary>Кого этот проход обращает в призрака.
///
/// РАСШИРЕНО 2026-09-18 С ТОРГОВЦЕВ НА ПИТОМЦЕВ, по указанию: "всех призванных существ
/// (зомбопёс, зомбомедведь, зомбоволк, зомбогриф) тоже делаем полупрозрачными как сейчас
/// торговцев". Плюс Дух крысы, который призраком и задуман.
///
/// Обобщение вышло механическим, и это не совпадение: ApplyGreyscale ниже никогда не
/// пользовался ничем трейдерским - только GetComponentsInChildren&lt;Renderer&gt;(),
/// EntityClass.entityClassName и entityId, а всё это есть у любой EntityAlive.
///
/// СПИСОК БЕРЁТСЯ ИЗ SummonPatch.LimitedPets, а не дублируется здесь: новый питомец
/// становится призраком автоматически, в одном месте, и разойтись эти два списка не могут.
/// Рой жуков в нём тоже есть, но он безвреден - его рендереры это системы частиц, а
/// ApplyGreyscale их намеренно пропускает (см. Convert), так что для него проход вхолостую.
///
/// Проверка по имени класса, а не по типу: все наземные питомцы мода - это C#-класс
/// EntityZombieDog (см. entityclasses.xml), то есть по типу их не отличить ни друг от
/// друга, ни от настоящего зомбопса из мира.</summary>
/// <summary>Исключения из призрачного вида. Зомбопёс - по прямому указанию 2026-09-18:
/// "зомбособаку не делай полупрозрачной или серой, пусть будет обычной".
///
/// Проверка идёт по ИМЕНИ КЛАССА, а не по типу, и это здесь важно: Дух крысы наследуется
/// от necroZombieWolf, а тот от necroZombieDog, то есть по типу они все EntityZombieDog и
/// исключение задело бы заодно и крысу, которая призраком как раз и задумана.</summary>
public static readonly HashSet<string> NotGhosted = new HashSet<string>
{
"necroZombieDog",
// Медведь и Волк добавлены 2026-09-18: "прозрачность им не нужна". Призрачными
// остаются Дух крысы, Зомбогриф и торговцы.
"necroZombieBear",
"necroZombieWolf",
// Гриф выведен 18.09 вместе с откатом: "не делай его прозрачным". На птичьей модели
// обесцвечивание к тому же брало не всё - прозрачными выходили только туловище и
// голова, что и выглядело жутко.
"necroZombieGriffin",
};
public static bool ShouldBeGhost(EntityAlive _entity)
{
if (_entity is EntityTrader)
{
return true;
}
EntityClass entityClass = EntityClass.list[_entity.entityClass];
if (entityClass == null || entityClass.entityClassName == null)
{
return false;
}
if (NotGhosted.Contains(entityClass.entityClassName))
{
return false;
}
return Patch_ItemActionSpawnEntity_Spawn_PetLimit.LimitedPets.ContainsKey(entityClass.entityClassName);
}
public static void OnUnityUpdate(ref ModEvents.SUnityUpdateData _data)
{
timer += Time.deltaTime;
@@ -284,11 +339,11 @@ namespace NecromancerTome
for (int i = 0; i < world.EntityAlives.Count; i++)
{
EntityAlive entity = world.EntityAlives[i];
if (!(entity is EntityTrader trader) || trader.IsDead())
if (entity == null || entity.IsDead() || !ShouldBeGhost(entity))
{
continue;
}
if (Ghosted.TryGetValue(trader.entityId, out GhostBody body))
if (Ghosted.TryGetValue(entity.entityId, out GhostBody body))
{
if (IsIntact(body))
{
@@ -297,14 +352,14 @@ namespace NecromancerTome
// His model was destroyed and rebuilt under him. Drop what is known about the
// old one before building the new, or Converted and TintedMaterials keep
// entries for renderers and materials that no longer exist.
Debug.Log("[NecromancerTome] GhostTraderPatch: entity " + trader.entityId +
Debug.Log("[NecromancerTome] GhostTraderPatch: entity " + entity.entityId +
" came back with a new model - ghosting him again");
Ghosted.Remove(trader.entityId);
Ghosted.Remove(entity.entityId);
Prune();
}
if (ApplyGreyscale(trader, out GhostBody fresh))
if (ApplyGreyscale(entity, out GhostBody fresh))
{
Ghosted[trader.entityId] = fresh;
Ghosted[entity.entityId] = fresh;
}
}
}
@@ -363,7 +418,7 @@ namespace NecromancerTome
/// leaves this trader unmarked and tries again on the next sweep. On true, _body carries
/// the renderers taken over, which is how the next sweep tells this trader from a rebuilt
/// one standing under the same entity id.</summary>
public static bool ApplyGreyscale(EntityTrader _trader, out GhostBody _body)
public static bool ApplyGreyscale(EntityAlive _trader, out GhostBody _body)
{
_body = default(GhostBody);
Renderer[] renderers = _trader.GetComponentsInChildren<Renderer>(true);
@@ -797,6 +852,21 @@ namespace NecromancerTome
break;
}
// НАЙДЕНО 2026-09-18, НО НЕ ИСПРАВЛЕНО - записано, чтобы не искать заново.
// Пользователь: "собака не полупрозрачная". По логу у неё для этого всё было -
// шейдер Standard с _Color, _SrcBlend, _DstBlend, "5 of 5 renderer(s) desaturated,
// 5 opacity lever(s) installed". То есть альфа проставлена, а прозрачности нет.
//
// Разница с трейдерами, у которых она работает: их тело идёт по ДРУГОЙ ветке -
// MakeBlendMaterial создаёт материал заново и ставит там _Mode = 2 (Fade). Здесь,
// на ветке "блендим на месте", _Mode и _ZWrite не трогаются вовсе, а материал,
// собранный как Opaque, приходит с _Mode = 0 и _ZWrite = 1. Юнити для перевода
// Standard в прозрачность требует всю шестёрку: _Mode, _SrcBlend, _DstBlend,
// _ZWrite, ключевые слова и очередь отрисовки - у нас выставлены четыре из шести.
//
// Собаку это чинить больше не нужно (она выведена из призраков совсем), но Медведь,
// Волк и Гриф идут по этой же ветке. Если и они окажутся непрозрачными - причина
// здесь, и лечится добавлением _Mode = 2 и _ZWrite = 0 рядом со строками ниже.
if (_material.HasProperty("_SrcBlend") && _material.HasProperty("_DstBlend"))
{
_material.SetFloat("_SrcBlend", (float)BlendMode.SrcAlpha);