Змея: выход из текстур, полёт на ПКМ; одержимый: приседание, удар по прицелу; коза

- Змея: проверка «внутри текстуры» лучами (земля игры - поверхность), каждый кадр выброс
  на ближайшее свободное место (на земле - из любых текстур, в полёте - из грунта);
  шаг и прыжок в текстуры не пускают. Полёт пока зажата ПКМ: 1.1 м/с (половина шага
  зомби), сквозь постройки можно, в грунт нельзя; Пробел вверх, C/Ctrl вниз.
  E - аварийный выход. Детализация зданий и область тела - как у одержимого.
- Одержимый: будит спящих (комнаты зданий и сам вид одержимого), приседание на C и Ctrl,
  удар по точке под прицелом с наведением на грудь ближнего врага.
- Хваты: толчок руки к прицелу на ЛКМ; «пиф-паф» вселяет на пике толчка.
  «Отрицание» стало «Козой» (13 языков): средний и безымянный поджаты, большой прижат;
  звук - бросок змеи.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-05 16:28:40 +03:00
co-authored by Claude Opus 5.5
parent bb7a66dd8e
commit fdd3782024
6 changed files with 429 additions and 60 deletions
+135 -5
View File
@@ -52,6 +52,26 @@ namespace NecromancerTome
public const float TauntRange = 30f;
public const float TauntPeriod = 1f;
public const int TauntTicks = 1200;
/// <summary>Спящие просыпаются от одержимого (тест 05.10: «не разбудил спящего»). Игра будит их
/// только игроком - увидел или услышал (EAISetNearestEntityAsTarget.FindTargetPlayer), одержимый для
/// неё просто зомби. Будим сами: видит одержимого ближе SleeperWakeRange (пригнувшегося - ближе
/// SleeperWakeCrouched) или тот вплотную, ближе SleeperHearRange, хоть и за спиной.</summary>
public const float SleeperWakeRange = 8f;
public const float SleeperWakeCrouched = 3f;
public const float SleeperHearRange = 2f;
/// <summary>Приседание (указание 05.10): пока зажата кнопка приседания. Высота - доля роста зомби;
/// ниже 0.7 игра включает ходьбу пригнувшись (walkType 8), как у её зомби под низким потолком.</summary>
public static bool Crouching;
/// <summary>Удар по прицелу (тест 05.10: «не попадаю по другим зомби»). Удар зомби - луч ~1.65 м из
/// головы по взгляду (ItemActionMelee, EntityHuman.GetLookVector), а взгляд брался из наклона камеры,
/// которая сзади и выше: под прицелом враг, а луч из головы уходит ниже и мимо. На время замаха
/// взгляд зомби - на точку под прицелом, а если рядом враг почти по прицелу - на его грудь.</summary>
public const float MeleeAssistRange = 2.5f;
public const float MeleeAssistDeg = 25f;
static float _meleeLookUntil;
/// <summary>Ctrl в игре - приседание переключателем (ToggleCrouch), C - пока держишь (Crouch).</summary>
static bool _crouchToggled;
public const float CrouchHeight = 0.6f;
/// <summary>Сколько чанков держать вокруг тела, пока игрок в зомби (указание 03.10: «сильно ограничить
/// видимость у тела - ему вообще нет смысла что-либо видеть»). Тест 03.10: две полные области обзора
/// (тело + одержимый) - память росла с каждым шагом зомби, а выход давал подвисание ~50 с. Трёх чанков
@@ -73,6 +93,7 @@ namespace NecromancerTome
public const float TurnSpeed = 540f;
static float _nextAttack;
static float _nextTaunt;
static float _sleeperCheck;
static ChunkManager.ChunkObserver _observer;
static readonly List<Animator> _frozen = new List<Animator>();
static readonly List<float> _frozenSpeed = new List<float>();
@@ -155,7 +176,11 @@ namespace NecromancerTome
return;
}
Active = false;
Crouching = false;
_meleeLookUntil = 0f;
_crouchToggled = false;
EntityAlive z = Zombie;
z?.SetLookPosition(Vector3.zero);
EntityPlayerLocal player = _player;
if (_rState != RState.Idle && z != null && !z.IsDead())
{
@@ -287,6 +312,11 @@ namespace NecromancerTome
// кнопка геймпада (тест 03.10: «подрыва нету»).
detonate = input.Activate.WasPressed || input.PermanentActions.Activate.WasPressed;
}
if (input != null && !gui && input.ToggleCrouch.WasPressed)
{
_crouchToggled = !_crouchToggled;
}
Crouching = input != null && !gui && (input.Crouch.IsPressed || _crouchToggled);
if (detonate && z is EntityZombieCop cop && !cop.isPrimed)
{
Debug.Log("[NecromancerTome] Possession: подрыв " + z.EntityName);
@@ -316,12 +346,32 @@ namespace NecromancerTome
{
Scream(world, z);
}
if (attack && Time.time >= _nextAttack && z.Attack(false))
if (attack && Time.time >= _nextAttack)
{
z.Attack(true);
_nextAttack = Time.time + Mathf.Max(0.3f, z.GetAttackTimeoutTicks() / 20f);
z.SetLookPosition(MeleeAim(world, z));
if (z.Attack(false))
{
z.Attack(true);
float timeout = Mathf.Max(0.3f, z.GetAttackTimeoutTicks() / 20f);
_nextAttack = Time.time + timeout;
_meleeLookUntil = Time.time + Mathf.Max(0.6f, timeout);
}
}
if (_meleeLookUntil > 0f && _rState == RState.Idle)
{
if (Time.time < _meleeLookUntil)
{
z.SetLookPosition(MeleeAim(world, z));
}
else
{
_meleeLookUntil = 0f;
z.SetLookPosition(Vector3.zero);
}
}
_observer?.SetPosition(z.position);
// Спящие в зданиях просыпаются от одержимого, как от змеи (указание 05.10).
SnakeControl.Sleepers(world, _player, z.position, ref _sleeperCheck, Time.deltaTime);
Taunt(world, z);
}
@@ -371,6 +421,37 @@ namespace NecromancerTome
GameManager.ShowTooltip(_p, text);
}
/// <summary>Куда бить: грудь врага ближе MeleeAssistRange к голове и не дальше MeleeAssistDeg от
/// направления на прицел (ближайший к прицелу), иначе - точка под прицелом.</summary>
static Vector3 MeleeAim(World _world, EntityAlive _z)
{
Vector3 aim = AimPoint();
Vector3 head = _z.getHeadPosition();
Vector3 dir = (aim - head).normalized;
EntityAlive best = null;
float bestAngle = MeleeAssistDeg;
for (int i = 0; i < _world.EntityAlives.Count; i++)
{
EntityAlive e = _world.EntityAlives[i];
if (e == null || e == _z || e.IsDead() || e is EntityPlayer || SnakeControl.IsNecroEntity(e))
{
continue;
}
Vector3 to = e.getChestPosition() - head;
if (to.sqrMagnitude > MeleeAssistRange * MeleeAssistRange)
{
continue;
}
float angle = Vector3.Angle(dir, to);
if (angle < bestAngle)
{
bestAngle = angle;
best = e;
}
}
return best != null ? best.getChestPosition() : aim;
}
/// <summary>Точка под прицелом камеры (в мировых координатах), до 50 м.</summary>
static Vector3 AimPoint()
{
@@ -566,8 +647,26 @@ namespace NecromancerTome
{
EntityAlive e = _world.EntityAlives[i];
if (e == null || e == _z || e.IsDead() || !(e is EntityZombie) || e.GetAttackTarget() == _z ||
e.aiManager == null || PetFollowPatch.FindOwnerOfPet(e.entityId) != null ||
(e.position - _z.position).sqrMagnitude > TauntRange * TauntRange || !e.CanSee(_z))
e.aiManager == null || PetFollowPatch.FindOwnerOfPet(e.entityId) != null)
{
continue;
}
float d2 = (e.position - _z.position).sqrMagnitude;
if (d2 > TauntRange * TauntRange)
{
continue;
}
if (e.IsSleeping)
{
float wake = Crouching ? SleeperWakeCrouched : SleeperWakeRange;
if (d2 > SleeperHearRange * SleeperHearRange && (d2 > wake * wake || !e.CanSee(_z)))
{
continue;
}
e.ConditionalTriggerSleeperWakeUp();
Debug.Log("[NecromancerTome] Possession: проснулся " + e.EntityName + " (" + e.entityId + "), " + Mathf.Sqrt(d2).ToString("F1") + " м");
}
else if (!e.CanSee(_z))
{
continue;
}
@@ -685,4 +784,35 @@ namespace NecromancerTome
GUI.Label(new Rect(r.x, r.y - 26f * scale, r.width, 24f * scale), name + " " + z.Health + " / " + z.GetMaxHealth(), _style);
}
}
/// <summary>Приседание одержимого: пока зажата кнопка, рост зомби опускается до CrouchHeight, как игра
/// опускает его под низким потолком (EntityAlive.CrouchHeightFixedUpdate, её же шаги). Отпустил - дальше
/// считает игра: встаёт, только если сверху свободно. Сама игра пригибает лишь под потолком над головой,
/// а проём впереди чует через moveHelper, который у одержимого остановлен.</summary>
[HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.CrouchHeightFixedUpdate))]
public static class Patch_EntityAlive_CrouchHeightFixedUpdate_Possession
{
public static bool Prefix(EntityAlive __instance)
{
if (!Possession.Crouching || !Possession.IsPossessed(__instance) || __instance.crouchType == 0 ||
__instance.physicsBaseHeight <= 1.3f)
{
return true;
}
float h = Utils.FastMoveTowards(__instance.physicsHeight, __instance.physicsBaseHeight * Possession.CrouchHeight, 0.099999994f);
__instance.SetHeight(h);
if (h <= __instance.physicsBaseHeight * 0.7f)
{
__instance.crouchBendPerTarget = 0f;
if (__instance.walkType != 8 && __instance.walkType != 21)
{
int was = __instance.walkType;
__instance.SetWalkType(8);
__instance.walkTypeBeforeCrouch = was;
}
}
__instance.crouchBendPer = Mathf.MoveTowards(__instance.crouchBendPer, __instance.crouchBendPerTarget, 0.099999994f);
return false;
}
}
}