Ночью по R с ножом - дух змеи: тело падает (-95% HP), игрок управляет призрачной змеёй от первого лица. Свободный ход по миру (свой наблюдатель чанков, отрисовка, звук, спавн и спящие считаются от змеи), прыжок, атака на ЛКМ - рывок сквозь препятствия и чёрный разрыв, ПКМ - выбраться из текстур. Тёмное чутьё, тревога у тела, отсчёт в радиации края карты, пурпурный фильтр зрения (necrovision). Утром змея развеивается. Чёрный разрыв (NecroBurst.cs): клон частиц confetti в чёрном, команда necroburst. Проверено в игре 28.09. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
306 lines
12 KiB
C#
306 lines
12 KiB
C#
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using UnityEngine;
|
||
using UnityEngine.Scripting;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// Зрение змеи (указание 28.09: «всё в более контрастном виде и в красных тонах», «как комикс»).
|
||
/// Своих фильтров не заводим: у игры в Resources/ScreenEffects есть готовые, которыми она сама не
|
||
/// пользуется (проверено по коду и XML):
|
||
/// - "Vibrant" - цветокоррекция Code/ColorCorrection_Favale: _color, contrast, saturation,
|
||
/// lows/mids/highs, whitepoint, _Gamma. Её материал перенастраиваем под красный и возвращаем
|
||
/// исходные значения при выходе;
|
||
/// - "Posterize" - ступени цвета (_steps), плоский «комиксный» вид.
|
||
/// Что именно значит каждое свойство в шейдере Favale - видно только в игре, поэтому всё крутится
|
||
/// командой `necrovision` на ходу; найденные значения потом переносятся в Default*.
|
||
/// </summary>
|
||
public static class SnakeVision
|
||
{
|
||
public const string GradeEffect = "Vibrant";
|
||
public const string PosterEffect = "Posterize";
|
||
/// <summary>Первая проверка 28.09 ночью (03:04): «слишком темно, ничего не разглядеть» -
|
||
/// контраст и ступени загнали всё тёмное в чёрный. Под красное кладём игровое ночное
|
||
/// зрение (осветляет), контраст обычный, ступеней больше.</summary>
|
||
public const string NightEffect = "NightVision";
|
||
/// Проверка 28.09: «фильтр не пропускает другие цвета», «наложи в пол-силы, как бы
|
||
/// полупрозрачным». Сила эффекта у игры - шейдерное свойство Intensity (ScreenEffect.Render:
|
||
/// Material.SetFloat("Intensity", Clamp01)), а оно есть только у Vibrant и Bright. У
|
||
/// NightVision и Posterize его нет - они всегда во всю силу, и ночное зрение делает картинку
|
||
/// одноцветной. Поэтому по умолчанию они выключены, а оттенок и осветление - в половину.
|
||
public static float DefaultNight = 0f;
|
||
/// <summary>«Яркость можно и повысить» (28.09): эффект "Bright" - та же цветокоррекция Favale,
|
||
/// настроенная игрой на осветление, и игрой не используемый.</summary>
|
||
public const string BrightEffect = "Bright";
|
||
public static float DefaultBright = 0.5f;
|
||
public static float DefaultStrength = 0.5f;
|
||
|
||
/// <summary>Фиолетово-пурпурный оттенок, а не перекраска всего (указание 28.09: «эффект потустороннего,
|
||
/// но чтобы был оттенок»): светлый пурпур и насыщенность 0.6 - свои цвета мира остаются.</summary>
|
||
public static Color DefaultColor = new Color(0.8f, 0.55f, 1f, 1f);
|
||
public static float DefaultContrast = 1f;
|
||
public static float DefaultSaturation = 0.6f;
|
||
public static int DefaultSteps = 8;
|
||
|
||
static readonly string[] FloatProps = { "contrast", "saturation", "lows", "mids", "highs", "whitepoint", "_Gamma", "Intensity" };
|
||
|
||
static bool _on;
|
||
static bool _posterize = false;
|
||
static float? _strength;
|
||
static Material _grade;
|
||
static readonly Dictionary<string, float> _savedFloats = new Dictionary<string, float>();
|
||
static Color _savedColor;
|
||
static readonly Dictionary<string, float> _tuned = new Dictionary<string, float>();
|
||
static Color? _tunedColor;
|
||
static int _steps;
|
||
static float? _night;
|
||
static float? _bright;
|
||
|
||
public static void On(EntityPlayerLocal _player)
|
||
{
|
||
ScreenEffects fx = _player?.ScreenEffectManager;
|
||
if (fx == null || _on)
|
||
{
|
||
return;
|
||
}
|
||
_grade = Find(fx, GradeEffect)?.Material;
|
||
if (_grade == null)
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] SnakeVision: эффекта '" + GradeEffect + "' нет");
|
||
return;
|
||
}
|
||
_savedFloats.Clear();
|
||
foreach (string p in FloatProps)
|
||
{
|
||
if (_grade.HasProperty(p))
|
||
{
|
||
_savedFloats[p] = _grade.GetFloat(p);
|
||
}
|
||
}
|
||
_savedColor = _grade.GetColor("_color");
|
||
_on = true;
|
||
Apply(fx);
|
||
Debug.Log("[NecromancerTome] SnakeVision: включено");
|
||
}
|
||
|
||
public static void Off(EntityPlayerLocal _player)
|
||
{
|
||
if (!_on)
|
||
{
|
||
return;
|
||
}
|
||
_on = false;
|
||
ScreenEffects fx = _player?.ScreenEffectManager;
|
||
fx?.SetScreenEffect(GradeEffect, 0f, 0f);
|
||
fx?.SetScreenEffect(PosterEffect, 0f, 0f);
|
||
fx?.SetScreenEffect(NightEffect, 0f, 0f);
|
||
fx?.SetScreenEffect(BrightEffect, 0f, 0f);
|
||
if (_grade != null)
|
||
{
|
||
foreach (KeyValuePair<string, float> kv in _savedFloats)
|
||
{
|
||
_grade.SetFloat(kv.Key, kv.Value);
|
||
}
|
||
_grade.SetColor("_color", _savedColor);
|
||
}
|
||
Debug.Log("[NecromancerTome] SnakeVision: выключено");
|
||
}
|
||
|
||
static void Apply(ScreenEffects _fx)
|
||
{
|
||
_grade.SetColor("_color", _tunedColor ?? DefaultColor);
|
||
SetF("contrast", DefaultContrast);
|
||
SetF("saturation", DefaultSaturation);
|
||
foreach (KeyValuePair<string, float> kv in _tuned)
|
||
{
|
||
_grade.SetFloat(kv.Key, kv.Value);
|
||
}
|
||
_fx.SetScreenEffect(GradeEffect, _strength ?? DefaultStrength, 0.3f);
|
||
Material poster = Find(_fx, PosterEffect)?.Material;
|
||
if (poster != null)
|
||
{
|
||
poster.SetFloat("_steps", _steps > 0 ? _steps : DefaultSteps);
|
||
}
|
||
_fx.SetScreenEffect(PosterEffect, _posterize ? 1f : 0f, 0.3f);
|
||
_fx.SetScreenEffect(NightEffect, _night ?? DefaultNight, 0.3f);
|
||
_fx.SetScreenEffect(BrightEffect, _bright ?? DefaultBright, 0.3f);
|
||
}
|
||
|
||
static void SetF(string _p, float _v)
|
||
{
|
||
if (_grade.HasProperty(_p))
|
||
{
|
||
_grade.SetFloat(_p, _v);
|
||
}
|
||
}
|
||
|
||
static ScreenEffects.ScreenEffect Find(ScreenEffects _fx, string _name)
|
||
{
|
||
foreach (ScreenEffects.ScreenEffect e in _fx.loadedEffects)
|
||
{
|
||
if (e != null && e.Name == _name)
|
||
{
|
||
return e;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static string Queue(string _name)
|
||
{
|
||
ScreenEffects fx = GameManager.Instance?.World?.GetPrimaryPlayer()?.ScreenEffectManager;
|
||
ScreenEffects.ScreenEffect e = fx != null ? Find(fx, _name) : null;
|
||
return e?.Material != null ? e.Material.renderQueue.ToString() : "?";
|
||
}
|
||
|
||
public static string Describe()
|
||
{
|
||
if (_grade == null)
|
||
{
|
||
return "necrovision: not initialised yet (enter snake mode or 'necrovision on')";
|
||
}
|
||
string s = "necrovision: " + (_on ? "ON" : "off") + ", posterize " + (_posterize ? (_steps > 0 ? _steps : DefaultSteps).ToString() : "off") +
|
||
", nv " + (_night ?? DefaultNight).ToString("0.##", CultureInfo.InvariantCulture) +
|
||
", bright " + (_bright ?? DefaultBright).ToString("0.##", CultureInfo.InvariantCulture) +
|
||
", strength " + (_strength ?? DefaultStrength).ToString("0.##", CultureInfo.InvariantCulture) +
|
||
", queue grade/poster/nv " + Queue(GradeEffect) + "/" + Queue(PosterEffect) + "/" + Queue(NightEffect) +
|
||
"\n color " + _grade.GetColor("_color").ToString("F2");
|
||
foreach (string p in FloatProps)
|
||
{
|
||
if (_grade.HasProperty(p))
|
||
{
|
||
s += "\n " + p + " " + _grade.GetFloat(p).ToString("0.###", CultureInfo.InvariantCulture);
|
||
}
|
||
}
|
||
return s;
|
||
}
|
||
|
||
/// <summary>Консоль: подстройка на ходу. Значения живут до конца сессии.</summary>
|
||
public static string Command(List<string> _params, EntityPlayerLocal _player)
|
||
{
|
||
if (_params.Count == 0)
|
||
{
|
||
return Describe();
|
||
}
|
||
string cmd = _params[0].ToLowerInvariant();
|
||
if (cmd == "on")
|
||
{
|
||
On(_player);
|
||
return Describe();
|
||
}
|
||
if (cmd == "off")
|
||
{
|
||
Off(_player);
|
||
return "necrovision: off";
|
||
}
|
||
if (cmd == "reset")
|
||
{
|
||
_tuned.Clear();
|
||
_tunedColor = null;
|
||
_steps = 0;
|
||
_posterize = false;
|
||
_night = null;
|
||
_strength = null;
|
||
_bright = null;
|
||
}
|
||
else if (cmd == "color" && _params.Count >= 4 &&
|
||
float.TryParse(_params[1], NumberStyles.Float, CultureInfo.InvariantCulture, out float r) &&
|
||
float.TryParse(_params[2], NumberStyles.Float, CultureInfo.InvariantCulture, out float g) &&
|
||
float.TryParse(_params[3], NumberStyles.Float, CultureInfo.InvariantCulture, out float b))
|
||
{
|
||
_tunedColor = new Color(r, g, b, 1f);
|
||
}
|
||
else if (cmd == "nv" && _params.Count >= 2 &&
|
||
float.TryParse(_params[1], NumberStyles.Float, CultureInfo.InvariantCulture, out float nv))
|
||
{
|
||
_night = nv;
|
||
}
|
||
else if (cmd == "strength" && _params.Count >= 2 &&
|
||
float.TryParse(_params[1], NumberStyles.Float, CultureInfo.InvariantCulture, out float st))
|
||
{
|
||
_strength = st;
|
||
}
|
||
else if (cmd == "bright" && _params.Count >= 2 &&
|
||
float.TryParse(_params[1], NumberStyles.Float, CultureInfo.InvariantCulture, out float br))
|
||
{
|
||
_bright = br;
|
||
}
|
||
else if (cmd == "posterize" && _params.Count >= 2)
|
||
{
|
||
if (_params[1] == "off")
|
||
{
|
||
_posterize = false;
|
||
}
|
||
else if (int.TryParse(_params[1], out int steps))
|
||
{
|
||
_posterize = true;
|
||
_steps = steps;
|
||
}
|
||
}
|
||
else if (_params.Count >= 2 && float.TryParse(_params[1], NumberStyles.Float, CultureInfo.InvariantCulture, out float v))
|
||
{
|
||
string prop = null;
|
||
foreach (string p in FloatProps)
|
||
{
|
||
if (p.ToLowerInvariant() == cmd || p.TrimStart('_').ToLowerInvariant() == cmd)
|
||
{
|
||
prop = p;
|
||
}
|
||
}
|
||
if (prop == null)
|
||
{
|
||
return "necrovision: unknown '" + cmd + "'. Try: help necrovision";
|
||
}
|
||
_tuned[prop] = v;
|
||
}
|
||
else
|
||
{
|
||
return "necrovision: bad arguments. Try: help necrovision";
|
||
}
|
||
if (_on && _player?.ScreenEffectManager != null)
|
||
{
|
||
Apply(_player.ScreenEffectManager);
|
||
}
|
||
return Describe();
|
||
}
|
||
}
|
||
|
||
[Preserve]
|
||
public class ConsoleCmdNecroVision : ConsoleCmdAbstract
|
||
{
|
||
public override bool IsExecuteOnClient => true;
|
||
|
||
public override bool AllowedInMainMenu => false;
|
||
|
||
public override string[] getCommands()
|
||
{
|
||
return new string[] { "necrovision" };
|
||
}
|
||
|
||
public override string getDescription()
|
||
{
|
||
return "Necromancer's Tome: tune the snake's vision filter live.";
|
||
}
|
||
|
||
public override string getHelp()
|
||
{
|
||
return "necrovision - show current values\n" +
|
||
"necrovision on | off - switch the filter (snake mode switches it itself)\n" +
|
||
"necrovision color R G B - tint, 0..1 each (default 0.8 0.55 1)\n" +
|
||
"necrovision contrast V - also: saturation, lows, mids, highs, whitepoint, gamma, intensity\n" +
|
||
"necrovision strength V - how strongly the tint lies over the picture, 0..1 (default 0.5)\n" +
|
||
"necrovision posterize N|off - flat colour steps, always full strength (default off)\n" +
|
||
"necrovision nv V - game night vision under the red, 0..1 (default 0 = off; full strength only, kills colour)\n" +
|
||
"necrovision bright V - game 'Bright' grade, 0 = off 0..1 (default 0.5)\n" +
|
||
"necrovision reset - back to defaults";
|
||
}
|
||
|
||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||
{
|
||
EntityPlayerLocal player = GameManager.Instance.World?.GetPrimaryPlayer();
|
||
SingletonMonoBehaviour<SdtdConsole>.Instance.Output(SnakeVision.Command(_params, player));
|
||
}
|
||
}
|
||
}
|