Логист - латунное кадило: два вида, дух 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
+4
View File
@@ -143,6 +143,10 @@ necroCinerariumDesc,blocks,Block,,,,"A funeral urn that houses a skull taken fro
necroCinerariumSkull,blocks,Block,,,,Harvest Cinerarium (with skull),,Ernte-Cinerarium (mit Schädel),Cinerario de cosecha (con cráneo),Cinéraire des moissons (avec crâne),Cinerario del raccolto (con teschio),収穫の納骨壺(頭蓋骨入り),수확의 유골함 (두개골 포함),Cinerarium żniw (z czaszką),Cinerário da colheita (com crânio),Цинерарий с черепом,Hasat Küllüğü (kafatasıyla),丰收骨灰瓮(含头骨),豐收骨灰甕(含頭骨)
necroCinerariumLogist,blocks,Block,,,,Logistics Cinerarium,,Logistik-Cinerarium,Cinerario logístico,Cinéraire logisticien,Cinerario logistico,物流の骨壺,물류 유골함,Cinerarium logistyczne,Cinerário logístico,Цинерарий-логист,Lojistik Küllüğü,物流骨灰瓮,物流骨灰甕
necroCinerariumLogistSkull,blocks,Block,,,,Logistics Cinerarium (with skull),,Logistik-Cinerarium (mit Schädel),Cinerario logístico (con cráneo),Cinéraire logisticien (avec crâne),Cinerario logistico (con teschio),物流の骨壺(頭蓋骨入り),물류 유골함 (두개골 포함),Cinerarium logistyczne (z czaszką),Cinerário logístico (com crânio),Цинерарий-логист (с черепом),Lojistik Küllüğü (kafatasıyla),物流骨灰瓮(含头骨),物流骨灰甕(含頭骨)
necroCinerariumLogistWall,blocks,Block,,,,Logistics Cinerarium (wall),,Logistik-Cinerarium (Wand),Cinerario logístico (pared),Cinéraire logisticien (mural),Cinerario logistico (a muro),物流の骨壺(壁掛け),물류 유골함 (벽걸이),Cinerarium logistyczne (ścienne),Cinerário logístico (parede),Цинерарий-логист (на стене),Lojistik Küllüğü (duvar),物流骨灰瓮(壁挂),物流骨灰甕(壁掛)
necroCinerariumLogistWallSkull,blocks,Block,,,,"Logistics Cinerarium (wall, with skull)",,"Logistik-Cinerarium (Wand, mit Schädel)","Cinerario logístico (pared, con cráneo)","Cinéraire logisticien (mural, avec crâne)","Cinerario logistico (a muro, con teschio)",物流の骨壺(壁掛け・頭蓋骨入り),"물류 유골함 (벽걸이, 두개골 포함)","Cinerarium logistyczne (ścienne, z czaszką)","Cinerário logístico (parede, com crânio)","Цинерарий-логист (на стене, с черепом)","Lojistik Küllüğü (duvar, kafatasıyla)",物流骨灰瓮(壁挂,含头骨),物流骨灰甕(壁掛,含頭骨)
necroCinerariumLogistFormCenter,UI,Text,,,,On a stand,,Auf einem Ständer,En un soporte,Sur un pied,Su un piedistallo,台座に置く,받침대 위,Na podstawie,Em um suporte,На подставке,Ayaklı,放在底座上,放在底座上
necroCinerariumLogistFormWall,UI,Text,,,,On a wall bracket,,An einer Wandhalterung,En un soporte de pared,Sur une applique murale,Su una mensola a muro,壁の腕木に掛ける,벽 받침대에,Na wsporniku ściennym,Em um suporte de parede,На кронштейне у стены,Duvar dirseğinde,挂在墙上支架,掛在牆上支架
necroCinerariumLogistDesc,blocks,Block,,,,"An urn for a spirit that carries things between chests. It has no zone of its own and can stand right next to other urns. Place a skull inside, then tie it to chests with ghostly threads - the Necromancer's Knife in the thread grip (R). In its Contents, choose which item groups the spirit carries. Does not count toward the urn limit.",,"Eine Urne für einen Geist, der Dinge zwischen Truhen trägt. Sie hat keine eigene Zone und kann direkt neben anderen Urnen stehen. Leg einen Schädel hinein und verbinde sie mit geisterhaften Fäden mit Truhen - mit dem Messer des Nekromanten im Fadengriff (R). Im Inhalt wählst du, welche Gegenstandsgruppen der Geist trägt. Zählt nicht zum Urnenlimit.",Una urna para un espíritu que transporta cosas entre cofres. No tiene zona propia y puede estar junto a otras urnas. Pon un cráneo dentro y únela a cofres con hilos fantasmales: el Cuchillo del nigromante con agarre de hilos (R). En su Contenido eliges qué grupos de objetos transporta el espíritu. No cuenta para el límite de urnas.,"Une urne pour un esprit qui transporte des objets entre des coffres. Elle n'a pas de zone propre et peut se placer contre d'autres urnes. Place un crâne à l'intérieur, puis relie-la à des coffres par des fils spectraux - le Couteau du nécromancien en prise des fils (R). Dans son Contenu, choisis les groupes d'objets que l'esprit transporte. Ne compte pas dans la limite d'urnes.",Un'urna per uno spirito che trasporta oggetti tra forzieri. Non ha una zona propria e può stare accanto ad altre urne. Metti dentro un teschio e collegala ai forzieri con fili spettrali - il Coltello del negromante con la presa dei fili (R). Nel suo Contenuto scegli quali gruppi di oggetti trasporta lo spirito. Non conta nel limite di urne.,箱から箱へ物を運ぶ霊のための骨壺。自分の範囲を持たず、ほかの骨壺のすぐ隣に置ける。頭蓋骨を納め、ネクロマンサーのナイフの糸の握り(R)で霊の糸を箱へ張る。「中身」で、霊が運ぶ品目グループを選ぶ。骨壺の上限には数えない。,"상자 사이로 물건을 옮기는 영혼을 위한 유골함. 자기 구역이 없어 다른 유골함 바로 옆에 놓을 수 있습니다. 두개골을 넣고, 강령술사의 단검 실 쥐기(R)로 상자와 유령 실을 잇습니다. 내용물에서 영혼이 옮길 물품 그룹을 고릅니다. 유골함 한도에 포함되지 않습니다.","Urna dla ducha, który przenosi rzeczy między skrzyniami. Nie ma własnej strefy i może stać tuż obok innych urn. Włóż do niej czaszkę i połącz ją ze skrzyniami widmowymi nićmi - nożem nekromanty w chwycie do nici (R). W Zawartości wybierasz, jakie grupy przedmiotów duch przenosi. Nie wlicza się do limitu urn.","Uma urna para um espírito que leva coisas entre baús. Não tem zona própria e pode ficar ao lado de outras urnas. Coloque um crânio dentro e ligue-a a baús com fios fantasmagóricos - a Faca do necromante na pegada de fios (R). No Conteúdo, escolha quais grupos de itens o espírito leva. Não conta no limite de urnas.","Урна для духа, который носит вещи между ящиками. Своей зоны у неё нет, она ставится вплотную к другим урнам. Положите в неё череп и свяжите с ящиками призрачными нитями - ножом некроманта хватом для нитей (R). В «Содержимом» выбирается, какие группы предметов дух носит. В лимит урн не входит.","Eşyaları sandıklar arasında taşıyan bir ruh için küllük. Kendi bölgesi yoktur, diğer küllüklerin hemen yanına konabilir. İçine bir kafatası koy ve Nekromantın Bıçağı'nın iplik tutuşuyla (R) onu sandıklara hayalet ipliklerle bağla. İçerik penceresinde ruhun hangi eşya gruplarını taşıyacağını seç. Küllük sınırına sayılmaz.",供在箱子之间搬运物品的灵所用的骨灰瓮。它没有自己的区域,可以紧挨其他骨灰瓮放置。放入头骨,再用亡灵法师之刃的牵线握法(R)以幽灵丝线连到箱子。在「内容」里选择灵搬运哪些物品分组。不计入骨灰瓮数量上限。,供在箱子之間搬運物品的靈所用的骨灰甕。它沒有自己的區域,可以緊挨其他骨灰甕放置。放入頭骨,再用亡靈法師之刃的牽線握法(R)以幽靈絲線連到箱子。在「內容」裡選擇靈搬運哪些物品分組。不計入骨灰甕數量上限。
necroCinerariumWorker,blocks,Block,,,,Worker Cinerarium,,Arbeiter-Cinerarium,Cinerario obrero,Cinéraire ouvrier,Cinerario operaio,職人の骨壺,일꾼 유골함,Cinerarium robotnika,Cinerário operário,Цинерарий-рабочий,İşçi Küllüğü,工匠骨灰瓮,工匠骨灰甕
necroCinerariumWorkerSkull,blocks,Block,,,,Worker Cinerarium (with skull),,Arbeiter-Cinerarium (mit Schädel),Cinerario obrero (con cráneo),Cinéraire ouvrier (avec crâne),Cinerario operaio (con teschio),職人の骨壺(頭蓋骨入り),일꾼 유골함 (두개골 포함),Cinerarium robotnika (z czaszką),Cinerário operário (com crânio),Цинерарий-рабочий (с черепом),İşçi Küllüğü (kafatasıyla),工匠骨灰瓮(含头骨),工匠骨灰甕(含頭骨)
1 Key File Type UsedInMainMenu NoTranslate KeepLoaded english Context / Alternate Text german spanish french italian japanese koreana polish brazilian russian turkish schinese tchinese
143 necroCinerariumSkull blocks Block Harvest Cinerarium (with skull) Ernte-Cinerarium (mit Schädel) Cinerario de cosecha (con cráneo) Cinéraire des moissons (avec crâne) Cinerario del raccolto (con teschio) 収穫の納骨壺(頭蓋骨入り) 수확의 유골함 (두개골 포함) Cinerarium żniw (z czaszką) Cinerário da colheita (com crânio) Цинерарий с черепом Hasat Küllüğü (kafatasıyla) 丰收骨灰瓮(含头骨) 豐收骨灰甕(含頭骨)
144 necroCinerariumLogist blocks Block Logistics Cinerarium Logistik-Cinerarium Cinerario logístico Cinéraire logisticien Cinerario logistico 物流の骨壺 물류 유골함 Cinerarium logistyczne Cinerário logístico Цинерарий-логист Lojistik Küllüğü 物流骨灰瓮 物流骨灰甕
145 necroCinerariumLogistSkull blocks Block Logistics Cinerarium (with skull) Logistik-Cinerarium (mit Schädel) Cinerario logístico (con cráneo) Cinéraire logisticien (avec crâne) Cinerario logistico (con teschio) 物流の骨壺(頭蓋骨入り) 물류 유골함 (두개골 포함) Cinerarium logistyczne (z czaszką) Cinerário logístico (com crânio) Цинерарий-логист (с черепом) Lojistik Küllüğü (kafatasıyla) 物流骨灰瓮(含头骨) 物流骨灰甕(含頭骨)
146 necroCinerariumLogistWall blocks Block Logistics Cinerarium (wall) Logistik-Cinerarium (Wand) Cinerario logístico (pared) Cinéraire logisticien (mural) Cinerario logistico (a muro) 物流の骨壺(壁掛け) 물류 유골함 (벽걸이) Cinerarium logistyczne (ścienne) Cinerário logístico (parede) Цинерарий-логист (на стене) Lojistik Küllüğü (duvar) 物流骨灰瓮(壁挂) 物流骨灰甕(壁掛)
147 necroCinerariumLogistWallSkull blocks Block Logistics Cinerarium (wall, with skull) Logistik-Cinerarium (Wand, mit Schädel) Cinerario logístico (pared, con cráneo) Cinéraire logisticien (mural, avec crâne) Cinerario logistico (a muro, con teschio) 物流の骨壺(壁掛け・頭蓋骨入り) 물류 유골함 (벽걸이, 두개골 포함) Cinerarium logistyczne (ścienne, z czaszką) Cinerário logístico (parede, com crânio) Цинерарий-логист (на стене, с черепом) Lojistik Küllüğü (duvar, kafatasıyla) 物流骨灰瓮(壁挂,含头骨) 物流骨灰甕(壁掛,含頭骨)
148 necroCinerariumLogistFormCenter UI Text On a stand Auf einem Ständer En un soporte Sur un pied Su un piedistallo 台座に置く 받침대 위 Na podstawie Em um suporte На подставке Ayaklı 放在底座上 放在底座上
149 necroCinerariumLogistFormWall UI Text On a wall bracket An einer Wandhalterung En un soporte de pared Sur une applique murale Su una mensola a muro 壁の腕木に掛ける 벽 받침대에 Na wsporniku ściennym Em um suporte de parede На кронштейне у стены Duvar dirseğinde 挂在墙上支架 掛在牆上支架
150 necroCinerariumLogistDesc blocks Block An urn for a spirit that carries things between chests. It has no zone of its own and can stand right next to other urns. Place a skull inside, then tie it to chests with ghostly threads - the Necromancer's Knife in the thread grip (R). In its Contents, choose which item groups the spirit carries. Does not count toward the urn limit. Eine Urne für einen Geist, der Dinge zwischen Truhen trägt. Sie hat keine eigene Zone und kann direkt neben anderen Urnen stehen. Leg einen Schädel hinein und verbinde sie mit geisterhaften Fäden mit Truhen - mit dem Messer des Nekromanten im Fadengriff (R). Im Inhalt wählst du, welche Gegenstandsgruppen der Geist trägt. Zählt nicht zum Urnenlimit. Una urna para un espíritu que transporta cosas entre cofres. No tiene zona propia y puede estar junto a otras urnas. Pon un cráneo dentro y únela a cofres con hilos fantasmales: el Cuchillo del nigromante con agarre de hilos (R). En su Contenido eliges qué grupos de objetos transporta el espíritu. No cuenta para el límite de urnas. Une urne pour un esprit qui transporte des objets entre des coffres. Elle n'a pas de zone propre et peut se placer contre d'autres urnes. Place un crâne à l'intérieur, puis relie-la à des coffres par des fils spectraux - le Couteau du nécromancien en prise des fils (R). Dans son Contenu, choisis les groupes d'objets que l'esprit transporte. Ne compte pas dans la limite d'urnes. Un'urna per uno spirito che trasporta oggetti tra forzieri. Non ha una zona propria e può stare accanto ad altre urne. Metti dentro un teschio e collegala ai forzieri con fili spettrali - il Coltello del negromante con la presa dei fili (R). Nel suo Contenuto scegli quali gruppi di oggetti trasporta lo spirito. Non conta nel limite di urne. 箱から箱へ物を運ぶ霊のための骨壺。自分の範囲を持たず、ほかの骨壺のすぐ隣に置ける。頭蓋骨を納め、ネクロマンサーのナイフの糸の握り(R)で霊の糸を箱へ張る。「中身」で、霊が運ぶ品目グループを選ぶ。骨壺の上限には数えない。 상자 사이로 물건을 옮기는 영혼을 위한 유골함. 자기 구역이 없어 다른 유골함 바로 옆에 놓을 수 있습니다. 두개골을 넣고, 강령술사의 단검 실 쥐기(R)로 상자와 유령 실을 잇습니다. 내용물에서 영혼이 옮길 물품 그룹을 고릅니다. 유골함 한도에 포함되지 않습니다. Urna dla ducha, który przenosi rzeczy między skrzyniami. Nie ma własnej strefy i może stać tuż obok innych urn. Włóż do niej czaszkę i połącz ją ze skrzyniami widmowymi nićmi - nożem nekromanty w chwycie do nici (R). W Zawartości wybierasz, jakie grupy przedmiotów duch przenosi. Nie wlicza się do limitu urn. Uma urna para um espírito que leva coisas entre baús. Não tem zona própria e pode ficar ao lado de outras urnas. Coloque um crânio dentro e ligue-a a baús com fios fantasmagóricos - a Faca do necromante na pegada de fios (R). No Conteúdo, escolha quais grupos de itens o espírito leva. Não conta no limite de urnas. Урна для духа, который носит вещи между ящиками. Своей зоны у неё нет, она ставится вплотную к другим урнам. Положите в неё череп и свяжите с ящиками призрачными нитями - ножом некроманта хватом для нитей (R). В «Содержимом» выбирается, какие группы предметов дух носит. В лимит урн не входит. Eşyaları sandıklar arasında taşıyan bir ruh için küllük. Kendi bölgesi yoktur, diğer küllüklerin hemen yanına konabilir. İçine bir kafatası koy ve Nekromantın Bıçağı'nın iplik tutuşuyla (R) onu sandıklara hayalet ipliklerle bağla. İçerik penceresinde ruhun hangi eşya gruplarını taşıyacağını seç. Küllük sınırına sayılmaz. 供在箱子之间搬运物品的灵所用的骨灰瓮。它没有自己的区域,可以紧挨其他骨灰瓮放置。放入头骨,再用亡灵法师之刃的牵线握法(R)以幽灵丝线连到箱子。在「内容」里选择灵搬运哪些物品分组。不计入骨灰瓮数量上限。 供在箱子之間搬運物品的靈所用的骨灰甕。它沒有自己的區域,可以緊挨其他骨灰甕放置。放入頭骨,再用亡靈法師之刃的牽線握法(R)以幽靈絲線連到箱子。在「內容」裡選擇靈搬運哪些物品分組。不計入骨灰甕數量上限。
151 necroCinerariumWorker blocks Block Worker Cinerarium Arbeiter-Cinerarium Cinerario obrero Cinéraire ouvrier Cinerario operaio 職人の骨壺 일꾼 유골함 Cinerarium robotnika Cinerário operário Цинерарий-рабочий İşçi Küllüğü 工匠骨灰瓮 工匠骨灰甕
152 necroCinerariumWorkerSkull blocks Block Worker Cinerarium (with skull) Arbeiter-Cinerarium (mit Schädel) Cinerario obrero (con cráneo) Cinéraire ouvrier (avec crâne) Cinerario operaio (con teschio) 職人の骨壺(頭蓋骨入り) 일꾼 유골함 (두개골 포함) Cinerarium robotnika (z czaszką) Cinerário operário (com crânio) Цинерарий-рабочий (с черепом) İşçi Küllüğü (kafatasıyla) 工匠骨灰瓮(含头骨) 工匠骨灰甕(含頭骨)
+19 -3
View File
@@ -568,20 +568,36 @@
</block>
<!-- Урна-логист (16.10.2026): «Я не могу ставить урны рядом. Может нам для логиста вообще сделать отдельные урны?»
Без зоны (ставится вплотную к любым урнам), только нити и перевозка (CinerariumThreads.cs), лимит урн общий.
Модель - та же; отличие - код (TEFeatureCinerarium.SyncGlow): зелёные руны и глазницы
(Resources/necroCinerarium_e_logist.png), зеленоватая глазурь. Иконка - своя (зелёные руны, нити из горлышка).
Своя модель логиста - позже, в Blender. -->
18.10 - своя модель: латунное кадило с чеканкой (BlenderWork/necroCinerariumLogist, бандл necrocinerarium), два вида -
на подставке по центру клетки (necroCinerariumLogist - блок уже в сейвах, остаётся видом по умолчанию) и на кронштейне
у стены (necroCinerariumLogistWall, стена - задняя грань, как у черепа-декора). Вид - R с урной в руке, «Форма»
(CinerariumLogistForm.cs). Дух в покое сидит в кадиле, вылетает из окон крышки только на перевозку, втрое меньше. -->
<block name="necroCinerariumLogist">
<property name="Extends" value="necroCinerarium"/>
<property name="CreativeMode" value="Player"/>
<property name="DescriptionKey" value="necroCinerariumLogistDesc"/>
<property name="CustomIcon" value="necroCinerariumLogist"/>
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necrocinerarium?necroCinerariumLogistPrefab.prefab"/>
<property name="Material" value="Mmetal"/>
</block>
<block name="necroCinerariumLogistSkull">
<property name="Extends" value="necroCinerariumLogist"/>
<property name="CreativeMode" value="None"/>
<property name="DescriptionKey" value="necroCinerariumLogistDesc"/>
</block>
<block name="necroCinerariumLogistWall">
<property name="Extends" value="necroCinerariumLogist"/>
<property name="CreativeMode" value="Player"/>
<property name="DescriptionKey" value="necroCinerariumLogistDesc"/>
<property name="CustomIcon" value="necroCinerariumLogist"/> <!-- своя иконка - после рендера кадила -->
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necrocinerarium?necroCinerariumLogistWallPrefab.prefab"/>
<property name="Place" value="TowardsPlacerInverted"/>
</block>
<block name="necroCinerariumLogistWallSkull">
<property name="Extends" value="necroCinerariumLogistWall"/>
<property name="CreativeMode" value="None"/>
<property name="DescriptionKey" value="necroCinerariumLogistDesc"/>
</block>
<!-- Урна рабочего (17-18.10.2026, BACKLOG «Урна рабочего»): чаша на трёх костях (BlenderWork/necroCinerariumWorker,
necroCinerariumWorkerPrefab в бандле necrocinerarium). Изготавливает предмет из второй ячейки «Содержимого» (образец)
на станциях по нитям, пока запас меньше стопки образца (CinerariumCraft.cs). Без зоны и вне лимита, как логист.
+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();
}
}
}
}
Binary file not shown.
Binary file not shown.