- Прицел всех одержимых - всегда перекрестие (свой 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>
172 lines
7.7 KiB
C#
172 lines
7.7 KiB
C#
using HarmonyLib;
|
||
using UnityEngine;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// Одержимый медведь - танк (решено 09.10, BACKLOG «Медведь и лютоволк в одержимости»):
|
||
/// - толстая шкура: пока в нём игрок, урон по нему вдвое меньше (вне одержимости медведь прежний);
|
||
/// - рёв на ПКМ: зомби ближе RoarRange бросают свои цели и идут на медведя, спящие просыпаются -
|
||
/// увести толпу от тела или базы. Та же провокация, что каждую секунду делает одержимость
|
||
/// (Possession.Taunt), только шире, без «видит ли» и на 20 с;
|
||
/// - удар лапой по блоку: ЛКМ по блоку под прицелом бьёт его на SmashDamage (дерево и двери за 2-3
|
||
/// удара, бетон дольше);
|
||
/// - на дыбы на E (указание 09.10: «на E - действие»): враги ближе RearRange сбиты с ног на RearStun с
|
||
/// и отброшены от медведя - передышка в толпе. Откат RearCooldown.
|
||
/// </summary>
|
||
public static class BearControl
|
||
{
|
||
public const string BearClass = "necroZombieBear";
|
||
public const float RoarRange = 15f;
|
||
public const float RoarCooldown = 8f;
|
||
public const int RoarTicks = 20 * 20;
|
||
public const string RoarSound = "bearalert";
|
||
/// <summary>Урон блоку за удар лапой. Для сравнения: удар ванильного зомби-медведя по блокам - десятки.</summary>
|
||
public const int SmashDamage = 300;
|
||
/// <summary>Досягаемость лапы от головы медведя, м.</summary>
|
||
public const float SmashReach = 2.6f;
|
||
public const float SkinFactor = 0.5f;
|
||
static float _nextRoar;
|
||
public const float RearRange = 3.5f;
|
||
public const float RearStun = 2f;
|
||
public const float RearCooldown = 6f;
|
||
/// <summary>Толчок от медведя, м за тик (как motion существ; гаснет сам).</summary>
|
||
public const float RearPush = 0.5f;
|
||
public const string RearSound = "bearattack";
|
||
static float _nextRear;
|
||
|
||
public static bool Is(EntityAlive _e)
|
||
{
|
||
return _e != null && EntityClass.list.TryGetValue(_e.entityClass, out EntityClass cls) && cls != null &&
|
||
cls.entityClassName == BearClass;
|
||
}
|
||
|
||
public static void Roar(World _world, EntityAlive _bear)
|
||
{
|
||
if (Time.time < _nextRoar)
|
||
{
|
||
// Рёв в толпе не отличить от удара (тест 09.10) - пишем, сколько ждать и сколько пошло на медведя.
|
||
Tip(string.Format(Localization.Get("necroBearRoarCooldown"), Mathf.CeilToInt(_nextRoar - Time.time)));
|
||
return;
|
||
}
|
||
_nextRoar = Time.time + RoarCooldown;
|
||
_bear.PlayOneShot(RoarSound);
|
||
_bear.emodel?.avatarController?.StartAnimationAttack();
|
||
int n = 0;
|
||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||
{
|
||
EntityAlive e = _world.EntityAlives[i];
|
||
if (e == null || e == _bear || e.IsDead() || !(e is EntityZombie) || e.aiManager == null ||
|
||
PetFollowPatch.FindOwnerOfPet(e.entityId) != null || (e.position - _bear.position).sqrMagnitude > RoarRange * RoarRange)
|
||
{
|
||
continue;
|
||
}
|
||
if (e.IsSleeping)
|
||
{
|
||
e.ConditionalTriggerSleeperWakeUp();
|
||
}
|
||
Possession.AllowZombieTarget(e, _bear);
|
||
e.SetAttackTarget(_bear, RoarTicks);
|
||
n++;
|
||
}
|
||
Debug.Log("[NecromancerTome] BearControl: рёв - на медведя идут " + n);
|
||
Tip(string.Format(Localization.Get("necroBearRoarResult"), n));
|
||
}
|
||
|
||
/// <summary>Сразу, мимо очереди подсказок: очередь игры (XUiC_PopupToolTip) отсеивает только одинаковый текст,
|
||
/// и «через 7 с», «через 6 с»... при частых ПКМ копились бы и всплывали с опозданием.</summary>
|
||
static void Tip(string _text)
|
||
{
|
||
if (Possession.Player != null)
|
||
{
|
||
GameManager.ShowTooltip(Possession.Player, _text, true);
|
||
}
|
||
}
|
||
|
||
/// <summary>E: на дыбы - враги рядом падают (тот же приём, что у броска волка: BeginStun Prone) и
|
||
/// отлетают от медведя.</summary>
|
||
public static void RearUp(World _world, EntityAlive _bear)
|
||
{
|
||
if (Time.time < _nextRear)
|
||
{
|
||
Tip(string.Format(Localization.Get("necroBearRearCooldown"), Mathf.CeilToInt(_nextRear - Time.time)));
|
||
return;
|
||
}
|
||
_nextRear = Time.time + RearCooldown;
|
||
_bear.PlayOneShot(RearSound);
|
||
_bear.emodel?.avatarController?.StartAnimationAttack();
|
||
int n = 0;
|
||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||
{
|
||
EntityAlive e = _world.EntityAlives[i];
|
||
if (!SnakeControl.IsFoe(e, _bear) || (e.position - _bear.position).sqrMagnitude > RearRange * RearRange)
|
||
{
|
||
continue;
|
||
}
|
||
Vector3 away = e.position - _bear.position;
|
||
away.y = 0f;
|
||
away = away.sqrMagnitude > 0.01f ? away.normalized : _bear.GetForwardVector();
|
||
e.motion += away * RearPush + Vector3.up * 0.15f;
|
||
if (e.bodyDamage.CurrentStun == EnumEntityStunType.None && e.emodel?.avatarController != null)
|
||
{
|
||
e.emodel.avatarController.BeginStun(EnumEntityStunType.Prone, EnumBodyPartHit.Torso, Utils.EnumHitDirection.Back, false, 0.5f);
|
||
e.SetStun(EnumEntityStunType.Prone);
|
||
e.bodyDamage.StunDuration = RearStun;
|
||
}
|
||
n++;
|
||
}
|
||
Tip(string.Format(Localization.Get("necroBearRearResult"), n));
|
||
Debug.Log("[NecromancerTome] BearControl: на дыбы - сбиты с ног " + n);
|
||
}
|
||
|
||
/// <summary>ЛКМ: блок под прицелом в досягаемости лапы (не существо) - урон SmashDamage.</summary>
|
||
public static void Smash(World _world, EntityPlayerLocal _player, EntityAlive _bear)
|
||
{
|
||
Camera cam = _player.playerCamera;
|
||
if (cam == null)
|
||
{
|
||
return;
|
||
}
|
||
Vector3 head = _bear.getHeadPosition();
|
||
Vector3 from = cam.transform.position + Origin.position;
|
||
Ray ray = new Ray(from, cam.transform.forward);
|
||
// С существами: если первым на луче стоит существо - это удар по нему, а не по блоку.
|
||
if (!Voxel.Raycast(_world, ray, Vector3.Distance(from, head) + SmashReach + 1f, -555528221, 69, 0f))
|
||
{
|
||
return;
|
||
}
|
||
WorldRayHitInfo hit = Voxel.voxelRayHitInfo;
|
||
if (hit.transform != null && GameUtils.GetHitRootTransform(hit.tag, hit.transform)?.GetComponent<Entity>() != null)
|
||
{
|
||
return;
|
||
}
|
||
if ((hit.hit.pos - head).sqrMagnitude > SmashReach * SmashReach)
|
||
{
|
||
return;
|
||
}
|
||
Vector3i pos = hit.hit.blockPos;
|
||
BlockValue bv = _world.GetBlock(pos);
|
||
if (bv.isair || bv.Block == null)
|
||
{
|
||
return;
|
||
}
|
||
bv.Block.DamageBlock(_world, new BlockValueRef(pos), bv, SmashDamage, _bear.entityId);
|
||
Debug.Log("[NecromancerTome] BearControl: лапой по " + bv.Block.GetBlockName() + " " + pos + " - " + SmashDamage +
|
||
" (прочность " + (bv.Block.MaxDamage - bv.damage) + " из " + bv.Block.MaxDamage + ")");
|
||
}
|
||
}
|
||
|
||
/// <summary>Толстая шкура одержимого медведя - вдвое меньше урона.</summary>
|
||
[HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.DamageEntity))]
|
||
public static class Patch_EntityAlive_DamageEntity_BearSkin
|
||
{
|
||
public static void Prefix(EntityAlive __instance, ref int _strength)
|
||
{
|
||
if (_strength > 1 && Possession.IsPossessed(__instance) && BearControl.Is(__instance))
|
||
{
|
||
_strength = Mathf.Max(1, Mathf.RoundToInt(_strength * BearControl.SkinFactor));
|
||
}
|
||
}
|
||
}
|
||
}
|