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; 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; _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 bool TryMove(Vector3 _d) { Vector3 c = _feet + _d; if (!Blocked(c)) { _feet = c; return true; } // Ступенька: на полблока выше свободно - шагаем вверх. if (_grounded && !Blocked(c + Vector3.up * StepUp)) { _feet = c + Vector3.up * StepUp; return true; } 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; } if (Physics.Raycast(_feet + Vector3.up * 0.5f - Origin.position, Vector3.down, out RaycastHit hit, 0.5f - dy + 0.05f, 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) { Vector3 eye = _feet + 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); } } }