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

- Змея: проверка «внутри текстуры» лучами (земля игры - поверхность), каждый кадр выброс
  на ближайшее свободное место (на земле - из любых текстур, в полёте - из грунта);
  шаг и прыжок в текстуры не пускают. Полёт пока зажата ПКМ: 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
+1 -1
View File
@@ -190,7 +190,7 @@ necroSnakeModeSnake,items,Item,,,,Snake,,Schlange,Serpiente,Serpent,Serpente,蛇
necroSnakeBodyDanger,UI,Label,,,,Enemy approaching your body,,Ein Feind nähert sich deinem Körper,Un enemigo se acerca a tu cuerpo,Un ennemi approche de ton corps,Un nemico si avvicina al tuo corpo,敵が体に近づいている,적이 몸에 다가오고 있다,Wróg zbliża się do twojego ciała,Um inimigo se aproxima do seu corpo,Враг приближается к телу,Bir düşman bedenine yaklaşıyor,敌人正在接近你的身体,敵人正在接近你的身體 necroSnakeBodyDanger,UI,Label,,,,Enemy approaching your body,,Ein Feind nähert sich deinem Körper,Un enemigo se acerca a tu cuerpo,Un ennemi approche de ton corps,Un nemico si avvicina al tuo corpo,敵が体に近づいている,적이 몸에 다가오고 있다,Wróg zbliża się do twojego ciała,Um inimigo se aproxima do seu corpo,Враг приближается к телу,Bir düşman bedenine yaklaşıyor,敌人正在接近你的身体,敵人正在接近你的身體
necroSnakeModeDay,items,Item,,,,It is daytime,,Es ist Tag,Es de día,Il fait jour,È giorno,今は昼だ,지금은 낮이다,Jest dzień,Agora é dia,Сейчас день,Şu an gündüz,现在是白天,現在是白天 necroSnakeModeDay,items,Item,,,,It is daytime,,Es ist Tag,Es de día,Il fait jour,È giorno,今は昼だ,지금은 낮이다,Jest dzień,Agora é dia,Сейчас день,Şu an gündüz,现在是白天,現在是白天
necroHoldNone,items,Item,,,,Bare hand,,Bloße Hand,Mano desnuda,Main nue,Mano nuda,素手,맨손,Goła ręka,Mão nua,Обычная рука,Çıplak el,空手,空手 necroHoldNone,items,Item,,,,Bare hand,,Bloße Hand,Mano desnuda,Main nue,Mano nuda,素手,맨손,Goła ręka,Mão nua,Обычная рука,Çıplak el,空手,空手
necroHoldDeny,items,Item,,,,Denial,,Verleugnung,Negación,Déni,Negazione,拒絶,거부,Zaprzeczenie,Negação,Отрицание,İnkâr,否认,否認 necroHoldDeny,items,Item,,,,Horns,,Pommesgabel,Cuernos,Cornes,Corna,メロイックサイン,악마의 뿔,Rogi,Chifres,Коза,Boynuzlar,恶魔角,惡魔角
necroHoldPifPaf,items,Item,,,,Bang-bang,,Peng-peng,Pum-pum,Pan-pan,Bang-bang,バンバン,빵야빵야,Pif-paf,Pou-pou,Пиф-паф,Bang-bang,砰砰,砰砰 necroHoldPifPaf,items,Item,,,,Bang-bang,,Peng-peng,Pum-pum,Pan-pan,Bang-bang,バンバン,빵야빵야,Pif-paf,Pou-pou,Пиф-паф,Bang-bang,砰砰,砰砰
necroPossessHintBase,items,Item,,,,"LMB - strike, R - leave the body",,"LMT - Schlag, R - Körper verlassen","LMB: golpe, R: salir del cuerpo","Clic gauche : frapper, R : quitter le corps","LMB - colpo, R - lascia il corpo",左クリック: 攻撃、R: 体から出る,"좌클릭 - 공격, R - 몸에서 나가기","LPM - cios, R - opuść ciało","LMB - golpe, R - sair do corpo","ЛКМ - удар, R - выйти из тела","Sol tık - vuruş, R - bedenden çık",左键 - 攻击,R - 离开身体,左鍵 - 攻擊,R - 離開身體 necroPossessHintBase,items,Item,,,,"LMB - strike, R - leave the body",,"LMT - Schlag, R - Körper verlassen","LMB: golpe, R: salir del cuerpo","Clic gauche : frapper, R : quitter le corps","LMB - colpo, R - lascia il corpo",左クリック: 攻撃、R: 体から出る,"좌클릭 - 공격, R - 몸에서 나가기","LPM - cios, R - opuść ciało","LMB - golpe, R - sair do corpo","ЛКМ - удар, R - выйти из тела","Sol tık - vuruş, R - bedenden çık",左键 - 攻击,R - 离开身体,左鍵 - 攻擊,R - 離開身體
necroPossessHintRanged,items,Item,,,,RMB - ranged attack,,RMT - Fernangriff,RMB: ataque a distancia,Clic droit : attaque à distance,RMB - attacco a distanza,右クリック: 遠距離攻撃,우클릭 - 원거리 공격,PPM - atak dystansowy,RMB - ataque à distância,ПКМ - дальняя атака,Sağ tık - menzilli saldırı,右键 - 远程攻击,右鍵 - 遠程攻擊 necroPossessHintRanged,items,Item,,,,RMB - ranged attack,,RMT - Fernangriff,RMB: ataque a distancia,Clic droit : attaque à distance,RMB - attacco a distanza,右クリック: 遠距離攻撃,우클릭 - 원거리 공격,PPM - atak dystansowy,RMB - ataque à distância,ПКМ - дальняя атака,Sağ tık - menzilli saldırı,右键 - 远程攻击,右鍵 - 遠程攻擊
1 Key File Type UsedInMainMenu NoTranslate KeepLoaded english Context / Alternate Text german spanish french italian japanese koreana polish brazilian russian turkish schinese tchinese
190 necroSnakeBodyDanger UI Label Enemy approaching your body Ein Feind nähert sich deinem Körper Un enemigo se acerca a tu cuerpo Un ennemi approche de ton corps Un nemico si avvicina al tuo corpo 敵が体に近づいている 적이 몸에 다가오고 있다 Wróg zbliża się do twojego ciała Um inimigo se aproxima do seu corpo Враг приближается к телу Bir düşman bedenine yaklaşıyor 敌人正在接近你的身体 敵人正在接近你的身體
191 necroSnakeModeDay items Item It is daytime Es ist Tag Es de día Il fait jour È giorno 今は昼だ 지금은 낮이다 Jest dzień Agora é dia Сейчас день Şu an gündüz 现在是白天 現在是白天
192 necroHoldNone items Item Bare hand Bloße Hand Mano desnuda Main nue Mano nuda 素手 맨손 Goła ręka Mão nua Обычная рука Çıplak el 空手 空手
193 necroHoldDeny items Item Denial Horns Verleugnung Pommesgabel Negación Cuernos Déni Cornes Negazione Corna 拒絶 メロイックサイン 거부 악마의 뿔 Zaprzeczenie Rogi Negação Chifres Отрицание Коза İnkâr Boynuzlar 否认 恶魔角 否認 惡魔角
194 necroHoldPifPaf items Item Bang-bang Peng-peng Pum-pum Pan-pan Bang-bang バンバン 빵야빵야 Pif-paf Pou-pou Пиф-паф Bang-bang 砰砰 砰砰
195 necroPossessHintBase items Item LMB - strike, R - leave the body LMT - Schlag, R - Körper verlassen LMB: golpe, R: salir del cuerpo Clic gauche : frapper, R : quitter le corps LMB - colpo, R - lascia il corpo 左クリック: 攻撃、R: 体から出る 좌클릭 - 공격, R - 몸에서 나가기 LPM - cios, R - opuść ciało LMB - golpe, R - sair do corpo ЛКМ - удар, R - выйти из тела Sol tık - vuruş, R - bedenden çık 左键 - 攻击,R - 离开身体 左鍵 - 攻擊,R - 離開身體
196 necroPossessHintRanged items Item RMB - ranged attack RMT - Fernangriff RMB: ataque a distancia Clic droit : attaque à distance RMB - attacco a distanza 右クリック: 遠距離攻撃 우클릭 - 원거리 공격 PPM - atak dystansowy RMB - ataque à distância ПКМ - дальняя атака Sağ tık - menzilli saldırı 右键 - 远程攻击 右鍵 - 遠程攻擊
+38 -11
View File
@@ -36,7 +36,10 @@ namespace NecromancerTome
/// <summary>Скорость бегства - выше бега зомби-людей (1.5), как у медведя (указание: «дать ему ускорение»).</summary> /// <summary>Скорость бегства - выше бега зомби-людей (1.5), как у медведя (указание: «дать ему ускорение»).</summary>
public const float DenySpeed = 2.2f; public const float DenySpeed = 2.2f;
public const float Cooldown = 0.6f; public const float Cooldown = 0.6f;
public const string DenySound = "twitch_celebrate"; /// <summary>Звук «козы» (05.10: «нужен короткий звук» - вместо twitch_celebrate): бросок змеи - тот же,
/// что у рывка духа змеи (SnakeControl.AttackSound): «раз зомби боятся духа змеи, некромант может
/// пугать им и в телесной форме».</summary>
public const string DenySound = SnakeControl.AttackSound;
/// <summary>«Пиф-паф» (указание 03.10): не вселился - звон разбитого стекла, вселился - щелчок /// <summary>«Пиф-паф» (указание 03.10): не вселился - звон разбитого стекла, вселился - щелчок
/// удавшейся отмычки (sounds.xml: glassdestroy, lockpick_success).</summary> /// удавшейся отмычки (sounds.xml: glassdestroy, lockpick_success).</summary>
public const string PifPafFailSound = "glassdestroy"; public const string PifPafFailSound = "glassdestroy";
@@ -47,6 +50,10 @@ namespace NecromancerTome
static int _applied = -1; static int _applied = -1;
static int _playerId = -1; static int _playerId = -1;
static float _nextCast; static float _nextCast;
/// <summary>Вселение - на пике толчка руки (05.10): иначе вид сразу уходит к зомби и толчка не видно.
/// Цель берётся в момент щелчка.</summary>
static EntityAlive _pifPafVictim;
static float _pifPafAt = -1f;
public static bool IsHand(ItemClass _item) => _item != null && _item.Name == HandItem; public static bool IsHand(ItemClass _item) => _item != null && _item.Name == HandItem;
@@ -74,9 +81,15 @@ namespace NecromancerTome
_applied = None; _applied = None;
_target = null; _target = null;
_targetFrame = -1; _targetFrame = -1;
_pifPafVictim = null;
_pifPafAt = -1f;
HandPose.ResetAll(); HandPose.ResetAll();
return; return;
} }
if (_pifPafAt >= 0f && Time.time >= _pifPafAt)
{
PifPafShot(player);
}
int want = Current(player); int want = Current(player);
if (want == _applied) if (want == _applied)
{ {
@@ -97,6 +110,26 @@ namespace NecromancerTome
} }
} }
static void PifPafShot(EntityPlayerLocal _player)
{
EntityAlive victim = _pifPafVictim;
_pifPafVictim = null;
_pifPafAt = -1f;
if (Current(_player) != PifPaf)
{
return; // за полтика сменил хват или руку - выстрела нет
}
if (victim != null && !victim.IsDead() && Possession.CanPossess(victim) && Possession.Enter(_player, victim))
{
_player.PlayOneShot(PifPafPossessSound);
}
else
{
_player.PlayOneShot(PifPafFailSound);
Debug.Log("[NecromancerTome] HandHolds: пиф-паф - " + (victim == null ? "в прицеле нет зомби" : victim.EntityName + " не гуманоид"));
}
}
public static void OnRadialCommand(XUiC_Radial _sender, int _commandIndex, XUiC_Radial.RadialContextAbs _context) public static void OnRadialCommand(XUiC_Radial _sender, int _commandIndex, XUiC_Radial.RadialContextAbs _context)
{ {
if (_commandIndex >= None && _commandIndex <= PifPaf) if (_commandIndex >= None && _commandIndex <= PifPaf)
@@ -116,18 +149,12 @@ namespace NecromancerTome
if (_hold == PifPaf) if (_hold == PifPaf)
{ {
// Одержимость (решение 03.10): «пиф-паф» по зомби-гуманоиду - управление им. // Одержимость (решение 03.10): «пиф-паф» по зомби-гуманоиду - управление им.
EntityAlive victim = Target(_player); HandPose.Jab();
if (Possession.CanPossess(victim) && Possession.Enter(_player, victim)) _pifPafVictim = Target(_player);
{ _pifPafAt = Time.time + HandPose.JabTime * 0.5f;
_player.PlayOneShot(PifPafPossessSound);
}
else
{
_player.PlayOneShot(PifPafFailSound);
Debug.Log("[NecromancerTome] HandHolds: пиф-паф - " + (victim == null ? "в прицеле нет зомби" : victim.EntityName + " не гуманоид"));
}
return; return;
} }
HandPose.Jab(); // толчок к прицелу, как у «пиф-пафа» (05.10)
_player.PlayOneShot(DenySound); _player.PlayOneShot(DenySound);
World world = _player.world; World world = _player.world;
if (world.IsRemote()) if (world.IsRemote())
+53 -6
View File
@@ -88,6 +88,7 @@ namespace NecromancerTome
Enabled = false; Enabled = false;
Overrides.Clear(); Overrides.Clear();
CamOverrides.Clear(); CamOverrides.Clear();
_jabStart = -1f;
Unfreeze(); Unfreeze();
_bones.Clear(); _bones.Clear();
_rig = null; _rig = null;
@@ -159,7 +160,8 @@ namespace NecromancerTome
b.localRotation = Quaternion.Euler(kv.Value); b.localRotation = Quaternion.Euler(kv.Value);
} }
} }
Transform cam = CamOverrides.Count > 0 ? Camera() : null; Transform cam = CamOverrides.Count > 0 || _jabStart >= 0f ? Camera() : null;
ApplyJab(cam);
if (cam != null) if (cam != null)
{ {
foreach (var kv in CamOverrides) foreach (var kv in CamOverrides)
@@ -173,6 +175,43 @@ namespace NecromancerTome
} }
} }
/// <summary>Толчок руки к прицелу (указание 05.10, «пиф-паф» на ЛКМ: «чуть сдвигает руку в сторону
/// прицела и возвращает»). Плечо доворачивается так, чтобы кисть прошла JabAmount пути до линии
/// взгляда и вернулась - по синусу за JabTime. Ставится до CamOverrides: кисть сохраняет поворот
/// относительно камеры, сдвигается только её место.</summary>
public const float JabTime = 0.25f;
public const float JabAmount = 0.35f;
static float _jabStart = -1f;
public static void Jab()
{
_jabStart = Time.time;
}
static void ApplyJab(Transform _cam)
{
if (_jabStart < 0f)
{
return;
}
float t = (Time.time - _jabStart) / JabTime;
if (t >= 1f)
{
_jabStart = -1f;
return;
}
Transform arm = Bone("RightArm");
Transform hand = Bone("RightHand");
if (_cam == null || arm == null || hand == null)
{
return;
}
Vector3 toHand = hand.position - arm.position;
Vector3 aim = _cam.position + _cam.forward * Vector3.Dot(hand.position - _cam.position, _cam.forward);
Quaternion full = Quaternion.FromToRotation(toHand, aim - arm.position);
arm.rotation = Quaternion.Slerp(Quaternion.identity, full, Mathf.Sin(t * Mathf.PI) * JabAmount) * arm.rotation;
}
/// <summary>Кисть в осях камеры. Оси кисти правой руки (лог necrobones 03.10): пальцы - локальная +X /// <summary>Кисть в осях камеры. Оси кисти правой руки (лог necrobones 03.10): пальцы - локальная +X
/// (суставы пальцев лежат по +X), ладонь - +Y (сгиб пальцев +Z ведёт +X к +Y), +Z - к мизинцу, /// (суставы пальцев лежат по +X), ладонь - +Y (сгиб пальцев +Z ведёт +X к +Y), +Z - к мизинцу,
/// большой палец - к -Z. Нулевая поза: пальцы вверх, ладонь от игрока, тыльная сторона к камере, /// большой палец - к -Z. Нулевая поза: пальцы вверх, ладонь от игрока, тыльная сторона к камере,
@@ -221,8 +260,9 @@ namespace NecromancerTome
Overrides["LeftArm"] = new Vector3(30f, 13.6f, 57.3f); Overrides["LeftArm"] = new Vector3(30f, 13.6f, 57.3f);
} }
/// <summary>Хват «отрицание» (утверждён пользователем 03.10): рука вперёд, как у браслета, ниже прицела, /// <summary>Хват «коза» (до 05.10 - «отрицание»; утверждён пользователем 03.10): рука вперёд, как у браслета, ниже прицела,
/// кисть в линию с предплечьем, пальцы раскрыты.</summary> /// кисть в линию с предплечьем, пальцы раскрыты. 05.10: средний и безымянный поджаты, как в кулаке
/// (сгиб - как у поджатых пальцев «пиф-пафа»), указательный и мизинец прямые.</summary>
public static void PresetDeny() public static void PresetDeny()
{ {
Overrides.Clear(); Overrides.Clear();
@@ -232,9 +272,16 @@ namespace NecromancerTome
Overrides["RightForeArm"] = new Vector3(0f, 88.4f, 0f); Overrides["RightForeArm"] = new Vector3(0f, 88.4f, 0f);
Overrides["RightHand"] = new Vector3(45f, 0f, 0f); Overrides["RightHand"] = new Vector3(45f, 0f, 0f);
SetFingers("Right", 3f, 12f); SetFingers("Right", 3f, 12f);
Overrides["RightHandThumb1"] = new Vector3(55f, 68.6f, 40.6f); for (int j = 1; j <= 3; j++)
Overrides["RightHandThumb2"] = new Vector3(0f, 0f, 0f); {
Overrides["RightHandThumb3"] = new Vector3(0f, 0f, 0f); float curl = j == 3 ? 55f : 80f;
Overrides["RightHandMiddle" + j] = new Vector3(0f, 0f, curl);
Overrides["RightHandRing" + j] = new Vector3(0f, 0f, curl);
}
// Большой палец прижимает поджатые (05.10: «полноценная коза»). Подбор на глаз - уточнять necropose.
Overrides["RightHandThumb1"] = new Vector3(55f, 68.6f, 75f);
Overrides["RightHandThumb2"] = new Vector3(0f, 0f, 40f);
Overrides["RightHandThumb3"] = new Vector3(0f, 0f, 30f);
LeftArmDown(); LeftArmDown();
Unfreeze(); Unfreeze();
Enabled = true; Enabled = true;
+134 -4
View File
@@ -52,6 +52,26 @@ namespace NecromancerTome
public const float TauntRange = 30f; public const float TauntRange = 30f;
public const float TauntPeriod = 1f; public const float TauntPeriod = 1f;
public const int TauntTicks = 1200; 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: «сильно ограничить /// <summary>Сколько чанков держать вокруг тела, пока игрок в зомби (указание 03.10: «сильно ограничить
/// видимость у тела - ему вообще нет смысла что-либо видеть»). Тест 03.10: две полные области обзора /// видимость у тела - ему вообще нет смысла что-либо видеть»). Тест 03.10: две полные области обзора
/// (тело + одержимый) - память росла с каждым шагом зомби, а выход давал подвисание ~50 с. Трёх чанков /// (тело + одержимый) - память росла с каждым шагом зомби, а выход давал подвисание ~50 с. Трёх чанков
@@ -73,6 +93,7 @@ namespace NecromancerTome
public const float TurnSpeed = 540f; public const float TurnSpeed = 540f;
static float _nextAttack; static float _nextAttack;
static float _nextTaunt; static float _nextTaunt;
static float _sleeperCheck;
static ChunkManager.ChunkObserver _observer; static ChunkManager.ChunkObserver _observer;
static readonly List<Animator> _frozen = new List<Animator>(); static readonly List<Animator> _frozen = new List<Animator>();
static readonly List<float> _frozenSpeed = new List<float>(); static readonly List<float> _frozenSpeed = new List<float>();
@@ -155,7 +176,11 @@ namespace NecromancerTome
return; return;
} }
Active = false; Active = false;
Crouching = false;
_meleeLookUntil = 0f;
_crouchToggled = false;
EntityAlive z = Zombie; EntityAlive z = Zombie;
z?.SetLookPosition(Vector3.zero);
EntityPlayerLocal player = _player; EntityPlayerLocal player = _player;
if (_rState != RState.Idle && z != null && !z.IsDead()) if (_rState != RState.Idle && z != null && !z.IsDead())
{ {
@@ -287,6 +312,11 @@ namespace NecromancerTome
// кнопка геймпада (тест 03.10: «подрыва нету»). // кнопка геймпада (тест 03.10: «подрыва нету»).
detonate = input.Activate.WasPressed || input.PermanentActions.Activate.WasPressed; 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) if (detonate && z is EntityZombieCop cop && !cop.isPrimed)
{ {
Debug.Log("[NecromancerTome] Possession: подрыв " + z.EntityName); Debug.Log("[NecromancerTome] Possession: подрыв " + z.EntityName);
@@ -316,12 +346,32 @@ namespace NecromancerTome
{ {
Scream(world, z); Scream(world, z);
} }
if (attack && Time.time >= _nextAttack && z.Attack(false)) if (attack && Time.time >= _nextAttack)
{
z.SetLookPosition(MeleeAim(world, z));
if (z.Attack(false))
{ {
z.Attack(true); z.Attack(true);
_nextAttack = Time.time + Mathf.Max(0.3f, z.GetAttackTimeoutTicks() / 20f); 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); _observer?.SetPosition(z.position);
// Спящие в зданиях просыпаются от одержимого, как от змеи (указание 05.10).
SnakeControl.Sleepers(world, _player, z.position, ref _sleeperCheck, Time.deltaTime);
Taunt(world, z); Taunt(world, z);
} }
@@ -371,6 +421,37 @@ namespace NecromancerTome
GameManager.ShowTooltip(_p, text); 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> /// <summary>Точка под прицелом камеры (в мировых координатах), до 50 м.</summary>
static Vector3 AimPoint() static Vector3 AimPoint()
{ {
@@ -566,8 +647,26 @@ namespace NecromancerTome
{ {
EntityAlive e = _world.EntityAlives[i]; EntityAlive e = _world.EntityAlives[i];
if (e == null || e == _z || e.IsDead() || !(e is EntityZombie) || e.GetAttackTarget() == _z || if (e == null || e == _z || e.IsDead() || !(e is EntityZombie) || e.GetAttackTarget() == _z ||
e.aiManager == null || PetFollowPatch.FindOwnerOfPet(e.entityId) != null || e.aiManager == null || PetFollowPatch.FindOwnerOfPet(e.entityId) != null)
(e.position - _z.position).sqrMagnitude > TauntRange * TauntRange || !e.CanSee(_z)) {
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; 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); 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;
}
}
} }
+200 -35
View File
@@ -56,6 +56,18 @@ namespace NecromancerTome
/// <summary>Радиус проверочных сфер при шаге. Меньше полублока - в дверной проём и щель в /// <summary>Радиус проверочных сфер при шаге. Меньше полублока - в дверной проём и щель в
/// один блок змея пролезает.</summary> /// один блок змея пролезает.</summary>
public const float BodyRadius = 0.25f; public const float BodyRadius = 0.25f;
/// <summary>Точка тела для проверки «внутри текстуры»: над корнем змеи, ниже головы.</summary>
public const float BodyUp = 0.35f;
/// <summary>Как далеко вверх искать поверхность, под которой змея может быть зарыта.</summary>
public const float InsideProbe = 32f;
/// <summary>Полёт (указание 05.10): пока зажата правая кнопка. Скорость - вдвое ниже шага
/// зомби (MindControl.WalkSpeed), и в воздухе, и сквозь постройки; Shift не ускоряет.
/// Сквозь грунт (земля, песок, камень) - никак.</summary>
static bool _flying;
public const float FlySpeed = MindControl.WalkSpeed * 0.5f;
/// <summary>Не нашлось, куда выбросить змею из текстур, - пауза, а не поиск каждый кадр.</summary>
static float _ejectWait;
static Vector3[] _ejectOffsets;
/// <summary>Параметр Forward аниматора змеи при ползании и на Shift. Подобраны на глаз: /// <summary>Параметр Forward аниматора змеи при ползании и на Shift. Подобраны на глаз:
/// у животного с RootMotion полный ход даёт landMovementFactor * 2.5 = 0.25.</summary> /// у животного с RootMotion полный ход даёт landMovementFactor * 2.5 = 0.25.</summary>
@@ -125,6 +137,11 @@ namespace NecromancerTome
/// место под сброс припасов (AIAirDrop) - только здесь с видимой сеткой и дальностью /// место под сброс припасов (AIAirDrop) - только здесь с видимой сеткой и дальностью
/// обзора из настроек.</summary> /// обзора из настроек.</summary>
static ChunkManager.ChunkObserver _observer; static ChunkManager.ChunkObserver _observer;
/// <summary>Область тела сжата до Possession.BodyViewDim, как у одержимого (указание 05.10:
/// «делай как у одержимого»): две полные области - лишняя память. Полный радиус - для систем,
/// рисующих мир у змеи (SnakeAudioSwap). -1 - не сжата.</summary>
static int _bodyViewDim = -1;
public static int FullViewDim => _bodyViewDim;
public static EntityAlive Target; public static EntityAlive Target;
static Vector3 _pos; static Vector3 _pos;
static float _vy; static float _vy;
@@ -154,6 +171,8 @@ namespace NecromancerTome
_dashTarget = null; _dashTarget = null;
_freeze = 0f; _freeze = 0f;
_kick = Vector3.zero; _kick = Vector3.zero;
_flying = false;
_ejectWait = 0f;
_killId = -1; _killId = -1;
_burst = null; _burst = null;
_sleeperCheck = 0f; _sleeperCheck = 0f;
@@ -169,6 +188,12 @@ namespace NecromancerTome
BodyDanger = false; BodyDanger = false;
_bodyCheck = 0f; _bodyCheck = 0f;
_observer = GameManager.Instance.AddChunkObserver(_pos, true, GameUtils.GetViewDistance(), -1); _observer = GameManager.Instance.AddChunkObserver(_pos, true, GameUtils.GetViewDistance(), -1);
ChunkManager.ChunkObserver bodyObserver = _player.ChunkObserver;
if (bodyObserver != null && bodyObserver.viewDim > Possession.BodyViewDim)
{
_bodyViewDim = bodyObserver.viewDim;
bodyObserver.viewDim = Possession.BodyViewDim;
}
Controlling = true; Controlling = true;
// Пояс прячется на весь режим (указание 28.09); обратно его откроет сама игра в OnHUD. // Пояс прячется на весь режим (указание 28.09); обратно его откроет сама игра в OnHUD.
_player.PlayerUI?.windowManager.Close(ToolbeltWindow); _player.PlayerUI?.windowManager.Close(ToolbeltWindow);
@@ -231,7 +256,13 @@ namespace NecromancerTome
_fearQueue.Clear(); _fearQueue.Clear();
_fearQueued.Clear(); _fearQueued.Clear();
Controlling = false; Controlling = false;
SnakeVision.Off(GameManager.Instance?.World?.GetPrimaryPlayer()); EntityPlayerLocal player = GameManager.Instance?.World?.GetPrimaryPlayer();
SnakeVision.Off(player);
if (_bodyViewDim > 0 && player?.ChunkObserver != null)
{
player.ChunkObserver.viewDim = _bodyViewDim;
}
_bodyViewDim = -1;
_dashTarget = null; _dashTarget = null;
Target = null; Target = null;
BodyDanger = false; BodyDanger = false;
@@ -258,6 +289,8 @@ namespace NecromancerTome
Vector2 move = Vector2.zero; Vector2 move = Vector2.zero;
bool run = false; bool run = false;
bool jump = false; bool jump = false;
bool jumpHeld = false;
bool downHeld = false;
bool attack = false; bool attack = false;
World world = GameManager.Instance.World; World world = GameManager.Instance.World;
if (_freeze > 0f) if (_freeze > 0f)
@@ -274,13 +307,25 @@ namespace NecromancerTome
move = new Vector2(input.Move.X, input.Move.Y); move = new Vector2(input.Move.X, input.Move.Y);
run = input.Run.IsPressed; run = input.Run.IsPressed;
jump = input.Jump.WasPressed; jump = input.Jump.WasPressed;
// Указание 28.09: атака на левую кнопку, правая - выбраться из застревания. jumpHeld = input.Jump.IsPressed;
downHeld = input.Crouch.IsPressed || input.ToggleCrouch.IsPressed;
// Указание 28.09: атака на левую кнопку. 05.10: E - аварийный выход из текстур
// (E в игре на PermanentActions.Activate, см. MindControl).
attack = input.Primary.WasPressed; attack = input.Primary.WasPressed;
if (input.Secondary.WasPressed) if ((input.Activate.WasPressed || input.PermanentActions.Activate.WasPressed) && _dashTarget == null)
{ {
Unstick(world); Eject(world, false, 0.5f);
} }
} }
// Полёт - пока зажата правая кнопка (указание 05.10); отпустил - змея падает.
bool fly = !gui && input != null && input.Secondary.IsPressed && _dashTarget == null;
if (fly != _flying)
{
_flying = fly;
_vy = 0f;
_airborne = !fly;
Debug.Log("[NecromancerTome] SnakeControl: полёт " + (fly ? "вкл" : "выкл"));
}
if (_dashTarget == null && attack && Target != null) if (_dashTarget == null && attack && Target != null)
{ {
@@ -294,6 +339,10 @@ namespace NecromancerTome
if (_dashTarget != null) if (_dashTarget != null)
{ {
Dash(_player); Dash(_player);
if (_dashTarget == null)
{
KeepOut(world, dt);
}
_observer?.SetPosition(_pos); _observer?.SetPosition(_pos);
_snake.SetPosition(_pos); _snake.SetPosition(_pos);
_snake.SetRotation(new Vector3(0f, _snakeYaw, 0f)); _snake.SetRotation(new Vector3(0f, _snakeYaw, 0f));
@@ -305,6 +354,12 @@ namespace NecromancerTome
} }
Quaternion yawRot = Quaternion.Euler(0f, _yaw, 0f); Quaternion yawRot = Quaternion.Euler(0f, _yaw, 0f);
Vector3 dir = yawRot * Vector3.forward * move.y + yawRot * Vector3.right * move.x; Vector3 dir = yawRot * Vector3.forward * move.y + yawRot * Vector3.right * move.x;
if (_flying)
{
// В полёте - куда смотрит камера, Пробел вверх, приседание вниз.
dir = Quaternion.Euler(-_pitch, _yaw, 0f) * Vector3.forward * move.y + yawRot * Vector3.right * move.x +
Vector3.up * ((jumpHeld ? 1f : 0f) - (downHeld ? 1f : 0f));
}
if (dir.sqrMagnitude > 1f) if (dir.sqrMagnitude > 1f)
{ {
dir.Normalize(); dir.Normalize();
@@ -312,13 +367,15 @@ namespace NecromancerTome
bool moving = dir.sqrMagnitude > 0.01f; bool moving = dir.sqrMagnitude > 0.01f;
if (moving) if (moving)
{ {
Vector3 delta = dir * (run ? RunSpeed : WalkSpeed) * dt; float speed = _flying ? FlySpeed : run ? RunSpeed : WalkSpeed;
Vector3 delta = dir * speed * dt;
int steps = Mathf.Max(1, Mathf.CeilToInt(delta.magnitude / MaxStep)); int steps = Mathf.Max(1, Mathf.CeilToInt(delta.magnitude / MaxStep));
Vector3 d = delta / steps; Vector3 d = delta / steps;
for (int i = 0; i < steps; i++) for (int i = 0; i < steps; i++)
{ {
// Упёрлись - пробуем скользить вдоль стены по одной из осей. // Упёрлись - пробуем скользить вдоль стены по одной из осей.
if (!TryStep(world, d) && !TryStep(world, new Vector3(d.x, 0f, 0f)) && !TryStep(world, new Vector3(0f, 0f, d.z))) if (!TryStep(world, d) && !TryStep(world, new Vector3(d.x, 0f, 0f)) && !TryStep(world, new Vector3(0f, 0f, d.z)) &&
!(_flying && TryStep(world, new Vector3(0f, d.y, 0f))))
{ {
break; break;
} }
@@ -340,7 +397,11 @@ namespace NecromancerTome
} }
// Вид от первого лица: змея смотрит туда же, куда игрок. // Вид от первого лица: змея смотрит туда же, куда игрок.
_snakeYaw = Mathf.LerpAngle(_snakeYaw, _yaw, dt * 12f); _snakeYaw = Mathf.LerpAngle(_snakeYaw, _yaw, dt * 12f);
Fall(dt, jump); if (!_flying)
{
Fall(world, dt, jump);
}
KeepOut(world, dt);
_observer?.SetPosition(_pos); _observer?.SetPosition(_pos);
_snake.SetPosition(_pos); _snake.SetPosition(_pos);
@@ -350,7 +411,7 @@ namespace NecromancerTome
_snake.moveDirection = Vector3.zero; _snake.moveDirection = Vector3.zero;
AnimForward = moving ? (run ? AnimRun : AnimWalk) : 0f; AnimForward = moving ? (run ? AnimRun : AnimWalk) : 0f;
CheckBody(world, _player, _snake, dt); CheckBody(world, _player, _snake, dt);
Sleepers(world, _player, dt); Sleepers(world, _player, _pos, ref _sleeperCheck, dt);
Sense(world, _snake, dt); Sense(world, _snake, dt);
FearQueue(world, _snake, dt); FearQueue(world, _snake, dt);
return Radiation(world, dt); return Radiation(world, dt);
@@ -485,17 +546,18 @@ namespace NecromancerTome
/// <summary>Комнаты со спящими в зданиях просыпаются, когда в них входит ИГРОК: /// <summary>Комнаты со спящими в зданиях просыпаются, когда в них входит ИГРОК:
/// EntityPlayer.CheckSleeperTriggers каждый тик зовёт World.CheckSleeperVolumeTouching и /// EntityPlayer.CheckSleeperTriggers каждый тик зовёт World.CheckSleeperVolumeTouching и
/// CheckTriggerVolumeTrigger, а те берут player.position (тест 28.09: «зомби в локациях не /// CheckTriggerVolumeTrigger, а те берут player.position (тест 28.09: «зомби в локациях не
/// спавнятся, если змея туда заходит»). Раз в 0,25 с зовём их же, подставив позицию змеи.</summary> /// спавнятся, если змея туда заходит»). Раз в 0,25 с зовём их же, подставив позицию змеи
static void Sleepers(World _world, EntityPlayerLocal _player, float _dt) /// (и одержимого - Possession, 05.10).</summary>
public static void Sleepers(World _world, EntityPlayerLocal _player, Vector3 _at, ref float _timer, float _dt)
{ {
_sleeperCheck -= _dt; _timer -= _dt;
if (_sleeperCheck > 0f || _world.IsRemote()) if (_timer > 0f || _world.IsRemote())
{ {
return; return;
} }
_sleeperCheck = 0.25f; _timer = 0.25f;
Vector3 body = _player.position; Vector3 body = _player.position;
_player.position = _pos; _player.position = _at;
try try
{ {
_world.CheckSleeperVolumeTouching(_player); _world.CheckSleeperVolumeTouching(_player);
@@ -667,6 +729,16 @@ namespace NecromancerTome
{ {
return false; return false;
} }
if (_flying)
{
// Сквозь постройки можно, в грунт - нет.
if (Inside(_world, c + Vector3.up * BodyUp, TerrainMask, out bool ground) && ground)
{
return false;
}
_pos = c;
return true;
}
// Один блок - ступенька, два - стена. Проверяем настоящую геометрию, а не тип блока: // Один блок - ступенька, два - стена. Проверяем настоящую геометрию, а не тип блока:
// у блока есть только флаг «твёрдый», и открытая дверь для него такая же стена, как // у блока есть только флаг «твёрдый», и открытая дверь для него такая же стена, как
// закрытая (тест 28.09: «змея не может пройти в открытые двери»). // закрытая (тест 28.09: «змея не может пройти в открытые двери»).
@@ -685,11 +757,16 @@ namespace NecromancerTome
_vy = 0f; _vy = 0f;
} }
} }
// Ни в грунт, ни в блоки: обрыв выше подъёма или стена, которую шар изнутри не чует.
if (Inside(_world, c + Vector3.up * BodyUp, WallMask, out _))
{
return false;
}
_pos = c; _pos = c;
return true; return true;
} }
static void Fall(float _dt, bool _jump) static void Fall(World _world, float _dt, bool _jump)
{ {
if (!Ground(_pos + Vector3.up * 0.3f, 300f, out float ground)) if (!Ground(_pos + Vector3.up * 0.3f, 300f, out float ground))
{ {
@@ -717,6 +794,12 @@ namespace NecromancerTome
_vy = 0f; _vy = 0f;
dy = 0f; dy = 0f;
} }
// Прыжок не должен заносить тело в текстуры (указание 05.10).
if (dy > 0f && Inside(_world, _pos + Vector3.up * (dy + BodyUp), WallMask, out _))
{
_vy = 0f;
dy = 0f;
}
_pos.y += dy; _pos.y += dy;
if (_pos.y <= ground) if (_pos.y <= ground)
{ {
@@ -731,38 +814,115 @@ namespace NecromancerTome
return Physics.CheckSphere(_p - Origin.position, BodyRadius, WallMask, QueryTriggerInteraction.Ignore); return Physics.CheckSphere(_p - Origin.position, BodyRadius, WallMask, QueryTriggerInteraction.Ignore);
} }
/// <summary>Правая кнопка: выбраться из текстур. Ближайшее свободное место - сначала выше /// <summary>Каждый кадр: змея в грунте (в полёте) или в любой текстуре (на земле) -
/// на месте, потом кольцами до 3 м и до 4 м вверх; там, где тело змеи ни во что не упирается. /// выбрасываем на ближайшее свободное место (указание 05.10).</summary>
/// Дальше змея сама падает на землю.</summary> static void KeepOut(World _world, float _dt)
static void Unstick(World _world)
{ {
if (_dashTarget != null) if (_ejectWait > 0f)
{
_ejectWait -= _dt;
return;
}
Vector3 body = _pos + Vector3.up * BodyUp;
if (!_world.IsChunkAreaCollidersLoaded(body))
{ {
return; return;
} }
for (float r = 0f; r <= 3f; r += 1f) bool ground;
bool stuck = _flying ? Inside(_world, body, TerrainMask, out ground) && ground : Inside(_world, body, WallMask, out ground);
if (stuck && !Eject(_world, _flying, 0f))
{
_ejectWait = 1f;
}
}
/// <summary>Ближайшее свободное место: до 4 м вбок, до 1 м вниз и до 6 м вверх; вниз
/// дороже, чем вверх. _groundOnly - мешает только грунт (полёт). E зовёт с _minShift 0.5:
/// проверка может счесть место свободным, а змея не идёт (тест 28.09: «сдвиг 0.0 м»).</summary>
static bool Eject(World _world, bool _groundOnly, float _minShift)
{
if (_ejectOffsets == null)
{
System.Collections.Generic.List<Vector3> list = new System.Collections.Generic.List<Vector3>();
for (float r = 0f; r <= 4f; r += 0.5f)
{ {
int dirs = r == 0f ? 1 : 8; int dirs = r == 0f ? 1 : 8;
// На месте - минимум на полметра вверх: проверка может счесть место свободным, а for (float h = -1f; h <= 6f; h += 0.5f)
// змея всё равно не идёт (тест 28.09: дважды «выбралась, сдвиг 0.0 м»).
for (float h = r == 0f ? 0.5f : 0f; h <= 4f; h += 0.5f)
{ {
for (int i = 0; i < dirs; i++) for (int i = 0; i < dirs; i++)
{ {
Vector3 p = _pos + Quaternion.Euler(0f, _yaw + i * 45f, 0f) * Vector3.forward * r + Vector3.up * h; list.Add(Quaternion.Euler(0f, i * 45f, 0f) * Vector3.forward * r + Vector3.up * h);
if (!_world.IsChunkAreaCollidersLoaded(p) || Blocked(p + Vector3.up * 0.4f) || Blocked(p + Vector3.up * 1f)) }
}
}
list.Sort((x, y) => EjectCost(x).CompareTo(EjectCost(y)));
_ejectOffsets = list.ToArray();
}
for (int i = 0; i < _ejectOffsets.Length; i++)
{
Vector3 o = _ejectOffsets[i];
if (o.magnitude < _minShift || !Free(_world, _pos + o, _groundOnly))
{ {
continue; continue;
} }
Debug.Log("[NecromancerTome] SnakeControl: выбралась, сдвиг " + (p - _pos).magnitude.ToString("F1") + " м"); Debug.Log("[NecromancerTome] SnakeControl: выброшена из текстур" + (_groundOnly ? " (грунт)" : "") + ", сдвиг " + o.magnitude.ToString("F1") + " м");
_pos = p; _pos += o;
_vy = 0f; _vy = 0f;
_airborne = true; _airborne = !_flying;
return; return true;
}
}
} }
Debug.Log("[NecromancerTome] SnakeControl: выбраться некуда"); Debug.Log("[NecromancerTome] SnakeControl: выбраться некуда");
return false;
}
static float EjectCost(Vector3 _o)
{
return _o.magnitude + (_o.y < 0f ? -_o.y : 0f);
}
static bool Free(World _world, Vector3 _p, bool _groundOnly)
{
if (!_world.IsChunkAreaCollidersLoaded(_p))
{
return false;
}
bool ground;
if (_groundOnly)
{
return !(Inside(_world, _p + Vector3.up * BodyUp, TerrainMask, out ground) && ground);
}
return !Inside(_world, _p + Vector3.up * BodyUp, WallMask, out ground) &&
!Blocked(_p + Vector3.up * 0.4f) && !Blocked(_p + Vector3.up * 1f);
}
/// <summary>Точка внутри текстуры? Земля игры - поверхность, а не сплошная масса, и шар
/// (Blocked) изнутри её не чует. Поэтому так: луч вверх до первого потолка (поверхность
/// лицом вниз), потом луч от потолка вниз до точки. Попалась поверхность лицом вверх -
/// точка под ней, то есть внутри. Лучи видят только лицевые стороны, так что пары хватает
/// и в пещере, и в доме. _ground - это грунт (блок-земля под той поверхностью или в самой
/// точке), а не постройка.</summary>
static bool Inside(World _world, Vector3 _p, int _mask, out bool _ground)
{
_ground = false;
Vector3 o = _p - Origin.position;
float top = InsideProbe;
if (Physics.Raycast(o, Vector3.up, out RaycastHit ceil, InsideProbe, _mask, QueryTriggerInteraction.Ignore))
{
top = ceil.distance;
}
if (top < 0.05f ||
!Physics.Raycast(o + Vector3.up * (top - 0.02f), Vector3.down, out RaycastHit floor, top - 0.02f, _mask, QueryTriggerInteraction.Ignore))
{
return false;
}
_ground = IsGround(_world, floor.point + Origin.position + Vector3.down * 0.05f) || IsGround(_world, _p);
return true;
}
static bool IsGround(World _world, Vector3 _p)
{
BlockValue b = _world.GetBlock(World.worldToBlockPos(_p));
return !b.isair && b.Block.shape.IsTerrain();
} }
static bool Solid(World _world, Vector3 _p) static bool Solid(World _world, Vector3 _p)
@@ -1266,6 +1426,10 @@ namespace NecromancerTome
} }
Vector3 body = player.position; Vector3 body = player.position;
player.position = snake.position; player.position = snake.position;
if (SnakeControl.FullViewDim > 0 && player.ChunkObserver != null)
{
player.ChunkObserver.viewDim = SnakeControl.FullViewDim;
}
return body; return body;
} }
@@ -1284,7 +1448,8 @@ namespace NecromancerTome
if (player != null) if (player != null)
{ {
player.position = _body; player.position = _body;
if (Possession.Active && Possession.FullViewDim > 0 && player.ChunkObserver != null) if (((Possession.Active && Possession.FullViewDim > 0) || (SnakeControl.Controlling && SnakeControl.FullViewDim > 0)) &&
player.ChunkObserver != null)
{ {
player.ChunkObserver.viewDim = Possession.BodyViewDim; player.ChunkObserver.viewDim = Possession.BodyViewDim;
} }
@@ -1303,11 +1468,11 @@ namespace NecromancerTome
public static void Postfix(Vector3 __state) { SnakeAudioSwap.Postfix(__state); } public static void Postfix(Vector3 __state) { SnakeAudioSwap.Postfix(__state); }
} }
/// <summary>Детализация зданий (POI LOD) - вокруг одержимого, с полным радиусом (03.10, сжатая область тела).</summary> /// <summary>Детализация зданий (POI LOD) - вокруг одержимого, с полным радиусом (03.10, сжатая область тела), и вокруг змеи (05.10: «текстуры вокруг неё отображаются коряво»).</summary>
[HarmonyPatch(typeof(PrefabLODManager), nameof(PrefabLODManager.FrameUpdate))] [HarmonyPatch(typeof(PrefabLODManager), nameof(PrefabLODManager.FrameUpdate))]
public static class Patch_PrefabLODManager_FrameUpdate_Possession public static class Patch_PrefabLODManager_FrameUpdate_Possession
{ {
public static void Prefix(out Vector3 __state) { __state = Possession.Active ? SnakeAudioSwap.Prefix() : Vector3.zero; } public static void Prefix(out Vector3 __state) { __state = Possession.Active || SnakeControl.Controlling ? SnakeAudioSwap.Prefix() : Vector3.zero; }
public static void Postfix(Vector3 __state) { SnakeAudioSwap.Postfix(__state); } public static void Postfix(Vector3 __state) { SnakeAudioSwap.Postfix(__state); }
} }
Binary file not shown.