using System.Collections.Generic;
using UnityEngine;
namespace NecromancerTome
{
///
/// Дух в чертогах разума - камера без сущности (указание 03.10: «тело можно вообще не делать,
/// нужен только прицел»). Ходит по полу как человек: столкновения - по тем же слоям, что у змеи
/// (SnakeControl.WallMask / TerrainMask), гравитация и ступенька. Взаимодействие одно - книга
/// памяти по E; остальное в чертогах неприкосновенно. Камеру ставят патчи камеры режима змеи
/// (SnakeControl.cs), звук и мир вокруг считаются от духа через SnakeAudioSwap.
///
/// СОН (указание: «тёплый фильтр, это как бы сон», эффекты - на моё усмотрение): готовые эффекты
/// экрана игры - Hot (тёплый оттенок), Blur (мягкое размытие), Drunk (медленное покачивание).
/// Силы крутятся командой necrodream на ходу.
///
public static class MindControl
{
public const float EyeHeight = 1.6f;
public const float Radius = 0.3f;
public const float Height = 1.75f;
public const float WalkSpeed = 2.2f;
public const float RunSpeed = 3.6f;
public const float Gravity = 20f;
public const float JumpSpeed = 6f;
public const float StepUp = 0.6f;
public const float ReachDistance = 3f;
public static float Hot = 0.45f;
public static float Blur = 0.25f;
public static float Drunk = 0.15f;
public static bool Controlling;
static Vector3 _feet;
static Vector3 _spawn;
static float _yaw, _pitch, _vy;
/// Высота камеры (ног) со сглаживанием: ступенька поднимает ноги рывком, камера догоняет
/// плавно (тест 09.10 на лестнице чертогов: «двигается рывками»).
static float _camY;
public const float CamSmooth = 18f;
static bool _grounded;
public static bool HasFocus;
static EntityPlayerLocal _player;
public static Vector3 FeetPos => _feet;
public static void Begin(EntityPlayerLocal _p, Vector3 _at, float _atYaw)
{
_player = _p;
_feet = _at;
_spawn = _at;
_camY = _at.y;
_yaw = _atYaw;
_pitch = 0f;
_vy = 0f;
_grounded = false;
HasFocus = false;
Controlling = true;
_p.PlayerUI?.windowManager.Close(SnakeControl.ToolbeltWindow);
Dream(_p, true);
Debug.Log("[NecromancerTome] MindControl: дух в чертогах, " + _at);
}
public static void End(EntityPlayerLocal _p)
{
if (!Controlling)
{
return;
}
Controlling = false;
HasFocus = false;
Dream(_p ?? _player, false);
_player = null;
}
public static void Dream(EntityPlayerLocal _p, bool _on)
{
ScreenEffects fx = _p?.ScreenEffectManager;
if (fx == null)
{
return;
}
fx.SetScreenEffect("Hot", _on ? Hot : 0f, _on ? 2f : 0.5f);
fx.SetScreenEffect("Blur", _on ? Blur : 0f, _on ? 2f : 0.5f);
fx.SetScreenEffect("Drunk", _on ? Drunk : 0f, _on ? 2f : 0.5f);
}
static bool Blocked(Vector3 _p)
{
Vector3 a = _p + Vector3.up * (Radius + 0.05f) - Origin.position;
Vector3 b = _p + Vector3.up * (Height - Radius) - Origin.position;
return Physics.CheckCapsule(a, b, Radius, SnakeControl.WallMask, QueryTriggerInteraction.Ignore);
}
static float _blockLogAt;
/// Диагностика (09.10): во что упёрся дух - раз в 2 с, имена коллайдеров.
static void LogBlocker(Vector3 _p)
{
if (Time.time < _blockLogAt)
{
return;
}
_blockLogAt = Time.time + 2f;
Vector3 a = _p + Vector3.up * (Radius + 0.05f) - Origin.position;
Vector3 b = _p + Vector3.up * (Height - Radius) - Origin.position;
var names = new List();
foreach (Collider col in Physics.OverlapCapsule(a, b, Radius, SnakeControl.WallMask, QueryTriggerInteraction.Ignore))
{
Transform r = col.transform.root;
names.Add(col.name + "@" + (r != null ? r.name : "?") + " " + col.bounds.min.y.ToString("0.00") + ".." + col.bounds.max.y.ToString("0.00"));
}
Debug.Log("[NecromancerTome] MindControl: упёрся у " + _p + " (ноги " + (_p.y - Origin.position.y).ToString("0.00") + "): " + string.Join(", ", names));
}
static bool TryMove(Vector3 _d)
{
Vector3 c = _feet + _d;
if (!Blocked(c))
{
_feet = c;
return true;
}
// Ступенька: шагаем вверх на наименьшую высоту (до StepUp), где свободно, ноги - на верх ступеньки
// (не выше: иначе на следующем кадре проседали - рывок). Сразу на StepUp нельзя: в проёме с порогом
// и низкой притолокой (сейфовая дверь, тест 09.10: «не получается пройти в проём») голова упиралась.
if (_grounded)
{
for (float h = StepUp / 4f; h <= StepUp + 0.001f; h += StepUp / 4f)
{
if (Blocked(c + Vector3.up * h))
{
continue;
}
Vector3 up = c + Vector3.up * h;
if (Physics.Raycast(up + Vector3.up * 0.05f - Origin.position, Vector3.down, out RaycastHit hit, h + 0.05f, SnakeControl.TerrainMask, QueryTriggerInteraction.Ignore))
{
up.y = hit.point.y + Origin.position.y;
}
_feet = up;
return true;
}
}
LogBlocker(c);
return false;
}
public static void Update(EntityPlayerLocal _p)
{
if (!Controlling)
{
return;
}
float dt = Time.deltaTime;
bool gui = _p.PlayerUI != null && _p.PlayerUI.windowManager.IsModalWindowOpen();
PlayerActionsLocal input = _p.playerInput;
Vector2 move = Vector2.zero;
bool run = false, jump = false;
if (!gui && input != null)
{
PlayerMoveController pmc = PlayerMoveController.Instance;
Vector2 sens = pmc != null ? pmc.mouseLookSensitivity : Vector2.one * 5f;
int invert = pmc != null && pmc.invertMouse != 0 ? pmc.invertMouse : 1;
_yaw += Input.GetAxis("Mouse X") * sens.x;
_pitch = Mathf.Clamp(_pitch + Input.GetAxis("Mouse Y") * sens.y * invert, -85f, 85f);
move = new Vector2(input.Move.X, input.Move.Y);
run = input.Run.IsPressed;
jump = input.Jump.WasPressed;
if (HasFocus && (input.Activate.WasPressed || input.PermanentActions.Activate.WasPressed))
{
BlockNecroMemoryBook.Open(_p);
}
}
Quaternion yawRot = Quaternion.Euler(0f, _yaw, 0f);
Vector3 dir = yawRot * new Vector3(move.x, 0f, move.y);
if (dir.sqrMagnitude > 1f)
{
dir.Normalize();
}
if (dir.sqrMagnitude > 0.0001f)
{
Vector3 delta = dir * (run ? RunSpeed : WalkSpeed) * dt;
int steps = Mathf.Max(1, Mathf.CeilToInt(delta.magnitude / 0.1f));
Vector3 d = delta / steps;
for (int i = 0; i < steps; i++)
{
if (!TryMove(d) && !TryMove(new Vector3(d.x, 0f, 0f)) && !TryMove(new Vector3(0f, 0f, d.z)))
{
break;
}
}
}
// Падение и прыжок: земля - луч вниз по слою ландшафта и блоков.
if (_grounded && jump)
{
_vy = JumpSpeed;
_grounded = false;
}
_vy -= Gravity * dt;
float dy = _vy * dt;
if (dy > 0f && Blocked(_feet + Vector3.up * dy))
{
_vy = 0f;
dy = 0f;
}
// Стоял на полу и сошёл со ступеньки - прилипаем к нижней (до StepUp), а не падаем с неё скачками.
float snap = _grounded && _vy <= 0f ? StepUp : 0f;
if (Physics.Raycast(_feet + Vector3.up * 0.5f - Origin.position, Vector3.down, out RaycastHit hit, 0.5f - dy + 0.05f + snap, SnakeControl.TerrainMask, QueryTriggerInteraction.Ignore) && dy <= 0f)
{
_feet.y = hit.point.y + Origin.position.y;
_vy = 0f;
_grounded = true;
}
else
{
_feet.y += dy;
_grounded = false;
}
// Провалился (коллизии свежепоставленных блоков ещё не построены) - назад на точку появления.
if (_feet.y < _spawn.y - 3f)
{
Debug.Log("[NecromancerTome] MindControl: дух провалился - назад на точку появления");
_feet = _spawn + Vector3.up * 0.3f;
_vy = 0f;
}
}
/// Из патчей камеры режима змеи: глаза духа и поиск книги под прицелом.
public static void PlaceCamera(Transform _camera)
{
// Камера догоняет ноги плавно; большой перепад (падение, возврат на точку появления) - сразу.
_camY = Mathf.Abs(_feet.y - _camY) > 1.2f ? _feet.y : Mathf.Lerp(_camY, _feet.y, 1f - Mathf.Exp(-CamSmooth * Time.deltaTime));
Vector3 eye = new Vector3(_feet.x, _camY, _feet.z) + Vector3.up * EyeHeight - Origin.position;
Quaternion rot = Quaternion.Euler(-_pitch, _yaw, 0f);
_camera.SetPositionAndRotation(eye, rot);
HasFocus = FindBook(_camera.position + Origin.position, _camera.forward);
}
/// Шагаем по лучу на ReachDistance: первая встреченная книга памяти - цель; твёрдый
/// блок раньше неё - нет цели (сквозь стены не читаем).
static bool FindBook(Vector3 _from, Vector3 _dir)
{
World world = GameManager.Instance?.World;
if (world == null)
{
return false;
}
Vector3i last = new Vector3i(int.MinValue, 0, 0);
for (float t = 0.1f; t <= ReachDistance; t += 0.05f)
{
Vector3i b = World.worldToBlockPos(_from + _dir * t);
if (b == last)
{
continue;
}
last = b;
Block block = world.GetBlock(b).Block;
if (block is BlockNecroMemoryBook)
{
return true;
}
if (block != null && block.IsCollideMovement && !block.shape.IsTerrain() && block.shape.IsSolidCube)
{
return false;
}
}
return false;
}
}
/// Прицел духа и подсказка у книги (OnGUI, как прицел змеи).
public class MindHud : MonoBehaviour
{
static GUIStyle _style;
public static void Create()
{
var go = new GameObject("NecroMindHud");
DontDestroyOnLoad(go);
go.AddComponent();
}
void OnGUI()
{
if (!MindControl.Controlling || Event.current.type != EventType.Repaint)
{
return;
}
EntityPlayerLocal player = GameManager.Instance?.World?.GetPrimaryPlayer();
if (player == null || (player.PlayerUI != null && player.PlayerUI.windowManager.IsModalWindowOpen()))
{
return;
}
float s = 4f * Screen.height / 1080f;
Color old = GUI.color;
GUI.color = MindControl.HasFocus ? new Color(1f, 0.85f, 0.5f, 1f) : new Color(1f, 0.95f, 0.85f, 0.7f);
GUI.DrawTexture(new Rect(Screen.width * 0.5f - s * 0.5f, Screen.height * 0.5f - s * 0.5f, s, s), Texture2D.whiteTexture);
if (MindControl.HasFocus)
{
if (_style == null)
{
_style = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontStyle = FontStyle.Bold };
}
_style.fontSize = Mathf.RoundToInt(26f * Screen.height / 1080f);
// E в игре привязана к PermanentActions.Activate; у обычного Activate привязок нет - он даёт «Нет».
string key = KeyName(player.playerInput?.PermanentActions?.Activate) ?? KeyName(player.playerInput?.Activate) ?? "E";
string text = string.Format(Localization.Get("necroMemoryBookPrompt"), key);
Rect r = new Rect(0f, Screen.height * 0.5f + 30f * Screen.height / 1080f, Screen.width, 40f * Screen.height / 1080f);
GUI.color = new Color(0f, 0f, 0f, 0.8f);
GUI.Label(new Rect(r.x + 2f, r.y + 2f, r.width, r.height), text, _style);
GUI.color = new Color(1f, 0.9f, 0.7f, 1f);
GUI.Label(r, text, _style);
}
GUI.color = old;
}
private static string KeyName(InControl.PlayerAction action)
{
if (action == null || action.Bindings.Count == 0) return null;
string name = action.GetBindingString(false);
return string.IsNullOrEmpty(name) ? null : name;
}
}
/// necrodream hot|blur|drunk N - подобрать силу сонных эффектов на ходу.
[UnityEngine.Scripting.Preserve]
public class ConsoleCmdNecroDream : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necrodream" };
public override string getDescription() => "NecromancerTome: сила эффектов сна - hot|blur|drunk N (0-1)";
public override void Execute(System.Collections.Generic.List _params, CommandSenderInfo _senderInfo)
{
if (_params.Count == 2 && float.TryParse(_params[1], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float v))
{
switch (_params[0])
{
case "hot": MindControl.Hot = v; break;
case "blur": MindControl.Blur = v; break;
case "drunk": MindControl.Drunk = v; break;
}
if (MindControl.Controlling)
{
MindControl.Dream(GameManager.Instance.World.GetPrimaryPlayer(), true);
}
}
SdtdConsole.Instance.Output("сон: hot " + MindControl.Hot + ", blur " + MindControl.Blur + ", drunk " + MindControl.Drunk);
}
}
}