- Главы питомцев с чертежами (Fig. III-VI), команда necroobj для выгрузки моделей - Книги призыва питомцев переименованы в гримуары (13 языков) - Чертоги: второй этаж; таблички necroDoorNumber (только в редакторе) нумеруют двери, дверь с номером закрыта до события (SetDoorOpen, necropalace door N open|close), с начала открыты 2, 4, 5, 6 - Дух: плавный шаг по ступенькам, проход в проёмы с порогом - Стервятник: снижение только C, без Ctrl-переключателя Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
351 lines
13 KiB
C#
351 lines
13 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// Дух в чертогах разума - камера без сущности (указание 03.10: «тело можно вообще не делать,
|
||
/// нужен только прицел»). Ходит по полу как человек: столкновения - по тем же слоям, что у змеи
|
||
/// (SnakeControl.WallMask / TerrainMask), гравитация и ступенька. Взаимодействие одно - книга
|
||
/// памяти по E; остальное в чертогах неприкосновенно. Камеру ставят патчи камеры режима змеи
|
||
/// (SnakeControl.cs), звук и мир вокруг считаются от духа через SnakeAudioSwap.
|
||
///
|
||
/// СОН (указание: «тёплый фильтр, это как бы сон», эффекты - на моё усмотрение): готовые эффекты
|
||
/// экрана игры - Hot (тёплый оттенок), Blur (мягкое размытие), Drunk (медленное покачивание).
|
||
/// Силы крутятся командой necrodream на ходу.
|
||
/// </summary>
|
||
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;
|
||
/// <summary>Высота камеры (ног) со сглаживанием: ступенька поднимает ноги рывком, камера догоняет
|
||
/// плавно (тест 09.10 на лестнице чертогов: «двигается рывками»).</summary>
|
||
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;
|
||
|
||
/// <summary>Диагностика (09.10): во что упёрся дух - раз в 2 с, имена коллайдеров.</summary>
|
||
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<string>();
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>Из патчей камеры режима змеи: глаза духа и поиск книги под прицелом.</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>Шагаем по лучу на ReachDistance: первая встреченная книга памяти - цель; твёрдый
|
||
/// блок раньше неё - нет цели (сквозь стены не читаем).</summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>Прицел духа и подсказка у книги (OnGUI, как прицел змеи).</summary>
|
||
public class MindHud : MonoBehaviour
|
||
{
|
||
static GUIStyle _style;
|
||
|
||
public static void Create()
|
||
{
|
||
var go = new GameObject("NecroMindHud");
|
||
DontDestroyOnLoad(go);
|
||
go.AddComponent<MindHud>();
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>necrodream hot|blur|drunk N - подобрать силу сонных эффектов на ходу.</summary>
|
||
[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<string> _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);
|
||
}
|
||
}
|
||
}
|