Змея: выход из текстур, полёт на ПКМ; одержимый: приседание, удар по прицелу; коза
- Змея: проверка «внутри текстуры» лучами (земля игры - поверхность), каждый кадр выброс на ближайшее свободное место (на земле - из любых текстур, в полёте - из грунта); шаг и прыжок в текстуры не пускают. Полёт пока зажата ПКМ: 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
+202
-37
@@ -56,6 +56,18 @@ namespace NecromancerTome
|
||||
/// <summary>Радиус проверочных сфер при шаге. Меньше полублока - в дверной проём и щель в
|
||||
/// один блок змея пролезает.</summary>
|
||||
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. Подобраны на глаз:
|
||||
/// у животного с RootMotion полный ход даёт landMovementFactor * 2.5 = 0.25.</summary>
|
||||
@@ -125,6 +137,11 @@ namespace NecromancerTome
|
||||
/// место под сброс припасов (AIAirDrop) - только здесь с видимой сеткой и дальностью
|
||||
/// обзора из настроек.</summary>
|
||||
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;
|
||||
static Vector3 _pos;
|
||||
static float _vy;
|
||||
@@ -154,6 +171,8 @@ namespace NecromancerTome
|
||||
_dashTarget = null;
|
||||
_freeze = 0f;
|
||||
_kick = Vector3.zero;
|
||||
_flying = false;
|
||||
_ejectWait = 0f;
|
||||
_killId = -1;
|
||||
_burst = null;
|
||||
_sleeperCheck = 0f;
|
||||
@@ -169,6 +188,12 @@ namespace NecromancerTome
|
||||
BodyDanger = false;
|
||||
_bodyCheck = 0f;
|
||||
_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;
|
||||
// Пояс прячется на весь режим (указание 28.09); обратно его откроет сама игра в OnHUD.
|
||||
_player.PlayerUI?.windowManager.Close(ToolbeltWindow);
|
||||
@@ -231,7 +256,13 @@ namespace NecromancerTome
|
||||
_fearQueue.Clear();
|
||||
_fearQueued.Clear();
|
||||
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;
|
||||
Target = null;
|
||||
BodyDanger = false;
|
||||
@@ -258,6 +289,8 @@ namespace NecromancerTome
|
||||
Vector2 move = Vector2.zero;
|
||||
bool run = false;
|
||||
bool jump = false;
|
||||
bool jumpHeld = false;
|
||||
bool downHeld = false;
|
||||
bool attack = false;
|
||||
World world = GameManager.Instance.World;
|
||||
if (_freeze > 0f)
|
||||
@@ -274,13 +307,25 @@ namespace NecromancerTome
|
||||
move = new Vector2(input.Move.X, input.Move.Y);
|
||||
run = input.Run.IsPressed;
|
||||
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;
|
||||
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)
|
||||
{
|
||||
@@ -294,6 +339,10 @@ namespace NecromancerTome
|
||||
if (_dashTarget != null)
|
||||
{
|
||||
Dash(_player);
|
||||
if (_dashTarget == null)
|
||||
{
|
||||
KeepOut(world, dt);
|
||||
}
|
||||
_observer?.SetPosition(_pos);
|
||||
_snake.SetPosition(_pos);
|
||||
_snake.SetRotation(new Vector3(0f, _snakeYaw, 0f));
|
||||
@@ -305,6 +354,12 @@ namespace NecromancerTome
|
||||
}
|
||||
Quaternion yawRot = Quaternion.Euler(0f, _yaw, 0f);
|
||||
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)
|
||||
{
|
||||
dir.Normalize();
|
||||
@@ -312,13 +367,15 @@ namespace NecromancerTome
|
||||
bool moving = dir.sqrMagnitude > 0.01f;
|
||||
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));
|
||||
Vector3 d = delta / steps;
|
||||
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;
|
||||
}
|
||||
@@ -340,7 +397,11 @@ namespace NecromancerTome
|
||||
}
|
||||
// Вид от первого лица: змея смотрит туда же, куда игрок.
|
||||
_snakeYaw = Mathf.LerpAngle(_snakeYaw, _yaw, dt * 12f);
|
||||
Fall(dt, jump);
|
||||
if (!_flying)
|
||||
{
|
||||
Fall(world, dt, jump);
|
||||
}
|
||||
KeepOut(world, dt);
|
||||
_observer?.SetPosition(_pos);
|
||||
|
||||
_snake.SetPosition(_pos);
|
||||
@@ -350,7 +411,7 @@ namespace NecromancerTome
|
||||
_snake.moveDirection = Vector3.zero;
|
||||
AnimForward = moving ? (run ? AnimRun : AnimWalk) : 0f;
|
||||
CheckBody(world, _player, _snake, dt);
|
||||
Sleepers(world, _player, dt);
|
||||
Sleepers(world, _player, _pos, ref _sleeperCheck, dt);
|
||||
Sense(world, _snake, dt);
|
||||
FearQueue(world, _snake, dt);
|
||||
return Radiation(world, dt);
|
||||
@@ -485,17 +546,18 @@ namespace NecromancerTome
|
||||
/// <summary>Комнаты со спящими в зданиях просыпаются, когда в них входит ИГРОК:
|
||||
/// EntityPlayer.CheckSleeperTriggers каждый тик зовёт World.CheckSleeperVolumeTouching и
|
||||
/// CheckTriggerVolumeTrigger, а те берут player.position (тест 28.09: «зомби в локациях не
|
||||
/// спавнятся, если змея туда заходит»). Раз в 0,25 с зовём их же, подставив позицию змеи.</summary>
|
||||
static void Sleepers(World _world, EntityPlayerLocal _player, float _dt)
|
||||
/// спавнятся, если змея туда заходит»). Раз в 0,25 с зовём их же, подставив позицию змеи
|
||||
/// (и одержимого - Possession, 05.10).</summary>
|
||||
public static void Sleepers(World _world, EntityPlayerLocal _player, Vector3 _at, ref float _timer, float _dt)
|
||||
{
|
||||
_sleeperCheck -= _dt;
|
||||
if (_sleeperCheck > 0f || _world.IsRemote())
|
||||
_timer -= _dt;
|
||||
if (_timer > 0f || _world.IsRemote())
|
||||
{
|
||||
return;
|
||||
}
|
||||
_sleeperCheck = 0.25f;
|
||||
_timer = 0.25f;
|
||||
Vector3 body = _player.position;
|
||||
_player.position = _pos;
|
||||
_player.position = _at;
|
||||
try
|
||||
{
|
||||
_world.CheckSleeperVolumeTouching(_player);
|
||||
@@ -667,6 +729,16 @@ namespace NecromancerTome
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (_flying)
|
||||
{
|
||||
// Сквозь постройки можно, в грунт - нет.
|
||||
if (Inside(_world, c + Vector3.up * BodyUp, TerrainMask, out bool ground) && ground)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_pos = c;
|
||||
return true;
|
||||
}
|
||||
// Один блок - ступенька, два - стена. Проверяем настоящую геометрию, а не тип блока:
|
||||
// у блока есть только флаг «твёрдый», и открытая дверь для него такая же стена, как
|
||||
// закрытая (тест 28.09: «змея не может пройти в открытые двери»).
|
||||
@@ -685,11 +757,16 @@ namespace NecromancerTome
|
||||
_vy = 0f;
|
||||
}
|
||||
}
|
||||
// Ни в грунт, ни в блоки: обрыв выше подъёма или стена, которую шар изнутри не чует.
|
||||
if (Inside(_world, c + Vector3.up * BodyUp, WallMask, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_pos = c;
|
||||
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))
|
||||
{
|
||||
@@ -717,6 +794,12 @@ namespace NecromancerTome
|
||||
_vy = 0f;
|
||||
dy = 0f;
|
||||
}
|
||||
// Прыжок не должен заносить тело в текстуры (указание 05.10).
|
||||
if (dy > 0f && Inside(_world, _pos + Vector3.up * (dy + BodyUp), WallMask, out _))
|
||||
{
|
||||
_vy = 0f;
|
||||
dy = 0f;
|
||||
}
|
||||
_pos.y += dy;
|
||||
if (_pos.y <= ground)
|
||||
{
|
||||
@@ -731,38 +814,115 @@ namespace NecromancerTome
|
||||
return Physics.CheckSphere(_p - Origin.position, BodyRadius, WallMask, QueryTriggerInteraction.Ignore);
|
||||
}
|
||||
|
||||
/// <summary>Правая кнопка: выбраться из текстур. Ближайшее свободное место - сначала выше
|
||||
/// на месте, потом кольцами до 3 м и до 4 м вверх; там, где тело змеи ни во что не упирается.
|
||||
/// Дальше змея сама падает на землю.</summary>
|
||||
static void Unstick(World _world)
|
||||
/// <summary>Каждый кадр: змея в грунте (в полёте) или в любой текстуре (на земле) -
|
||||
/// выбрасываем на ближайшее свободное место (указание 05.10).</summary>
|
||||
static void KeepOut(World _world, float _dt)
|
||||
{
|
||||
if (_dashTarget != null)
|
||||
if (_ejectWait > 0f)
|
||||
{
|
||||
_ejectWait -= _dt;
|
||||
return;
|
||||
}
|
||||
Vector3 body = _pos + Vector3.up * BodyUp;
|
||||
if (!_world.IsChunkAreaCollidersLoaded(body))
|
||||
{
|
||||
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;
|
||||
// На месте - минимум на полметра вверх: проверка может счесть место свободным, а
|
||||
// змея всё равно не идёт (тест 28.09: дважды «выбралась, сдвиг 0.0 м»).
|
||||
for (float h = r == 0f ? 0.5f : 0f; h <= 4f; h += 0.5f)
|
||||
_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)
|
||||
{
|
||||
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;
|
||||
if (!_world.IsChunkAreaCollidersLoaded(p) || Blocked(p + Vector3.up * 0.4f) || Blocked(p + Vector3.up * 1f))
|
||||
for (int i = 0; i < dirs; i++)
|
||||
{
|
||||
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: выбраться некуда");
|
||||
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)
|
||||
@@ -1266,6 +1426,10 @@ namespace NecromancerTome
|
||||
}
|
||||
Vector3 body = player.position;
|
||||
player.position = snake.position;
|
||||
if (SnakeControl.FullViewDim > 0 && player.ChunkObserver != null)
|
||||
{
|
||||
player.ChunkObserver.viewDim = SnakeControl.FullViewDim;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -1284,7 +1448,8 @@ namespace NecromancerTome
|
||||
if (player != null)
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -1303,11 +1468,11 @@ namespace NecromancerTome
|
||||
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))]
|
||||
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); }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user