Логист - латунное кадило: два вида, дух 1/3 из окон, мигание; команды разработки

- Блоки логиста на модели кадила: по центру на подставке (necroCinerariumLogist) и новый вид у
  стены на кронштейне (necroCinerariumLogistWall/+Skull); выбор вида - R с урной в руке.
- Дух логиста втрое меньше, в покое сидит в кадиле, на перевозку вылетает через окно крышки
  (у стены - не в стену) и возвращается тем же окном.
- Застрял (ящик «куда» полон) - урна мигает (CinerariumBlink) вместо почерневшего духа;
  прежняя зелёная подмена свечения старой модели убрана.
- Команды разработки без мыши: necrospawn, necroexit [quit], necrolook, necroaim, necroplace,
  necrohold, necrobox, necrourn, necroclick.

Проверено в игре (test8): перевозка обоими видами, вылет духа, вид у стены от стены.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-18 14:19:50 +03:00
co-authored by Claude Opus 5.5
parent b5bd4e4b2c
commit 6693d96d5e
11 changed files with 630 additions and 71 deletions
+42 -50
View File
@@ -35,11 +35,20 @@ namespace NecromancerTome
/// без зоны - ставится вплотную к любым урнам, только нити и перевозка, глазницы всегда зелёные. Лимит - общий.</summary>
public const string LogistBlockName = "necroCinerariumLogist";
public const string LogistSkullBlockName = "necroCinerariumLogistSkull";
/// <summary>Логист у стены (18.10, кадило на кронштейне): тот же логист, другой вид - выбор в меню R (CinerariumLogistForm).</summary>
public const string LogistWallBlockName = "necroCinerariumLogistWall";
public const string LogistWallSkullBlockName = "necroCinerariumLogistWallSkull";
public static bool IsLogistBlock(Block _b)
{
string n = _b?.GetBlockName();
return n == LogistBlockName || n == LogistSkullBlockName;
return n == LogistBlockName || n == LogistSkullBlockName || n == LogistWallBlockName || n == LogistWallSkullBlockName;
}
public static bool IsLogistWallBlock(Block _b)
{
string n = _b?.GetBlockName();
return n == LogistWallBlockName || n == LogistWallSkullBlockName;
}
/// <summary>Урна рабочего (17.10, BACKLOG «Урна рабочего»): изготавливает предмет из ячейки образца на станциях, к которым
/// от неё тянутся нити; ингредиенты - из ящиков с нитями к урне, готовое - в ящики с нитями от урны. Без зоны и вне лимита,
@@ -143,7 +152,7 @@ namespace NecromancerTome
_worker = IsWorkerBlock(_blockValue.Block);
_logistKnown = true;
string placed = _blockValue.Block?.GetBlockName();
if (placed == SkullBlockName || placed == LogistSkullBlockName || placed == WorkerSkullBlockName)
if (placed == SkullBlockName || placed == LogistSkullBlockName || placed == LogistWallSkullBlockName || placed == WorkerSkullBlockName)
{
HasSkull = true; // поставили урну, поднятую с черепом
}
@@ -543,9 +552,10 @@ namespace NecromancerTome
if (stuck != Stuck)
{
Stuck = stuck;
Debug.Log("[NecromancerTome] Cinerarium: дух урны " + ToWorldPos() + (stuck ? " почернел - готовое класть некуда" : " ожил"));
_glowState = -1; // логист - мигание
Debug.Log("[NecromancerTome] Cinerarium: дух урны " + ToWorldPos() + (stuck ? (IsLogist ? " застрял - ящик «куда» полон, урна мигает" : " почернел - готовое класть некуда") : " ожил"));
}
if (motion != null)
if (motion != null && !IsLogist)
{
motion.SetStuck(Stuck && at == null);
}
@@ -683,7 +693,13 @@ namespace NecromancerTome
SyncGlow();
if (HasSkull && _genie == null)
{
_genie = CinerariumGenie.Spawn(ModelTransform(), IsWorker ? CinerariumGenie.WorkerDrop : 0f);
int logist = 0;
if (IsLogist)
{
World w = GameManager.Instance?.World;
logist = w != null && IsLogistWallBlock(w.GetBlock(ToWorldPos()).Block) ? 2 : 1;
}
_genie = CinerariumGenie.Spawn(ModelTransform(), IsWorker ? CinerariumGenie.WorkerDrop : 0f, logist);
if (_genie != null && _sound)
{
Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), GenieSound);
@@ -705,14 +721,15 @@ namespace NecromancerTome
void SyncGlow()
{
Transform t = ModelTransform();
// Логист (16.10: «Так сразу будет видно что логистика, а что технолог») - всегда своя текстура свечения (руны и
// глазницы зелёные) и зеленоватая глазурь; светится, как и обычная урна, только с черепом.
int want = (HasSkull ? 1 : 0) + (IsLogist ? 2 : 0);
Texture logist = IsLogist ? LogistEmission() : null;
// Логист (18.10, кадило): своё свечение в материале - внутренность кадила, светятся окна крышки. Застрял (класть
// некуда) - вместо почерневшего духа мигает (18.10: «При застревании должно мигать»): CinerariumBlink на модели.
bool blink = HasSkull && IsLogist && Stuck;
int want = (HasSkull ? 1 : 0) + (blink ? 2 : 0);
if (t == null || (_glowState == want && _glowModel == t))
{
return;
}
List<Renderer> own = new List<Renderer>();
foreach (Renderer r in t.GetComponentsInChildren<Renderer>(true))
{
if (_genie != null && r.transform.IsChildOf(_genie.transform) || _bounds != null && r.transform.IsChildOf(_bounds.transform) ||
@@ -720,53 +737,25 @@ namespace NecromancerTome
{
continue;
}
own.Add(r);
r.GetPropertyBlock(_glowBlock);
_glowBlock.SetColor(EmissionColorId, HasSkull ? GlowColor : Color.black);
if (logist != null)
{
_glowBlock.SetTexture(EmissionMapId, logist);
_glowBlock.SetColor(AlbedoColorId, LogistTint);
}
r.SetPropertyBlock(_glowBlock);
}
CinerariumBlink b = t.GetComponent<CinerariumBlink>();
if (blink && b == null)
{
b = t.gameObject.AddComponent<CinerariumBlink>();
}
if (b != null)
{
b.Renderers = own;
b.enabled = blink;
}
_glowState = want;
_glowModel = t;
}
/// <summary>Глазурь урны-логиста - множитель цвета материала (чёрная глазурь с зелёным отливом).</summary>
public static Color LogistTint = new Color(0.55f, 1f, 0.75f, 1f);
static readonly int AlbedoColorId = Shader.PropertyToID("_Color");
static readonly int EmissionMapId = Shader.PropertyToID("_EmissionMap");
static Texture2D _logistTex;
static bool _logistTried;
public const string LogistTextureFile = "necroCinerarium_e_logist.png";
/// <summary>Свечение урны-логиста (16.10: «пусть глаза черепов светятся другим цветом. Зелёным»): копия запечённой
/// necroCinerarium_e с зелёными глазницами (руны - те же), из папки Resources мода - бандл не пересобирается.</summary>
static Texture2D LogistEmission()
{
if (!_logistTried)
{
_logistTried = true;
try
{
string path = Path.Combine(ModEntry.Instance.Path, "Resources", LogistTextureFile);
Texture2D tex = new Texture2D(2, 2, TextureFormat.RGBA32, true);
if (tex.LoadImage(File.ReadAllBytes(path)))
{
tex.name = "necroCinerarium_e_logist";
tex.anisoLevel = 4;
_logistTex = tex;
}
}
catch (Exception e)
{
Debug.LogWarning("[NecromancerTome] Cinerarium: свечение логиста не загружено (" + e.Message + ")");
}
}
return _logistTex;
}
Transform _glowModel;
void RemoveGenie(bool _sound)
@@ -864,7 +853,7 @@ namespace NecromancerTome
{
string n = _b?.GetBlockName();
return n == BlockName || n == SkullBlockName || n == LogistBlockName || n == LogistSkullBlockName ||
n == WorkerBlockName || n == WorkerSkullBlockName;
n == LogistWallBlockName || n == LogistWallSkullBlockName || n == WorkerBlockName || n == WorkerSkullBlockName;
}
public static int UrnLimit(EntityPlayer _player)
@@ -1224,8 +1213,11 @@ namespace NecromancerTome
return false;
}
bool logist = TEFeatureCinerarium.IsLogistBlock(bv.Block);
bool wall = TEFeatureCinerarium.IsLogistWallBlock(bv.Block);
bool worker = TEFeatureCinerarium.IsWorkerBlock(bv.Block);
ItemValue iv = ItemClass.GetItem(logist
ItemValue iv = ItemClass.GetItem(wall
? (urn.HasSkull ? TEFeatureCinerarium.LogistWallSkullBlockName : TEFeatureCinerarium.LogistWallBlockName)
: logist
? (urn.HasSkull ? TEFeatureCinerarium.LogistSkullBlockName : TEFeatureCinerarium.LogistBlockName)
: worker
? (urn.HasSkull ? TEFeatureCinerarium.WorkerSkullBlockName : TEFeatureCinerarium.WorkerBlockName)
+87 -12
View File
@@ -74,7 +74,17 @@ namespace NecromancerTome
return _prefab != null;
}
public static GameObject Spawn(Transform _urnModel, float _drop)
/// <summary>Дух логиста (18.10: «Пусть дух вылетает когда нужно что-то нести. Но уменьшим его в три раза»): размер.</summary>
public static float LogistScale = 1f / 3f;
/// <summary>Дом духа логиста в осях модели урны - корень модели духа внутри кадила, середина тела на высоте окон крышки
/// (по центру - окна на 0.63 м, у стены - 0.73 м, кадило в 0.22 м от стены, стена - -z).</summary>
public static Vector3 LogistHome = new Vector3(0f, 0.25f, 0f);
public static Vector3 LogistWallHome = new Vector3(0f, 0.35f, -0.22f);
/// <summary>Окна крышки - на расстоянии LogistExit от оси кадила (радиус крышки 0.21 м), по осям модели.</summary>
public static float LogistExit = 0.3f;
/// <summary>_logist: 0 - обычный дух над горлышком, 1 - дух логиста в кадиле по центру, 2 - в кадиле у стены.</summary>
public static GameObject Spawn(Transform _urnModel, float _drop, int _logist = 0)
{
if (_urnModel == null || !Load())
{
@@ -107,6 +117,12 @@ namespace NecromancerTome
}
CinerariumGenieMotion m = g.AddComponent<CinerariumGenieMotion>();
m.Drop = _drop;
if (_logist > 0)
{
m.Small = true;
m.Wall = _logist == 2;
g.transform.localScale = Vector3.one * LogistScale;
}
m.Pose();
return g;
}
@@ -273,8 +289,14 @@ namespace NecromancerTome
{
public Vector3 Hover, Look; // в осях модели урны
public float Fly;
public bool Pass; // точка пути (окно кадила) - без жеста
}
/// <summary>Дух логиста (18.10): втрое меньше, дома (в кадиле) не виден, вылетает и влетает через окно крышки.</summary>
public bool Small;
/// <summary>Кадило у стены: дом сдвинут к кронштейну, окно в стену (-z) не используется.</summary>
public bool Wall;
readonly List<Stop> _stops = new List<Stop>();
float _backFly;
float _t = -1f;
@@ -374,11 +396,23 @@ namespace NecromancerTome
Vector3 home = Home();
Vector3 prev = home;
float delay = 0f;
Vector3 exit = home;
if (Small)
{
// Сначала - к окну крышки в сторону первого места (у стены окно в стену не годится), оттуда - к месту.
exit = Window(home, transform.parent.InverseTransformPoint(_list[0].stand) - home);
_stops.Add(new Stop { Hover = exit, Look = exit + (exit - home), Fly = 0.35f, Pass = true });
delay += 0.35f;
prev = exit;
}
foreach ((Vector3 stand, Vector3 look, bool beside) in _list)
{
// Высота корня духа над низом клетки места: обычный дух - над станцией 0.8 (сбоку 0.15) с поправкой урны;
// маленький - над верхом клетки (1.05) или у её середины сбоку (0.4).
float up = Small ? (beside ? 0.4f : 1.05f) : CinerariumGenie.OffsetY + Drop + (beside ? 0.15f : 0.8f);
Stop st = new Stop
{
Hover = transform.parent.InverseTransformPoint(stand) + new Vector3(0f, CinerariumGenie.OffsetY + Drop + (beside ? 0.15f : 0.8f), 0f),
Hover = transform.parent.InverseTransformPoint(stand) + new Vector3(0f, up, 0f),
Look = transform.parent.InverseTransformPoint(look)
};
st.Fly = Mathf.Max(OutSeconds, Vector3.Distance(prev, st.Hover) / FlySpeed);
@@ -386,13 +420,44 @@ namespace NecromancerTome
prev = st.Hover;
_stops.Add(st);
}
_backFly = Mathf.Max(BackSeconds, Vector3.Distance(prev, home) / FlySpeed);
if (Small)
{
// Назад - тем же окном.
_stops.Add(new Stop { Hover = exit, Look = home, Fly = Mathf.Max(BackSeconds, Vector3.Distance(prev, exit) / FlySpeed), Pass = true });
prev = exit;
}
_backFly = Small ? 0.35f : Mathf.Max(BackSeconds, Vector3.Distance(prev, home) / FlySpeed);
_t = 0f;
return delay - WorkSeconds * 0.5f;
}
/// <summary>Окно крышки кадила по направлению _dir (оси модели): одно из четырёх по осям x/z, у стены - не в стену.</summary>
Vector3 Window(Vector3 _home, Vector3 _dir)
{
Vector3 best = Vector3.forward;
float bestDot = float.MinValue;
foreach (Vector3 w in new[] { Vector3.forward, Vector3.back, Vector3.left, Vector3.right })
{
if (Wall && w == Vector3.back)
{
continue;
}
float d = Vector3.Dot(w, new Vector3(_dir.x, 0f, _dir.z));
if (d > bestDot)
{
bestDot = d;
best = w;
}
}
return _home + best * CinerariumGenie.LogistExit;
}
Vector3 Home()
{
if (Small)
{
return Wall ? CinerariumGenie.LogistWallHome : CinerariumGenie.LogistHome;
}
return new Vector3(0f, CinerariumGenie.OffsetY + Drop, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward);
}
@@ -483,14 +548,16 @@ namespace NecromancerTome
transform.localPosition = Home();
}
_asleep = asleep;
if (_hiddenApplied != CinerariumGenie.Hidden)
}
// Виден ли: не скрыт командой; дух логиста - только в полёте (дома сидит в кадиле). Рендереры - только при смене
// (16.10, проверка на утечки: раньше список рендереров собирался каждые 0.5 с - мусор для GC).
bool show = !CinerariumGenie.Hidden && (!Small || _t >= 0f);
if (_hiddenApplied != !show)
{
_hiddenApplied = !show;
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
{
// Только при смене (16.10, проверка на утечки: раньше список рендереров собирался каждые 0.5 с - мусор для GC).
_hiddenApplied = CinerariumGenie.Hidden;
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
{
r.enabled = !_hiddenApplied;
}
r.enabled = show;
}
}
if (_stuck || _asleep)
@@ -515,7 +582,7 @@ namespace NecromancerTome
return;
}
acc += st.Fly;
if (_t < acc + WorkSeconds)
if (!st.Pass && _t < acc + WorkSeconds)
{
transform.localPosition = st.Hover;
Face(st.Look - st.Hover); // за работой - лицом к месту
@@ -523,7 +590,10 @@ namespace NecromancerTome
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
return;
}
acc += WorkSeconds;
if (!st.Pass)
{
acc += WorkSeconds;
}
from = st.Hover;
}
if (_t < acc + _backFly)
@@ -537,6 +607,11 @@ namespace NecromancerTome
_t = -1f;
Gesture(0f);
}
if (Small)
{
transform.localPosition = home; // в кадиле, не виден
return;
}
_yaw = yawTarget % 360f;
float bob = CinerariumGenie.BobPeriod > 0.01f
? Mathf.Sin((Time.time + _phase) * Mathf.PI * 2f / CinerariumGenie.BobPeriod) * CinerariumGenie.BobHeight
+126
View File
@@ -0,0 +1,126 @@
using System.Collections.Generic;
using GUI_2;
using HarmonyLib;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Урна логиста - латунное кадило (18.10.2026, BACKLOG «Урна логиста - своя модель: кадило»): два вида - на подставке по
/// центру клетки (necroCinerariumLogist) и на кронштейне у стены (necroCinerariumLogistWall). Пользователь: «Должна быть
/// возможность крепить её и к стене, и по центру какого-нибудь столба... как мы делали с черепом. Только череп у нас изначально
/// не блок, а тут должно получиться попроще». Вид выбирается, как у черепа-декора: урна в руке, R - меню блока игры, к нему
/// добавлен второй вид; выбор превращает стопку в ячейке пояса в другой блок (тот же счёт). Пустая урна и урна с черепом -
/// каждая в свой вид того же рода.
/// </summary>
public static class CinerariumLogistForm
{
/// <summary>Номера наших пунктов в меню блока - вне занятых игрой (-2..10) и черепом (40..42).</summary>
public const int MenuCenter = 43, MenuWall = 44;
static readonly Dictionary<string, (string center, string wall)> Pairs = new Dictionary<string, (string, string)>
{
{ TEFeatureCinerarium.LogistBlockName, (TEFeatureCinerarium.LogistBlockName, TEFeatureCinerarium.LogistWallBlockName) },
{ TEFeatureCinerarium.LogistWallBlockName, (TEFeatureCinerarium.LogistBlockName, TEFeatureCinerarium.LogistWallBlockName) },
{ TEFeatureCinerarium.LogistSkullBlockName, (TEFeatureCinerarium.LogistSkullBlockName, TEFeatureCinerarium.LogistWallSkullBlockName) },
{ TEFeatureCinerarium.LogistWallSkullBlockName, (TEFeatureCinerarium.LogistSkullBlockName, TEFeatureCinerarium.LogistWallSkullBlockName) },
};
public static bool IsLogistItem(ItemClass _item) => _item != null && Pairs.ContainsKey(_item.Name);
static void Convert(EntityPlayerLocal _pl, bool _wall)
{
ItemStack cur = _pl?.inventory?.holdingItemStack;
if (cur == null || cur.IsEmpty() || !Pairs.TryGetValue(cur.itemValue.ItemClass.Name, out var pair))
{
return;
}
string name = _wall ? pair.wall : pair.center;
if (cur.itemValue.ItemClass.Name == name)
{
return;
}
ItemValue iv = ItemClass.GetItem(name);
if (iv == null || iv.IsEmpty())
{
return;
}
_pl.inventory.SetItem(_pl.inventory.holdingItemIdx, new ItemStack(iv, cur.count));
}
public static void OnBlockMenu(XUiC_Radial _sender, int _commandIndex, XUiC_Radial.RadialContextAbs _context)
{
EntityPlayerLocal pl = _sender?.xui?.playerUI?.entityPlayer;
switch (_commandIndex)
{
case MenuCenter: Convert(pl, false); return;
case MenuWall: Convert(pl, true); return;
}
_sender.handleBlockShapeCommand(_sender, _commandIndex, _context);
}
}
/// <summary>Меню блока с урной логиста в руке: к пунктам игры - второй вид (подставка / у стены).</summary>
[HarmonyPatch(typeof(XUiC_Radial), nameof(XUiC_Radial.SetupBlockShapeData))]
public static class Patch_XUiC_Radial_SetupBlockShapeData_LogistForm
{
public static void Postfix(XUiC_Radial __instance)
{
EntityPlayerLocal pl = __instance.xui?.playerUI?.entityPlayer;
ItemClass held = pl?.inventory?.holdingItem;
if (!CinerariumLogistForm.IsLogistItem(held))
{
return;
}
bool wall = held.Name == TEFeatureCinerarium.LogistWallBlockName || held.Name == TEFeatureCinerarium.LogistWallSkullBlockName;
if (wall)
{
__instance.CreateRadialEntry(CinerariumLogistForm.MenuCenter, TEFeatureCinerarium.LogistBlockName, "ItemIconAtlas", "", Localization.Get("necroCinerariumLogistFormCenter"));
}
else
{
__instance.CreateRadialEntry(CinerariumLogistForm.MenuWall, TEFeatureCinerarium.LogistWallBlockName, "ItemIconAtlas", "", Localization.Get("necroCinerariumLogistFormWall"));
}
__instance.SetCommonData(UIUtils.GetButtonIconForAction(__instance.xui.playerUI.playerInput.Activate), CinerariumLogistForm.OnBlockMenu,
new XUiC_Radial.RadialContextHoldingSlotIndex(pl.inventory.holdingItemIdx), -1, true, XUiC_Radial.RadialValidSameHoldingSlotIndex);
}
}
/// <summary>Мигание урны логиста, когда класть некуда (18.10: «При застревании должно мигать»): свечение модели
/// (MaterialPropertyBlock, как у TEFeatureCinerarium.SyncGlow) пульсирует от тёмного до обычного. Выключено - обычное свечение.</summary>
[Preserve]
public class CinerariumBlink : MonoBehaviour
{
public List<Renderer> Renderers = new List<Renderer>();
public static float Period = 0.8f;
static readonly MaterialPropertyBlock _block = new MaterialPropertyBlock();
static readonly int EmissionColorId = Shader.PropertyToID("_EmissionColor");
void Update()
{
// Резкий переход - мигание, а не дыхание: половину периода горит, половину почти погасла.
float k = Mathf.Repeat(Time.time, Period) < Period * 0.5f ? 1f : 0.08f;
Set(TEFeatureCinerarium.GlowColor * k);
}
void OnDisable()
{
Set(TEFeatureCinerarium.GlowColor);
}
void Set(Color _c)
{
foreach (Renderer r in Renderers)
{
if (r == null)
{
continue;
}
r.GetPropertyBlock(_block);
_block.SetColor(EmissionColorId, _c);
r.SetPropertyBlock(_block);
}
}
}
}
+12 -5
View File
@@ -69,7 +69,7 @@ namespace NecromancerTome
// Урна с нитями - логист (16.10: «занимается только перетаскиванием из ящиков в ящики»).
if (_urn.IsLogist)
{
return Ferry(_world, _urn);
return Ferry(_world, _urn, out _stuck);
}
// Урна рабочего - изготовление по образцу на станциях по нитям (CinerariumCraft.cs).
if (_urn.IsWorker)
@@ -139,10 +139,11 @@ namespace NecromancerTome
}
/// <summary>Перевозка по нитям: одна стопка из ящика «откуда» в ящик «куда». Только предметы включённых в урне групп
/// (фильтр логиста, CinerariumGroups; 16.10 заменил прежнее правило «по образцу»); «куда» полон - ждём (дух не чернеет). Открытые у игрока ящики и закреплённые
/// ячейки не трогаются. Возвращает ящик «куда», LastFrom - откуда взято.</summary>
static Vector3i? Ferry(World _world, TEFeatureCinerarium _urn)
/// (фильтр логиста, CinerariumGroups; 16.10 заменил прежнее правило «по образцу»); «куда» полон - ждём, _stuck - урна
/// мигает (18.10). Открытые у игрока ящики и закреплённые ячейки не трогаются. Возвращает ящик «куда», LastFrom - откуда взято.</summary>
static Vector3i? Ferry(World _world, TEFeatureCinerarium _urn, out bool _stuck)
{
_stuck = false;
if (!_urn.HasThreadTo)
{
return null;
@@ -166,11 +167,17 @@ namespace NecromancerTome
for (int k = 0; k < src.ItemGrid.Length; k++)
{
ItemStack s = src.ItemGrid[k];
if (s == null || s.IsEmpty() || Locked(src, k) || !_urn.GroupOn(CinerariumGroups.Of(s.itemValue)) || !Fits(dst, s))
if (s == null || s.IsEmpty() || Locked(src, k) || !_urn.GroupOn(CinerariumGroups.Of(s.itemValue)))
{
continue;
}
if (!Fits(dst, s))
{
_stuck = true; // есть что везти, но некуда
continue;
}
LastFrom = f;
_stuck = false; // везти есть куда - не застрял
if (_dry)
{
return _urn.ThreadTo;
+1 -1
View File
@@ -20,7 +20,7 @@ namespace NecromancerTome
static bool Ours(string _name)
{
return _name == "necroBookMindPrefab" || _name == "necroKnifeDecorPrefab" || _name == "necroMortarPrefab" || _name.StartsWith("necroBloodRose") || _name.StartsWith("necroPoster") || _name == "necroBookDoorPrefab" || _name.StartsWith("necroSkullDecor") || _name == "necroCinerariumPrefab" || _name == "necroCinerariumWorkerPrefab";
return _name == "necroBookMindPrefab" || _name == "necroKnifeDecorPrefab" || _name == "necroMortarPrefab" || _name.StartsWith("necroBloodRose") || _name.StartsWith("necroPoster") || _name == "necroBookDoorPrefab" || _name.StartsWith("necroSkullDecor") || _name == "necroCinerariumPrefab" || _name == "necroCinerariumWorkerPrefab" || _name.StartsWith("necroCinerariumLogist");
}
public static void Postfix(Transform __result)
+278
View File
@@ -0,0 +1,278 @@
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Команды разработки для проверок без мыши (18.10.2026: «И управление персонажем тоже, чтобы ты мог его повернуть как надо,
/// довести куда надо»). Набираются через почтовый ящик (NecroMailbox), ответы - в лог. Перемещение - teleport игры
/// (tp x y z), время - settime, предметы - giveself; здесь - то, чего у игры нет.
/// necrolook yaw [pitch] | necrolook at x y z - повернуть взгляд (градусы; север 0, восток 90; pitch вверх +, вниз -)
/// necroaim - позиция игрока и блок под прицелом
/// necroplace блок x y z [поворот 0-3] - поставить блок (как игрок: OnBlockAdded, TE)
/// necrohold предмет [число] - положить в текущую ячейку пояса и взять в руку
/// necrobox x y z list|clear|add предмет число - содержимое хранилища (ящик, сумка урны)
/// necrourn x y z info|skull 0|1|from|to|out bx by bz|clear|sample предмет число|group все|нет - урна
/// necroclick 0|1 - отпущенная ЛКМ/ПКМ ножом для нитей по блоку под прицелом
/// </summary>
public static class NecroDev
{
static void Out(string _s) => SdtdConsole.Instance.Output(_s);
static float F(string _s) => float.Parse(_s, CultureInfo.InvariantCulture);
static Vector3i P(List<string> _p, int _i) => new Vector3i(int.Parse(_p[_i]), int.Parse(_p[_i + 1]), int.Parse(_p[_i + 2]));
/// <summary>Общий вход всех команд: игрок есть - выполнить, ошибка - строкой в ответ.</summary>
public static void Run(string _cmd, List<string> _params)
{
World w = GameManager.Instance.World;
EntityPlayerLocal pl = w?.GetPrimaryPlayer();
if (pl == null)
{
Out(_cmd + ": нет игрока");
return;
}
try
{
switch (_cmd)
{
case "necrolook": Look(pl, _params); break;
case "necroaim": Aim(w, pl); break;
case "necroplace": PlaceBlock(w, _params); break;
case "necrohold": Hold(pl, _params); break;
case "necrobox": Box(w, _params); break;
case "necrourn": Urn(w, _params); break;
case "necroclick": CinerariumThreads.Click(pl, _params.Count > 0 && _params[0] == "1"); Out("necroclick: ok"); break;
}
}
catch (System.Exception e)
{
Out(_cmd + ": ошибка - " + e.Message);
}
}
static void Look(EntityPlayerLocal _pl, List<string> _p)
{
float yaw, pitch = 0f;
if (_p.Count >= 4 && _p[0] == "at")
{
Vector3 eye = _pl.getHeadPosition();
Vector3 t = new Vector3(F(_p[1]) + 0.5f, F(_p[2]) + 0.5f, F(_p[3]) + 0.5f);
Vector3 d = t - eye;
yaw = Mathf.Atan2(d.x, d.z) * Mathf.Rad2Deg;
pitch = Mathf.Atan2(d.y, new Vector2(d.x, d.z).magnitude) * Mathf.Rad2Deg; // в игре + вверх
}
else
{
yaw = F(_p[0]);
if (_p.Count > 1)
{
pitch = F(_p[1]);
}
}
_pl.TeleportToPosition(_pl.position, false, new Vector3(pitch, yaw, 0f));
Out("necrolook: yaw " + yaw.ToString("F0") + ", pitch " + pitch.ToString("F0"));
}
static void Aim(World _w, EntityPlayerLocal _pl)
{
StringBuilder sb = new StringBuilder("necroaim: игрок ").Append(_pl.position.ToString("F1")).Append(", взгляд ").Append(_pl.rotation.ToString("F0"));
WorldRayHitInfo hi = _pl.HitInfo;
if (hi != null && hi.bHitValid)
{
BlockValue bv = _w.GetBlock(hi.hit.blockPos);
sb.Append(", под прицелом ").Append(hi.hit.blockPos).Append(' ').Append(bv.Block?.GetBlockName()).Append(" rot ").Append(bv.rotation)
.Append(", дальность ").Append(Mathf.Sqrt(hi.hit.distanceSq).ToString("F1"));
}
else
{
sb.Append(", под прицелом ничего");
}
Out(sb.ToString());
}
static void PlaceBlock(World _w, List<string> _p)
{
Block b = Block.GetBlockByName(_p[0], true);
if (b == null)
{
Out("necroplace: нет блока " + _p[0]);
return;
}
BlockValue bv = b.ToBlockValue();
if (_p.Count > 4)
{
bv.rotation = (byte)int.Parse(_p[4]);
}
Vector3i pos = P(_p, 1);
_w.SetBlockRPC(pos, bv);
// Как поставленный игроком (хозяин - мы): ящики без хозяина нити и духи не считают ящиками игрока.
if (_w.GetTileEntity(pos) is TileEntityComposite comp && comp.Owner == null)
{
comp.SetOwner(Platform.PlatformManager.InternalLocalUserIdentifier);
}
Out("necroplace: " + b.GetBlockName() + " в " + pos + " rot " + bv.rotation);
}
static void Hold(EntityPlayerLocal _pl, List<string> _p)
{
ItemValue iv = ItemClass.GetItem(_p[0], true);
if (iv == null || iv.IsEmpty())
{
Out("necrohold: нет предмета " + _p[0]);
return;
}
int n = _p.Count > 1 ? int.Parse(_p[1]) : 1;
if (iv.ItemClass.HasQuality)
{
iv = new ItemValue(iv.type, 1, 1, true);
}
_pl.inventory.SetItem(_pl.inventory.holdingItemIdx, new ItemStack(iv, n));
Out("necrohold: " + iv.ItemClass.GetItemName() + " x" + n + " в ячейке " + _pl.inventory.holdingItemIdx);
}
static TEFeatureStorage StorageAt(World _w, Vector3i _p)
{
BlockValue bv = _w.GetBlock(_p);
Vector3i owner = bv.ischild ? _p + bv.parent : _p;
return _w.GetTileEntity(owner) is TileEntityComposite c && c.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage s) ? s : null;
}
static void Box(World _w, List<string> _p)
{
TEFeatureStorage s = StorageAt(_w, P(_p, 0));
if (s == null)
{
Out("necrobox: нет хранилища в " + P(_p, 0));
return;
}
string op = _p.Count > 3 ? _p[3] : "list";
if (op == "clear")
{
for (int i = 0; i < s.ItemGrid.Length; i++)
{
s.UpdateSlot(i, ItemStack.Empty.Clone());
}
s.SetModified();
}
else if (op == "add")
{
ItemValue iv = ItemClass.GetItem(_p[4], true);
ItemStack st = new ItemStack(iv, int.Parse(_p[5]));
CinerariumWork.Place(st, new[] { s });
s.SetModified();
if (st.count > 0)
{
Out("necrobox: не влезло " + st.count);
}
}
StringBuilder sb = new StringBuilder("necrobox: ").Append(P(_p, 0)).Append(':');
for (int i = 0; i < s.ItemGrid.Length; i++)
{
ItemStack st = s.ItemGrid[i];
if (st != null && !st.IsEmpty())
{
sb.Append(' ').Append(st.itemValue.ItemClass?.GetItemName()).Append('x').Append(st.count);
}
}
Out(sb.ToString());
}
static void Urn(World _w, List<string> _p)
{
Vector3i pos = P(_p, 0);
TEFeatureCinerarium u = CinerariumThreads.UrnAt(_w, pos);
if (u == null)
{
Out("necrourn: нет урны в " + pos);
return;
}
string op = _p.Count > 3 ? _p[3] : "info";
switch (op)
{
case "skull": u.SetSkull(_p[4] == "1"); break;
case "from": u.AddThreadFrom(P(_p, 4)); break;
case "to": u.SetThreadTo(P(_p, 4)); break;
case "out": u.AddThreadOut(P(_p, 4)); break;
case "clear": u.ClearThreads(); break;
case "sample":
u.SetSample(_p.Count > 5 ? new ItemStack(ItemClass.GetItem(_p[4], true), int.Parse(_p[5])) : ItemStack.Empty.Clone());
break;
case "group":
u.GroupMask = _p[4] == "нет" || _p[4] == "none" ? 0u : CinerariumGroups.All;
u.SetModified();
break;
}
Out("necrourn: " + pos + " " + _w.GetBlock(pos).Block?.GetBlockName() + ", череп " + u.HasSkull + ", застрял " + u.Stuck +
", откуда " + u.ThreadFrom.Count + ", куда " + (u.HasThreadTo ? u.ThreadTo.ToString() : "нет") + ", от урны " + u.ThreadOut.Count +
", образец " + (u.Sample == null || u.Sample.IsEmpty() ? "нет" : u.Sample.itemValue.ItemClass?.GetItemName() + "x" + u.Sample.count));
}
}
[Preserve]
public class ConsoleCmdNecroLook : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necrolook" };
public override string getDescription() => "NecromancerTome (разработка): повернуть взгляд: yaw [pitch] | at x y z";
public override bool AllowedInMainMenu => false;
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrolook", _params);
}
[Preserve]
public class ConsoleCmdNecroAim : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necroaim" };
public override string getDescription() => "NecromancerTome (разработка): позиция игрока и блок под прицелом";
public override bool AllowedInMainMenu => false;
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necroaim", _params);
}
[Preserve]
public class ConsoleCmdNecroPlace : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necroplace" };
public override string getDescription() => "NecromancerTome (разработка): поставить блок: имя x y z [поворот]";
public override bool AllowedInMainMenu => false;
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necroplace", _params);
}
[Preserve]
public class ConsoleCmdNecroHold : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necrohold" };
public override string getDescription() => "NecromancerTome (разработка): предмет в руку: имя [число]";
public override bool AllowedInMainMenu => false;
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrohold", _params);
}
[Preserve]
public class ConsoleCmdNecroBox : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necrobox" };
public override string getDescription() => "NecromancerTome (разработка): хранилище: x y z list|clear|add предмет число";
public override bool AllowedInMainMenu => false;
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrobox", _params);
}
[Preserve]
public class ConsoleCmdNecroUrn : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necrourn" };
public override string getDescription() => "NecromancerTome (разработка): урна: x y z info|skull|from|to|out|clear|sample|group";
public override bool AllowedInMainMenu => false;
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrourn", _params);
}
[Preserve]
public class ConsoleCmdNecroClick : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necroclick" };
public override string getDescription() => "NecromancerTome (разработка): нож для нитей: 0 - ЛКМ, 1 - ПКМ по блоку под прицелом";
public override bool AllowedInMainMenu => false;
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necroclick", _params);
}
}
+61
View File
@@ -88,4 +88,65 @@ namespace NecromancerTome
SdtdConsole.Instance.Output("снимок: " + path);
}
}
/// <summary>necrospawn - нажать «Появиться» в окне появления (18.10: ассистент сам запускает игру с сейвом
/// -LoadSaveGame=true и входит в мир без мыши). То же, что кнопка: XUiC_SpawnSelectionWindow.SpawnButtonPressed.</summary>
[Preserve]
public class ConsoleCmdNecroSpawn : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necrospawn" };
public override string getDescription() => "NecromancerTome: нажать «Появиться» (разработка, почтовый ящик)";
public override bool AllowedInMainMenu => false;
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{
foreach (LocalPlayerUI ui in LocalPlayerUI.PlayerUIs)
{
XUiC_SpawnSelectionWindow w = ui?.xui?.GetWindowByType<XUiC_SpawnSelectionWindow>();
if (w != null && w.IsOpen)
{
w.SpawnButtonPressed(SpawnMethod.Invalid);
SdtdConsole.Instance.Output("necrospawn: нажато «Появиться»");
return;
}
}
SdtdConsole.Instance.Output("necrospawn: окно появления не открыто");
}
}
/// <summary>necroexit [quit] - выйти из мира с сохранением, как «Выход» в меню паузы (GameManager.Disconnect); с quit -
/// затем закрыть игру. Нужно перед сборкой бандлов и заменой DLL (игра держит файлы).</summary>
[Preserve]
public class ConsoleCmdNecroExit : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necroexit" };
public override string getDescription() => "NecromancerTome: выйти из мира с сохранением [quit - и закрыть игру] (разработка)";
public override bool AllowedInMainMenu => true;
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{
bool quit = _params.Count > 0 && _params[0] == "quit";
SdtdConsole.Instance.Output("necroexit: выход" + (quit ? " и закрытие игры" : " в меню"));
ThreadManager.StartCoroutine(ExitCo(quit));
}
static System.Collections.IEnumerator ExitCo(bool _quit)
{
if (GameManager.Instance.World != null)
{
GameManager.Instance.Disconnect();
while (GameManager.Instance.World != null)
{
yield return null;
}
Debug.Log("[NecromancerTome] necroexit: мир сохранён и закрыт");
}
if (_quit)
{
yield return new WaitForSecondsRealtime(2f);
Debug.Log("[NecromancerTome] necroexit: закрываю игру");
Application.Quit();
}
}
}
}