diff --git a/Config/Localization.csv b/Config/Localization.csv
index df4f950..7c81a49 100644
--- a/Config/Localization.csv
+++ b/Config/Localization.csv
@@ -44,7 +44,7 @@ necroThreadTo,UI,Tooltip,,,,Thread: urn to chest - the spirit will bring things
necroThreadRemoved,UI,Tooltip,,,,Threads cut,,Fäden getrennt,Hilos cortados,Fils coupés,Fili tagliati,糸を断った,실을 끊었다,Nici przecięte,Fios cortados,Нити обрезаны,İplikler kesildi,丝线已剪断,絲線已剪斷
necroThreadLimit,UI,Tooltip,,,,This urn already has 8 incoming threads,,Diese Urne hat schon 8 eingehende Fäden,Esta urna ya tiene 8 hilos de entrada,Cette urne a déjà 8 fils entrants,Quest'urna ha già 8 fili in entrata,この骨壺にはすでに入りの糸が8本ある,이 유골함에는 이미 들어오는 실이 8개 있다,Ta urna ma już 8 nici wejściowych,Esta urna já tem 8 fios de entrada,У этой урны уже 8 нитей «откуда»,Bu küllüğün zaten 8 gelen ipliği var,这个骨灰瓮已有8根来源丝线,這個骨灰甕已有8根來源絲線
necroThreadCancel,UI,Tooltip,,,,Thread dropped,,Faden fallen gelassen,Hilo soltado,Fil lâché,Filo lasciato,糸を手放した,실을 놓았다,Nić porzucona,Fio solto,Нить брошена,İplik bırakıldı,丝线已放下,絲線已放下
-necroThreadNeedLogist,UI,Tooltip,,,,Threads tie only to a Logistics Cinerarium,,Fäden nur an ein Logistik-Cinerarium,Los hilos solo se atan a un cinerario logístico,Les fils ne se nouent qu'à un cinéraire logisticien,I fili si legano solo a un cinerario logistico,糸は物流の骨壺にしか張れない,실은 물류 유골함에만 이을 수 있다,Nici łączą się tylko z cinerarium logistycznym,Os fios só se ligam a um cinerário logístico,Нити вяжутся только к цинерарию-логисту,İplikler yalnızca lojistik küllüğe bağlanır,丝线只能连到物流骨灰瓮,絲線只能連到物流骨灰甕
+necroThreadNeedLogist,UI,Tooltip,,,,Threads tie only to a Logistics or Worker Cinerarium,,Fäden nur an ein Logistik- oder Arbeiter-Cinerarium,Los hilos solo se atan a un cinerario logístico u obrero,Les fils ne se nouent qu'à un cinéraire logisticien ou ouvrier,I fili si legano solo a un cinerario logistico o operaio,糸は物流か職人の骨壺にしか張れない,실은 물류 또는 일꾼 유골함에만 이을 수 있다,Nici łączą się tylko z cinerarium logistycznym lub robotnika,Os fios só se ligam a um cinerário logístico ou operário,Нити вяжутся только к цинерарию-логисту или рабочему,İplikler yalnızca lojistik ya da işçi küllüğüne bağlanır,丝线只能连到物流或工匠骨灰瓮,絲線只能連到物流或工匠骨灰甕
necroGroup0,UI,Tooltip,,,,Cooked food,,Gekochtes Essen,Comida cocinada,Plats cuisinés,Cibo cucinato,料理,요리된 음식,Gotowe jedzenie,Comida pronta,Готовая еда,Pişmiş yemek,熟食,熟食
necroGroup1,UI,Tooltip,,,,Canned food,,Konserven,Conservas,Conserves,Cibo in scatola,缶詰,통조림,Konserwy,Enlatados,Консервы,Konserveler,罐头,罐頭
necroGroup2,UI,Tooltip,,,,Raw food,,Rohe Lebensmittel,Alimentos crudos,Aliments crus,Cibo crudo,生の食材,날것 식재료,Surowe produkty,Alimentos crus,Сырые продукты,Çiğ gıdalar,生食材,生食材
@@ -144,6 +144,12 @@ necroCinerariumSkull,blocks,Block,,,,Harvest Cinerarium (with skull),,Ernte-Cine
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),物流骨灰瓮(含头骨),物流骨灰甕(含頭骨)
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),工匠骨灰瓮(含头骨),工匠骨灰甕(含頭骨)
+necroCinerariumWorkerDesc,blocks,Block,,,,"An urn for a spirit that crafts. In its Contents, put a skull in the first slot and a sample in the second: the spirit makes that item on workstations until there are as many as in the sample's stack. Tie threads with the Necromancer's Knife in the thread grip (R): chest to urn - ingredients and fuel; urn to workstation - where to craft; urn to chest - where to put the goods (without it they stay in the station). Lights and puts out campfires, forges and chemistry stations by itself. Has no zone of its own and does not count toward the urn limit.",,"Eine Urne für einen Geist, der herstellt. Im Inhalt kommt ein Schädel in das erste Feld und ein Muster in das zweite: Der Geist stellt diesen Gegenstand an Werkstationen her, bis es so viele gibt wie im Stapel des Musters. Fäden ziehst du mit dem Messer des Nekromanten im Fadengriff (R): Truhe zur Urne - Zutaten und Brennstoff; Urne zur Werkstation - wo hergestellt wird; Urne zur Truhe - wohin die Ware kommt (ohne sie bleibt sie in der Station). Zündet Lagerfeuer, Schmiede und Chemiestation selbst an und löscht sie. Hat keine eigene Zone und zählt nicht zum Urnenlimit.","Una urna para un espíritu que fabrica. En su Contenido, pon un cráneo en la primera casilla y una muestra en la segunda: el espíritu fabrica ese objeto en estaciones de trabajo hasta tener tantos como en la pila de la muestra. Los hilos se tienden con el Cuchillo del nigromante en agarre de hilos (R): cofre a urna - ingredientes y combustible; urna a estación - dónde fabricar; urna a cofre - dónde dejar lo hecho (sin él se queda en la estación). Enciende y apaga solo hogueras, forjas y estaciones de química. No tiene zona propia y no cuenta para el límite de urnas.","Une urne pour un esprit qui fabrique. Dans son Contenu, mets un crâne dans la première case et un modèle dans la seconde : l'esprit fabrique cet objet aux postes de travail jusqu'à en avoir autant que dans la pile du modèle. Les fils se tendent avec le Couteau du nécromancien en prise des fils (R) : coffre vers urne - ingrédients et combustible ; urne vers poste - où fabriquer ; urne vers coffre - où ranger la production (sans lui, elle reste dans le poste). Allume et éteint seul feux de camp, forges et stations de chimie. N'a pas de zone propre et ne compte pas dans la limite d'urnes.","Un'urna per uno spirito che fabbrica. Nel suo Contenuto metti un teschio nella prima casella e un campione nella seconda: lo spirito fabbrica quell'oggetto alle postazioni finché non ce ne sono tanti quanti nella pila del campione. I fili si tendono con il Coltello del negromante nella presa dei fili (R): forziere verso urna - ingredienti e combustibile; urna verso postazione - dove fabbricare; urna verso forziere - dove mettere i prodotti (senza, restano nella postazione). Accende e spegne da solo falò, forge e stazioni chimiche. Non ha una zona propria e non conta nel limite di urne.",作る霊のための骨壺。「中身」の1つ目の枠に頭蓋骨、2つ目に見本を入れると、見本の数と同じだけ揃うまで、霊が作業台でその品を作る。糸はネクロマンサーのナイフの糸の握り(R)で張る:箱から骨壺へ - 材料と燃料、骨壺から作業台へ - どこで作るか、骨壺から箱へ - 完成品の置き場(無ければ作業台に残る)。たき火・炉・化学ステーションの火は霊が自分で点けて消す。自分の範囲を持たず、骨壺の上限には数えない。,"제작하는 영혼을 위한 유골함. 내용물의 첫 칸에 두개골, 둘째 칸에 견본을 넣으면, 견본 묶음 수만큼 쌓일 때까지 영혼이 작업대에서 그 물건을 만듭니다. 실은 강령술사의 단검 실 쥐기(R)로 잇습니다: 상자에서 유골함 - 재료와 연료, 유골함에서 작업대 - 어디서 만들지, 유골함에서 상자 - 완성품을 둘 곳(없으면 작업대에 남음). 모닥불, 용광로, 화학 작업대는 스스로 켜고 끕니다. 자기 구역이 없고 유골함 한도에 포함되지 않습니다.","Urna dla ducha, który wytwarza. W Zawartości włóż czaszkę do pierwszego pola, a wzór do drugiego: duch wytwarza ten przedmiot na stanowiskach, aż będzie ich tyle, ile w stosie wzoru. Nici zaczepiasz nożem nekromanty w chwycie do nici (R): skrzynia do urny - składniki i paliwo; urna do stanowiska - gdzie wytwarzać; urna do skrzyni - gdzie odkładać wyroby (bez niej zostają w stanowisku). Sam rozpala i gasi ogniska, kuźnie i stacje chemiczne. Nie ma własnej strefy i nie wlicza się do limitu urn.","Uma urna para um espírito que fabrica. No Conteúdo, coloque um crânio no primeiro espaço e uma amostra no segundo: o espírito fabrica esse item nas estações até haver tantos quanto na pilha da amostra. Os fios se puxam com a Faca do necromante na pegada de fios (R): baú para urna - ingredientes e combustível; urna para estação - onde fabricar; urna para baú - onde guardar o produto (sem ele, fica na estação). Acende e apaga sozinho fogueiras, forjas e estações químicas. Não tem zona própria e não conta no limite de urnas.","Урна для духа-ремесленника. В «Содержимом» в первую ячейку кладётся череп, во вторую - образец: дух изготавливает этот предмет на станциях, пока их не станет столько, сколько в стопке образца. Нити тянутся ножом некроманта хватом для нитей (R): ящик -> урна - ингредиенты и топливо; урна -> станция - где изготавливать; урна -> ящик - куда класть готовое (без неё оно остаётся в станции). Костёр, кузню и химстанцию дух разжигает и гасит сам. Своей зоны у урны нет, в лимит урн она не входит.","Üreten bir ruh için küllük. İçerik penceresinde ilk yuvaya bir kafatası, ikinciye bir örnek koy: ruh, örnek yığınındaki kadar olana dek o eşyayı tezgâhlarda üretir. İplikler Nekromantın Bıçağı'nın iplik tutuşuyla (R) çekilir: sandıktan küllüğe - malzeme ve yakıt; küllükten tezgâha - nerede üretileceği; küllükten sandığa - ürünlerin konacağı yer (yoksa tezgâhta kalır). Kamp ateşini, demirci ocağını ve kimya istasyonunu kendisi yakar ve söndürür. Kendi bölgesi yoktur ve küllük sınırına sayılmaz.",供制作物品的灵所用的骨灰瓮。在「内容」里第一格放头骨,第二格放样品:灵会在工作台上制作该物品,直到数量与样品堆叠数相同。用亡灵法师之刃的牵线握法(R)拉线:箱子到骨灰瓮——材料和燃料;骨灰瓮到工作台——在哪里制作;骨灰瓮到箱子——成品放在哪里(没有则留在工作台里)。篝火、熔炉和化学工作台由灵自己点燃和熄灭。它没有自己的区域,不计入骨灰瓮数量上限。,供製作物品的靈所用的骨灰甕。在「內容」裡第一格放頭骨,第二格放樣品:靈會在工作台上製作該物品,直到數量與樣品堆疊數相同。用亡靈法師之刃的牽線握法(R)拉線:箱子到骨灰甕——材料和燃料;骨灰甕到工作台——在哪裡製作;骨灰甕到箱子——成品放在哪裡(沒有則留在工作台裡)。篝火、熔爐和化學工作台由靈自己點燃和熄滅。它沒有自己的區域,不計入骨灰甕數量上限。
+necroThreadNeedWorker,UI,Tooltip,,,,Threads to workstations tie only to a Worker Cinerarium,,Fäden zu Werkstationen nur an ein Arbeiter-Cinerarium,Los hilos a estaciones solo se atan a un cinerario obrero,Les fils vers les postes ne se nouent qu'à un cinéraire ouvrier,I fili verso le postazioni si legano solo a un cinerario operaio,作業台への糸は職人の骨壺にしか張れない,작업대로 가는 실은 일꾼 유골함에만 이을 수 있다,Nici do stanowisk łączą się tylko z cinerarium robotnika,Fios para estações só se ligam a um cinerário operário,Нити к станциям вяжутся только к цинерарию-рабочему,Tezgâh iplikleri yalnızca işçi küllüğüne bağlanır,连到工作台的丝线只能连工匠骨灰瓮,連到工作台的絲線只能連工匠骨灰甕
+necroThreadStation,UI,Tooltip,,,,Thread: urn to workstation - the spirit will craft here,,Faden: Urne zur Werkstation - der Geist stellt hier her,Hilo: urna a estación - el espíritu fabricará aquí,Fil : urne vers poste - l'esprit fabriquera ici,Filo: urna verso postazione - lo spirito fabbricherà qui,糸:骨壺から作業台へ - 霊はここで作る,실: 유골함에서 작업대로 - 영혼이 여기서 만든다,Nić: urna do stanowiska - duch będzie tu wytwarzał,Fio: urna para estação - o espírito fabricará aqui,Нить: урна -> станция - дух будет изготавливать здесь,İplik: küllükten tezgâha - ruh burada üretecek,丝线:骨灰瓮到工作台——灵会在这里制作,絲線:骨灰甕到工作台——靈會在這裡製作
+necroThreadOut,UI,Tooltip,,,,Thread: urn to chest - the spirit will put the goods here,,Faden: Urne zur Truhe - der Geist legt die Ware hierhin,Hilo: urna a cofre - el espíritu dejará aquí lo fabricado,Fil : urne vers coffre - l'esprit rangera ici la production,Filo: urna verso forziere - lo spirito metterà qui i prodotti,糸:骨壺から箱へ - 霊は完成品をここに置く,실: 유골함에서 상자로 - 영혼이 완성품을 여기 둔다,Nić: urna do skrzyni - duch odłoży tu wyroby,Fio: urna para baú - o espírito guardará aqui o produto,Нить: урна -> ящик - сюда дух будет класть готовое,İplik: küllükten sandığa - ruh ürünleri buraya koyacak,丝线:骨灰瓮到箱子——灵会把成品放在这里,絲線:骨灰甕到箱子——靈會把成品放在這裡
blockcommand_TEFeatureCinerarium:trunk,blocks,Block,,,,Contents,,Inhalt,Contenido,Contenu,Contenuto,中身,내용물,Zawartość,Conteúdo,Содержимое,İçerik,内容,內容
blockcommand_trunk,blocks,Block,,,,Contents,,Inhalt,Contenido,Contenu,Contenuto,中身,내용물,Zawartość,Conteúdo,Содержимое,İçerik,内容,內容
blockcommand_TEFeatureCinerarium:bag,blocks,Block,,,,Bag,,Tasche,Bolsa,Sac,Borsa,鞄,가방,Torba,Bolsa,Сумка,Çanta,背包,背包
diff --git a/Config/blocks.xml b/Config/blocks.xml
index b8f658c..fff4df8 100644
--- a/Config/blocks.xml
+++ b/Config/blocks.xml
@@ -582,13 +582,20 @@
-
+
+
+
+
+
+
+
diff --git a/HarmonySrc/Cinerarium.cs b/HarmonySrc/Cinerarium.cs
index 0cff44b..13aebca 100644
--- a/HarmonySrc/Cinerarium.cs
+++ b/HarmonySrc/Cinerarium.cs
@@ -41,12 +41,26 @@ namespace NecromancerTome
string n = _b?.GetBlockName();
return n == LogistBlockName || n == LogistSkullBlockName;
}
+ /// Урна рабочего (17.10, BACKLOG «Урна рабочего»): изготавливает предмет из ячейки образца на станциях, к которым
+ /// от неё тянутся нити; ингредиенты - из ящиков с нитями к урне, готовое - в ящики с нитями от урны. Без зоны и вне лимита,
+ /// как логист.
+ public const string WorkerBlockName = "necroCinerariumWorker";
+ public const string WorkerSkullBlockName = "necroCinerariumWorkerSkull";
+
+ public static bool IsWorkerBlock(Block _b)
+ {
+ string n = _b?.GetBlockName();
+ return n == WorkerBlockName || n == WorkerSkullBlockName;
+ }
+
+ /// Урна без зоны (логист, рабочий): ставится вплотную к любым урнам, в лимит не входит, работает по нитям.
+ public static bool IsZonelessBlock(Block _b) => IsLogistBlock(_b) || IsWorkerBlock(_b);
public const string SkullItemName = "necroSkull";
public const int HalfWidth = 2;
public const int Below = 1;
public const int Above = 4;
public const float KillsPerUrn = 500f;
- const byte SaveVersion = 3; // 2 - нити, 3 - фильтр логиста (16.10)
+ const byte SaveVersion = 4; // 2 - нити, 3 - фильтр логиста (16.10), 4 - нити рабочего «куда» и образец (18.10)
public bool HasSkull;
public bool ZoneShown;
@@ -54,7 +68,23 @@ namespace NecromancerTome
public readonly List ThreadFrom = new List();
public bool HasThreadTo;
public Vector3i ThreadTo;
- public bool HasThreads => HasThreadTo || ThreadFrom.Count > 0;
+ public bool HasThreads => HasThreadTo || ThreadFrom.Count > 0 || ThreadOut.Count > 0;
+ /// Нити рабочего «от урны»: станции (где изготавливать) и ящики (куда класть готовое), главные клетки блоков.
+ public readonly List ThreadOut = new List();
+ /// Образец рабочего: что изготавливать; число в стопке - сколько держать в запасе (ящики «куда» и выход станций).
+ /// Лежит в «Содержимом» урны второй ячейкой.
+ public ItemStack Sample = ItemStack.Empty.Clone();
+ /// Урна рабочего - по блоку, как логист.
+ public bool IsWorker
+ {
+ get
+ {
+ _ = IsLogist; // узнаёт тип блока
+ return _worker;
+ }
+ }
+ bool _worker;
+ public bool IsZoneless => IsLogist || IsWorker;
/// Урна-логист - по блоку (necroCinerariumLogist / ...Skull); узнаётся при постановке или по миру.
public bool IsLogist
{
@@ -67,6 +97,7 @@ namespace NecromancerTome
if (IsUrn(bv.Block))
{
_logist = IsLogistBlock(bv.Block);
+ _worker = IsWorkerBlock(bv.Block);
_logistKnown = true;
}
}
@@ -96,6 +127,9 @@ namespace NecromancerTome
ThreadFrom.AddRange(other.ThreadFrom);
HasThreadTo = other.HasThreadTo;
ThreadTo = other.ThreadTo;
+ ThreadOut.Clear();
+ ThreadOut.AddRange(other.ThreadOut);
+ Sample = other.Sample.Clone();
GroupMask = other.GroupMask;
_threadsDirty = true;
}
@@ -106,15 +140,16 @@ namespace NecromancerTome
{
base.OnAdded(_blockPos, _blockValue);
_logist = IsLogistBlock(_blockValue.Block);
+ _worker = IsWorkerBlock(_blockValue.Block);
_logistKnown = true;
string placed = _blockValue.Block?.GetBlockName();
- if (placed == SkullBlockName || placed == LogistSkullBlockName)
+ if (placed == SkullBlockName || placed == LogistSkullBlockName || placed == WorkerSkullBlockName)
{
HasSkull = true; // поставили урну, поднятую с черепом
}
if (ConnectionManager.Instance.IsServer)
{
- CinerariumRegistry.Add(_blockPos, Parent?.Owner, _logist);
+ CinerariumRegistry.Add(_blockPos, Parent?.Owner, _logist || _worker);
}
}
@@ -144,6 +179,12 @@ namespace NecromancerTome
Debug.Log("[NecromancerTome] Cinerarium: урна " + pos + " убрана - череп выпал");
}
}
+ if (Sample != null && !Sample.IsEmpty() && _world != null)
+ {
+ _world.gameManager.ItemDropServer(Sample.Clone(), World.blockToTransformPos(pos) + Vector3.up * 0.7f, Vector3.zero);
+ Sample = ItemStack.Empty.Clone();
+ Debug.Log("[NecromancerTome] Cinerarium: урна " + pos + " убрана - образец выпал");
+ }
}
public override void OnUnload(World _world)
@@ -170,6 +211,13 @@ namespace NecromancerTome
CinerariumWriteHelper.WriteInts(_bw, t.ToArray());
// Второе число - сколько групп было при записи: новые группы (в конец) у старых урн включаются по правилу ниже.
CinerariumWriteHelper.WriteInts(_bw, new[] { unchecked((int)GroupMask), CinerariumGroups.Count });
+ List o = new List { ThreadOut.Count };
+ foreach (Vector3i p in ThreadOut)
+ {
+ o.Add(p.x); o.Add(p.y); o.Add(p.z);
+ }
+ CinerariumWriteHelper.WriteInts(_bw, o.ToArray());
+ (Sample ?? ItemStack.Empty).Clone().Write(_bw);
}
public override void Read(PooledBinaryReader _br, StreamModeRead _eStreamMode)
@@ -224,6 +272,23 @@ namespace NecromancerTome
GroupMask = on ? GroupMask | (1u << g) : GroupMask & ~(1u << g);
}
}
+ ThreadOut.Clear();
+ Sample = ItemStack.Empty.Clone();
+ if (version >= 4)
+ {
+ int n = _br.ReadInt32();
+ int[] v = new int[n];
+ for (int i = 0; i < n; i++)
+ {
+ v[i] = _br.ReadInt32();
+ }
+ int cnt = n > 0 ? v[0] : 0;
+ for (int i = 0, k = 1; i < cnt && k + 2 < n; i++, k += 3)
+ {
+ ThreadOut.Add(new Vector3i(v[k], v[k + 1], v[k + 2]));
+ }
+ Sample = ItemStack.Empty.Clone().Read(_br);
+ }
_threadsDirty = true;
if (version > SaveVersion)
{
@@ -296,10 +361,26 @@ namespace NecromancerTome
ThreadsChanged();
}
+ public bool AddThreadOut(Vector3i _target)
+ {
+ if (ThreadOut.Contains(_target))
+ {
+ return true;
+ }
+ if (ThreadOut.Count >= CinerariumThreads.MaxFrom)
+ {
+ return false;
+ }
+ ThreadOut.Add(_target);
+ ThreadsChanged();
+ return true;
+ }
+
public int ClearThreads()
{
- int n = ThreadFrom.Count + (HasThreadTo ? 1 : 0);
+ int n = ThreadFrom.Count + (HasThreadTo ? 1 : 0) + ThreadOut.Count;
ThreadFrom.Clear();
+ ThreadOut.Clear();
HasThreadTo = false;
if (n > 0)
{
@@ -310,7 +391,7 @@ namespace NecromancerTome
public int RemoveThreadsTo(Vector3i _box)
{
- int n = ThreadFrom.RemoveAll(p => p == _box);
+ int n = ThreadFrom.RemoveAll(p => p == _box) + ThreadOut.RemoveAll(p => p == _box);
if (HasThreadTo && ThreadTo == _box)
{
HasThreadTo = false;
@@ -328,16 +409,17 @@ namespace NecromancerTome
_threadsDirty = true;
_glowState = -1; // логист - другие глаза
SetModified();
- Debug.Log("[NecromancerTome] Cinerarium: нити урны " + ToWorldPos() + " - откуда " + ThreadFrom.Count + ", куда " + (HasThreadTo ? ThreadTo.ToString() : "нет"));
+ Debug.Log("[NecromancerTome] Cinerarium: нити урны " + ToWorldPos() + " - откуда " + ThreadFrom.Count + ", куда " + (HasThreadTo ? ThreadTo.ToString() : "нет") + (ThreadOut.Count > 0 ? ", рабочему " + ThreadOut.Count : ""));
}
/// Нити к убранным ящикам снимаются (раз в 2 с); вид перестраивается после изменений.
void SyncThreads(World _world)
{
- if (_world != null && ConnectionManager.Instance.IsServer && HasThreads && !IsLogist && _logistKnown)
+ if (_world != null && ConnectionManager.Instance.IsServer && HasThreads && !IsZoneless && _logistKnown)
{
- Debug.Log("[NecromancerTome] Cinerarium: нити у обычной урны " + ToWorldPos() + " сняты - нити только у урны-логиста");
+ Debug.Log("[NecromancerTome] Cinerarium: нити у обычной урны " + ToWorldPos() + " сняты - нити только у логиста и рабочего");
ThreadFrom.Clear();
+ ThreadOut.Clear();
HasThreadTo = false;
ThreadsChanged();
}
@@ -362,6 +444,15 @@ namespace NecromancerTome
HasThreadTo = false;
gone = true;
}
+ foreach (Vector3i p in ThreadOut.ToArray())
+ {
+ bool box = CinerariumThreads.Resolve(_world, p, out Vector3i _, out TEFeatureCinerarium u3) && u3 == null;
+ if (!box && CinerariumThreads.StationAt(_world, p) == null && _world.GetChunkFromWorldPos(p) != null)
+ {
+ ThreadOut.Remove(p);
+ gone = true;
+ }
+ }
if (gone)
{
ThreadsChanged();
@@ -399,6 +490,10 @@ namespace NecromancerTome
{
pairs.Add((CinerariumThreads.UrnPoint(me), CinerariumThreads.BoxPoint(ThreadTo, me)));
}
+ foreach (Vector3i p in ThreadOut)
+ {
+ pairs.Add((CinerariumThreads.UrnPoint(me), CinerariumThreads.BoxPoint(p, me)));
+ }
_threadView.GetComponent().Build(pairs);
}
@@ -522,6 +617,20 @@ namespace NecromancerTome
Debug.Log("[NecromancerTome] Cinerarium: урна " + ToWorldPos() + (_has ? " - череп положен" : " - череп вынут"));
}
+ /// Образец рабочего положен, изменён или вынут.
+ public void SetSample(ItemStack _s)
+ {
+ ItemStack was = Sample ?? ItemStack.Empty;
+ if (was.itemValue.type == _s.itemValue.type && was.count == _s.count)
+ {
+ return;
+ }
+ Sample = _s;
+ SetModified();
+ Debug.Log("[NecromancerTome] Cinerarium: образец урны " + ToWorldPos() + " - " +
+ (_s.IsEmpty() ? "вынут" : _s.itemValue.ItemClass?.GetItemName() + " x" + _s.count));
+ }
+
/// «Починить», как у транспорта: целиком, за RepairBones костей из рюкзака (сколько берёт и молоток -
/// RepairItems в blocks.xml).
public const string RepairItemName = "resourceBone";
@@ -574,7 +683,7 @@ namespace NecromancerTome
SyncGlow();
if (HasSkull && _genie == null)
{
- _genie = CinerariumGenie.Spawn(ModelTransform());
+ _genie = CinerariumGenie.Spawn(ModelTransform(), IsWorker ? CinerariumGenie.WorkerDrop : 0f);
if (_genie != null && _sound)
{
Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), GenieSound);
@@ -754,7 +863,8 @@ namespace NecromancerTome
public static bool IsUrn(Block _b)
{
string n = _b?.GetBlockName();
- return n == BlockName || n == SkullBlockName || n == LogistBlockName || n == LogistSkullBlockName;
+ return n == BlockName || n == SkullBlockName || n == LogistBlockName || n == LogistSkullBlockName ||
+ n == WorkerBlockName || n == WorkerSkullBlockName;
}
public static int UrnLimit(EntityPlayer _player)
@@ -766,13 +876,13 @@ namespace NecromancerTome
/// Ключ подсказки, почему урну сюда нельзя, или null.
public static string PlacementRefusal(Vector3i _pos, Block _placing)
{
- if (!IsLogistBlock(_placing) && CinerariumRegistry.Overlaps(_pos))
+ if (!IsZonelessBlock(_placing) && CinerariumRegistry.Overlaps(_pos))
{
return "necroCinerariumOverlap";
}
- if (IsLogistBlock(_placing))
+ if (IsZonelessBlock(_placing))
{
- return null; // логисты в лимит не входят (16.10, пользователь)
+ return null; // логисты и рабочие в лимит не входят (16.10, 17.10 - пользователь)
}
World world = GameManager.Instance?.World;
EntityPlayerLocal player = world?.GetPrimaryPlayer();
@@ -892,7 +1002,7 @@ namespace NecromancerTome
{
// Логисты не считаются; записанные до метки «L» узнаются по блоку в мире (чанк рядом загружен).
if (kv.Value == id && !_logists.Contains(kv.Key) &&
- !(world != null && TEFeatureCinerarium.IsLogistBlock(world.GetBlock(kv.Key).Block)))
+ !(world != null && TEFeatureCinerarium.IsZonelessBlock(world.GetBlock(kv.Key).Block)))
{
n++;
}
@@ -914,9 +1024,9 @@ namespace NecromancerTome
World world = GameManager.Instance?.World;
foreach (Vector3i p in Urns().Keys)
{
- if (world != null && TEFeatureCinerarium.IsLogistBlock(world.GetBlock(p).Block))
+ if (world != null && TEFeatureCinerarium.IsZonelessBlock(world.GetBlock(p).Block))
{
- continue; // у логиста зоны нет
+ continue; // у логиста и рабочего зоны нет
}
if (p != _pos && Math.Abs(p.x - _pos.x) <= w && Math.Abs(p.z - _pos.z) <= w && Math.Abs(p.y - _pos.y) <= h)
{
@@ -969,11 +1079,11 @@ namespace NecromancerTome
}
else if (text.EndsWith("bounds_show"))
{
- _commands[i].enabled = !urn.ZoneShown && !urn.IsLogist;
+ _commands[i].enabled = !urn.ZoneShown && !urn.IsZoneless;
}
else if (text.EndsWith("bounds_hide"))
{
- _commands[i].enabled = urn.ZoneShown && !urn.IsLogist;
+ _commands[i].enabled = urn.ZoneShown && !urn.IsZoneless;
}
}
}
@@ -1107,15 +1217,19 @@ namespace NecromancerTome
GameManager.ShowTooltip(player, Localization.Get("ttCantPickupInUse"), string.Empty, "ui_denied");
return false;
}
- if (composite.TryGetSelfOrFeature(out TEFeatureStorage bag) && !bag.IsEmpty())
+ if (composite.TryGetSelfOrFeature(out TEFeatureStorage bag) && !bag.IsEmpty() ||
+ urn.Sample != null && !urn.Sample.IsEmpty())
{
GameManager.ShowTooltip(player, Localization.Get("necroCinerariumBagNotEmpty"), string.Empty, "ui_denied");
return false;
}
bool logist = TEFeatureCinerarium.IsLogistBlock(bv.Block);
+ bool worker = TEFeatureCinerarium.IsWorkerBlock(bv.Block);
ItemValue iv = ItemClass.GetItem(logist
? (urn.HasSkull ? TEFeatureCinerarium.LogistSkullBlockName : TEFeatureCinerarium.LogistBlockName)
- : (urn.HasSkull ? TEFeatureCinerarium.SkullBlockName : TEFeatureCinerarium.BlockName));
+ : worker
+ ? (urn.HasSkull ? TEFeatureCinerarium.WorkerSkullBlockName : TEFeatureCinerarium.WorkerBlockName)
+ : (urn.HasSkull ? TEFeatureCinerarium.SkullBlockName : TEFeatureCinerarium.BlockName));
if (iv == null || iv.type <= 0)
{
return true;
diff --git a/HarmonySrc/CinerariumCraft.cs b/HarmonySrc/CinerariumCraft.cs
new file mode 100644
index 0000000..07cfee7
--- /dev/null
+++ b/HarmonySrc/CinerariumCraft.cs
@@ -0,0 +1,583 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace NecromancerTome
+{
+ ///
+ /// Работа духа урны рабочего (18.10.2026, BACKLOG «Урна рабочего»): изготовить предмет из ячейки образца, пока его в запасе
+ /// меньше, чем в стопке образца. Решения 17.10: нити ящик -> урна - откуда ингредиенты и топливо, урна -> станция - где
+ /// изготавливать, урна -> ящик - куда готовое (нет такой нити - готовое остаётся в выходе станции, выход полон - станция
+ /// гаснет); только прямые рецепты (цепочки - несколько урн); качество 1; топливо - костру, химстанции, кузне, и станция
+ /// гасится, когда делать нечего (верстаку, ступке, бетономешалке топливо не нужно). Изготавливает сама станция игры: дух
+ /// только кладёт рецепт в её очередь, забрав ингредиенты, - время, огонь и выход считает игра.
+ /// Запас = такие же предметы в ящиках «куда», в выходах станций и в их очередях.
+ /// Одно дело за шаг, как у прочих духов (CinerariumWork.Step), _dry - пробный шаг без изменений.
+ ///
+ public static class CinerariumCraft
+ {
+ const int FuelModule = 3, MaterialModule = 4;
+
+ sealed class Station
+ {
+ public Vector3i Pos;
+ public TileEntityWorkstation Te;
+ public string[] Areas;
+ }
+
+ public static Vector3i? Step(World _world, TEFeatureCinerarium _urn, bool _dry, out bool _stuck, out Vector3i? _from)
+ {
+ _stuck = false;
+ _from = null;
+ ItemStack sample = _urn.Sample;
+ List<(Vector3i p, TEFeatureStorage box)> src = new List<(Vector3i, TEFeatureStorage)>();
+ foreach (Vector3i p in _urn.ThreadFrom)
+ {
+ TEFeatureStorage b = CinerariumWork.BoxAt(_world, p);
+ if (b != null)
+ {
+ src.Add((p, b));
+ }
+ }
+ List stations = new List();
+ List<(Vector3i p, TEFeatureStorage box)> outs = new List<(Vector3i, TEFeatureStorage)>();
+ foreach (Vector3i p in _urn.ThreadOut)
+ {
+ TileEntityWorkstation te = CinerariumThreads.StationAt(_world, p);
+ if (te != null)
+ {
+ if (!te.IsUserAccessing())
+ {
+ stations.Add(new Station { Pos = p, Te = te, Areas = AreasOf(_world.GetBlock(p).Block) });
+ }
+ continue;
+ }
+ TEFeatureStorage b = CinerariumWork.BoxAt(_world, p);
+ if (b != null)
+ {
+ outs.Add((p, b));
+ }
+ }
+ bool hasSample = sample != null && !sample.IsEmpty();
+
+ // 1. Готовое (только предмет образца - чужое изготовленное игроком не трогаем) - из выхода станции в ящики «куда».
+ if (hasSample && outs.Count > 0)
+ {
+ foreach (Station st in stations)
+ {
+ ItemStack[] output = st.Te.Output;
+ for (int i = 0; i < output.Length; i++)
+ {
+ ItemStack s = output[i];
+ if (s == null || s.IsEmpty() || s.itemValue.type != sample.itemValue.type)
+ {
+ continue;
+ }
+ Vector3i? dst = null;
+ foreach ((Vector3i p, TEFeatureStorage box) o in outs)
+ {
+ if (CinerariumWork.Fits(o.box, s))
+ {
+ dst = o.p;
+ break;
+ }
+ }
+ if (!dst.HasValue)
+ {
+ _stuck = true; // ящики «куда» полны
+ continue;
+ }
+ _from = st.Pos;
+ if (_dry)
+ {
+ return dst;
+ }
+ ItemStack moving = s.Clone();
+ List boxes = new List();
+ foreach ((Vector3i p, TEFeatureStorage box) o in outs)
+ {
+ boxes.Add(o.box);
+ }
+ CinerariumWork.Place(moving, boxes);
+ output[i] = moving.count > 0 ? moving : ItemStack.Empty.Clone();
+ st.Te.Output = output;
+ Debug.Log("[NecromancerTome] Cinerarium: рабочий перенёс " + (s.count - moving.count) + " " + s.itemValue.ItemClass?.GetItemName() + " из станции " + st.Pos);
+ return dst;
+ }
+ }
+ }
+
+ // 2. Изготовить, если запаса мало.
+ EntityPlayer owner = OwnerPlayer(_world, _urn);
+ if (hasSample && Stock(sample, stations, outs) < sample.count)
+ {
+ string name = sample.itemValue.ItemClass?.GetItemName();
+ foreach (Station st in stations)
+ {
+ int slot = FreeQueueSlot(st.Te);
+ if (slot < 0)
+ {
+ continue;
+ }
+ if (!OutputHasRoom(st.Te, sample) && outs.Count == 0)
+ {
+ continue; // выход полон, класть некуда - ниже станция погаснет
+ }
+ foreach (Recipe r in CraftingManager.GetRecipes(name))
+ {
+ if (r.IsScrap || !AreaFits(r, st.Areas) || !ToolFits(r, st.Te) || owner != null && !r.IsUnlocked(owner))
+ {
+ continue;
+ }
+ if (!Ingredients(r, st.Te, src, out Vector3i? srcAt, out string shortUnit))
+ {
+ // Кузня: не хватает переплавленного материала - положить сырьё во вход (плавит сама, с огнём).
+ if (shortUnit != null && FeedMaterial(st, shortUnit, src, _dry, out Vector3i? rawAt))
+ {
+ _from = rawAt;
+ if (!_dry)
+ {
+ Ignite(st.Te, src, false);
+ }
+ return st.Pos;
+ }
+ continue;
+ }
+ _from = srcAt;
+ if (_dry)
+ {
+ return st.Pos;
+ }
+ TakeIngredients(r, st.Te, src);
+ Enqueue(st.Te, slot, r);
+ Ignite(st.Te, src, false);
+ Debug.Log("[NecromancerTome] Cinerarium: рабочий поставил " + name + " на станцию " + st.Pos);
+ return st.Pos;
+ }
+ }
+ }
+
+ // 3. Огонь: работа есть - топливо и огонь; нет - погасить (экономия топлива).
+ foreach (Station st in stations)
+ {
+ if (!st.Te.isModuleUsed[FuelModule])
+ {
+ continue;
+ }
+ bool busy = st.Te.hasRecipeInQueue() || RawInInput(st.Te);
+ bool full = hasSample && !OutputHasRoom(st.Te, sample) && outs.Count == 0;
+ if (busy && !full)
+ {
+ if (st.Te.BurnTotalTimeLeft <= 0f)
+ {
+ Vector3i? fuelAt = FuelSource(src);
+ if (fuelAt.HasValue)
+ {
+ _from = fuelAt;
+ if (_dry)
+ {
+ return st.Pos;
+ }
+ Ignite(st.Te, src, true);
+ return st.Pos;
+ }
+ }
+ else if (!_dry && !st.Te.IsBurning)
+ {
+ st.Te.IsBurning = true;
+ }
+ }
+ else if (!_dry && st.Te.IsBurning)
+ {
+ st.Te.IsBurning = false;
+ Debug.Log("[NecromancerTome] Cinerarium: рабочий погасил станцию " + st.Pos + (full ? " - выход полон" : " - делать нечего"));
+ }
+ if (full)
+ {
+ _stuck = true;
+ }
+ }
+ return null;
+ }
+
+ /// Чьи рецепты изучены: хозяин урны, если он в игре; в одиночной - игрок. Нет никого - не проверяем.
+ static EntityPlayer OwnerPlayer(World _world, TEFeatureCinerarium _urn)
+ {
+ PlatformUserIdentifierAbs id = _urn.Parent?.Owner;
+ if (id != null)
+ {
+ PersistentPlayerData ppd = GameManager.Instance.persistentPlayers?.GetPlayerData(id);
+ if (ppd != null && _world.GetEntity(ppd.EntityId) is EntityPlayer p)
+ {
+ return p;
+ }
+ }
+ return _world.GetPrimaryPlayer();
+ }
+
+ /// Какие рецепты станция делает: Workstation.CraftingAreaRecipes или имя блока («player» - рецепты без станции).
+ static string[] AreasOf(Block _b)
+ {
+ if (_b == null)
+ {
+ return new string[0];
+ }
+ string a = _b.Properties.Contains("Workstation", "CraftingAreaRecipes") ? _b.Properties.GetString("Workstation", "CraftingAreaRecipes") : _b.GetBlockName();
+ return a.Replace(" ", "").Split(',');
+ }
+
+ static bool AreaFits(Recipe _r, string[] _areas)
+ {
+ string area = string.IsNullOrEmpty(_r.craftingArea) ? "player" : _r.craftingArea;
+ foreach (string a in _areas)
+ {
+ if (a.EqualsCaseInsensitive(area))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static bool ToolFits(Recipe _r, TileEntityWorkstation _te)
+ {
+ if (_r.craftingToolType == 0)
+ {
+ return true;
+ }
+ foreach (ItemStack t in _te.Tools)
+ {
+ if (t != null && !t.IsEmpty() && t.itemValue.type == _r.craftingToolType)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /// Запас: в ящиках «куда», в выходе станций и в их очередях.
+ static int Stock(ItemStack _sample, List _stations, List<(Vector3i p, TEFeatureStorage box)> _outs)
+ {
+ int type = _sample.itemValue.type;
+ int n = 0;
+ foreach ((Vector3i p, TEFeatureStorage box) o in _outs)
+ {
+ foreach (ItemStack s in o.box.ItemGrid.items)
+ {
+ if (s != null && !s.IsEmpty() && s.itemValue.type == type)
+ {
+ n += s.count;
+ }
+ }
+ }
+ foreach (Station st in _stations)
+ {
+ foreach (ItemStack s in st.Te.Output)
+ {
+ if (s != null && !s.IsEmpty() && s.itemValue.type == type)
+ {
+ n += s.count;
+ }
+ }
+ foreach (RecipeQueueItem q in st.Te.Queue)
+ {
+ if (q != null && q.Recipe != null && q.Multiplier > 0 && q.Recipe.itemValueType == type)
+ {
+ n += q.Multiplier * q.Recipe.count;
+ }
+ }
+ }
+ return n;
+ }
+
+ /// Свободная ячейка очереди: самая близкая к работающей (последней).
+ static int FreeQueueSlot(TileEntityWorkstation _te)
+ {
+ RecipeQueueItem[] q = _te.Queue;
+ for (int i = q.Length - 1; i >= 0; i--)
+ {
+ if (q[i] == null || q[i].Recipe == null || q[i].Multiplier <= 0)
+ {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ static bool OutputHasRoom(TileEntityWorkstation _te, ItemStack _sample)
+ {
+ foreach (ItemStack s in _te.Output)
+ {
+ if (s == null || s.IsEmpty())
+ {
+ return true;
+ }
+ int n = 1;
+ if (s.itemValue.type == _sample.itemValue.type && s.CanStackPartly(ref n))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /// Индекс ячейки переплавленного материала кузни для unit_X (вход 3 + номер материала), или -1.
+ static int UnitSlot(TileEntityWorkstation _te, string _itemName)
+ {
+ if (!_te.isModuleUsed[MaterialModule] || !_itemName.StartsWith("unit_"))
+ {
+ return -1;
+ }
+ string mat = _itemName.Substring(5);
+ for (int j = 0; j < _te.materialNames.Length; j++)
+ {
+ if (_te.materialNames[j].EqualsCaseInsensitive(mat) && 3 + j < _te.Input.Length)
+ {
+ return 3 + j;
+ }
+ }
+ return -1;
+ }
+
+ /// Хватает ли всего: переплавленное - в кузне, прочее - в ящиках «откуда». _srcAt - ящик, откуда первое;
+ /// _shortUnit - какого переплавленного не хватило (кузне можно подложить сырья).
+ static bool Ingredients(Recipe _r, TileEntityWorkstation _te, List<(Vector3i p, TEFeatureStorage box)> _src, out Vector3i? _srcAt, out string _shortUnit)
+ {
+ _srcAt = null;
+ _shortUnit = null;
+ foreach (ItemStack ing in _r.ingredients)
+ {
+ if (ing == null || ing.IsEmpty() || ing.count <= 0)
+ {
+ continue;
+ }
+ string iname = ing.itemValue.ItemClass?.GetItemName() ?? "";
+ int us = UnitSlot(_te, iname);
+ if (us >= 0)
+ {
+ if (_te.Input[us].count < ing.count)
+ {
+ _shortUnit = iname;
+ return false;
+ }
+ continue;
+ }
+ int have = 0;
+ foreach ((Vector3i p, TEFeatureStorage box) s in _src)
+ {
+ ItemStack[] grid = s.box.ItemGrid.items;
+ for (int k = 0; k < grid.Length; k++)
+ {
+ if (grid[k] != null && !grid[k].IsEmpty() && grid[k].itemValue.type == ing.itemValue.type && !CinerariumWork.Locked(s.box, k))
+ {
+ have += grid[k].count;
+ if (!_srcAt.HasValue)
+ {
+ _srcAt = s.p;
+ }
+ }
+ }
+ }
+ if (have < ing.count)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static void TakeIngredients(Recipe _r, TileEntityWorkstation _te, List<(Vector3i p, TEFeatureStorage box)> _src)
+ {
+ ItemStack[] input = _te.Input;
+ bool inputChanged = false;
+ foreach (ItemStack ing in _r.ingredients)
+ {
+ if (ing == null || ing.IsEmpty() || ing.count <= 0)
+ {
+ continue;
+ }
+ int us = UnitSlot(_te, ing.itemValue.ItemClass?.GetItemName() ?? "");
+ if (us >= 0)
+ {
+ input[us].count -= ing.count;
+ inputChanged = true;
+ continue;
+ }
+ int need = ing.count;
+ foreach ((Vector3i p, TEFeatureStorage box) s in _src)
+ {
+ ItemStack[] grid = s.box.ItemGrid.items;
+ for (int k = 0; k < grid.Length && need > 0; k++)
+ {
+ if (grid[k] != null && !grid[k].IsEmpty() && grid[k].itemValue.type == ing.itemValue.type && !CinerariumWork.Locked(s.box, k))
+ {
+ int take = Mathf.Min(need, grid[k].count);
+ CinerariumWork.Take(s.box, k, take);
+ need -= take;
+ }
+ }
+ if (need <= 0)
+ {
+ break;
+ }
+ }
+ }
+ if (inputChanged)
+ {
+ _te.Input = input;
+ }
+ }
+
+ static void Enqueue(TileEntityWorkstation _te, int _slot, Recipe _r)
+ {
+ RecipeQueueItem[] q = _te.Queue;
+ float t = _r.craftingTime > 0f ? _r.craftingTime : 1f;
+ ItemClass outClass = ItemClass.GetForId(_r.itemValueType);
+ q[_slot] = new RecipeQueueItem
+ {
+ Recipe = _r,
+ Multiplier = 1,
+ CraftingTimeLeft = t,
+ OneItemCraftTime = t,
+ IsCrafting = _slot == q.Length - 1,
+ Quality = (byte)(outClass != null && outClass.HasQuality ? 1 : 0),
+ StartingEntityId = -1
+ };
+ _te.Queue = q;
+ }
+
+ static bool RawInInput(TileEntityWorkstation _te)
+ {
+ if (!_te.isModuleUsed[MaterialModule])
+ {
+ return false;
+ }
+ for (int i = 0; i < 3 && i < _te.Input.Length; i++)
+ {
+ if (_te.Input[i] != null && !_te.Input[i].IsEmpty())
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /// Сырьё для переплавки в unit_X: предмет из ящика «откуда», сделанный из этого материала, - во вход кузни.
+ static bool FeedMaterial(Station _st, string _unit, List<(Vector3i p, TEFeatureStorage box)> _src, bool _dry, out Vector3i? _at)
+ {
+ _at = null;
+ string mat = _unit.Substring(5);
+ ItemStack[] input = _st.Te.Input;
+ foreach ((Vector3i p, TEFeatureStorage box) s in _src)
+ {
+ ItemStack[] grid = s.box.ItemGrid.items;
+ for (int k = 0; k < grid.Length; k++)
+ {
+ ItemStack g = grid[k];
+ ItemClass ic = g != null && !g.IsEmpty() ? g.itemValue.ItemClass : null;
+ if (ic == null || ic.MadeOfMaterial?.ForgeCategory == null || !ic.MadeOfMaterial.ForgeCategory.EqualsCaseInsensitive(mat) ||
+ ic.GetItemName().StartsWith("unit_") || CinerariumWork.Locked(s.box, k))
+ {
+ continue;
+ }
+ for (int i = 0; i < 3 && i < input.Length; i++)
+ {
+ bool empty = input[i] == null || input[i].IsEmpty();
+ int room = empty ? ic.MaxCount : input[i].itemValue.type == g.itemValue.type ? ic.MaxCount - input[i].count : 0;
+ if (room <= 0)
+ {
+ continue;
+ }
+ _at = s.p;
+ if (_dry)
+ {
+ return true;
+ }
+ int take = Mathf.Min(room, g.count);
+ if (empty)
+ {
+ input[i] = new ItemStack(g.itemValue.Clone(), take);
+ }
+ else
+ {
+ input[i].count += take;
+ }
+ CinerariumWork.Take(s.box, k, take);
+ _st.Te.Input = input;
+ Debug.Log("[NecromancerTome] Cinerarium: рабочий положил в кузню " + _st.Pos + " " + take + " " + ic.GetItemName());
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ static Vector3i? FuelSource(List<(Vector3i p, TEFeatureStorage box)> _src)
+ {
+ foreach ((Vector3i p, TEFeatureStorage box) s in _src)
+ {
+ ItemStack[] grid = s.box.ItemGrid.items;
+ for (int k = 0; k < grid.Length; k++)
+ {
+ if (grid[k] != null && !grid[k].IsEmpty() && ItemClass.GetFuelValue(grid[k].itemValue) > 0 && !CinerariumWork.Locked(s.box, k))
+ {
+ return s.p;
+ }
+ }
+ }
+ return null;
+ }
+
+ /// Огонь станции с топливом: нет топлива - стопку горючего из ящиков «откуда» (_addFuel), затем зажечь.
+ static void Ignite(TileEntityWorkstation _te, List<(Vector3i p, TEFeatureStorage box)> _src, bool _addFuel)
+ {
+ if (!_te.isModuleUsed[FuelModule])
+ {
+ return;
+ }
+ if (_addFuel || _te.BurnTotalTimeLeft <= 0f)
+ {
+ ItemStack[] fuel = _te.Fuel;
+ foreach ((Vector3i p, TEFeatureStorage box) s in _src)
+ {
+ ItemStack[] grid = s.box.ItemGrid.items;
+ for (int k = 0; k < grid.Length; k++)
+ {
+ ItemStack g = grid[k];
+ if (g == null || g.IsEmpty() || ItemClass.GetFuelValue(g.itemValue) <= 0 || CinerariumWork.Locked(s.box, k))
+ {
+ continue;
+ }
+ for (int i = 0; i < fuel.Length; i++)
+ {
+ bool empty = fuel[i] == null || fuel[i].IsEmpty();
+ int max = g.itemValue.ItemClass.MaxCount;
+ int room = empty ? max : fuel[i].itemValue.type == g.itemValue.type ? max - fuel[i].count : 0;
+ if (room <= 0)
+ {
+ continue;
+ }
+ int take = Mathf.Min(room, g.count);
+ if (empty)
+ {
+ fuel[i] = new ItemStack(g.itemValue.Clone(), take);
+ }
+ else
+ {
+ fuel[i].count += take;
+ }
+ CinerariumWork.Take(s.box, k, take);
+ _te.Fuel = fuel;
+ Debug.Log("[NecromancerTome] Cinerarium: рабочий подбросил топливо " + take + " " + g.itemValue.ItemClass.GetItemName());
+ goto lit;
+ }
+ }
+ }
+ }
+ lit:
+ if (_te.BurnTotalTimeLeft > 0f && !_te.IsBurning)
+ {
+ _te.IsBurning = true;
+ }
+ }
+ }
+}
diff --git a/HarmonySrc/CinerariumGenie.cs b/HarmonySrc/CinerariumGenie.cs
index 490b750..c980cc1 100644
--- a/HarmonySrc/CinerariumGenie.cs
+++ b/HarmonySrc/CinerariumGenie.cs
@@ -25,6 +25,8 @@ namespace NecromancerTome
/// Высота корня модели над низом блока урны, м. 16.10 в Blender по модели урны: -0.15 («духа чуть приподними»).
/// Подгоняется командой necrogenie.
public static float OffsetY = -0.15f;
+ /// У урны рабочего горлышко ниже (земля в чаше на 0.45 м против 0.53 у урны сборщика) - дух ниже на столько же.
+ public static float WorkerDrop = -0.08f;
/// Сдвиг вперёд в системе самого духа, м (16.10: «чуть вперёд, чтобы хвост не просвечивал сквозь урну сзади»
/// - загнутый назад кончик хвоста; в Blender 0.06 - ни одной вершины за стенкой, взято 0.07). Поворачивается вместе
/// с духом, поэтому хвост всегда в горлышке. Знак проверить в игре: necrogenie fwd.
@@ -79,7 +81,7 @@ namespace NecromancerTome
return _prefab != null;
}
- public static GameObject Spawn(Transform _urnModel)
+ public static GameObject Spawn(Transform _urnModel, float _drop)
{
if (_urnModel == null || !Load())
{
@@ -110,7 +112,9 @@ namespace NecromancerTome
{
Object.Destroy(c);
}
- g.AddComponent().Pose();
+ CinerariumGenieMotion m = g.AddComponent();
+ m.Drop = _drop;
+ m.Pose();
return g;
}
@@ -267,6 +271,8 @@ namespace NecromancerTome
{
float _yaw;
float _phase;
+ /// Своя поправка высоты урны (CinerariumGenie.WorkerDrop у рабочего).
+ public float Drop;
/// Полёт к месту работы и обратно (16.10: «возвращается задом», «делал действие не сразу, а хоть руками
/// пошевелив»): туда - лицом к месту, над местом - жест руками (дело делается в его середине, см. Visit),
/// обратно - развернувшись лицом к урне. Точка - в координатах модели урны; _t - время с начала полёта, -1 - дома.
@@ -439,7 +445,7 @@ namespace NecromancerTome
{
Stop st = new Stop
{
- Hover = transform.parent.InverseTransformPoint(stand) + new Vector3(0f, CinerariumGenie.OffsetY + (beside ? 0.15f : 0.8f), 0f),
+ Hover = transform.parent.InverseTransformPoint(stand) + new Vector3(0f, CinerariumGenie.OffsetY + Drop + (beside ? 0.15f : 0.8f), 0f),
Look = transform.parent.InverseTransformPoint(look)
};
st.Fly = Mathf.Max(OutSeconds, Vector3.Distance(prev, st.Hover) / FlySpeed);
@@ -454,7 +460,7 @@ namespace NecromancerTome
Vector3 Home()
{
- return new Vector3(0f, CinerariumGenie.OffsetY, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward);
+ return new Vector3(0f, CinerariumGenie.OffsetY + Drop, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward);
}
/// Повернуться лицом по направлению _dir (горизонтально), не быстрее 300°/с.
diff --git a/HarmonySrc/CinerariumThreads.cs b/HarmonySrc/CinerariumThreads.cs
index 67eae3f..1730e26 100644
--- a/HarmonySrc/CinerariumThreads.cs
+++ b/HarmonySrc/CinerariumThreads.cs
@@ -21,6 +21,10 @@ namespace NecromancerTome
/// (ItemValue.Clone со сменой типа). Как у стриппера: ПКМ - начать нить и закончить её (порядок = направление: ящик, потом
/// урна - «откуда»; урна, потом ящик - «куда»), ЛКМ по урне или ящику - снять его нити.
/// Первая версия - одиночная игра и хост (связи меняются на месте, клиентам сети - позже).
+ ///
+ /// Урна рабочего (18.10): нити ящик -> урна - откуда брать ингредиенты и топливо, урна -> станция - где изготавливать,
+ /// урна -> ящик - куда класть готовое; станций и ящиков сколько угодно (до MaxFrom в каждую сторону). Нить между станцией
+ /// и урной рабочего - всегда «от урны», в каком бы порядке её ни тянули.
///
public static class CinerariumThreads
{
@@ -66,6 +70,7 @@ namespace NecromancerTome
static Vector3i? _start;
static bool _startUrn;
+ static bool _startStation;
static GameObject _preview;
public static Vector3i? Start => _start;
@@ -98,6 +103,16 @@ namespace NecromancerTome
return comp.PlayerPlaced && comp.TryGetSelfOrFeature(out TEFeatureStorage _);
}
+ /// Станция игры (верстак, костёр, кузня, химстанция, бетономешалка, ступка) в клетке _p; _owner - главная клетка.
+ public static TileEntityWorkstation StationAt(World _w, Vector3i _p, out Vector3i _owner)
+ {
+ BlockValue bv = _w.GetBlock(_p);
+ _owner = bv.ischild ? _p + bv.parent : _p;
+ return _w.GetTileEntity(_owner) as TileEntityWorkstation;
+ }
+
+ public static TileEntityWorkstation StationAt(World _w, Vector3i _p) => StationAt(_w, _p, out Vector3i _);
+
public static TEFeatureCinerarium UrnAt(World _w, Vector3i _p)
{
return _w.GetTileEntity(_p) is TileEntityComposite c && c.TryGetSelfOrFeature(out TEFeatureCinerarium u) ? u : null;
@@ -113,6 +128,13 @@ namespace NecromancerTome
return;
}
bool ok = Resolve(w, hi.hit.blockPos, out Vector3i owner, out TEFeatureCinerarium urn);
+ bool station = false;
+ if (!ok && StationAt(w, hi.hit.blockPos, out Vector3i stOwner) != null)
+ {
+ ok = true;
+ station = true;
+ owner = stOwner;
+ }
if (!_connect)
{
if (_start.HasValue)
@@ -135,13 +157,14 @@ namespace NecromancerTome
}
if (!_start.HasValue)
{
- if (urn != null && !urn.IsLogist)
+ if (urn != null && !urn.IsZoneless)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedLogist"));
return;
}
_start = owner;
_startUrn = urn != null;
+ _startStation = station;
_preview = new GameObject("NecroThreadPreview");
_preview.AddComponent();
_pl.PlayOneShot("wiretool_grab");
@@ -150,19 +173,44 @@ namespace NecromancerTome
}
Vector3i s = _start.Value;
bool sUrn = _startUrn;
+ bool sStation = _startStation;
Cancel();
if (s == owner)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadCancel"));
return;
}
+ // Станция - только с урной рабочего, нить всегда «от урны».
+ if (sStation || station)
+ {
+ TEFeatureCinerarium wu = sStation ? urn : UrnAt(w, s);
+ Vector3i st = sStation ? s : owner;
+ if (wu == null || !wu.IsWorker)
+ {
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedWorker"));
+ return;
+ }
+ if (Vector3.Distance(s.ToVector3(), owner.ToVector3()) > MaxLength)
+ {
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTooFar"));
+ return;
+ }
+ if (!wu.AddThreadOut(st))
+ {
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadLimit"));
+ return;
+ }
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadStation"));
+ _pl.PlayOneShot("wiretool_place");
+ return;
+ }
if (sUrn == (urn != null))
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedPair"));
return;
}
TEFeatureCinerarium end = sUrn ? UrnAt(w, s) : urn;
- if (end == null || !end.IsLogist)
+ if (end == null || !end.IsZoneless)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedLogist"));
return;
@@ -179,8 +227,21 @@ namespace NecromancerTome
{
return;
}
- u.SetThreadTo(owner);
- GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTo"));
+ if (u.IsWorker)
+ {
+ // У рабочего ящиков «куда» сколько угодно - туда готовое.
+ if (!u.AddThreadOut(owner))
+ {
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadLimit"));
+ return;
+ }
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadOut"));
+ }
+ else
+ {
+ u.SetThreadTo(owner);
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTo"));
+ }
}
else
{
diff --git a/HarmonySrc/CinerariumTrunk.cs b/HarmonySrc/CinerariumTrunk.cs
index dcae0bb..de59b6c 100644
--- a/HarmonySrc/CinerariumTrunk.cs
+++ b/HarmonySrc/CinerariumTrunk.cs
@@ -9,6 +9,7 @@ namespace NecromancerTome
/// ячейкой коллектора не принимало череп ни перетаскиванием, ни Shift+кликом). Своё здесь одно: после каждого изменения
/// в ячейке остаётся один череп, всё остальное (лишние черепа, чужие предметы) - назад в рюкзак, и урна узнаёт, есть ли
/// в ней череп. Череп - обычный предмет (ItemClass): он уже в сейвах, менять ему класс на модификацию нельзя.
+ /// У урны рабочего (18.10) ячеек две: вторая - образец (любой предмет, стопка целиком - сколько держать в запасе).
///
public static class CinerariumContents
{
@@ -30,11 +31,15 @@ namespace NecromancerTome
}
_urn = _u;
_player = _p;
- _bag = new Bag(new Vector2i(1, 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null);
+ _bag = new Bag(new Vector2i(_u.IsWorker ? 2 : 1, 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null);
if (_u.HasSkull && skull != null && skull.type > 0)
{
_bag.SetSlot(0, new ItemStack(skull, 1));
}
+ if (_u.IsWorker && _u.Sample != null && !_u.Sample.IsEmpty())
+ {
+ _bag.SetSlot(1, _u.Sample.Clone());
+ }
XUiC_BagStorageWindowGroup.Open(ui.xui, _p, _bag, LootContainer.GetLootContainer(LootTemplate),
Localization.Get("necroCinerariumTrunk"), Check, () =>
{
@@ -46,13 +51,27 @@ namespace NecromancerTome
Debug.Log("[NecromancerTome] Cinerarium: «Содержимое» урны " + _u.ToWorldPos() + " открыто, череп " + (_u.HasSkull ? "есть" : "нет"));
}
- /// Оставить в ячейке один череп, остальное - игроку; отметить в урне.
+ /// Оставить в ячейке один череп, остальное - игроку; отметить в урне. У рабочего - запомнить образец; череп во
+ /// второй ячейке - не образец, переложить в первую (или вернуть).
static void Check()
{
if (_bag == null || _urn == null)
{
return;
}
+ if (_urn.IsWorker && _bag.ItemGrid.items.Length > 1)
+ {
+ ItemStack smp = _bag.ItemGrid.items[1];
+ ItemStack first = _bag.ItemGrid.items[0];
+ if (smp != null && !smp.IsEmpty() && smp.itemValue.ItemClass?.GetItemName() == TEFeatureCinerarium.SkullItemName &&
+ (first == null || first.IsEmpty()))
+ {
+ _bag.SetSlot(0, smp.Clone());
+ _bag.SetSlot(1, ItemStack.Empty.Clone());
+ smp = _bag.ItemGrid.items[1];
+ }
+ _urn.SetSample(smp != null && !smp.IsEmpty() ? smp.Clone() : ItemStack.Empty.Clone());
+ }
ItemStack s = _bag.ItemGrid.items[0];
bool has = false;
if (s != null && !s.IsEmpty())
diff --git a/HarmonySrc/CinerariumWork.cs b/HarmonySrc/CinerariumWork.cs
index 9701bfd..ac75853 100644
--- a/HarmonySrc/CinerariumWork.cs
+++ b/HarmonySrc/CinerariumWork.cs
@@ -71,6 +71,13 @@ namespace NecromancerTome
{
return Ferry(_world, _urn);
}
+ // Урна рабочего - изготовление по образцу на станциях по нитям (CinerariumCraft.cs).
+ if (_urn.IsWorker)
+ {
+ Vector3i? at = CinerariumCraft.Step(_world, _urn, _dry, out _stuck, out Vector3i? from);
+ LastFrom = from;
+ return at;
+ }
Zone z = Scan(_world, _urn);
// 1. Расходники.
foreach (TileEntityCollector c in z.Collectors)
@@ -184,7 +191,7 @@ namespace NecromancerTome
return null;
}
- static TEFeatureStorage BoxAt(World _world, Vector3i _p)
+ internal static TEFeatureStorage BoxAt(World _world, Vector3i _p)
{
return _world.GetTileEntity(_p) is TileEntityComposite c && !c.IsUserAccessing() &&
!c.TryGetSelfOrFeature(out TEFeatureCinerarium _) &&
@@ -285,7 +292,7 @@ namespace NecromancerTome
return _z.Bag != null && Fits(_z.Bag, _s);
}
- static bool Fits(TEFeatureStorage _t, ItemStack _s)
+ internal static bool Fits(TEFeatureStorage _t, ItemStack _s)
{
for (int i = 0; i < _t.ItemGrid.Length; i++)
{
@@ -519,7 +526,7 @@ namespace NecromancerTome
return false;
}
- static void Place(ItemStack _s, IList _dst)
+ internal static void Place(ItemStack _s, IList _dst)
{
for (int i = 0; i < _dst.Count && _s.count > 0; i++)
{
@@ -548,13 +555,13 @@ namespace NecromancerTome
}
/// Ячейка, закреплённая игроком (замок в окне ящика), - не трогать.
- static bool Locked(TEFeatureStorage _src, int _slot)
+ internal static bool Locked(TEFeatureStorage _src, int _slot)
{
PackedBoolArray locks = _src.ItemGrid.SlotLocks;
return locks != null && _slot < locks.Length && locks[_slot];
}
- static void Take(TEFeatureStorage _src, int _slot, int _count)
+ internal static void Take(TEFeatureStorage _src, int _slot, int _count)
{
ItemStack s = _src.ItemGrid[_slot].Clone();
s.count -= _count;
diff --git a/NecromancerHarmony.dll b/NecromancerHarmony.dll
index 928d7c0..507a3cc 100644
Binary files a/NecromancerHarmony.dll and b/NecromancerHarmony.dll differ