Files
necromants-tome-7d2d-3-2/HarmonySrc/CinerariumGenie.cs
T
AlexCubeandClaude Opus 5.5 0890ffe1e9 Некротехнолог: работа духа - расходники коллекторов и готовое из станций
Раз в 3 с одно дело: (1) пополнить из ящиков зоны и сумки урны ячейки
расходников любого коллектора (банки, цветы, корм, сферы) и катализатора
(куры, по одной в пустую ячейку); (2) забрать готовое из коллекторов и
станций (верстак, кузня, химстанция, костёр...) в ящики зоны - сначала
к таким же, потом в пустое, ящики полны - в сумку урны; некуда - дух
чернеет и замирает, оживает сам. Модификации, инструменты, топливо и
сырьё станций не трогаются; открытое у игрока и закреплённые ячейки -
тоже. Модель духа плывёт к месту работы и обратно. Грядки - позже.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:43:36 +03:00

296 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Дух-работник урны - «джинн из урны» (решение 16.10): кончик хвоста уходит в горлышко, дух вырастает над урной,
/// без работы висит, чуть покачиваясь и медленно поворачиваясь. Этап 3 некротехнолога.
///
/// Не сущность игры, а видимая модель - дочерний объект модели блока урны, как каркас зоны (решение 16.10 вместо
/// плана 15.10 «сущность на префабе медсестры»): хитбокса, ИИ и сохранения у неё нет, поэтому «убить нельзя»,
/// «зомби не замечают», «сквозь стены» выходят сами, а сейвы дух не трогает - он появляется по черепу в урне
/// (TEFeatureCinerarium.HasSkull) и пропадает без него. Сдвиги Origin и выгрузку чанка переживает с моделью урны.
///
/// Модель - бандл necrotechnologist (NecroTechnologistSetup.cs): своя сетка на своём скелете NT_rig в позе покоя,
/// глаза - отдельные рендереры (в имени «eye»). Тело - полупрозрачное, как у торговцев-призраков (alpha 0.3, Standard
/// в режиме Fade, как у крысы при проходе); текстура уже серая (сделана под обесцвечивание). Глаза - свой
/// непрозрачный светящийся материал из бандла, очередь отрисовки 3100 - после тела.
/// </summary>
public static class CinerariumGenie
{
public const string ModelPath = "#@modfolder(NecromancerTome):Resources/necrotechnologist?necroTechnologistPrefab.prefab";
/// <summary>Высота корня модели над низом блока урны, м (кончик хвоста в модели - на 0.45 м). Подгоняется командой necrogenie.</summary>
public static float OffsetY = -0.25f;
/// <summary>Поворот без работы, градусов в секунду.</summary>
public static float TurnSpeed = 6f;
/// <summary>Покачивание: размах (м) и период (с).</summary>
public static float BobHeight = 0.04f;
public static float BobPeriod = 4f;
public static float Alpha = 0.3f;
static GameObject _prefab;
static bool _tried;
static readonly Dictionary<Material, Material> _ghostMats = new Dictionary<Material, Material>();
/// <summary>Все джинны на сцене - команда necrogenie правит их сразу.</summary>
public static readonly List<CinerariumGenieMotion> Live = new List<CinerariumGenieMotion>();
static bool Load()
{
if (!_tried)
{
_tried = true;
_prefab = DataLoader.LoadAsset<GameObject>(ModelPath);
Debug.Log("[NecromancerTome] CinerariumGenie: модель " + (_prefab != null ? "загружена" : "НЕ загружена: " + ModelPath));
}
return _prefab != null;
}
public static GameObject Spawn(Transform _urnModel)
{
if (_urnModel == null || !Load())
{
return null;
}
GameObject g = Object.Instantiate(_prefab, _urnModel, false);
g.name = "NecroCinerariumGenie";
foreach (Renderer r in g.GetComponentsInChildren<Renderer>(true))
{
r.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
r.receiveShadows = false;
if (r is SkinnedMeshRenderer smr)
{
smr.updateWhenOffscreen = true; // скелет свой и неподвижный - границы не пересчитываются, не пропадать с краю экрана
}
if (r.name.Contains("eye"))
{
continue;
}
Material[] mats = r.sharedMaterials;
for (int i = 0; i < mats.Length; i++)
{
mats[i] = Ghost(mats[i]);
}
r.sharedMaterials = mats;
}
foreach (Collider c in g.GetComponentsInChildren<Collider>(true))
{
Object.Destroy(c);
}
g.AddComponent<CinerariumGenieMotion>();
return g;
}
/// <summary>Полупрозрачная копия материала тела - одна на исходный материал на всю игру.</summary>
static Material Ghost(Material _src)
{
if (_src == null)
{
return null;
}
if (_ghostMats.TryGetValue(_src, out Material m) && m != null)
{
return m;
}
m = new Material(_src) { name = _src.name + "_ghost" };
Shader std = Shader.Find("Standard");
if (std != null && m.shader != std)
{
m.shader = std;
}
m.SetFloat("_Mode", 2f);
m.SetOverrideTag("RenderType", "Transparent");
m.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
m.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
m.SetInt("_ZWrite", 0);
m.DisableKeyword("_ALPHATEST_ON");
m.EnableKeyword("_ALPHABLEND_ON");
m.DisableKeyword("_ALPHAPREMULTIPLY_ON");
m.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
SetAlpha(m, Alpha);
_ghostMats[_src] = m;
return m;
}
static void SetAlpha(Material _m, float _a)
{
if (_m != null && _m.HasProperty("_Color"))
{
Color c = _m.color;
c.a = _a;
_m.color = c;
}
}
public static void ApplyAlpha()
{
foreach (Material m in _ghostMats.Values)
{
SetAlpha(m, Alpha);
}
}
}
/// <summary>Покачивание и поворот джинна над урной. Только вид - никакой логики.</summary>
[Preserve]
public class CinerariumGenieMotion : MonoBehaviour
{
float _yaw;
float _phase;
/// <summary>Полёт к месту работы и обратно: точка в координатах модели урны и сколько осталось.</summary>
Vector3 _visit;
float _visitLeft;
public const float VisitSeconds = 2.2f;
/// <summary>Почернел и замер (готовое класть некуда, решение 06.10).</summary>
bool _stuck;
static readonly MaterialPropertyBlock _mpb = new MaterialPropertyBlock();
static readonly int ColorId = Shader.PropertyToID("_Color");
/// <summary>_worldUnity - точка в координатах сцены (без Origin): низ блока, где дух работал.</summary>
public void Visit(Vector3 _worldUnity)
{
if (transform.parent == null)
{
return;
}
_visit = transform.parent.InverseTransformPoint(_worldUnity);
_visitLeft = VisitSeconds;
}
public void SetStuck(bool _on)
{
if (_stuck == _on)
{
return;
}
_stuck = _on;
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
{
if (r.name.Contains("eye"))
{
continue;
}
if (_on)
{
r.GetPropertyBlock(_mpb);
_mpb.SetColor(ColorId, new Color(0.04f, 0.035f, 0.045f, Mathf.Max(CinerariumGenie.Alpha, 0.6f)));
r.SetPropertyBlock(_mpb);
}
else
{
r.SetPropertyBlock(null);
}
}
}
void OnEnable()
{
CinerariumGenie.Live.Add(this);
_phase = Random.value * 10f;
_yaw = Random.value * 360f;
}
void OnDisable()
{
CinerariumGenie.Live.Remove(this);
}
void Update()
{
if (_stuck)
{
return; // замер как был
}
Vector3 home = new Vector3(0f, CinerariumGenie.OffsetY, 0f);
float yawTarget = _yaw + CinerariumGenie.TurnSpeed * Time.deltaTime;
if (_visitLeft > 0f)
{
// Туда и обратно по синусу: на полпути - над местом работы, лицом к нему.
_visitLeft -= Time.deltaTime;
float k = Mathf.Sin(Mathf.Clamp01(1f - _visitLeft / VisitSeconds) * Mathf.PI);
Vector3 over = _visit + new Vector3(0f, CinerariumGenie.OffsetY + 0.6f, 0f);
transform.localPosition = Vector3.Lerp(home, over, k);
Vector3 d = _visit - home;
if (d.x * d.x + d.z * d.z > 0.01f)
{
yawTarget = Mathf.LerpAngle(_yaw, Mathf.Atan2(d.x, d.z) * Mathf.Rad2Deg, Time.deltaTime * 4f);
}
_yaw = yawTarget % 360f;
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
return;
}
_yaw = yawTarget % 360f;
float bob = CinerariumGenie.BobPeriod > 0.01f
? Mathf.Sin((Time.time + _phase) * Mathf.PI * 2f / CinerariumGenie.BobPeriod) * CinerariumGenie.BobHeight
: 0f;
transform.localPosition = home + new Vector3(0f, bob, 0f);
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
}
}
/// <summary>Подгонка джинна в игре: высота над урной, скорость поворота, покачивание, прозрачность. Не сохраняется -
/// найденные значения переносятся в константы CinerariumGenie.</summary>
[Preserve]
public class ConsoleCmdNecroGenie : ConsoleCmdAbstract
{
public override bool IsExecuteOnClient => true;
public override bool AllowedInMainMenu => false;
public override string[] getCommands()
{
return new string[] { "necrogenie" };
}
public override string getDescription()
{
return "Necromancer's Tome: tune the cinerarium spirit (height over the urn, turning, bobbing, transparency).";
}
public override string getHelp()
{
return "necrogenie - show the current values and how many spirits are on screen\n" +
"necrogenie y <m> - model root height above the urn block's bottom (tail tip is 0.45 m above the root)\n" +
"necrogenie turn <deg/s> - idle turning speed\n" +
"necrogenie bob <m> <s> - bobbing height and period\n" +
"necrogenie alpha <0-1> - body opacity";
}
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{
SdtdConsole con = SingletonMonoBehaviour<SdtdConsole>.Instance;
if (_params.Count >= 2 && float.TryParse(_params[1], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float v))
{
switch (_params[0].ToLowerInvariant())
{
case "y":
CinerariumGenie.OffsetY = v;
break;
case "turn":
CinerariumGenie.TurnSpeed = v;
break;
case "bob":
CinerariumGenie.BobHeight = v;
if (_params.Count >= 3 && float.TryParse(_params[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float per))
{
CinerariumGenie.BobPeriod = per;
}
break;
case "alpha":
CinerariumGenie.Alpha = Mathf.Clamp01(v);
CinerariumGenie.ApplyAlpha();
break;
default:
con.Output("necrogenie: unknown '" + _params[0] + "' - see 'help necrogenie'");
return;
}
}
con.Output("necrogenie: y " + CinerariumGenie.OffsetY.ToString("F2") + ", turn " + CinerariumGenie.TurnSpeed.ToString("F1") +
", bob " + CinerariumGenie.BobHeight.ToString("F2") + " / " + CinerariumGenie.BobPeriod.ToString("F1") + " s, alpha " +
CinerariumGenie.Alpha.ToString("F2") + "; spirits on screen: " + CinerariumGenie.Live.Count);
}
}
}