using System.Collections.Generic; using System.Globalization; using HarmonyLib; using UnityEngine; using UnityEngine.Scripting; namespace NecromancerTome { /// /// Осмотр рук от первого лица со стороны (16.10: «зафиксировать командой позу игрока и осматривать его камерой... хват /// от первого лица не позволяет видеть всех нюансов»). Руки первого лица (baseRigFP) - отдельная модель под камерой /// игрока, в виде от третьего лица их нет, а у тела свои анимации. Поэтому вид остаётся от первого лица, а поверх /// рисует своя камера, которая облетает руки. Её место задано в осях камеры игрока: повернёшь голову - вид на руки /// не меняется. Поза замирает (снимок всех костей, как у хватов мода). /// Управление - цифровой блок: 4/6 вокруг, 8/2 выше/ниже, +/- ближе/дальше, 5 - сначала; или командой necroview. /// public static class NecroView { public static bool On; public static float Yaw, Pitch, Dist = 0.6f; /// Точка, вокруг которой летает камера (между кистями), - в осях камеры игрока. static Vector3 _target; static Camera _cam; const float TurnSpeed = 60f, ZoomSpeed = 0.4f; /// Мышь облетает руки, персонаж стоит (16.10: «я поворачиваю мышь, а туловище проворачивается следом, и я /// вижу тело только с одной позиции»). Поворот игрока каждый кадр возвращается к снимку при включении. public static bool Mouse = true; public static float MouseSpeed = 3f; static Vector3 _lockRot; static Quaternion _lockCam; static bool _mouseBroken; /// «Выйти из тела» (16.10: «временно мог выйти из тела, когда попрошу? Чтобы мог ходить вокруг тела и /// осматривать»): камера летает свободно - WASD, мышь, пробел/Ctrl - выше/ниже, Shift - быстрее; тело стоит /// (PlayerMoveController.Update пропускается, как при одержимости), поза рук замерла. Выход - End или necroview off. public static bool Free; static Vector3 _freePos; static float _freeYaw, _freePitch; const float FlySpeed = 1.5f; public static string StartFree() { string r = Start(0f, 0f, 0.6f); Transform pc = HandPose.Camera(); if (!On || pc == null) { return r; } Free = true; _freePos = pc.position; Vector3 e = pc.eulerAngles; _freeYaw = e.y; _freePitch = e.x > 180f ? e.x - 360f : e.x; return "вне тела: WASD, мышь, пробел/Ctrl - выше/ниже, Shift - быстрее, End - вернуться"; } static void UpdateFree(float _dt) { PlayerMoveController pmc = PlayerMoveController.Instance; Vector2 sens = pmc != null ? pmc.mouseLookSensitivity : Vector2.one * 5f; if (!_mouseBroken) { try { _freeYaw += Input.GetAxis("Mouse X") * sens.x; _freePitch = Mathf.Clamp(_freePitch - Input.GetAxis("Mouse Y") * sens.y, -89f, 89f); } catch (System.Exception e) { _mouseBroken = true; Debug.LogWarning("[NecromancerTome] necroview: мышь не читается (" + e.Message + ")"); } } Quaternion rot = Quaternion.Euler(_freePitch, _freeYaw, 0f); Vector3 move = Vector3.zero; if (Input.GetKey(KeyCode.W)) move += Vector3.forward; if (Input.GetKey(KeyCode.S)) move += Vector3.back; if (Input.GetKey(KeyCode.A)) move += Vector3.left; if (Input.GetKey(KeyCode.D)) move += Vector3.right; Vector3 world = rot * move; if (Input.GetKey(KeyCode.Space)) world += Vector3.up; if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.C)) world += Vector3.down; float speed = FlySpeed * (Input.GetKey(KeyCode.LeftShift) ? 4f : 1f); _freePos += world * speed * _dt; _cam.transform.SetPositionAndRotation(_freePos, rot); } public static string Start(float _yaw, float _pitch, float _dist) { Transform pc = HandPose.Camera(); EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer(); if (pc == null || pl?.playerCamera == null) { return "нет камеры игрока"; } Transform l = HandPose.Bone("LeftHand"), r = HandPose.Bone("RightHand"); Vector3 mid = l != null && r != null ? (l.position + r.position) * 0.5f : pc.position + pc.forward * 0.4f; _target = pc.InverseTransformPoint(mid); Yaw = _yaw; Pitch = _pitch; Dist = _dist; if (_cam == null) { GameObject go = new GameObject("NecroViewCam"); Object.DontDestroyOnLoad(go); _cam = go.AddComponent(); } _cam.CopyFrom(pl.playerCamera); // Сразу над камерой игрока, но ПОД камерами интерфейса (16.10: с +50 рисовала поверх меню - «мне из игры не выйти даже»). float above = float.MaxValue; foreach (Camera c in Camera.allCameras) { if (c != _cam && c.depth > pl.playerCamera.depth && c.depth < above) { above = c.depth; } } _cam.depth = above < float.MaxValue ? (pl.playerCamera.depth + above) * 0.5f : pl.playerCamera.depth + 1f; _cam.nearClipPlane = 0.01f; _cam.gameObject.SetActive(true); HandPose.HoldStill = true; _lockRot = pl.rotation; _lockCam = pc.localRotation; On = true; return "осмотр включён: вокруг " + Yaw + ", высота " + Pitch + ", расстояние " + Dist; } public static void Stop() { On = false; Free = false; HandPose.HoldStill = false; if (_cam != null) { Object.Destroy(_cam.gameObject); _cam = null; } } /// Из HandPoseDriver.LateUpdate - после позы. public static void Update() { if (!On) { return; } Transform pc = HandPose.Camera(); if (pc == null || _cam == null) { Stop(); return; } float dt = Time.unscaledDeltaTime; if (Free) { if (Input.GetKeyDown(KeyCode.End)) { Stop(); return; } UpdateFree(dt); return; } EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer(); if (Mouse && pl != null && !_mouseBroken) { try { Yaw += Input.GetAxis("Mouse X") * MouseSpeed; Pitch = Mathf.Clamp(Pitch - Input.GetAxis("Mouse Y") * MouseSpeed, -85f, 85f); } catch (System.Exception e) { _mouseBroken = true; // нет оси мыши в старом вводе - остаётся цифровой блок Debug.LogWarning("[NecromancerTome] necroview: мышь не читается (" + e.Message + ")"); } pl.rotation = _lockRot; pc.localRotation = _lockCam; } if (Input.GetKey(KeyCode.Keypad4)) Yaw -= TurnSpeed * dt; if (Input.GetKey(KeyCode.Keypad6)) Yaw += TurnSpeed * dt; if (Input.GetKey(KeyCode.Keypad8)) Pitch = Mathf.Min(85f, Pitch + TurnSpeed * dt); if (Input.GetKey(KeyCode.Keypad2)) Pitch = Mathf.Max(-85f, Pitch - TurnSpeed * dt); if (Input.GetKey(KeyCode.KeypadPlus)) Dist = Mathf.Max(0.1f, Dist - ZoomSpeed * dt); if (Input.GetKey(KeyCode.KeypadMinus)) Dist = Mathf.Min(3f, Dist + ZoomSpeed * dt); if (Input.GetKeyDown(KeyCode.Keypad5)) { Yaw = 0f; Pitch = 0f; Dist = 0.6f; } // Yaw 0, Pitch 0 - смотрим на руки со стороны глаз игрока (сзади), 180 - спереди. Quaternion local = Quaternion.Euler(Pitch, Yaw, 0f); Vector3 offset = local * new Vector3(0f, 0f, -Dist); Vector3 t = pc.TransformPoint(_target); _cam.transform.SetPositionAndRotation(t + pc.rotation * offset, pc.rotation * local); } public static string F(float _v) => _v.ToString("0.#", CultureInfo.InvariantCulture); } /// necroview on [вокруг высота расстояние] | off | set вокруг высота расстояние | show [Preserve] public class ConsoleCmdNecroView : ConsoleCmdAbstract { public override string[] getCommands() => new[] { "necroview" }; public override string getDescription() => "NecromancerTome: осмотр рук со стороны - on [вокруг высота расстояние]|off|set вокруг высота расстояние|free (выйти из тела, End - назад)|mouse [off]|show; мышь или цифровой блок 4/6 8/2 +/- 5"; static bool Num(List _p, int _i, out float _v) { _v = 0f; return _p.Count > _i && float.TryParse(_p[_i].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out _v); } void Out(string _s) { SdtdConsole.Instance.Output(_s); Debug.Log("[NecromancerTome] necroview: " + _s); } public override void Execute(List _params, CommandSenderInfo _senderInfo) { string cmd = _params.Count > 0 ? _params[0] : ""; switch (cmd) { case "on": Out(NecroView.Start(Num(_params, 1, out float y) ? y : 150f, Num(_params, 2, out float p) ? p : 10f, Num(_params, 3, out float d) ? d : 0.6f)); return; case "off": NecroView.Stop(); Out("осмотр выключен"); return; case "set" when NecroView.On && Num(_params, 1, out float y2) && Num(_params, 2, out float p2) && Num(_params, 3, out float d2): NecroView.Yaw = y2; NecroView.Pitch = p2; NecroView.Dist = d2; break; case "free": Out(NecroView.StartFree()); return; case "mouse": NecroView.Mouse = !(_params.Count > 1 && _params[1] == "off"); Out("мышь " + (NecroView.Mouse ? "облетает руки" : "крутит персонажа, облёт - цифровой блок")); return; case "show": break; default: Out(getDescription()); return; } Out(NecroView.On ? "вокруг " + NecroView.F(NecroView.Yaw) + ", высота " + NecroView.F(NecroView.Pitch) + ", расстояние " + NecroView.F(NecroView.Dist) : "осмотр выключен"); } } /// Вне тела (NecroView.Free) тело не ходит и не крутится - как при одержимости. [HarmonyPatch(typeof(PlayerMoveController), nameof(PlayerMoveController.Update))] public static class Patch_PlayerMoveController_Update_NecroView { public static bool Prefix(PlayerMoveController __instance) { if (!NecroView.Free) { return true; } __instance.stopMoving(); return false; } } }