Одержимость питомцев, стервятник-разведчик, метки на карте, череп, глава письма
- Одержимость питомца: ПКМ с книгой - вселиться, E - отозвать (и изнутри питомца), пояс скрыт, надпись о враге у тела. - Одержимый стервятник: полёт, рывок на ЛКМ с 10 м с прямым уроном, звуком и кровотечением, обычный прицел краснеет на цели; посадка в позу 0 и взлёт на пробел; ПКМ - клич-приманка (паникующая курица, откат 0.5 с); F - питаться трупами и блоками-падалью, доеденное исчезает. - Дух змеи атакует только нежить; фиолетовая метка на карте у змеи и одержимого. - Череп из красного трупа (этап 1 цинерария); глава книги «Письмо архимага» с видео. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a79bb085ab
commit
beab1640b0
@@ -0,0 +1,64 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Метка на карте для того, кем сейчас управляет игрок: дух змеи и одержимый (зомби или питомец).
|
||||
/// Указание 06.10: «Змея всё ещё не появляется на карте. Подозреваю, что питомцы в режиме одержимости тоже».
|
||||
///
|
||||
/// ПОЧЕМУ НЕ Entity.AddNavObject (так было у змеи с 02.10). У сущности одна общая NavObject на все её
|
||||
/// классы меток; у змеи от ванильного animalSnake там уже лежат twitch_spawn, animaltracking_snake,
|
||||
/// clear_sleeper. AddNavObject только дописывает класс в этот список, а какой из них показывать,
|
||||
/// игра решает сама (NavObject.HandleActiveNavClass), и карта (XUiC_MapArea.updateNavObjectList)
|
||||
/// заводит спрайт метки один раз по её ключу. Метка змеи так и не появилась ни разу.
|
||||
///
|
||||
/// ЗДЕСЬ - как у транспорта (EntityVehicle.HandleNavObject: RegisterNavObject(класс,
|
||||
/// vehicle.GetMeshTransform()), разбор 06.10): своя, отдельная NavObject, привязанная к Transform (перегрузка
|
||||
/// NavObjectManager.RegisterNavObject(className, Transform)): своя запись, свой ключ, один класс.
|
||||
/// Позиция - transform.position (NavObject.GetPosition для TrackTypes.Transform), т.е. сама модель.
|
||||
/// Снимается UnRegisterNavObject; если сущность исчезла, игра снимет метку сама (IsValid).
|
||||
///
|
||||
/// ЧУЖУЮ МЕТКУ НЕ ТРОГАТЬ (проверка на утечки 06.10). Снятые метки игра кладёт в пул
|
||||
/// (removedNavObjectPool) и раздаёт заново. Если модель исчезла раньше нашего Hide, игра сама сняла
|
||||
/// метку, и пул мог уже отдать этот же объект другому (метка чутья, транспорт). Поэтому снимаем, только
|
||||
/// если объект ещё в NavObjectList и следит за НАШЕЙ моделью; иначе просто забываем ссылку.
|
||||
/// Класс necroSnakeSpirit (nav_objects.xml) - только карта, фиолетовый; значок - OverrideSpriteName.
|
||||
/// </summary>
|
||||
public static class NecroMapMarker
|
||||
{
|
||||
public const string NavClass = "necroSnakeSpirit";
|
||||
|
||||
/// <summary>Метка и модель, за которой она следит.</summary>
|
||||
public class Marker
|
||||
{
|
||||
public NavObject Nav;
|
||||
public Transform Tracked;
|
||||
}
|
||||
|
||||
public static Marker Show(EntityAlive _e, string _sprite)
|
||||
{
|
||||
if (_e == null || _e.transform == null || NavObjectManager.Instance == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
NavObject nav = NavObjectManager.Instance.RegisterNavObject(NavClass, _e.transform, _sprite, true);
|
||||
Debug.Log("[NecromancerTome] NecroMapMarker: метка на карте для " + _e.EntityName + " (" + _e.entityId + ") - " + nav);
|
||||
return new Marker { Nav = nav, Tracked = _e.transform };
|
||||
}
|
||||
|
||||
public static void Hide(ref Marker _m)
|
||||
{
|
||||
NavObjectManager manager = NavObjectManager.Instance;
|
||||
if (_m != null && _m.Nav != null && manager != null && _m.Tracked != null &&
|
||||
manager.NavObjectList.Contains(_m.Nav) && _m.Nav.IsTrackedTransform(_m.Tracked))
|
||||
{
|
||||
manager.UnRegisterNavObject(_m.Nav);
|
||||
}
|
||||
_m = null;
|
||||
}
|
||||
|
||||
/// <summary>Значок одержимого (зомби или питомца) - значок дебаффа «Почти смерть» (buffNearDeathTrauma,
|
||||
/// вешается игроку после гибели), указание 06.10.</summary>
|
||||
public const string PossessedSprite = "ui_game_symbol_near_death_trauma";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Череп из красного трупа (BACKLOG «Некротехнолог v2», указание 06.10): тот же жест, что снятие
|
||||
/// блока браслетом, - зажатая обычная атака с браслетом в руке, круговой таймер. Только целью
|
||||
/// здесь не блок, а труп зомби, погибшего под дебаффом жертвы (CorpseLoot.IsSearchable). Труп -
|
||||
/// сущность, а не блок, поэтому SpatialVaultPickup.Begin сперва спрашивает этот класс.
|
||||
///
|
||||
/// Шанс 2% («так ресурс станет ценнее»): удача - череп в пространственное хранилище, труп остаётся
|
||||
/// (его можно обыскать), второй попытки на нём нет (cvar без точки - пишется в сейв вместе с трупом);
|
||||
/// неудача - труп уходит в чёрный разрыв (NecroBurst), вместе с недобранной добычей.
|
||||
///
|
||||
/// Цена та же, что у блоков: без сферы или камня в слоте браслет не работает, сфера платит
|
||||
/// прочностью за секунды канала. Разрыв делает сервер - в мультиплеере клиенту пока только удача
|
||||
/// (труп не исчезает); решать вместе с прочим мультиплеером урн.
|
||||
/// </summary>
|
||||
public static class NecroSkull
|
||||
{
|
||||
public const string ItemName = "necroSkull";
|
||||
/// <summary>На трупе: попытка уже была удачной.</summary>
|
||||
public const string TakenCVar = "necroSkullTaken";
|
||||
public const float Chance = 0.02f;
|
||||
/// <summary>Короче десяти секунд блока: шанс мал, попытка должна быть дешёвой.</summary>
|
||||
public const float ChannelSeconds = 3f;
|
||||
/// <summary>Дальше этого тянуть череп нельзя - как дистанция обыска трупа у игры.</summary>
|
||||
public static float MaxDistance => Constants.cCollectItemDistance;
|
||||
|
||||
public const string MsgChanneling = "necroSkullChanneling";
|
||||
public const string MsgTaken = "necroSkullAlreadyTaken";
|
||||
public const string MsgExtracted = "necroSkullExtracted";
|
||||
public const string MsgFailed = "necroSkullFailed";
|
||||
|
||||
public class SkullJob : SpatialVaultPickup.PickupJob
|
||||
{
|
||||
public int CorpseId;
|
||||
}
|
||||
|
||||
/// <summary>Красный труп под прицелом (так же, как игра ищет сущность для E -
|
||||
/// PlayerMoveController.TryHandleEntity), иначе null.</summary>
|
||||
public static EntityAlive CorpseAtCrosshair(EntityPlayerLocal _player)
|
||||
{
|
||||
WorldRayHitInfo hit = _player.HitInfo;
|
||||
if (hit == null || !hit.bHitValid || hit.tag == null || !hit.tag.StartsWith("E_") ||
|
||||
hit.hit.distanceSq > MaxDistance * MaxDistance)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Transform root = GameUtils.GetHitRootTransform(hit.tag, hit.transform);
|
||||
EntityAlive corpse = root != null ? root.GetComponent<Entity>() as EntityAlive : null;
|
||||
return corpse != null && corpse.IsDead() && CorpseLoot.IsSearchable(corpse) ? corpse : null;
|
||||
}
|
||||
|
||||
/// <summary>Обычная атака браслетом: true - под прицелом красный труп, нажатие наше.</summary>
|
||||
public static bool TryBegin(EntityPlayerLocal _player, ItemValue _bracelet)
|
||||
{
|
||||
EntityAlive corpse = CorpseAtCrosshair(_player);
|
||||
if (corpse == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (corpse.Buffs.GetCustomVar(TakenCVar) > 0f)
|
||||
{
|
||||
SpatialVaultPickup.Deny(_player, MsgTaken);
|
||||
return true;
|
||||
}
|
||||
Bag bag = SpatialVaultPickup.GetVault(_player);
|
||||
ItemValue skull = ItemClass.GetItem(ItemName);
|
||||
if (bag == null || skull == null || skull.IsEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!bag.CanTakeItem(new ItemStack(skull, 1)))
|
||||
{
|
||||
SpatialVaultPickup.Deny(_player, SpatialVaultPickup.MsgVaultFull);
|
||||
return true;
|
||||
}
|
||||
|
||||
TimerEventData timerData = new TimerEventData
|
||||
{
|
||||
Data = new SkullJob
|
||||
{
|
||||
Player = _player,
|
||||
Bracelet = _bracelet,
|
||||
ChannelSeconds = ChannelSeconds,
|
||||
CorpseId = corpse.entityId
|
||||
},
|
||||
CloseOnHit = true,
|
||||
CancelWithActivateButton = true
|
||||
};
|
||||
timerData.FullTimeFinishEvent += OnChannelComplete;
|
||||
timerData.CloseEvent += delegate
|
||||
{
|
||||
ChannelVision.End(_player);
|
||||
};
|
||||
LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPlayer(_player);
|
||||
XUiC_Timer.OpenTimer(playerUI.xui, ChannelSeconds, timerData, -1f, Localization.Get(MsgChanneling));
|
||||
ChannelVision.Begin(_player);
|
||||
Debug.Log("[NecromancerTome] NecroSkull: " + _player.entityId + " тянет череп из трупа " + corpse.entityId);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void OnChannelComplete(TimerEventData _timerData)
|
||||
{
|
||||
if (!(_timerData.Data is SkullJob job) || job.Player == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ChannelVision.End(job.Player);
|
||||
World world = GameManager.Instance != null ? GameManager.Instance.World : null;
|
||||
EntityAlive corpse = world?.GetEntity(job.CorpseId) as EntityAlive;
|
||||
// Труп обыскали до дна и он исчез, его разорвали или уже вытянули череп - пока шёл круг.
|
||||
if (corpse == null || !corpse.IsDead() || !CorpseLoot.IsSearchable(corpse))
|
||||
{
|
||||
SpatialVaultPickup.Deny(job.Player, SpatialVaultPickup.MsgBlockMissing);
|
||||
return;
|
||||
}
|
||||
if (corpse.Buffs.GetCustomVar(TakenCVar) > 0f)
|
||||
{
|
||||
SpatialVaultPickup.Deny(job.Player, MsgTaken);
|
||||
return;
|
||||
}
|
||||
Bag bag = SpatialVaultPickup.GetVault(job.Player);
|
||||
ItemValue skull = ItemClass.GetItem(ItemName);
|
||||
if (bag == null || skull == null || skull.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float roll = Random.value;
|
||||
bool success = roll < Chance;
|
||||
Debug.Log("[NecromancerTome] NecroSkull: труп " + corpse.entityId + " - бросок " +
|
||||
roll.ToString("0.000") + " против " + Chance.ToString("0.00") + " -> " + (success ? "ЧЕРЕП" : "разрыв"));
|
||||
if (success)
|
||||
{
|
||||
if (!bag.AddItem(new ItemStack(skull, 1)))
|
||||
{
|
||||
SpatialVaultPickup.Deny(job.Player, SpatialVaultPickup.MsgVaultFull);
|
||||
return;
|
||||
}
|
||||
corpse.Buffs.SetCustomVar(TakenCVar, 1f);
|
||||
GameManager.ShowTooltip(job.Player, MsgExtracted);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameManager.ShowTooltip(job.Player, MsgFailed);
|
||||
NecroBurst.Burst(corpse, job.Player);
|
||||
}
|
||||
// Попытка оплачена в обоих исходах: канал отработан.
|
||||
SpatialVaultPickup.SpendCharge(job);
|
||||
if (success)
|
||||
{
|
||||
GameManager.Instance.SaveLocalPlayerData();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
-1
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Scripting;
|
||||
using UnityEngine.Video;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
@@ -24,11 +25,29 @@ namespace NecromancerTome
|
||||
public string TitleKey;
|
||||
public string[] PageKeys;
|
||||
public Func<EntityPlayerLocal, bool> Unlocked;
|
||||
/// <summary>Иллюстрация справа от текста на всех страницах главы: картинка (путь вида
|
||||
/// "@modfolder(NecromancerTome):Textures/x.png") или зацикленное видео без звука. Ни того,
|
||||
/// ни другого - текст на всю ширину.</summary>
|
||||
public string Image, Video;
|
||||
}
|
||||
|
||||
/// <summary>cvar «письмо архимага прочитано» - ставит записка noteDuke01 (Config/items.xml).</summary>
|
||||
public const string LetterReadCVar = "necroTomeLetterRead";
|
||||
|
||||
public static readonly List<Chapter> Chapters = new List<Chapter>
|
||||
{
|
||||
// Глава 1 (указание 03.10): статья о некроэнергии, сокращённая - без примеров и бытовой речи.
|
||||
// Глава 1 (указание 06.10): письмо архимага - та самая стартовая записка, текст берётся из её
|
||||
// же ключа. Открывается ТОЛЬКО когда записку прочли («появляется в книге, когда прочитано
|
||||
// письмо», 06.10): cvar ставит сама записка по «ОК» (Config/items.xml). Запасного условия по
|
||||
// уровню нет - в старых сейвах, где записку прочли до 1.7.0, главы не будет.
|
||||
new Chapter
|
||||
{
|
||||
TitleKey = "necroTomeLetterTitle",
|
||||
PageKeys = new[] { "noteDuke01Desc" },
|
||||
Video = Patch_XUiC_MessageBoxWindowGroup_ShowOkCancel_NoteFlashback.NoteFlashbackVideoPath,
|
||||
Unlocked = _p => _p.Buffs.GetCustomVar(LetterReadCVar) > 0f
|
||||
},
|
||||
// Глава 2 (указание 03.10): статья о некроэнергии, сокращённая - без примеров и бытовой речи.
|
||||
new Chapter
|
||||
{
|
||||
TitleKey = "necroTomeCh1Title",
|
||||
@@ -50,6 +69,10 @@ namespace NecromancerTome
|
||||
const int MaxButtons = 8;
|
||||
|
||||
XUiV_Label _title, _text, _page;
|
||||
XUiV_Texture _img;
|
||||
XUiV_Video _vid;
|
||||
/// <summary>Ширина текста без иллюстрации и с ней (windows.xml: lblText 1180, картинка с x 680).</summary>
|
||||
const int TextWidthFull = 1180, TextWidthHalf = 590;
|
||||
XUiC_SimpleButton _toc, _prev, _next, _close;
|
||||
readonly XUiC_SimpleButton[] _chapterButtons = new XUiC_SimpleButton[MaxButtons];
|
||||
/// <summary>Глава по кнопке оглавления (только открытые главы, по порядку).</summary>
|
||||
@@ -62,6 +85,8 @@ namespace NecromancerTome
|
||||
_title = GetChildById("lblTitle")?.ViewComponent as XUiV_Label;
|
||||
_text = GetChildById("lblText")?.ViewComponent as XUiV_Label;
|
||||
_page = GetChildById("lblPage")?.ViewComponent as XUiV_Label;
|
||||
_img = GetChildById("imgPage")?.ViewComponent as XUiV_Texture;
|
||||
_vid = GetChildById("vidPage")?.ViewComponent as XUiV_Video;
|
||||
_toc = GetChildById("btnToc") as XUiC_SimpleButton;
|
||||
_prev = GetChildById("btnPrev") as XUiC_SimpleButton;
|
||||
_next = GetChildById("btnNext") as XUiC_SimpleButton;
|
||||
@@ -93,6 +118,7 @@ namespace NecromancerTome
|
||||
SetLabel(_title, Localization.Get("necroTomeToc"));
|
||||
SetLabel(_text, "");
|
||||
SetLabel(_page, "");
|
||||
SetMedia(null, null);
|
||||
EntityPlayerLocal player = xui.playerUI.entityPlayer;
|
||||
int b = 0;
|
||||
for (int c = 0; c < NecroTomeBook.Chapters.Count && b < MaxButtons; c++)
|
||||
@@ -138,6 +164,7 @@ namespace NecromancerTome
|
||||
SetLabel(_title, Localization.Get(ch.TitleKey));
|
||||
SetLabel(_text, Localization.Get(ch.PageKeys[_pageIndex]));
|
||||
SetLabel(_page, (_pageIndex + 1) + " / " + ch.PageKeys.Length);
|
||||
SetMedia(ch.Image, ch.Video);
|
||||
SetVisible(_toc, true);
|
||||
SetVisible(_prev, true);
|
||||
SetVisible(_next, true);
|
||||
@@ -145,6 +172,52 @@ namespace NecromancerTome
|
||||
if (_next != null) _next.Enabled = _pageIndex < ch.PageKeys.Length - 1;
|
||||
}
|
||||
|
||||
public override void OnClose()
|
||||
{
|
||||
SetMedia(null, null);
|
||||
base.OnClose();
|
||||
}
|
||||
|
||||
/// <summary>Показать картинку или видео справа (или убрать обе). Видео задаётся только здесь,
|
||||
/// а не в вёрстке: XUiV_Video сам запускает ролик при открытии окна, и в оглавлении он играл
|
||||
/// бы невидимым. Пустой uri - XUiV_Video.startVideo сам освобождает свою текстуру.</summary>
|
||||
void SetMedia(string _image, string _video)
|
||||
{
|
||||
bool img = !string.IsNullOrEmpty(_image), vid = !img && !string.IsNullOrEmpty(_video);
|
||||
if (_img != null)
|
||||
{
|
||||
if (img && _img.Path != _image)
|
||||
{
|
||||
_img.Path = _image;
|
||||
}
|
||||
_img.IsVisible = img;
|
||||
}
|
||||
if (_vid != null)
|
||||
{
|
||||
string uri = vid ? _video : "";
|
||||
if (_vid.VideoUri != uri)
|
||||
{
|
||||
_vid.VideoUri = uri;
|
||||
}
|
||||
if (_vid.videoPlayer != null)
|
||||
{
|
||||
_vid.videoPlayer.audioOutputMode = VideoAudioOutputMode.None;
|
||||
// Проверка на утечки 06.10: XUiV_Video гасит ролик при смене uri, только если тот уже
|
||||
// играет; ушли в оглавление во время подготовки - догрузился бы и играл невидимым до
|
||||
// закрытия окна. Останавливаем сами (Stop освобождает декодер).
|
||||
if (!vid)
|
||||
{
|
||||
_vid.videoPlayer.Stop();
|
||||
}
|
||||
}
|
||||
_vid.IsVisible = vid;
|
||||
}
|
||||
if (_text != null)
|
||||
{
|
||||
_text.Size = new Vector2i(img || vid ? TextWidthHalf : TextWidthFull, _text.Size.y);
|
||||
}
|
||||
}
|
||||
|
||||
void Turn(int _d)
|
||||
{
|
||||
if (_chapter >= 0)
|
||||
|
||||
@@ -49,6 +49,12 @@
|
||||
<!-- EntityAlive.PlayOneShot(string)'s own overload set touches AnimationEvent (2026-08-29,
|
||||
BansheePatch.cs) - needed even though this mod never uses AnimationEvent directly, just
|
||||
to satisfy the compiler's reference-resolution for that overload. -->
|
||||
<!-- NecroTome.cs (2026-10-06): видео-иллюстрация в книге играет без звука -
|
||||
VideoPlayer.audioOutputMode живёт в VideoModule. -->
|
||||
<Reference Include="UnityEngine.VideoModule">
|
||||
<HintPath>..\..\..\7DaysToDie_Data\Managed\UnityEngine.VideoModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.AnimationModule">
|
||||
<HintPath>..\..\..\7DaysToDie_Data\Managed\UnityEngine.AnimationModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
|
||||
@@ -519,6 +519,13 @@ namespace NecromancerTome
|
||||
tracked.CollisionIgnored = TryIgnoreCollisionWithOwner(owner, pet);
|
||||
}
|
||||
|
||||
// Одержимый питомец (указание 06.10) ведётся игроком: без охраны, поводка, «дома» и
|
||||
// телепорта - удаляться можно на любое расстояние. После выхода всё это вернёт его к хозяину.
|
||||
if (Possession.IsPossessed(pet))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Охрана и поводок 40 м - до блока про «дом» летающих: тот в этом же тике должен
|
||||
// увидеть уже назначенную или снятую цель. Рой (не спутник) сюда не попадает.
|
||||
if (tracked.Guards || tracked.OwnFollowTask || tracked.FlyingHomeRadius > 0)
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// УБИЙСТВА ПИТОМЦЕМ ТЕПЕРЬ ЗАСЧИТЫВАЮТСЯ ВЛАДЕЛЬЦУ - 2026-09-18.
|
||||
/// Одержимый питомец (06.10) - тоже питомец: его убийства идут хозяину этим же префиксом.
|
||||
/// А его СОБСТВЕННАЯ гибель не засчитывается никому (06.10, первая проверка префикса).
|
||||
///
|
||||
/// ПОЧЕМУ РАНЬШЕ НЕ ЗАСЧИТЫВАЛИСЬ. Ваниль решает, чей это фраг, ровно в одном месте:
|
||||
///
|
||||
@@ -53,6 +55,14 @@ namespace NecromancerTome
|
||||
{
|
||||
public static bool Prefix(EntityAlive __instance, DamageSource _source, ref EntityAlive _killingEntity)
|
||||
{
|
||||
// Гибель СВОЕГО существа (питомцы, рой, дух змеи - все классы necro*) игроку в плюс не идёт
|
||||
// никогда (указание 06.10): ни опыт, ни некромантия - даже если добил его сам игрок. Оригинал
|
||||
// пропускаем целиком, он раздал бы опыт убийце.
|
||||
if (__instance != null && SnakeControl.IsNecroEntity(__instance))
|
||||
{
|
||||
Debug.Log("[NecromancerTome] PetKillCredit: гибель своего существа " + __instance.EntityName + " (" + __instance.entityId + ") - не засчитывается");
|
||||
return false;
|
||||
}
|
||||
if (_source == null)
|
||||
{
|
||||
return true;
|
||||
|
||||
+785
-12
@@ -37,6 +37,16 @@ namespace NecromancerTome
|
||||
/// E - подрыв у EntityZombieCop (коп, мутант, подрывник): HandlePrimingDetonator, через несколько
|
||||
/// секунд взрыв от имени зомби - он гибнет, игрок возвращается в тело.
|
||||
///
|
||||
/// ОДЕРЖИМОСТЬ ПИТОМЦА (указание 06.10): ПКМ с книгой призыва - вселение в своего питомца (все, включая
|
||||
/// стервятника и Духа крысы), тем же механизмом. Отличия: питомец не пугается на выходе, а возвращается
|
||||
/// к хозяину своим ИИ (охрана, поводок - PetFollowPatch пропускает одержимого); стервятник летает
|
||||
/// (EntityVulture ведёт полёт сам в своём updateTasks - его мы подменяем на FlyTick). Удаляться от тела
|
||||
/// можно на любое расстояние, область тела сжата так же, тело так же уязвимо.
|
||||
///
|
||||
/// ВРАГ У ТЕЛА (указание 06.10, как у змеи): раз в полсекунды - есть ли враг ближе
|
||||
/// SnakeControl.BodyDangerRange к телу; надпись necroSnakeBodyDanger под прицелом (PossessionHud).
|
||||
/// Для одержимого зомби - тоже.
|
||||
///
|
||||
/// ПАМЯТЬ. Ничего не сохраняется. Ссылки - только на зомби, тело и замороженные аниматоры, на время
|
||||
/// одержимости; на выходе и при выходе из мира всё обнуляется.
|
||||
/// </summary>
|
||||
@@ -44,6 +54,70 @@ namespace NecromancerTome
|
||||
{
|
||||
public static bool Active;
|
||||
public static EntityAlive Zombie;
|
||||
/// <summary>Вселились в своего питомца (книга призыва), а не в чужого зомби.</summary>
|
||||
public static bool IsPet;
|
||||
/// <summary>Питомец-стервятник: летает (FlyTick из его updateTasks).</summary>
|
||||
public static bool Flying;
|
||||
/// <summary>Враг у тела (CheckBody) - для надписи в PossessionHud.</summary>
|
||||
public static bool BodyDanger;
|
||||
static float _bodyCheck;
|
||||
// Ввод полёта - кадр пишет, тик стервятника (20 в секунду) читает.
|
||||
static Vector2 _flyMove;
|
||||
static bool _flyUp, _flyDown, _flyRun;
|
||||
/// <summary>Разгон полёта - как у ванильного стервятника (EntityVulture.updateTasks: 0.55 обычный,
|
||||
/// 0.95 пикирование); на бегу - пикирующий.</summary>
|
||||
public const float FlyAccel = 0.55f, FlyAccelRun = 0.95f;
|
||||
/// <summary>Удар стервятника - его же пауза между клевками (cAttackDelay 18 тиков).</summary>
|
||||
public const float FlyAttackDelay = 0.9f;
|
||||
/// <summary>Рывок стервятника на ЛКМ (указание 06.10: «как у змеи, но с 5 метров и на простой
|
||||
/// удар» - иначе в полёте никого не достать). Цель - на луче прицела не дальше FlyDashRange от
|
||||
/// стервятника; летит к её голове FlyDashSpeed м за тик (20 тиков в секунду), долетел ближе
|
||||
/// FlyDashHit (у ванильного клевка 0.9 м) - обычный клевок. Не долетел за FlyDashTime - отбой.</summary>
|
||||
public const float FlyDashRange = 10f; // было 5 м; 10 - указание 06.10 (с высоты полёта 5 м не хватало)
|
||||
/// <summary>Насколько луч прицела может пройти мимо тела цели (у змеи 0.9 м от груди - с высоты мало).</summary>
|
||||
public const float FlyDashAimRadius = 1.5f;
|
||||
public const float FlyDashSpeed = 0.6f;
|
||||
public const float FlyDashHit = 0.9f;
|
||||
public const float FlyDashTime = 1f; // 10 м при 12 м/с - 0.85 с
|
||||
/// <summary>Урон, если оружие стервятника не прочиталось (у грифа-питомца - 20, items.xml).</summary>
|
||||
public const float FlyDashDamage = 20f;
|
||||
/// <summary>Бафф кровотечения от клевка грифа-питомца (necroMeleeHandZombieGriffin).</summary>
|
||||
public const string FlyDashBleedBuff = "buffNecroRatBleed";
|
||||
/// <summary>Звук попадания рывка - ванильный удар рукой зомби по телу (sounds.xml).</summary>
|
||||
public const string FlyDashHitSound = "organichitorganic";
|
||||
static EntityAlive _flyDash;
|
||||
/// <summary>Цель рывка под прицелом сейчас (каждый кадр) - обычный прицел игры краснеет (AimTint).</summary>
|
||||
public static EntityAlive FlyAim;
|
||||
static float _flyDashUntil;
|
||||
/// <summary>Сел на землю без ввода - поза спящего стервятника (указание 06.10), как у ванильного
|
||||
/// sleeperVultureAnimal (поза 0, sleeperSit). Только анимация: IsSleeping не трогаем - его читают
|
||||
/// ИИ и звуки. Взлёт - поза «проснулся» (-1).</summary>
|
||||
public const int VultureSitPose = 0;
|
||||
public const float FlySitGround = 0.3f;
|
||||
/// <summary>Остановился - скорость меньше этой, м за тик (0.02 = 0.4 м/с).</summary>
|
||||
public const float FlySitSpeed = 0.02f;
|
||||
/// <summary>Толчок вверх при взлёте с земли на пробел, м за тик.</summary>
|
||||
public const float FlyTakeOff = 0.3f;
|
||||
static bool _flySitting;
|
||||
/// <summary>Питание падалью (указание 06.10): сидя у трупа, F - EatTime секунд клюёт его и лечится на
|
||||
/// EatHealFrac здоровья. У трупа запас мяса - его полное здоровье при жизни; поедание отнимает
|
||||
/// столько же, сколько лечит (и на полном здоровье тоже). Запас кончился - труп исчезает (указание
|
||||
/// 06.10), вместе с содержимым, если это красный труп-добыча. Сдвинулся, взлетел, ударил - еда
|
||||
/// прервана, вылеченное и съеденное остаются; к трупу можно вернуться.</summary>
|
||||
public const float EatRange = 2.5f;
|
||||
public const float EatTime = 3f;
|
||||
public const float EatHealFrac = 0.25f;
|
||||
static EntityAlive _eatCorpse;
|
||||
static float _eatLeft, _eatHeal, _eatBite;
|
||||
static int _eatCount;
|
||||
static readonly Dictionary<int, float> _carrion = new Dictionary<int, float>();
|
||||
static readonly Dictionary<Vector3i, float> _carrionBlocks = new Dictionary<Vector3i, float>();
|
||||
static readonly Dictionary<int, bool> _isCarrion = new Dictionary<int, bool>();
|
||||
static bool _eating;
|
||||
static Vector3i _eatBlock;
|
||||
static int _eatBlockType;
|
||||
/// <summary>Признак блока-падали: среди его добычи есть гнилая плоть.</summary>
|
||||
public const string CarrionItem = "foodRottingFlesh";
|
||||
|
||||
/// <summary>Камера - позади и выше головы зомби.</summary>
|
||||
public const float CamBack = 2.6f;
|
||||
@@ -83,9 +157,15 @@ namespace NecromancerTome
|
||||
public static int FullViewDim => _bodyViewDim;
|
||||
public const float ScreamRange = 50f;
|
||||
public const float ScreamCooldown = 10f;
|
||||
/// <summary>Клич грифа - крик паникующей курицы (указание 06.10; сначала был chickenattack), ванильный
|
||||
/// sounds.xml, 6 вариантов. Перезарядка - полсекунды, а не 10 с крикуньи (тест 06.10: «у звука КД»).</summary>
|
||||
public const string FlyScreamSound = "chickenstressmax";
|
||||
public const float FlyScreamCooldown = 0.5f;
|
||||
static float _nextScream;
|
||||
|
||||
static EntityPlayerLocal _player;
|
||||
/// <summary>Игрок, который сейчас в одержимом (тело).</summary>
|
||||
public static EntityPlayerLocal Player => _player;
|
||||
static float _yaw, _pitch;
|
||||
/// <summary>Куда повёрнуто тело зомби. Шага вбок и назад у зомби нет (тест 03.10: «не работают A, D и S») -
|
||||
/// при движении тело разворачивается в сторону шага и идёт вперёд, без движения - смотрит туда же, куда камера.</summary>
|
||||
@@ -108,6 +188,8 @@ namespace NecromancerTome
|
||||
static float _rTime, _rNext;
|
||||
|
||||
static GameObject _hud;
|
||||
/// <summary>Метка одержимого на карте (указание 06.10, как у змеи) - NecroMapMarker.</summary>
|
||||
static NecroMapMarker.Marker _mapNav;
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
@@ -127,13 +209,32 @@ namespace NecromancerTome
|
||||
|
||||
/// <summary>True - вселились (для звука «пиф-пафа»).</summary>
|
||||
public static bool Enter(EntityPlayerLocal _player0, EntityAlive _zombie)
|
||||
{
|
||||
return CanPossess(_zombie) && Begin(_player0, _zombie, false);
|
||||
}
|
||||
|
||||
/// <summary>Вселение в своего питомца (ПКМ с книгой призыва, SummonPatch).</summary>
|
||||
public static bool EnterPet(EntityPlayerLocal _player0, EntityAlive _pet)
|
||||
{
|
||||
return _pet != null && !_pet.IsDead() && Begin(_player0, _pet, true);
|
||||
}
|
||||
|
||||
static bool Begin(EntityPlayerLocal _player0, EntityAlive _zombie, bool _pet)
|
||||
{
|
||||
if (Active || SnakeMode.Active || MindPalace.Active || _player0 == null || _player0.IsDead() ||
|
||||
_player0.AttachedToEntity != null || !CanPossess(_zombie) || _player0.world.IsRemote())
|
||||
_player0.AttachedToEntity != null || _player0.world.IsRemote())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Active = true;
|
||||
IsPet = _pet;
|
||||
Flying = _zombie is EntityVulture;
|
||||
BodyDanger = false;
|
||||
_bodyCheck = 0f;
|
||||
_flyMove = Vector2.zero;
|
||||
_flyUp = _flyDown = _flyRun = false;
|
||||
_flyDash = null;
|
||||
_flySitting = false;
|
||||
_player = _player0;
|
||||
Zombie = _zombie;
|
||||
_yaw = _player0.rotation.y;
|
||||
@@ -143,6 +244,12 @@ namespace NecromancerTome
|
||||
_nextTaunt = 0f;
|
||||
_nextScream = 0f;
|
||||
_loggedCamera = false;
|
||||
// Пояс одержимому питомцу ни к чему (указание 06.10); открыться заново не даёт
|
||||
// Patch_GUIWindowManager_openInternal_SnakeToolbelt, после выхода игра откроет его сама.
|
||||
if (_pet)
|
||||
{
|
||||
_player0.PlayerUI?.windowManager.Close(SnakeControl.ToolbeltWindow);
|
||||
}
|
||||
|
||||
// Зомби - в руки игрока: цели, путь и шаги ИИ сброшены.
|
||||
_zombie.SetAttackTarget(null, 0);
|
||||
@@ -165,7 +272,13 @@ namespace NecromancerTome
|
||||
}
|
||||
ReadRanged(_zombie);
|
||||
Hint(_player0, _zombie);
|
||||
Debug.Log("[NecromancerTome] Possession: вселение в " + _zombie.EntityName + " (" + _zombie.entityId + ")");
|
||||
NecroMapMarker.Hide(ref _mapNav); // старая ссылка после закрытия мира - снимется, только если ещё наша
|
||||
_mapNav = NecroMapMarker.Show(_zombie, NecroMapMarker.PossessedSprite);
|
||||
if (Flying)
|
||||
{
|
||||
_zombie.motion = Vector3.zero;
|
||||
}
|
||||
Debug.Log("[NecromancerTome] Possession: вселение в " + (_pet ? "питомца " : "") + _zombie.EntityName + " (" + _zombie.entityId + ")" + (Flying ? ", полёт" : ""));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -176,11 +289,25 @@ namespace NecromancerTome
|
||||
return;
|
||||
}
|
||||
Active = false;
|
||||
NecroMapMarker.Hide(ref _mapNav);
|
||||
bool pet = IsPet;
|
||||
IsPet = false;
|
||||
Flying = false;
|
||||
BodyDanger = false;
|
||||
Crouching = false;
|
||||
_meleeLookUntil = 0f;
|
||||
_crouchToggled = false;
|
||||
EntityAlive z = Zombie;
|
||||
z?.SetLookPosition(Vector3.zero);
|
||||
_flyDash = null;
|
||||
FlyAim = null;
|
||||
AimTint(false);
|
||||
StopEat(z, null);
|
||||
if (_flySitting)
|
||||
{
|
||||
_flySitting = false;
|
||||
SetSitPose(z, false);
|
||||
}
|
||||
EntityPlayerLocal player = _player;
|
||||
if (_rState != RState.Idle && z != null && !z.IsDead())
|
||||
{
|
||||
@@ -220,6 +347,13 @@ namespace NecromancerTome
|
||||
e.SetAttackTarget(null, 0);
|
||||
}
|
||||
}
|
||||
// Питомец просто отпущен: его ИИ (следование, охрана, поводок) сам вернёт его к хозяину.
|
||||
if (pet)
|
||||
{
|
||||
z.SetMoveForwardWithModifiers(0f, 0f, 0f, false);
|
||||
z.SetAttackTarget(null, 0);
|
||||
return;
|
||||
}
|
||||
// Сам зомби в ужасе бежит от тела игрока (решение 03.10: «пугается и убегает как при отрицании»).
|
||||
if (!z.IsDead() && player != null)
|
||||
{
|
||||
@@ -266,13 +400,23 @@ namespace NecromancerTome
|
||||
if (Active || Zombie != null || _frozen.Count > 0)
|
||||
{
|
||||
Active = false;
|
||||
IsPet = false;
|
||||
Flying = false;
|
||||
BodyDanger = false;
|
||||
_mapNav = null;
|
||||
Zombie = null;
|
||||
_player = null;
|
||||
_observer = null;
|
||||
_bodyViewDim = -1;
|
||||
_frozen.Clear();
|
||||
_frozenSpeed.Clear();
|
||||
_flyDash = null;
|
||||
FlyAim = null;
|
||||
_eating = false;
|
||||
_eatCorpse = null;
|
||||
}
|
||||
// Цвет прицела - общая таблица игры, переживает выход из мира: вернуть всегда.
|
||||
AimTint(false);
|
||||
return;
|
||||
}
|
||||
if (!Active)
|
||||
@@ -317,6 +461,20 @@ namespace NecromancerTome
|
||||
_crouchToggled = !_crouchToggled;
|
||||
}
|
||||
Crouching = input != null && !gui && (input.Crouch.IsPressed || _crouchToggled);
|
||||
if (detonate && IsPet)
|
||||
{
|
||||
// E у питомца - отзыв (как E с книгой в руке); выход из тела - внутри RecallPet.
|
||||
EntityPlayerLocal owner = _player;
|
||||
if (Patch_ItemActionSpawnEntity_Spawn_PetLimit.RecallPossessedPet(owner, z))
|
||||
{
|
||||
// Звук отзыва - как у E с книгой (soundWarn книги в руке; вселялись с ней же).
|
||||
if (owner.inventory?.holdingItem?.Actions?.Length > 0 && owner.inventory.holdingItem.Actions[0] is ItemActionSpawnEntity spawn)
|
||||
{
|
||||
owner.PlayOneShot(spawn.soundWarn);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (detonate && z is EntityZombieCop cop && !cop.isPrimed)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Possession: подрыв " + z.EntityName);
|
||||
@@ -324,6 +482,65 @@ namespace NecromancerTome
|
||||
}
|
||||
Ranged(z, ranged, rangedHeld);
|
||||
|
||||
if (Flying)
|
||||
{
|
||||
// Полёт: направление - камера, пробел - вверх, присесть - вниз; сам шаг - в FlyTick.
|
||||
_flyMove = move;
|
||||
_flyRun = run;
|
||||
_flyUp = input != null && !gui && input.Jump.IsPressed;
|
||||
_flyDown = input != null && !gui && (input.Crouch.IsPressed || _crouchToggled);
|
||||
Crouching = false;
|
||||
FlyAim = _flyDash == null ? FlyDashTarget(world, z) : null;
|
||||
AimTint(FlyAim != null);
|
||||
if (attack && Time.time >= _nextAttack && FlyAim is EntityAlive dashTo)
|
||||
{
|
||||
// Рывок к цели под прицелом; клевок - по прилёте, в FlyTick.
|
||||
_flyDash = dashTo;
|
||||
_flyDashUntil = Time.time + FlyDashTime;
|
||||
_nextAttack = Time.time + FlyDashTime + FlyAttackDelay;
|
||||
Debug.Log("[NecromancerTome] Possession: рывок стервятника к " + dashTo.EntityName + " (" + dashTo.entityId + "), " +
|
||||
(dashTo.getHeadPosition() - z.position).magnitude.ToString("F1") + " м");
|
||||
}
|
||||
else if (attack && Time.time >= _nextAttack && _flyDash == null)
|
||||
{
|
||||
if (_aimMissNear != null)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Possession: рывка нет - ближайший " + _aimMissNear.EntityName + " в " +
|
||||
_aimMissDist.ToString("F1") + " м (нужно до " + FlyDashRange + "), мимо прицела на " +
|
||||
_aimMissOff.ToString("F1") + " м (нужно до " + FlyDashAimRadius + ")");
|
||||
}
|
||||
// Клевок стервятника - его оружие (UseHoldingItem 0), как EntityVulture.AttackAndAdjust;
|
||||
// куда клюёт - GetLookVector стервятника берёт lookAtPosition.
|
||||
z.SetLookPosition(MeleeAim(world, z));
|
||||
if (z.UseHoldingItem(0, false))
|
||||
{
|
||||
z.UseHoldingItem(0, true);
|
||||
}
|
||||
_nextAttack = Time.time + FlyAttackDelay;
|
||||
_meleeLookUntil = Time.time + FlyAttackDelay;
|
||||
}
|
||||
// ПКМ - клич-приманка (указание 06.10): крик крикуньи, только голосом паникующей курицы - зомби
|
||||
// в ScreamRange идут на него; с воздуха уводит толпу от базы или от тела.
|
||||
if (ranged)
|
||||
{
|
||||
Scream(world, z);
|
||||
}
|
||||
// F - питаться падалью (указание 06.10: «сесть на труп - по F стервятник питается»). F у игрока -
|
||||
// фонарик, но его читает только PlayerMoveController.Update, а он на время одержимости выключен.
|
||||
bool eat = input != null && !gui && (input.PermanentActions.ToggleFlashlight.WasPressed || input.ToggleFlashlight.WasPressed);
|
||||
Eat(world, z, eat, attack || ranged);
|
||||
if (_meleeLookUntil > 0f && Time.time >= _meleeLookUntil)
|
||||
{
|
||||
_meleeLookUntil = 0f;
|
||||
z.SetLookPosition(Vector3.zero);
|
||||
}
|
||||
_observer?.SetPosition(z.position);
|
||||
SnakeControl.Sleepers(world, _player, z.position, ref _sleeperCheck, Time.deltaTime);
|
||||
Taunt(world, z);
|
||||
CheckBody(world, z, Time.deltaTime);
|
||||
return;
|
||||
}
|
||||
|
||||
bool moving = move.sqrMagnitude > 0.01f;
|
||||
float wantYaw = moving ? _yaw + Mathf.Atan2(move.x, move.y) * Mathf.Rad2Deg : _yaw;
|
||||
_bodyYaw = Mathf.MoveTowardsAngle(_bodyYaw, wantYaw, TurnSpeed * Time.deltaTime);
|
||||
@@ -373,6 +590,503 @@ namespace NecromancerTome
|
||||
// Спящие в зданиях просыпаются от одержимого, как от змеи (указание 05.10).
|
||||
SnakeControl.Sleepers(world, _player, z.position, ref _sleeperCheck, Time.deltaTime);
|
||||
Taunt(world, z);
|
||||
CheckBody(world, z, Time.deltaTime);
|
||||
}
|
||||
|
||||
/// <summary>Тик полёта одержимого стервятника (из его updateTasks, 20 в секунду) - формула его же
|
||||
/// полёта: motion = motion * 0.9 + направление * (разгон * 0.1), разгон умножен на скорость класса.
|
||||
/// Без ввода гасим скорость - зависает: у EntityFlying нет гравитации.</summary>
|
||||
public static void FlyTick(EntityVulture _v)
|
||||
{
|
||||
if (_flyDash != null)
|
||||
{
|
||||
FlyDashTick(_v);
|
||||
return;
|
||||
}
|
||||
Quaternion rot = Quaternion.Euler(-_pitch, _yaw, 0f);
|
||||
Vector3 dir = rot * Vector3.forward * _flyMove.y + Quaternion.Euler(0f, _yaw, 0f) * Vector3.right * _flyMove.x;
|
||||
if (_flyUp)
|
||||
{
|
||||
dir += Vector3.up;
|
||||
}
|
||||
if (_flyDown)
|
||||
{
|
||||
dir += Vector3.down;
|
||||
}
|
||||
bool moving = dir.sqrMagnitude > 0.01f;
|
||||
float accel = _flyRun ? FlyAccelRun * _v.moveSpeedAggro : FlyAccel * _v.moveSpeed;
|
||||
if (moving)
|
||||
{
|
||||
_v.motion = _v.motion * 0.9f + dir.normalized * (accel * 0.1f);
|
||||
}
|
||||
else
|
||||
{
|
||||
_v.motion *= 0.8f;
|
||||
}
|
||||
Vector3 flat = new Vector3(_v.motion.x, 0f, _v.motion.z);
|
||||
float wantYaw = flat.sqrMagnitude > 0.0004f ? Mathf.Atan2(flat.x, flat.z) * Mathf.Rad2Deg : _yaw;
|
||||
_bodyYaw = Mathf.MoveTowardsAngle(_bodyYaw, wantYaw, 20f);
|
||||
_v.SetRotation(new Vector3(0f, _bodyYaw, 0f));
|
||||
if (_v.emodel != null && _v.emodel.avatarController != null)
|
||||
{
|
||||
_v.glidingCurrentPercent = Mathf.MoveTowards(_v.glidingCurrentPercent, _flyRun && moving ? 1f : 0f, 0.06f);
|
||||
_v.emodel.avatarController.UpdateFloat("Gliding", _v.glidingCurrentPercent);
|
||||
}
|
||||
// «Вниз» на земле посадке не мешает: Ctrl - переключатель, и с ним стервятник не сел бы никогда.
|
||||
// Сесть = на земле И остановился (указание 06.10: «опуститься на землю - это ещё и остановиться»):
|
||||
// без ввода скорость гаснет за несколько тиков, поза - когда она почти ноль.
|
||||
// За едой сидит всегда - даже там, где опору не видно (тест 06.10: мешок с трупом на прицепе -
|
||||
// модель машины, луч вниз её не находит, и «стервятник не сидит»).
|
||||
bool sit = _eating || _flyMove.sqrMagnitude < 0.01f && !_flyUp && _meleeLookUntil <= 0f &&
|
||||
(_flySitting || flat.sqrMagnitude < FlySitSpeed * FlySitSpeed) && OnGround(_v);
|
||||
if (sit)
|
||||
{
|
||||
_v.motion = Vector3.zero;
|
||||
}
|
||||
if (sit != _flySitting)
|
||||
{
|
||||
if (!sit && _flyUp)
|
||||
{
|
||||
// Взлёт с земли на пробел (указание 06.10) - толчок вверх, а не медленный отрыв.
|
||||
_v.motion.y = Mathf.Max(_v.motion.y, FlyTakeOff);
|
||||
}
|
||||
_flySitting = sit;
|
||||
SetSitPose(_v, sit);
|
||||
}
|
||||
}
|
||||
|
||||
static void Eat(World _world, EntityAlive _v, bool _pressed, bool _acted)
|
||||
{
|
||||
if (_eating)
|
||||
{
|
||||
bool moved = _flyMove.sqrMagnitude > 0.01f || _flyUp || _acted || !_flySitting;
|
||||
bool gone = _eatCorpse != null ? _eatCorpse.IsMarkedForUnload() : _world.GetBlock(_eatBlock).type != _eatBlockType;
|
||||
if (moved || gone)
|
||||
{
|
||||
StopEat(_v, gone ? "падаль пропала" : "прервано");
|
||||
return;
|
||||
}
|
||||
// Как едят зомби в игре (EAIApproachAndAttackTarget, isEating): раз в 1.25-1.75 с брызги крови
|
||||
// blood_eat у головы, через раз - звук eat_player.
|
||||
_eatBite -= Time.deltaTime;
|
||||
if (_eatBite <= 0f)
|
||||
{
|
||||
_eatBite = 1.25f + Random.value * 0.5f;
|
||||
if ((_eatCount & 1) == 0)
|
||||
{
|
||||
_v.PlayOneShot("eat_player");
|
||||
}
|
||||
ParticleEffect pe = new ParticleEffect("blood_eat", new Vector3(0f, 0.04f, 0.08f), 1f, Color.white, null, _v.entityId, ParticleEffect.Attachment.Head);
|
||||
GameManager.Instance.SpawnParticleEffectServer(pe, _v.entityId);
|
||||
_eatCount++;
|
||||
}
|
||||
float dt = Mathf.Min(Time.deltaTime, _eatLeft);
|
||||
_eatLeft -= dt;
|
||||
float bite = _v.GetMaxHealth() * EatHealFrac * dt / EatTime;
|
||||
_eatHeal += bite;
|
||||
float meat = EatMeat(-bite);
|
||||
int add = Mathf.Min((int)_eatHeal, _v.GetMaxHealth() - _v.Health);
|
||||
if (add > 0)
|
||||
{
|
||||
_v.AddHealth(add);
|
||||
}
|
||||
_eatHeal -= (int)_eatHeal;
|
||||
if (meat <= 0f)
|
||||
{
|
||||
string what = EatName();
|
||||
EntityAlive corpse = _eatCorpse;
|
||||
Vector3i pos = _eatBlock;
|
||||
StopEat(_v, "доедено");
|
||||
if (corpse != null)
|
||||
{
|
||||
_carrion.Remove(corpse.entityId);
|
||||
_world.RemoveEntity(corpse.entityId, EnumRemoveEntityReason.Despawned);
|
||||
}
|
||||
else
|
||||
{
|
||||
_carrionBlocks.Remove(pos);
|
||||
_world.SetBlocksRPC(new List<BlockChangeInfo> { new BlockChangeInfo(pos, BlockValue.Air, MarchingCubes.DensityAir) });
|
||||
}
|
||||
Debug.Log("[NecromancerTome] Possession: падаль " + what + " исчезла");
|
||||
}
|
||||
else if (_eatLeft <= 0f)
|
||||
{
|
||||
StopEat(_v, "кусок съеден, у падали осталось " + Mathf.CeilToInt(meat));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!_pressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Есть можно сидя или просто остановившись рядом (зависнув над падалью): без ввода и почти без скорости.
|
||||
bool still = _flySitting || _flyMove.sqrMagnitude < 0.01f && !_flyUp &&
|
||||
new Vector2(_v.motion.x, _v.motion.z).sqrMagnitude < FlySitSpeed * FlySitSpeed;
|
||||
if (!still || !FindCarrion(_world, _v))
|
||||
{
|
||||
GameManager.ShowTooltip(_player, Localization.Get("necroPossessNoCarrion"));
|
||||
Debug.Log("[NecromancerTome] Possession: падали нет" + (still ? "" : " - стервятник не остановился"));
|
||||
return;
|
||||
}
|
||||
_eating = true;
|
||||
if (!_flySitting)
|
||||
{
|
||||
// Сел за еду сразу, не дожидаясь тика полёта: иначе проверка «не сидит» тут же прервала бы еду.
|
||||
_flySitting = true;
|
||||
_v.motion = Vector3.zero;
|
||||
SetSitPose(_v, true);
|
||||
}
|
||||
_eatLeft = EatTime;
|
||||
_eatHeal = 0f;
|
||||
_eatBite = 0f;
|
||||
_eatCount = 0;
|
||||
_v.SetLookPosition(_eatCorpse != null ? _eatCorpse.getChestPosition() : _eatBlock.ToVector3() + new Vector3(0.5f, 0.3f, 0.5f));
|
||||
_v.IsEating = true; // как у зомби: анимация поедания и синхронизация по сети
|
||||
Debug.Log("[NecromancerTome] Possession: стервятник ест " + EatName() + ", здоровье " + _v.Health + "/" + _v.GetMaxHealth() +
|
||||
", мяса у падали " + Mathf.CeilToInt(EatMeat(0f)));
|
||||
}
|
||||
|
||||
/// <summary>Запас мяса текущей падали плюс _delta; первый укус - запас с нуля: у трупа - полное
|
||||
/// здоровье при жизни, у блока - его прочность (MaxDamage минус уже нанесённый урон).</summary>
|
||||
static float EatMeat(float _delta)
|
||||
{
|
||||
float meat;
|
||||
if (_eatCorpse != null)
|
||||
{
|
||||
if (!_carrion.TryGetValue(_eatCorpse.entityId, out meat))
|
||||
{
|
||||
meat = Mathf.Max(1, _eatCorpse.GetMaxHealth());
|
||||
}
|
||||
meat += _delta;
|
||||
_carrion[_eatCorpse.entityId] = meat;
|
||||
return meat;
|
||||
}
|
||||
if (!_carrionBlocks.TryGetValue(_eatBlock, out meat))
|
||||
{
|
||||
BlockValue bv = GameManager.Instance.World.GetBlock(_eatBlock);
|
||||
meat = Mathf.Max(1, bv.Block.MaxDamage - bv.damage);
|
||||
}
|
||||
meat += _delta;
|
||||
_carrionBlocks[_eatBlock] = meat;
|
||||
return meat;
|
||||
}
|
||||
|
||||
static string EatName()
|
||||
{
|
||||
return _eatCorpse != null
|
||||
? _eatCorpse.EntityName + " (" + _eatCorpse.entityId + ")"
|
||||
: Block.list[_eatBlockType].GetBlockName() + " " + _eatBlock;
|
||||
}
|
||||
|
||||
static void StopEat(EntityAlive _v, string _why)
|
||||
{
|
||||
if (!_eating)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_eating = false;
|
||||
_eatCorpse = null;
|
||||
if (_v != null && !_v.IsDead())
|
||||
{
|
||||
_v.IsEating = false;
|
||||
_v.SetLookPosition(Vector3.zero);
|
||||
}
|
||||
if (_why != null)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Possession: еда - " + _why + ", здоровье " + (_v != null ? _v.Health + "/" + _v.GetMaxHealth() : "?"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ближайшая падаль ближе EatRange. Сначала трупы - любое мёртвое существо, кроме игроков и
|
||||
/// существ мода (питомцы, рой, змея); нет трупа - блок-падаль (указание 06.10: «в игре кроме трупов
|
||||
/// зомби много другой падали»): тела людей, останки животных, мешки с трупами - всё, с чего при
|
||||
/// разделке падает гнилая плоть (IsCarrionBlock). У составного блока берём его главный блок.</summary>
|
||||
static bool FindCarrion(World _world, EntityAlive _v)
|
||||
{
|
||||
_eatCorpse = null;
|
||||
float bestD = EatRange * EatRange;
|
||||
List<Entity> list = _world.Entities.list;
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
if (!(list[i] is EntityAlive e) || e == _v || !e.IsDead() || e is EntityPlayer || SnakeControl.IsNecroEntity(e) ||
|
||||
e.IsMarkedForUnload())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
float d = (e.position - _v.position).sqrMagnitude;
|
||||
if (d < bestD)
|
||||
{
|
||||
bestD = d;
|
||||
_eatCorpse = e;
|
||||
}
|
||||
}
|
||||
if (_eatCorpse != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
bool found = false;
|
||||
Vector3i c = World.worldToBlockPos(_v.position);
|
||||
int r = Mathf.CeilToInt(EatRange);
|
||||
for (int x = -r; x <= r; x++)
|
||||
{
|
||||
for (int y = -r; y <= r; y++)
|
||||
{
|
||||
for (int z = -r; z <= r; z++)
|
||||
{
|
||||
Vector3i p = new Vector3i(c.x + x, c.y + y, c.z + z);
|
||||
BlockValue bv = _world.GetBlock(p);
|
||||
if (bv.isair || !IsCarrionBlock(bv.type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
float d = (p.ToVector3() + new Vector3(0.5f, 0.5f, 0.5f) - _v.position).sqrMagnitude;
|
||||
if (d >= bestD)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (bv.ischild)
|
||||
{
|
||||
p += bv.parent;
|
||||
bv = _world.GetBlock(p);
|
||||
if (bv.isair || !IsCarrionBlock(bv.type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
bestD = d;
|
||||
_eatBlock = p;
|
||||
_eatBlockType = bv.type;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <summary>Блок - падаль, если хоть одна его добыча - гнилая плоть (foodRottingFlesh).</summary>
|
||||
static bool IsCarrionBlock(int _type)
|
||||
{
|
||||
if (_isCarrion.TryGetValue(_type, out bool yes))
|
||||
{
|
||||
return yes;
|
||||
}
|
||||
Block b = Block.list[_type];
|
||||
if (b?.itemsToDrop != null)
|
||||
{
|
||||
foreach (KeyValuePair<EnumDropEvent, List<Block.SItemDropProb>> drops in b.itemsToDrop)
|
||||
{
|
||||
for (int i = 0; drops.Value != null && i < drops.Value.Count && !yes; i++)
|
||||
{
|
||||
yes = drops.Value[i].name == CarrionItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
_isCarrion[_type] = yes;
|
||||
return yes;
|
||||
}
|
||||
|
||||
/// <summary>Прицел краснеет на цели рывка (указание 06.10: прицел обычный, не змеиный). Игра каждый кадр
|
||||
/// красит прицел цветом crosshairColors[OptionsCrosshairColor] (EntityPlayerLocal.guiDrawCrosshair) -
|
||||
/// пока цель под прицелом, этот цвет в таблице красный, без цели и на выходе - прежний. (Первая версия -
|
||||
/// подмена на время рисования патчем guiDrawCrosshair - в игре не сработала, тест 06.10.)</summary>
|
||||
static void AimTint(bool _on)
|
||||
{
|
||||
List<Color> colors = EntityPlayerLocal.crosshairColors;
|
||||
if (_on == (_tintIndex >= 0) || colors == null || colors.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_on)
|
||||
{
|
||||
int i = GamePrefs.GetInt(EnumGamePrefs.OptionsCrosshairColor);
|
||||
_tintIndex = i >= 0 && i < colors.Count ? i : 0;
|
||||
_tintSaved = colors[_tintIndex];
|
||||
colors[_tintIndex] = AimOnTarget;
|
||||
}
|
||||
else
|
||||
{
|
||||
colors[_tintIndex] = _tintSaved;
|
||||
_tintIndex = -1;
|
||||
}
|
||||
Debug.Log("[NecromancerTome] Possession: прицел " + (_on ? "на цели - красный" : "без цели"));
|
||||
}
|
||||
|
||||
static readonly Color AimOnTarget = new Color(1f, 0.15f, 0.1f, 1f);
|
||||
static int _tintIndex = -1;
|
||||
static Color _tintSaved;
|
||||
|
||||
/// <summary>Стоит ли стервятник на земле: флаг движка или опора ближе FlySitGround под ногами
|
||||
/// (у EntityFlying нет гравитации - сам он на землю не «падает» и onGround бывает не всегда).</summary>
|
||||
static bool OnGround(EntityAlive _v)
|
||||
{
|
||||
if (_v.onGround)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
Vector3 from = _v.position - Origin.position + Vector3.up * 0.1f;
|
||||
return Physics.Raycast(from, Vector3.down, FlySitGround + 0.1f, SnakeControl.WallMask, QueryTriggerInteraction.Ignore);
|
||||
}
|
||||
|
||||
static void SetSitPose(EntityAlive _v, bool _sit)
|
||||
{
|
||||
if (_v?.emodel?.avatarController == null || _v.IsDead())
|
||||
{
|
||||
return;
|
||||
}
|
||||
_v.emodel.avatarController.TriggerSleeperPose(_sit ? VultureSitPose : AvatarController.cSleeperPoseAwake);
|
||||
Debug.Log("[NecromancerTome] Possession: стервятник " + (_sit ? "сел" : "взлетел"));
|
||||
}
|
||||
|
||||
/// <summary>Тик рывка: прямо к голове цели; долетел - клевок, как ванильный AttackAndAdjust.</summary>
|
||||
static void FlyDashTick(EntityVulture _v)
|
||||
{
|
||||
EntityAlive t = _flyDash;
|
||||
if (_flySitting)
|
||||
{
|
||||
_flySitting = false;
|
||||
SetSitPose(_v, false);
|
||||
}
|
||||
if (t == null || t.IsDead() || t.IsMarkedForUnload() || Time.time > _flyDashUntil)
|
||||
{
|
||||
_flyDash = null;
|
||||
_v.motion *= 0.5f;
|
||||
Debug.Log("[NecromancerTome] Possession: рывок стервятника сорван");
|
||||
return;
|
||||
}
|
||||
Vector3 head = t.getHeadPosition();
|
||||
Vector3 to = head - _v.position;
|
||||
Vector3 flat = new Vector3(to.x, 0f, to.z);
|
||||
if (flat.sqrMagnitude > 0.0001f)
|
||||
{
|
||||
_bodyYaw = Mathf.Atan2(flat.x, flat.z) * Mathf.Rad2Deg;
|
||||
_v.SetRotation(new Vector3(0f, _bodyYaw, 0f));
|
||||
}
|
||||
float dist = to.magnitude;
|
||||
if (dist > FlyDashHit)
|
||||
{
|
||||
_v.motion = to / dist * Mathf.Min(FlyDashSpeed, dist - FlyDashHit * 0.5f);
|
||||
return;
|
||||
}
|
||||
// Удар - прямым уроном его оружия (тест 06.10: «рывок вообще никак»). Клевок игры бьёт лучом из
|
||||
// головы через ~0.5 с после нажатия, а стервятник к тому времени пролетал мимо - ни одного
|
||||
// попадания. Анимация и звук клевка - его же; урон - от стервятника, убийство засчитает
|
||||
// PetKillCreditPatch (питомец) хозяину.
|
||||
_flyDash = null;
|
||||
_v.motion = Vector3.zero;
|
||||
_v.SetLookPosition(head);
|
||||
_v.emodel?.avatarController?.StartAnimationAttack();
|
||||
string snd = _v.GetSoundAttack();
|
||||
if (!string.IsNullOrEmpty(snd))
|
||||
{
|
||||
Audio.Manager.BroadcastPlay(_v, snd);
|
||||
}
|
||||
float dmg = FlyDashDamage;
|
||||
if (_v.inventory?.holdingItem?.Actions != null && _v.inventory.holdingItem.Actions.Length > 0 &&
|
||||
_v.inventory.holdingItem.Actions[0] is ItemActionAttack melee)
|
||||
{
|
||||
dmg = melee.GetDamageEntity(_v.inventory.holdingItemItemValue, _v, 0);
|
||||
}
|
||||
int dealt = t.DamageEntity(new DamageSourceEntity(EnumDamageSource.External, EnumDamageTypes.Piercing, _v.entityId),
|
||||
Mathf.Max(1, Mathf.RoundToInt(dmg)), false);
|
||||
// Звук попадания (тест 06.10: «нет звука в момент удара - дезориентирует»): прямой урон звуков
|
||||
// удара не даёт, их играет только клевок игры - удар органики об органику, как у руки зомби.
|
||||
Audio.Manager.BroadcastPlay(t, FlyDashHitSound);
|
||||
// Кровотечение - как у клевка грифа (items.xml, onSelfAttackedOther; указание 06.10).
|
||||
if (!t.IsDead() && t.Buffs != null)
|
||||
{
|
||||
t.Buffs.AddBuff(FlyDashBleedBuff, _v.entityId);
|
||||
}
|
||||
_meleeLookUntil = Time.time + FlyAttackDelay;
|
||||
Debug.Log("[NecromancerTome] Possession: клевок после рывка по " + t.EntityName + " - урон " + dealt + " из " + dmg.ToString("F0") +
|
||||
(t.IsDead() ? ", убит" : ", здоровье " + t.Health));
|
||||
}
|
||||
|
||||
/// <summary>Цель рывка: ближайшая к стервятнику по лучу прицела, не дальше FlyDashRange от него и
|
||||
/// не дальше TargetRadius змеи от луча. Свои (питомцы, игроки, зачарованные) - не цель.</summary>
|
||||
static EntityAlive FlyDashTarget(World _world, EntityAlive _v)
|
||||
{
|
||||
Transform cam = _player?.cameraTransform;
|
||||
if (cam == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Vector3 from = cam.position + Origin.position;
|
||||
Vector3 dir = cam.forward;
|
||||
EntityAlive best = null;
|
||||
float bestAlong = float.MaxValue;
|
||||
_aimMissNear = null;
|
||||
_aimMissDist = _aimMissOff = float.MaxValue;
|
||||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||||
{
|
||||
EntityAlive e = _world.EntityAlives[i];
|
||||
if (e == null || e == _v || e.IsDead() || e is EntityPlayer || SnakeControl.IsNecroEntity(e) ||
|
||||
NecroCharmSide.IsCharmed(e) || GhostTraderPatch.ShouldBeGhost(e))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Тело цели - отрезок от ног до головы (тест 06.10: «прицел не краснеет, рывок не срабатывает» -
|
||||
// по одной точке груди с высоты почти не попасть). Берём точку отрезка ближе всех к лучу прицела;
|
||||
// дальность - от стервятника до ближайшей точки тела.
|
||||
Vector3 feet = e.position, head = e.getHeadPosition();
|
||||
float dist = float.MaxValue, off = float.MaxValue, along = 0f;
|
||||
for (int k = 0; k <= 4; k++)
|
||||
{
|
||||
Vector3 p = Vector3.Lerp(feet, head, k / 4f);
|
||||
dist = Mathf.Min(dist, (p - _v.position).magnitude);
|
||||
float a = Vector3.Dot(p - from, dir);
|
||||
float o = Vector3.Cross(p - from, dir).magnitude;
|
||||
if (a > 0f && o < off)
|
||||
{
|
||||
off = o;
|
||||
along = a;
|
||||
}
|
||||
}
|
||||
if (dist < FlyDashRange + 5f && dist < _aimMissDist)
|
||||
{
|
||||
_aimMissNear = e;
|
||||
_aimMissDist = dist;
|
||||
_aimMissOff = off;
|
||||
}
|
||||
if (dist > FlyDashRange || off > FlyDashAimRadius || along >= bestAlong)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
best = e;
|
||||
bestAlong = along;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// Для лога промаха: ближайший к стервятнику враг (до FlyDashRange + 5 м), его дальность и отступ от луча прицела.
|
||||
static EntityAlive _aimMissNear;
|
||||
static float _aimMissDist, _aimMissOff;
|
||||
|
||||
/// <summary>Раз в полсекунды: есть ли живой враг ближе BodyDangerRange к телу (как у змеи).</summary>
|
||||
static void CheckBody(World _world, EntityAlive _z, float _dt)
|
||||
{
|
||||
_bodyCheck -= _dt;
|
||||
if (_bodyCheck > 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_bodyCheck = 0.5f;
|
||||
bool danger = false;
|
||||
Vector3 body = _player.position;
|
||||
float r2 = SnakeControl.BodyDangerRange * SnakeControl.BodyDangerRange;
|
||||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||||
{
|
||||
EntityAlive e = _world.EntityAlives[i];
|
||||
if (SnakeControl.IsFoe(e, _z) && (e.position - body).sqrMagnitude < r2)
|
||||
{
|
||||
danger = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (danger != BodyDanger)
|
||||
{
|
||||
BodyDanger = danger;
|
||||
Debug.Log("[NecromancerTome] Possession: " + (danger ? "враг у тела" : "у тела чисто"));
|
||||
}
|
||||
}
|
||||
|
||||
static void ReadRanged(EntityAlive _z)
|
||||
@@ -402,14 +1116,26 @@ namespace NecromancerTome
|
||||
static void Hint(EntityPlayerLocal _p, EntityAlive _z)
|
||||
{
|
||||
string text = Localization.Get("necroPossessHintBase");
|
||||
if (IsPet)
|
||||
{
|
||||
text += ", " + Localization.Get("necroPetRecallHint");
|
||||
}
|
||||
if (Flying)
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintFly");
|
||||
}
|
||||
if (_hasRanged)
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintRanged");
|
||||
}
|
||||
if (IsScreamer(_z))
|
||||
if (IsScreamer(_z) || Flying)
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintScream");
|
||||
}
|
||||
if (Flying)
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintEat");
|
||||
}
|
||||
if (_z.jumpMaxDistance >= 5f)
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintLeap");
|
||||
@@ -593,15 +1319,16 @@ namespace NecromancerTome
|
||||
return EntityClass.list.TryGetValue(_z.entityClass, out EntityClass c) && c.entityClassName.StartsWith("zombieScreamer");
|
||||
}
|
||||
|
||||
/// <summary>Крик одержимой крикуньи: её звук тревоги и все зомби в ScreamRange - на неё (танкование).</summary>
|
||||
/// <summary>Крик одержимой крикуньи (и клич одержимого стервятника): его звук тревоги и все зомби в
|
||||
/// ScreamRange - на него (танкование, приманка).</summary>
|
||||
static void Scream(World _world, EntityAlive _z)
|
||||
{
|
||||
if (Time.time < _nextScream)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_nextScream = Time.time + ScreamCooldown;
|
||||
string snd = _z.GetSoundAlert();
|
||||
_nextScream = Time.time + (Flying ? FlyScreamCooldown : ScreamCooldown);
|
||||
string snd = Flying ? FlyScreamSound : _z.GetSoundAlert();
|
||||
if (!string.IsNullOrEmpty(snd))
|
||||
{
|
||||
Audio.Manager.BroadcastPlay(_z, snd);
|
||||
@@ -616,11 +1343,11 @@ namespace NecromancerTome
|
||||
continue;
|
||||
}
|
||||
e.ConditionalTriggerSleeperWakeUp();
|
||||
AllowZombieTarget(e);
|
||||
AllowZombieTarget(e, _z);
|
||||
e.SetAttackTarget(_z, TauntTicks);
|
||||
n++;
|
||||
}
|
||||
Debug.Log("[NecromancerTome] Possession: крик крикуньи - на зов идут " + n);
|
||||
Debug.Log("[NecromancerTome] Possession: крик " + _z.EntityName + " - на зов идут " + n);
|
||||
}
|
||||
|
||||
static void Look()
|
||||
@@ -670,13 +1397,16 @@ namespace NecromancerTome
|
||||
{
|
||||
continue;
|
||||
}
|
||||
AllowZombieTarget(e);
|
||||
AllowZombieTarget(e, _z);
|
||||
e.SetAttackTarget(_z, TauntTicks);
|
||||
}
|
||||
}
|
||||
|
||||
static void AllowZombieTarget(EntityAlive _e)
|
||||
/// <summary>Разрешить зомби подход к цели класса одержимого: EntityZombie, у питомцев - их класс
|
||||
/// (EntityZombieDog, EntityVulture).</summary>
|
||||
static void AllowZombieTarget(EntityAlive _e, EntityAlive _target)
|
||||
{
|
||||
System.Type type = _target is EntityZombie ? typeof(EntityZombie) : _target.GetType();
|
||||
List<EAIApproachAndAttackTarget> tasks = _e.aiManager.GetTasks<EAIApproachAndAttackTarget>();
|
||||
if (tasks == null)
|
||||
{
|
||||
@@ -687,11 +1417,11 @@ namespace NecromancerTome
|
||||
bool has = false;
|
||||
foreach (EAIApproachAndAttackTarget.TargetClass c in t.targetClasses)
|
||||
{
|
||||
has |= c.type == typeof(EntityZombie);
|
||||
has |= c.type == type;
|
||||
}
|
||||
if (!has)
|
||||
{
|
||||
t.targetClasses.Add(new EAIApproachAndAttackTarget.TargetClass { type = typeof(EntityZombie), chaseTimeMax = 0f });
|
||||
t.targetClasses.Add(new EAIApproachAndAttackTarget.TargetClass { type = type, chaseTimeMax = 0f });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -732,6 +1462,22 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Одержимый стервятник: его собственный полёт-ИИ (EntityVulture.updateTasks - своё, без
|
||||
/// вызова базового) заменён полётом по вводу игрока.</summary>
|
||||
[HarmonyPatch(typeof(EntityVulture), nameof(EntityVulture.updateTasks))]
|
||||
public static class Patch_EntityVulture_updateTasks_Possession
|
||||
{
|
||||
public static bool Prefix(EntityVulture __instance)
|
||||
{
|
||||
if (!Possession.IsPossessed(__instance))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
Possession.FlyTick(__instance);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Тело оцепенело: ввод игрока не идёт (как у тела в рэгдолле - stopMoving и выход).</summary>
|
||||
[HarmonyPatch(typeof(PlayerMoveController), nameof(PlayerMoveController.Update))]
|
||||
public static class Patch_PlayerMoveController_Update_Possession
|
||||
@@ -757,6 +1503,9 @@ namespace NecromancerTome
|
||||
static readonly Color Fill = new Color(0.55f, 0.08f, 0.06f, 0.95f);
|
||||
static GUIStyle _style;
|
||||
|
||||
static readonly Color Danger = new Color(1f, 0.15f, 0.1f, 1f);
|
||||
static GUIStyle _dangerStyle;
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
EntityAlive z = Possession.Active ? Possession.Zombie : null;
|
||||
@@ -764,6 +1513,10 @@ namespace NecromancerTome
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Possession.BodyDanger)
|
||||
{
|
||||
DrawBodyDanger();
|
||||
}
|
||||
float scale = Screen.height / 1080f;
|
||||
float w = Width * scale, h = Height * scale;
|
||||
Rect r = new Rect((Screen.width - w) / 2f, Screen.height - Bottom * scale - h, w, h);
|
||||
@@ -783,6 +1536,26 @@ namespace NecromancerTome
|
||||
string name = Localization.Get(z.EntityName);
|
||||
GUI.Label(new Rect(r.x, r.y - 26f * scale, r.width, 24f * scale), name + " " + z.Health + " / " + z.GetMaxHealth(), _style);
|
||||
}
|
||||
|
||||
/// <summary>Надпись «враг у тела» - та же, что у змеи (SnakeCrosshair.DrawBodyDanger): под прицелом,
|
||||
/// красная с тенью.</summary>
|
||||
static void DrawBodyDanger()
|
||||
{
|
||||
if (_dangerStyle == null)
|
||||
{
|
||||
_dangerStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.UpperCenter, fontStyle = FontStyle.Bold };
|
||||
}
|
||||
_dangerStyle.fontSize = Mathf.RoundToInt(26f * Screen.height / 1080f);
|
||||
string text = Localization.Get("necroSnakeBodyDanger");
|
||||
float top = Screen.height * 0.5f + 40f * Screen.height / 1080f;
|
||||
Rect rect = new Rect(0f, top, Screen.width, 60f * Screen.height / 1080f);
|
||||
Color old = GUI.color;
|
||||
GUI.color = new Color(0f, 0f, 0f, 0.8f);
|
||||
GUI.Label(new Rect(rect.x + 2f, rect.y + 2f, rect.width, rect.height), text, _dangerStyle);
|
||||
GUI.color = Danger;
|
||||
GUI.Label(rect, text, _dangerStyle);
|
||||
GUI.color = old;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Приседание одержимого: пока зажата кнопка, рост зомби опускается до CrouchHeight, как игра
|
||||
|
||||
+21
-15
@@ -116,8 +116,7 @@ namespace NecromancerTome
|
||||
static readonly System.Collections.Generic.Queue<int> _fearQueue = new System.Collections.Generic.Queue<int>();
|
||||
static readonly System.Collections.Generic.HashSet<int> _fearQueued = new System.Collections.Generic.HashSet<int>();
|
||||
static float _fearCheck;
|
||||
public const string SnakeMapNavObject = "necroSnakeSpirit";
|
||||
static EntityAlive _mapSnake;
|
||||
static NecroMapMarker.Marker _mapNav;
|
||||
public const float FearInterval = 0.25f;
|
||||
/// <summary>Убитые змеёй: с них не падает мешок с добычей (указание 28.09: «в режиме змеи
|
||||
/// лут не выпадал вообще»). Снимается в патче dropItemOnDeath.</summary>
|
||||
@@ -180,9 +179,10 @@ namespace NecromancerTome
|
||||
_fearQueue.Clear();
|
||||
_fearQueued.Clear();
|
||||
_fearCheck = 0f;
|
||||
// Фиолетовая змея на карте (nav_objects.xml, necroSnakeSpirit), указание 02.10.
|
||||
_snake.AddNavObject(SnakeMapNavObject, null, null);
|
||||
_mapSnake = _snake;
|
||||
// Фиолетовая змея на карте (nav_objects.xml, necroSnakeSpirit), указание 02.10. С 06.10 - своя
|
||||
// метка по Transform (NecroMapMarker): через AddNavObject она так и не показалась.
|
||||
NecroMapMarker.Hide(ref _mapNav); // старая ссылка после закрытия мира - снимется, только если ещё наша
|
||||
_mapNav = NecroMapMarker.Show(_snake, "ui_game_symbol_tracking_snake");
|
||||
Target = null;
|
||||
_radiationCheck = 0f;
|
||||
BodyDanger = false;
|
||||
@@ -248,11 +248,7 @@ namespace NecromancerTome
|
||||
public static void End()
|
||||
{
|
||||
ClearSense();
|
||||
if (_mapSnake != null)
|
||||
{
|
||||
_mapSnake.RemoveNavObject(SnakeMapNavObject);
|
||||
_mapSnake = null;
|
||||
}
|
||||
NecroMapMarker.Hide(ref _mapNav);
|
||||
_fearQueue.Clear();
|
||||
_fearQueued.Clear();
|
||||
Controlling = false;
|
||||
@@ -432,7 +428,7 @@ namespace NecromancerTome
|
||||
_fearQueued.Remove(id);
|
||||
EntityAlive e = _world.GetEntity(id) as EntityAlive;
|
||||
// Ушёл из чутья, умер, стал своим, началась луна - пропускаем и берём следующего.
|
||||
if (e == null || bloodMoon || _world.IsRemote() || !IsFoe(e, _snake) ||
|
||||
if (e == null || bloodMoon || _world.IsRemote() || !IsFoe(e, _snake) || !IsUndead(e) ||
|
||||
(e.position - _pos).sqrMagnitude > SenseRange * SenseRange)
|
||||
{
|
||||
continue;
|
||||
@@ -598,7 +594,8 @@ namespace NecromancerTome
|
||||
continue;
|
||||
}
|
||||
float d2 = (e.position - _pos).sqrMagnitude;
|
||||
bool foe = IsFoe(e, _snake);
|
||||
// Чутьё и ужас - власть над мёртвыми (указание 06.10): только нежить, живых змея не чует и не пугает.
|
||||
bool foe = IsFoe(e, _snake) && IsUndead(e);
|
||||
if (foe && d2 < SenseRange * SenseRange)
|
||||
{
|
||||
// Только наземные: зомби и звери-зомби. Стервятникам пафайндер не нужен.
|
||||
@@ -654,6 +651,13 @@ namespace NecromancerTome
|
||||
!GhostTraderPatch.ShouldBeGhost(_e) && !NecroCharmSide.IsCharmed(_e);
|
||||
}
|
||||
|
||||
/// <summary>Нежить - поднятые мертвецы: всё с меткой zombie (зомби-люди, зомби-собаки, -медведи,
|
||||
/// -кабаны, стервятники, Стиратель). Живые звери, бандиты, игроки и торговцы - не нежить. Та же
|
||||
/// метка, по которой считаются убийства в Некромантию (NecromancyKillCreditPatch.ZombieTag).</summary>
|
||||
static readonly FastTags<TagGroup.Global> UndeadTag = FastTags<TagGroup.Global>.Parse("zombie");
|
||||
|
||||
public static bool IsUndead(EntityAlive _e) => _e != null && _e.HasAnyTags(UndeadTag);
|
||||
|
||||
/// <summary>Питомцы и змея мода - тоже EntityEnemy (зомби-основа), и без этой проверки
|
||||
/// охрана выбирала целью самого питомца: он ближе всех к хозяину (тест 30.09, лог
|
||||
/// «pet 3838 идёт на 3838»). Все наши классы сущностей начинаются с necro.</summary>
|
||||
@@ -1002,7 +1006,8 @@ namespace NecromancerTome
|
||||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||||
{
|
||||
EntityAlive e = _world.EntityAlives[i];
|
||||
if (!IsFoe(e, _snake))
|
||||
// Атака духа змеи - только по нежити (указание 06.10): живые звери и люди ей не подвластны.
|
||||
if (!IsFoe(e, _snake) || !IsUndead(e))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -1340,7 +1345,8 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Пояс в режиме змеи скрыт (указание 28.09: «прятать оверлей с поясом»). Игра каждый
|
||||
/// <summary>Пояс в режиме змеи скрыт (указание 28.09: «прятать оверлей с поясом»), так же в
|
||||
/// чертогах и у одержимого питомца (06.10). Игра каждый
|
||||
/// кадр в EntityPlayerLocal.OnHUD открывает окно "toolbelt", если оно закрыто, - поэтому не
|
||||
/// закрываем его раз за разом, а не даём открыться: все Open сходятся в openInternal.</summary>
|
||||
[HarmonyPatch(typeof(GUIWindowManager), "openInternal")]
|
||||
@@ -1348,7 +1354,7 @@ namespace NecromancerTome
|
||||
{
|
||||
public static bool Prefix(GUIWindow _w)
|
||||
{
|
||||
return !((SnakeControl.Controlling || MindControl.Controlling) && _w != null && _w.Id == SnakeControl.ToolbeltWindow);
|
||||
return !((SnakeControl.Controlling || MindControl.Controlling || (Possession.Active && Possession.IsPet)) && _w != null && _w.Id == SnakeControl.ToolbeltWindow);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -238,6 +238,12 @@ namespace NecromancerTome
|
||||
return;
|
||||
}
|
||||
|
||||
// Красный труп под прицелом - не блок: череп для урны (NecroSkull.cs, 06.10).
|
||||
if (NecroSkull.TryBegin(_player, _bracelet))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WorldRayHitInfo hitInfo = _player.HitInfo;
|
||||
if (hitInfo == null || !hitInfo.bHitValid)
|
||||
{
|
||||
|
||||
+102
-18
@@ -316,14 +316,9 @@ namespace NecromancerTome
|
||||
|
||||
if (_actionData.indexInEntityOfAction == RecallActionIndex)
|
||||
{
|
||||
if (owned.Count > 0)
|
||||
{
|
||||
RecallPet(holdingEntity, owned[0].Id, tooltips.RecalledKey, tooltips.ConsumesBook ? tooltips.SummonItemName : null);
|
||||
}
|
||||
else if (holdingEntity.world != null)
|
||||
{
|
||||
GameManager.ShowTooltip(GameManager.Instance.World.GetPrimaryPlayer(), tooltips.NothingToRecallKey);
|
||||
}
|
||||
// С 06.10 ПКМ - одержимость питомца (Patch_..._ExecuteAction_InstantRecall делает её на
|
||||
// нажатии, сюда доходит только если та пропустила); отзыв - на E (PetRecallKey).
|
||||
TryPossessPet(holdingEntity, owned, tooltips);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -436,8 +431,66 @@ namespace NecromancerTome
|
||||
|
||||
/// <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>
|
||||
/// <summary>ПКМ с книгой призыва (указание 06.10): вселиться в призванного питомца этой книги.
|
||||
/// Нет питомца - надпись necroPetNothingToPossess.</summary>
|
||||
public static void TryPossessPet(EntityAlive holdingEntity, List<OwnedEntityData> owned, PetInfo tooltips)
|
||||
{
|
||||
EntityPlayerLocal local = holdingEntity as EntityPlayerLocal;
|
||||
if (local == null || tooltips.RecalledKey == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
EntityAlive pet = owned.Count > 0 && holdingEntity.world != null ? holdingEntity.world.GetEntity(owned[0].Id) as EntityAlive : null;
|
||||
if (pet == null || pet.IsDead())
|
||||
{
|
||||
GameManager.ShowTooltip(local, "necroPetNothingToPossess");
|
||||
return;
|
||||
}
|
||||
if (!Possession.EnterPet(local, pet))
|
||||
{
|
||||
Debug.Log("[NecromancerTome] SummonPatch: вселиться в питомца " + pet.entityId + " нельзя (змея, чертоги, транспорт или уже одержимость)");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Отозвать питомца книги, которую держит игрок (E, PetRecallKey).</summary>
|
||||
public static void RecallHeldBookPet(EntityAlive holdingEntity, string entityToSpawn, PetInfo tooltips)
|
||||
{
|
||||
int petClassId = EntityClass.GetId(entityToSpawn);
|
||||
if (petClassId == -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<OwnedEntityData> owned = holdingEntity.GetOwnedEntities(petClassId);
|
||||
if (owned.Count > 0)
|
||||
{
|
||||
RecallPet(holdingEntity, owned[0].Id, tooltips.RecalledKey, tooltips.ConsumesBook ? tooltips.SummonItemName : null);
|
||||
}
|
||||
else if (holdingEntity.world != null)
|
||||
{
|
||||
GameManager.ShowTooltip(GameManager.Instance.World.GetPrimaryPlayer(), tooltips.NothingToRecallKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>E в одержимом питомце (тест 06.10: «E не сработал, срабатывает R») - отзыв его самого:
|
||||
/// игрок возвращается в тело, питомец уходит в книгу. HandleInteraction во время одержимости не
|
||||
/// идёт (PlayerMoveController.Update выключен), поэтому зовёт Possession. True - отозван.</summary>
|
||||
public static bool RecallPossessedPet(EntityAlive owner, EntityAlive pet)
|
||||
{
|
||||
if (owner == null || pet == null || !EntityClass.list.TryGetValue(pet.entityClass, out EntityClass ec) ||
|
||||
!LimitedPets.TryGetValue(ec.entityClassName, out PetInfo info) || info.RecalledKey == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
RecallPet(owner, pet.entityId, info.RecalledKey, info.ConsumesBook ? info.SummonItemName : null);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void RecallPet(EntityAlive owner, int petEntityId, string recalledTooltipKey, string summonItemName)
|
||||
{
|
||||
if (Possession.Active && Possession.Zombie != null && Possession.Zombie.entityId == petEntityId)
|
||||
{
|
||||
Possession.Exit("питомец отозван");
|
||||
}
|
||||
World world = owner.world;
|
||||
if (world != null)
|
||||
{
|
||||
@@ -551,17 +604,9 @@ namespace NecromancerTome
|
||||
}
|
||||
lastRecallTime = Time.time;
|
||||
|
||||
// С 06.10 ПКМ - одержимость питомца, отзыв переехал на E (PetRecallKey).
|
||||
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);
|
||||
}
|
||||
Patch_ItemActionSpawnEntity_Spawn_PetLimit.TryPossessPet(holdingEntity, owned, petInfo);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -619,6 +664,40 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ОТЗЫВ НА E - указание 06.10: «Отозвать питомца снимаем с ПКМ и ставим на E» (ПКМ отдана
|
||||
/// одержимости питомца). E у игрока - «взаимодействие»: PlayerMoveController.HandleInteraction
|
||||
/// получает _activate и открывает то, что под прицелом (блок, предмет, сущность). Отзываем только
|
||||
/// когда под прицелом взаимодействовать не с чем - иначе E с книгой в руке перестала бы открывать
|
||||
/// двери и ящики. «Не с чем» = у игры нет подсказки «Нажмите E…» (strTextLabelPointingTo пуст: на
|
||||
/// кадре нажатия она не обновляется и держит прошлый кадр - то, куда игрок смотрел).
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(PlayerMoveController), nameof(PlayerMoveController.HandleInteraction))]
|
||||
public static class Patch_PlayerMoveController_HandleInteraction_PetRecallKey
|
||||
{
|
||||
public static void Postfix(PlayerMoveController __instance, bool _activate)
|
||||
{
|
||||
if (!_activate || Possession.Active || !string.IsNullOrEmpty(__instance.strTextLabelPointingTo))
|
||||
{
|
||||
return;
|
||||
}
|
||||
EntityPlayerLocal player = __instance.entityPlayerLocal;
|
||||
if (player == null || player.AttachedToEntity != null || player.IsDead() || player.inventory == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ItemClass held = player.inventory.holdingItem;
|
||||
if (held?.Actions == null || held.Actions.Length == 0 || !(held.Actions[0] is ItemActionSpawnEntity spawn) ||
|
||||
!Patch_ItemActionSpawnEntity_Spawn_PetLimit.LimitedPets.TryGetValue(spawn.entityToSpawn, out Patch_ItemActionSpawnEntity_Spawn_PetLimit.PetInfo info) ||
|
||||
info.RecalledKey == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Patch_ItemActionSpawnEntity_Spawn_PetLimit.RecallHeldBookPet(player, spawn.entityToSpawn, info);
|
||||
player.PlayOneShot(spawn.soundWarn);
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(EntityFactory), "CreateEntity", new System.Type[] { typeof(int), typeof(Vector3), typeof(Vector3) })]
|
||||
public static class Patch_EntityFactory_CreateEntity_PetOwnership
|
||||
{
|
||||
@@ -683,6 +762,11 @@ namespace NecromancerTome
|
||||
PetFollowPatch.Register(owner, __result, petInfo.UsesFollowTask, collisionIgnored, petInfo.LitEyes,
|
||||
petInfo.FlyingHomeRadius, petInfo.Guards);
|
||||
AttachFollowTask(owner, __result, petInfo);
|
||||
// Указание 06.10: при призыве ненадолго - «Отозвать питомца - E». Рой не отзывается.
|
||||
if (petInfo.RecalledKey != null && owner is EntityPlayerLocal ownerLocal)
|
||||
{
|
||||
GameManager.ShowTooltip(ownerLocal, "necroPetRecallHint");
|
||||
}
|
||||
// Полупрозрачность больше НЕ делается здесь. Бывший ApplyGhostlyTransparency ставил
|
||||
// альфу в material.color и не работал ни разу: шейдеры моделей мобов альфу не
|
||||
// поддерживают (на Рое это выяснилось ещё 28.08 - он только засорял лог). Призрачный
|
||||
|
||||
Reference in New Issue
Block a user