Одержимость: прицел-перекрестие с покраснением, кнопки E/F, лут мешков и багажник волка, медведь на дыбы

- Прицел всех одержимых - всегда перекрестие (свой guiDrawCrosshair: точка бралась из проверки
  камеры от 3-го лица у тела); краснеет на враге: у стервятника - цель рывка, у наземных - враг
  на луче прицела до 30 м, не за стеной
- Падаль: отхил вдвое меньше (EatHealFrac 0.125); ест на F стервятник, медведь, собака, волк и
  одержимые зомби
- E - действие: стервятник - клич (был F), крыса/волк - обыск, медведь - на дыбы (враги до 3.5 м
  сбиты с ног и отброшены, откат 6 с)
- Рёв медведя: надписи «на медведя идут N» и откат, мимо очереди подсказок
- Крыса и волк обыскивают мешки с зомби (EntityLootContainer), волк берёт в сумку
- Багажник волка: окно сумки как у браслета, без рюкзака; выброс - у волка
- Отзыв волка: мешок на месте и метка «Сумка волка» на карте, снимается с мешком
- Тексты на 13 языках

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-09 13:31:55 +03:00
co-authored by Claude Opus 5.5
parent 31aa99860c
commit e94e967b8e
7 changed files with 572 additions and 55 deletions
+132 -27
View File
@@ -122,14 +122,14 @@ namespace NecromancerTome
/// <summary>Толчок вверх при взлёте с земли на пробел, м за тик.</summary>
public const float FlyTakeOff = 0.3f;
static bool _flySitting;
/// <summary>Питание падалью (указание 06.10): сидя у трупа, F - EatTime секунд клюёт его и лечится на
/// <summary>Питание падалью (указание 06.10; у медведя и собаки - 09.10): сидя или стоя у трупа, E - EatTime секунд клюёт его и лечится на
/// EatHealFrac здоровья. У трупа запас мяса - его полное здоровье при жизни; поедание отнимает
/// столько же, сколько лечит (и на полном здоровье тоже). Запас кончился - труп исчезает (указание
/// 06.10), вместе с содержимым, если это красный труп-добыча. Сдвинулся, взлетел, ударил - еда
/// прервана, вылеченное и съеденное остаются; к трупу можно вернуться.</summary>
public const float EatRange = 2.5f;
public const float EatTime = 3f;
public const float EatHealFrac = 0.25f;
public const float EatHealFrac = 0.125f; // было 0.25 - указание 09.10: «лечится слишком быстро, вдвое»
static EntityAlive _eatCorpse;
static float _eatLeft, _eatHeal, _eatBite;
static int _eatCount;
@@ -325,6 +325,7 @@ namespace NecromancerTome
}
Active = false;
RatLoot.CloseIfViewing();
WolfBag.CloseWindow();
RatPhase.Stop(GameManager.Instance?.World, Zombie, "выход из одержимости");
DogSense.Clear();
WolfControl.Clear();
@@ -588,20 +589,23 @@ namespace NecromancerTome
_nextAttack = Time.time + FlyAttackDelay;
_meleeLookUntil = Time.time + FlyAttackDelay;
}
// F - клич-приманка (указание 06.10; кнопка - 08.10, была ПКМ): крик крикуньи, только голосом
// паникующей курицы - зомби в ScreamRange идут на него; с воздуха уводит толпу от базы или от тела.
// F у игрока - фонарик, но его читает только выключенный PlayerMoveController.Update.
bool scream = input != null && !gui && (input.PermanentActions.ToggleFlashlight.WasPressed || input.ToggleFlashlight.WasPressed);
// E - клич-приманка (указание 06.10; кнопка - 08.10 F, была ПКМ; 09.10 - E: «на E действие, у стервятника крик»):
// крик крикуньи, только голосом паникующей курицы - зомби в ScreamRange идут на него; с воздуха уводит
// толпу от базы или от тела. detonate - это и есть нажатие E.
bool scream = detonate;
if (scream)
{
Scream(world, z);
}
// ПКМ (держать) - «зоркий глаз» (указание 08.10: «приближение как у прицела»), в PlaceCamera.
_flyZoomHeld = rangedHeld;
// E - питаться падалью (указание 06.10, кнопка - 08.10: «пусть ест по кнопке E» - у игрока E это
// взаимодействие, лут; было F). detonate - это и есть нажатие E.
bool eat = detonate;
Eat(world, z, eat, attack || scream);
// F - питаться падалью (указание 06.10; кнопка 08.10 - E, 09.10 - F у всех одержимых питомцев).
// F у игрока - фонарик, но его читает только выключенный PlayerMoveController.Update.
bool eat = FKey(input, gui);
// Есть можно сидя или просто остановившись рядом (зависнув над падалью): без ввода и почти без скорости.
bool still = _flySitting || _flyMove.sqrMagnitude < 0.01f && !_flyUp &&
new Vector2(z.motion.x, z.motion.z).sqrMagnitude < FlySitSpeed * FlySitSpeed;
Eat(world, z, eat, attack || scream || _flyMove.sqrMagnitude > 0.01f || _flyUp || !_flySitting, still);
if (_meleeLookUntil > 0f && Time.time >= _meleeLookUntil)
{
_meleeLookUntil = 0f;
@@ -643,16 +647,29 @@ namespace NecromancerTome
{
// ПКМ - бросок на цель, след крови раненых (решено 09.10).
WolfControl.Tick(world, _player, z, ranged, ref _bodyYaw);
// E: у тела - выгрузить сумку в рюкзак, у контейнера - лутать в сумку (3 ячейки).
if (!(detonate && WolfBag.TryUnload(_player, z)))
// E: на контейнер или мешок с зомби - лутать в сумку (3 ячейки); у тела - выгрузить сумку в рюкзак;
// иначе - «багажник», окно сумки (указание 09.10).
bool looted = RatLoot.Tick(world, _player, z, detonate, true);
if (detonate && !looted && !RatLoot.Viewing && !WolfBag.TryUnload(_player, z))
{
RatLoot.Tick(world, _player, z, detonate, true);
WolfBag.OpenWindow(_player, z);
}
}
if (ranged && IsPet && BearControl.Is(z))
{
BearControl.Roar(world, z); // ПКМ - рёв-провокация (решено 09.10)
}
if (detonate && IsPet && BearControl.Is(z))
{
BearControl.RearUp(world, z); // E - на дыбы, враги рядом падают (указание 09.10)
}
if (EatsCarrion(z))
{
// F - есть падаль, как стервятник (указание 09.10: «поедание падали на F одержимым питомцам, на E -
// действие»; и одержимым зомби - их ИИ в игре и так ест трупы), стоя на месте рядом с трупом.
Eat(world, z, FKey(input, gui), moving || jump || attack || ranged, !moving && z.onGround);
}
AimTint(GroundAimTarget(world, z) != null);
if (IsPet && DogSense.Is(z))
{
// Нюх (указание 09.10): все существа рядом с собакой - на компасе и карте.
@@ -758,11 +775,20 @@ namespace NecromancerTome
}
}
static void Eat(World _world, EntityAlive _v, bool _pressed, bool _acted)
/// <summary>Едят падаль на F, кроме стервятника (указание 09.10): чужие одержимые зомби и питомцы медведь,
/// собака, волк. Крыса - дух, не ест.</summary>
static bool EatsCarrion(EntityAlive _z) => !IsPet || BearControl.Is(_z) || DogSense.Is(_z) || WolfControl.Is(_z);
/// <summary>F на клавиатуре или геймпаде (у игрока - фонарик).</summary>
static bool FKey(PlayerActionsLocal _input, bool _gui) =>
_input != null && !_gui && (_input.PermanentActions.ToggleFlashlight.WasPressed || _input.ToggleFlashlight.WasPressed);
/// <summary>_moved - сдвинулся или что-то сделал (еда прерывается), _still - стоит/сидит, можно начать.</summary>
static void Eat(World _world, EntityAlive _v, bool _pressed, bool _moved, bool _still)
{
if (_eating)
{
bool moved = _flyMove.sqrMagnitude > 0.01f || _flyUp || _acted || !_flySitting;
bool moved = _moved;
bool gone = _eatCorpse != null ? _eatCorpse.IsMarkedForUnload() : _world.GetBlock(_eatBlock).type != _eatBlockType;
if (moved || gone)
{
@@ -822,17 +848,14 @@ namespace NecromancerTome
{
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))
if (!_still || !FindCarrion(_world, _v))
{
GameManager.ShowTooltip(_player, Localization.Get("necroPossessNoCarrion"));
Debug.Log("[NecromancerTome] Possession: падали нет" + (still ? "" : " - стервятник не остановился"));
GameManager.ShowTooltip(_player, Localization.Get(Flying ? "necroPossessNoCarrion" : "necroPossessNoCarrionGround"));
Debug.Log("[NecromancerTome] Possession: падали нет" + (_still ? "" : " - " + _v.EntityName + " не остановился"));
return;
}
_eating = true;
if (!_flySitting)
if (Flying && !_flySitting)
{
// Сел за еду сразу, не дожидаясь тика полёта: иначе проверка «не сидит» тут же прервала бы еду.
_flySitting = true;
@@ -845,7 +868,7 @@ namespace NecromancerTome
_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() +
Debug.Log("[NecromancerTome] Possession: " + _v.EntityName + " ест " + EatName() + ", здоровье " + _v.Health + "/" + _v.GetMaxHealth() +
", мяса у падали " + Mathf.CeilToInt(EatMeat(0f)));
}
@@ -1011,7 +1034,6 @@ namespace NecromancerTome
colors[_tintIndex] = _tintSaved;
_tintIndex = -1;
}
Debug.Log("[NecromancerTome] Possession: прицел " + (_on ? "на цели - красный" : "без цели"));
}
static readonly Color AimOnTarget = new Color(1f, 0.15f, 0.1f, 1f);
@@ -1219,6 +1241,47 @@ namespace NecromancerTome
static EntityAlive _aimMissNear;
static float _aimMissDist, _aimMissOff;
/// <summary>Враг под прицелом наземного одержимого (указание 09.10: прицел краснеет на противнике у
/// всех одержимых): тело врага - отрезок от ног до головы - не дальше GroundAimRadius от луча
/// камеры и не дальше GroundAimRange от одержимого, и не за стеной - не дальше первого попадания
/// луча (AimPoint).</summary>
static EntityAlive GroundAimTarget(World _world, EntityAlive _z)
{
Transform cam = _player?.cameraTransform;
if (cam == null)
{
return null;
}
Vector3 from = cam.position + Origin.position;
Vector3 dir = cam.forward;
float wall = Vector3.Dot(AimPoint() - from, dir) + 0.5f;
EntityAlive best = null;
float bestAlong = float.MaxValue;
for (int i = 0; i < _world.EntityAlives.Count; i++)
{
EntityAlive e = _world.EntityAlives[i];
if (!SnakeControl.IsFoe(e, _z) || (e.position - _z.position).sqrMagnitude > GroundAimRange * GroundAimRange)
{
continue;
}
Vector3 feet = e.position, head = e.getHeadPosition();
for (int k = 0; k <= 4; k++)
{
Vector3 p = Vector3.Lerp(feet, head, k / 4f);
float a = Vector3.Dot(p - from, dir);
if (a > 0f && a < wall && a < bestAlong && Vector3.Cross(p - from, dir).magnitude < GroundAimRadius)
{
best = e;
bestAlong = a;
}
}
}
return best;
}
public const float GroundAimRange = 30f; // зрение зомби, как TauntRange
public const float GroundAimRadius = 0.6f;
/// <summary>Раз в полсекунды: есть ли живой враг ближе BodyDangerRange к телу (как у змеи).</summary>
static void CheckBody(World _world, EntityAlive _z, float _dt)
{
@@ -1288,11 +1351,11 @@ namespace NecromancerTome
}
if (Flying)
{
text += ", " + Localization.Get("necroPossessHintZoom") + ", " + Localization.Get("necroPossessHintScreamF");
text += ", " + Localization.Get("necroPossessHintZoom") + ", " + Localization.Get("necroPossessHintScreamE");
}
if (Flying)
{
text += ", " + Localization.Get("necroPossessHintEat");
text += ", " + Localization.Get("necroPossessHintEatF");
}
if (IsPet && RatModel.Is(_z))
{
@@ -1309,9 +1372,14 @@ namespace NecromancerTome
}
if (IsPet && BearControl.Is(_z))
{
text += ", " + Localization.Get("necroPossessHintRoar") + ", " + Localization.Get("necroPossessHintSmash") + ", " +
text += ", " + Localization.Get("necroPossessHintRoar") + ", " + Localization.Get("necroPossessHintRear") + ", " +
Localization.Get("necroPossessHintSmash") + ", " +
Localization.Get("necroPossessHintSkin");
}
if (EatsCarrion(_z))
{
text += ", " + Localization.Get("necroPossessHintEatGroundF");
}
if (_z.jumpMaxDistance >= 5f)
{
text += ", " + Localization.Get("necroPossessHintLeap");
@@ -1765,6 +1833,43 @@ namespace NecromancerTome
}
}
/// <summary>Прицел одержимого - всегда перекрестие (указание 09.10: «то перекрестие, то точка»). Вид прицела
/// игра берёт у предмета в руке тела, а у книги призыва и пустой руки он зависит от проверки камеры от
/// третьего лица (ItemAction.GetCrosshairType - EntityPlayerLocal.CharacterCameraAngleValid): камера дальше
/// 20° от того, куда повёрнуто тело, - прицела нет, и игра рисует точку. В одержимости камера - глаза
/// одержимого, тело стоит, так что вид менялся от поворота мыши. Рисуем сами то же, что ветка Plus
/// guiDrawCrosshair; цвет - из таблицы игры, его красит Possession.AimTint.</summary>
[HarmonyPatch(typeof(EntityPlayerLocal), nameof(EntityPlayerLocal.guiDrawCrosshair))]
public static class Patch_EntityPlayerLocal_guiDrawCrosshair_Possession
{
public static bool Prefix(EntityPlayerLocal __instance, NGuiWdwInGameHUD _guiInGame, bool bModalWindowOpen)
{
if (!Possession.Active || __instance != Possession.Player)
{
return true;
}
if (!_guiInGame.showCrosshair || Event.current.type != EventType.Repaint || __instance.IsDead() ||
!GamePrefs.GetBool(EnumGamePrefs.OptionsCrosshairEnabled) || bModalWindowOpen || _guiInGame.CrosshairTexture == null)
{
return false;
}
int i = GamePrefs.GetInt(EnumGamePrefs.OptionsCrosshairColor);
if (i < 0 || i >= EntityPlayerLocal.crosshairColors.Count)
{
i = 0;
}
Color c = EntityPlayerLocal.crosshairColors[i];
Vector2 center = __instance.GetCrosshairPosition2D();
center.y = Screen.height - center.y;
float size = Screen.height * 0.059f * GamePrefs.GetFloat(EnumGamePrefs.OptionsCrosshairScale);
Color was = GUI.color;
GUI.color = new Color(c.r, c.g, c.b, GamePrefs.GetFloat(EnumGamePrefs.OptionsCrosshairOpacity));
GUI.DrawTexture(new Rect(center.x - size / 2f, center.y - size / 2f, size, size), _guiInGame.CrosshairTexture, ScaleMode.StretchToFill);
GUI.color = was;
return false;
}
}
/// <summary>Тело оцепенело: ввод игрока не идёт (как у тела в рэгдолле - stopMoving и выход).</summary>
[HarmonyPatch(typeof(PlayerMoveController), nameof(PlayerMoveController.Update))]
public static class Patch_PlayerMoveController_Update_Possession