Змея: выход из текстур, полёт на ПКМ; одержимый: приседание, удар по прицелу; коза
- Змея: проверка «внутри текстуры» лучами (земля игры - поверхность), каждый кадр выброс на ближайшее свободное место (на земле - из любых текстур, в полёте - из грунта); шаг и прыжок в текстуры не пускают. Полёт пока зажата ПКМ: 1.1 м/с (половина шага зомби), сквозь постройки можно, в грунт нельзя; Пробел вверх, C/Ctrl вниз. E - аварийный выход. Детализация зданий и область тела - как у одержимого. - Одержимый: будит спящих (комнаты зданий и сам вид одержимого), приседание на C и Ctrl, удар по точке под прицелом с наведением на грудь ближнего врага. - Хваты: толчок руки к прицелу на ЛКМ; «пиф-паф» вселяет на пике толчка. «Отрицание» стало «Козой» (13 языков): средний и безымянный поджаты, большой прижат; звук - бросок змеи. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
bb7a66dd8e
commit
fdd3782024
@@ -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ı,右键 - 远程攻击,右鍵 - 遠程攻擊
|
||||||
|
|||||||
|
+38
-11
@@ -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())
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+135
-5
@@ -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.Attack(true);
|
z.SetLookPosition(MeleeAim(world, z));
|
||||||
_nextAttack = Time.time + Mathf.Max(0.3f, z.GetAttackTimeoutTicks() / 20f);
|
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);
|
_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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+202
-37
@@ -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))
|
||||||
{
|
{
|
||||||
int dirs = r == 0f ? 1 : 8;
|
_ejectWait = 1f;
|
||||||
// На месте - минимум на полметра вверх: проверка может счесть место свободным, а
|
}
|
||||||
// змея всё равно не идёт (тест 28.09: дважды «выбралась, сдвиг 0.0 м»).
|
}
|
||||||
for (float h = r == 0f ? 0.5f : 0f; h <= 4f; h += 0.5f)
|
|
||||||
|
/// <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)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < dirs; i++)
|
int dirs = r == 0f ? 1 : 8;
|
||||||
|
for (float h = -1f; h <= 6f; h += 0.5f)
|
||||||
{
|
{
|
||||||
Vector3 p = _pos + Quaternion.Euler(0f, _yaw + i * 45f, 0f) * Vector3.forward * r + Vector3.up * h;
|
for (int i = 0; i < dirs; i++)
|
||||||
if (!_world.IsChunkAreaCollidersLoaded(p) || Blocked(p + Vector3.up * 0.4f) || Blocked(p + Vector3.up * 1f))
|
|
||||||
{
|
{
|
||||||
continue;
|
list.Add(Quaternion.Euler(0f, i * 45f, 0f) * Vector3.forward * r + Vector3.up * h);
|
||||||
}
|
}
|
||||||
Debug.Log("[NecromancerTome] SnakeControl: выбралась, сдвиг " + (p - _pos).magnitude.ToString("F1") + " м");
|
|
||||||
_pos = p;
|
|
||||||
_vy = 0f;
|
|
||||||
_airborne = true;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
Debug.Log("[NecromancerTome] SnakeControl: выброшена из текстур" + (_groundOnly ? " (грунт)" : "") + ", сдвиг " + o.magnitude.ToString("F1") + " м");
|
||||||
|
_pos += o;
|
||||||
|
_vy = 0f;
|
||||||
|
_airborne = !_flying;
|
||||||
|
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.
Reference in New Issue
Block a user