diff --git a/Config/Localization.csv b/Config/Localization.csv
index b7d5052..c0fb0c3 100644
--- a/Config/Localization.csv
+++ b/Config/Localization.csv
@@ -153,6 +153,21 @@ necroCinerariumLogistDesc,blocks,Block,,,,"A brass censer for a spirit that carr
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)拉線:箱子到骨灰甕——材料和燃料;骨灰甕到工作台——在哪裡製作;骨灰甕到箱子——成品放在哪裡(沒有則留在工作台裡)。篝火、熔爐和化學工作台由靈自己點燃和熄滅。它沒有自己的區域,不計入骨灰甕數量上限。
+necroCinerariumRepair,blocks,Block,,,,Repair Cinerarium,,Reparatur-Cinerarium,Cinerario reparador,Cinéraire réparateur,Cinerario riparatore,修繕の骨壺,수리 유골함,Cinerarium naprawcze,Cinerário reparador,Цинерарий-ремонтник,Tamirci Küllüğü,修缮骨灰瓮,修繕骨灰甕
+necroCinerariumRepairSkull,blocks,Block,,,,Repair Cinerarium (with skull),,Reparatur-Cinerarium (mit Schädel),Cinerario reparador (con cráneo),Cinéraire réparateur (avec crâne),Cinerario riparatore (con teschio),修繕の骨壺(頭蓋骨入り),수리 유골함 (두개골 포함),Cinerarium naprawcze (z czaszką),Cinerário reparador (com crânio),Цинерарий-ремонтник (с черепом),Tamirci Küllüğü (kafatasıyla),修缮骨灰瓮(含头骨),修繕骨灰甕(含頭骨)
+necroCinerariumLoot,blocks,Block,,,,Scavenger Cinerarium,,Sammler-Cinerarium,Cinerario saqueador,Cinéraire pilleur,Cinerario predatore,拾い集めの骨壺,수집 유골함,Cinerarium zbieracza,Cinerário saqueador,Цинерарий-лутер,Yağmacı Küllüğü,拾荒骨灰瓮,拾荒骨灰甕
+necroCinerariumLootSkull,blocks,Block,,,,Scavenger Cinerarium (with skull),,Sammler-Cinerarium (mit Schädel),Cinerario saqueador (con cráneo),Cinéraire pilleur (avec crâne),Cinerario predatore (con teschio),拾い集めの骨壺(頭蓋骨入り),수집 유골함 (두개골 포함),Cinerarium zbieracza (z czaszką),Cinerário saqueador (com crânio),Цинерарий-лутер (с черепом),Yağmacı Küllüğü (kafatasıyla),拾荒骨灰瓮(含头骨),拾荒骨灰甕(含頭骨)
+necroCinerariumRepairDesc,blocks,Block,,,,"An urn for a spirit that mends your base. Place a skull inside and tie chests with repair materials to it with ghostly threads - the Necromancer's Knife in the thread grip (R). Its zone is as wide as a land claim or smaller, and spans the whole height of the world. Press Start and the spirit walks the zone once, slowly mending every damaged block with materials from the chests; destroyed blocks are yours to rebuild. The skull buttons on the urn show its state: yellow - waiting, blinking yellow - out of materials, green - at work, red - blood tide. Zones of such urns may overlap. Does not count toward the urn limit.",,"Eine Urne für einen Geist, der deine Basis ausbessert. Leg einen Schädel hinein und verbinde Truhen mit Reparaturmaterial über geisterhafte Fäden mit ihr - mit dem Messer des Nekromanten im Fadengriff (R). Ihre Zone ist so breit wie ein Landanspruch oder kleiner und reicht über die ganze Höhe der Welt. Drück Start, und der Geist geht die Zone einmal ab und repariert langsam jeden beschädigten Block mit Material aus den Truhen; Zerstörtes baust du selbst wieder auf. Die Schädelknöpfe an der Urne zeigen ihren Zustand: gelb - wartet, gelb blinkend - kein Material, grün - arbeitet, rot - Blutflut. Zonen solcher Urnen dürfen sich überschneiden. Zählt nicht zum Urnenlimit.","Una urna para un espíritu que repara tu base. Pon un cráneo dentro y átale cofres con materiales de reparación mediante hilos fantasmales: el Cuchillo del Nigromante en agarre de hilo (R). Su zona es tan ancha como una reclamación de tierra o menor y abarca toda la altura del mundo. Pulsa Iniciar y el espíritu recorre la zona una vez, reparando despacio cada bloque dañado con materiales de los cofres; lo destruido lo reconstruyes tú. Los botones de calavera de la urna muestran su estado: amarillo - espera, amarillo parpadeante - sin materiales, verde - trabajando, rojo - marea de sangre. Las zonas de estas urnas pueden solaparse. No cuenta para el límite de urnas.","Une urne pour un esprit qui répare ta base. Mets un crâne à l'intérieur et relie-lui des coffres de matériaux de réparation par des fils fantomatiques - le Couteau du Nécromancien en prise de fil (R). Sa zone est aussi large qu'une revendication de terrain ou plus petite et couvre toute la hauteur du monde. Appuie sur Démarrer et l'esprit parcourt la zone une fois, réparant lentement chaque bloc endommagé avec les matériaux des coffres ; ce qui est détruit, c'est à toi de le reconstruire. Les boutons-crânes de l'urne montrent son état : jaune - en attente, jaune clignotant - plus de matériaux, vert - au travail, rouge - marée de sang. Les zones de ces urnes peuvent se chevaucher. Ne compte pas dans la limite d'urnes.","Un'urna per uno spirito che ripara la tua base. Metti un teschio dentro e collegale con fili spettrali forzieri con materiali di riparazione: il Coltello del Negromante nella presa da filo (R). La sua zona è larga quanto una rivendicazione di terreno o meno e copre tutta l'altezza del mondo. Premi Avvia e lo spirito percorre la zona una volta, riparando lentamente ogni blocco danneggiato con i materiali dei forzieri; ciò che è distrutto lo ricostruisci tu. I pulsanti a teschio sull'urna mostrano lo stato: giallo - attesa, giallo lampeggiante - mancano materiali, verde - al lavoro, rosso - marea di sangue. Le zone di queste urne possono sovrapporsi. Non conta nel limite di urne.",拠点を直す霊のための骨壺。頭蓋骨を入れ、修理材料の入った箱を霊の糸でつなぐ――糸の握りにした死霊術師のナイフ(R)で。ゾーンは土地の権利と同じ幅かそれ以下で、世界の高さ全体に及ぶ。開始を押すと霊がゾーンを一巡し、箱の材料で傷んだブロックをゆっくり直していく。壊れたものは自分で建て直すこと。骨壺の頭蓋骨ボタンが状態を示す:黄――待機、黄の点滅――材料切れ、緑――作業中、赤――血の潮。この種の骨壺のゾーンは重なってもよい。骨壺の上限には数えない。,"기지를 고치는 영혼을 위한 유골함. 두개골을 넣고 수리 재료가 든 상자를 유령 실로 잇는다 - 실 잡기로 쥔 강령술사의 칼(R)로. 구역은 토지 소유권만큼 넓거나 더 작고, 세계의 높이 전체에 걸친다. 시작을 누르면 영혼이 구역을 한 바퀴 돌며 상자의 재료로 손상된 블록을 천천히 고친다. 부서진 것은 직접 다시 지어야 한다. 유골함의 두개골 버튼이 상태를 보여 준다: 노랑 - 대기, 노랑 깜빡임 - 재료 부족, 초록 - 작업 중, 빨강 - 피의 조수. 이런 유골함의 구역은 겹쳐도 된다. 유골함 한도에 포함되지 않는다.","Urna dla ducha, który naprawia twoją bazę. Włóż czaszkę i połącz z nią widmowymi nićmi skrzynie z materiałami do napraw - Nożem Nekromanty w chwycie do nici (R). Jej strefa jest szeroka jak działka lub mniejsza i obejmuje całą wysokość świata. Naciśnij Start, a duch raz obejdzie strefę, powoli naprawiając każdy uszkodzony blok materiałami ze skrzyń; zniszczone odbudowujesz sam. Czaszkowe przyciski na urnie pokazują stan: żółty - czeka, żółty migający - brak materiałów, zielony - pracuje, czerwony - krwawy przypływ. Strefy takich urn mogą na siebie nachodzić. Nie liczy się do limitu urn.","Uma urna para um espírito que conserta sua base. Coloque um crânio dentro e ligue a ela, com fios fantasmagóricos, baús com materiais de reparo - a Faca do Necromante na pegada de fio (R). Sua zona é tão larga quanto uma reivindicação de terra ou menor e abrange toda a altura do mundo. Pressione Iniciar e o espírito percorre a zona uma vez, consertando devagar cada bloco danificado com materiais dos baús; o que foi destruído você reconstrói. Os botões de caveira da urna mostram o estado: amarelo - esperando, amarelo piscando - sem materiais, verde - trabalhando, vermelho - maré de sangue. Zonas dessas urnas podem se sobrepor. Não conta para o limite de urnas.","Урна для духа, который чинит твою базу. Положи внутрь череп и протяни к урне призрачные нити от ящиков с материалами для ремонта - нож некроманта хватом для нитей (R). Зона - шириной с землю притязания или меньше, по высоте - весь мир. Нажми «Запустить», и дух один раз обойдёт зону и неспешно починит каждый повреждённый блок материалами из ящиков; разрушенное отстраиваешь сам. Кнопки с черепом на урне показывают состояние: жёлтая - ждёт, жёлтая мигает - не хватает материалов, зелёная - работает, красная - кровавый прилив. Зоны таких урн могут пересекаться. В лимит урн не входит.","Üssünü onaran bir ruh için küllük. İçine bir kafatası koy ve onarım malzemeli sandıkları hayalet ipliklerle bağla - iplik tutuşundaki Nekromansör Bıçağıyla (R). Bölgesi bir arazi talebi kadar geniş ya da daha dardır ve dünyanın tüm yüksekliğini kapsar. Başlat'a bas, ruh bölgeyi bir kez dolaşıp sandıklardaki malzemeyle hasarlı her bloğu yavaşça onarsın; yıkılanı sen yeniden kurarsın. Küllükteki kafatası düğmeleri durumu gösterir: sarı - bekliyor, yanıp sönen sarı - malzeme yok, yeşil - çalışıyor, kırmızı - kan gelgiti. Bu küllüklerin bölgeleri çakışabilir. Küllük sınırına sayılmaz.",一座骨灰瓮,供修缮你基地的灵魂栖身。放入一颗头骨,再用幽灵丝线把装有修理材料的箱子连到它上——用丝线握法的死灵法师之刃(R)。它的区域与领地一样宽或更小,并覆盖整个世界的高度。按下“启动”,灵魂会把区域巡视一遍,用箱子里的材料慢慢修好每个受损的方块;被摧毁的需要你自己重建。骨灰瓮上的头骨按钮显示状态:黄——等待,黄色闪烁——材料不足,绿——工作中,红——血潮。这类骨灰瓮的区域可以重叠。不计入骨灰瓮上限。,一座骨灰甕,供修繕你基地的靈魂棲身。放入一顆頭骨,再用幽靈絲線把裝有修理材料的箱子連到它上——用絲線握法的死靈法師之刃(R)。它的區域與領地一樣寬或更小,並覆蓋整個世界的高度。按下「啟動」,靈魂會把區域巡視一遍,用箱子裡的材料慢慢修好每個受損的方塊;被摧毀的需要你自己重建。骨灰甕上的頭骨按鈕顯示狀態:黃——等待,黃色閃爍——材料不足,綠——工作中,紅——血潮。這類骨灰甕的區域可以重疊。不計入骨灰甕上限。
+necroCinerariumLootDesc,blocks,Block,,,,"An urn for a spirit that gathers the spoils. Place a skull inside and tie a chest to it with a ghostly thread in either direction - the Necromancer's Knife in the thread grip (R). Its zone is as wide as a land claim or smaller, and spans the whole height of the world. Press Start and the spirit sweeps the zone once: items on the ground, zombie loot bags and red corpses, everything into the chest. It leaves building containers, airdrops and fallen players' backpacks alone. The skull buttons on the urn show its state: yellow - waiting, blinking yellow - the chest is full, green - at work, red - blood tide. Does not count toward the urn limit.",,"Eine Urne für einen Geist, der die Beute einsammelt. Leg einen Schädel hinein und verbinde eine Truhe in beliebiger Richtung über einen geisterhaften Faden mit ihr - mit dem Messer des Nekromanten im Fadengriff (R). Ihre Zone ist so breit wie ein Landanspruch oder kleiner und reicht über die ganze Höhe der Welt. Drück Start, und der Geist kehrt die Zone einmal ab: Gegenstände am Boden, Zombie-Beutel und rote Leichen, alles in die Truhe. Behälter in Gebäuden, Abwürfe und Rucksäcke gefallener Spieler lässt er liegen. Die Schädelknöpfe an der Urne zeigen ihren Zustand: gelb - wartet, gelb blinkend - Truhe voll, grün - arbeitet, rot - Blutflut. Zählt nicht zum Urnenlimit.","Una urna para un espíritu que recoge el botín. Pon un cráneo dentro y átale un cofre con un hilo fantasmal en cualquier dirección: el Cuchillo del Nigromante en agarre de hilo (R). Su zona es tan ancha como una reclamación de tierra o menor y abarca toda la altura del mundo. Pulsa Iniciar y el espíritu barre la zona una vez: objetos en el suelo, bolsas de botín de zombis y cadáveres rojos, todo al cofre. Deja en paz los contenedores de edificios, los suministros aéreos y las mochilas de jugadores caídos. Los botones de calavera de la urna muestran su estado: amarillo - espera, amarillo parpadeante - cofre lleno, verde - trabajando, rojo - marea de sangre. No cuenta para el límite de urnas.","Une urne pour un esprit qui ramasse le butin. Mets un crâne à l'intérieur et relie-lui un coffre par un fil fantomatique, dans un sens ou dans l'autre - le Couteau du Nécromancien en prise de fil (R). Sa zone est aussi large qu'une revendication de terrain ou plus petite et couvre toute la hauteur du monde. Appuie sur Démarrer et l'esprit balaie la zone une fois : objets au sol, sacs de butin de zombies et cadavres rouges, tout dans le coffre. Il laisse les conteneurs des bâtiments, les largages et les sacs des joueurs tombés. Les boutons-crânes de l'urne montrent son état : jaune - en attente, jaune clignotant - coffre plein, vert - au travail, rouge - marée de sang. Ne compte pas dans la limite d'urnes.","Un'urna per uno spirito che raccoglie il bottino. Metti un teschio dentro e collegale un forziere con un filo spettrale in qualsiasi direzione: il Coltello del Negromante nella presa da filo (R). La sua zona è larga quanto una rivendicazione di terreno o meno e copre tutta l'altezza del mondo. Premi Avvia e lo spirito spazza la zona una volta: oggetti a terra, sacche di bottino degli zombi e cadaveri rossi, tutto nel forziere. Lascia stare i contenitori degli edifici, i lanci aerei e gli zaini dei giocatori caduti. I pulsanti a teschio sull'urna mostrano lo stato: giallo - attesa, giallo lampeggiante - forziere pieno, verde - al lavoro, rosso - marea di sangue. Non conta nel limite di urne.",戦利品を集める霊のための骨壺。頭蓋骨を入れ、箱をどちら向きでもよいので霊の糸でつなぐ――糸の握りにした死霊術師のナイフ(R)で。ゾーンは土地の権利と同じ幅かそれ以下で、世界の高さ全体に及ぶ。開始を押すと霊がゾーンを一巡する:地面のアイテム、ゾンビの戦利品袋、赤い死体――すべて箱へ。建物の容器、補給物資、倒れたプレイヤーのバックパックには手を出さない。骨壺の頭蓋骨ボタンが状態を示す:黄――待機、黄の点滅――箱が満杯、緑――作業中、赤――血の潮。骨壺の上限には数えない。,"전리품을 모으는 영혼을 위한 유골함. 두개골을 넣고 상자 하나를 어느 방향으로든 유령 실로 잇는다 - 실 잡기로 쥔 강령술사의 칼(R)로. 구역은 토지 소유권만큼 넓거나 더 작고, 세계의 높이 전체에 걸친다. 시작을 누르면 영혼이 구역을 한 번 훑는다: 땅의 아이템, 좀비 전리품 가방, 붉은 시체 - 모두 상자로. 건물의 보관함, 보급품, 쓰러진 플레이어의 배낭은 건드리지 않는다. 유골함의 두개골 버튼이 상태를 보여 준다: 노랑 - 대기, 노랑 깜빡임 - 상자 가득, 초록 - 작업 중, 빨강 - 피의 조수. 유골함 한도에 포함되지 않는다.","Urna dla ducha, który zbiera łupy. Włóż czaszkę i połącz z nią skrzynię widmową nicią w dowolną stronę - Nożem Nekromanty w chwycie do nici (R). Jej strefa jest szeroka jak działka lub mniejsza i obejmuje całą wysokość świata. Naciśnij Start, a duch raz przeczesze strefę: przedmioty na ziemi, worki z łupem zombie i czerwone zwłoki, wszystko do skrzyni. Pojemników w budynkach, zrzutów i plecaków poległych graczy nie rusza. Czaszkowe przyciski na urnie pokazują stan: żółty - czeka, żółty migający - skrzynia pełna, zielony - pracuje, czerwony - krwawy przypływ. Nie liczy się do limitu urn.","Uma urna para um espírito que recolhe o saque. Coloque um crânio dentro e ligue a ela um baú com um fio fantasmagórico, em qualquer direção - a Faca do Necromante na pegada de fio (R). Sua zona é tão larga quanto uma reivindicação de terra ou menor e abrange toda a altura do mundo. Pressione Iniciar e o espírito varre a zona uma vez: itens no chão, sacos de saque de zumbis e cadáveres vermelhos, tudo para o baú. Deixa em paz contêineres de prédios, suprimentos aéreos e mochilas de jogadores caídos. Os botões de caveira da urna mostram o estado: amarelo - esperando, amarelo piscando - baú cheio, verde - trabalhando, vermelho - maré de sangue. Não conta para o limite de urnas.","Урна для духа, который собирает добычу. Положи внутрь череп и протяни призрачную нить между урной и ящиком в любую сторону - нож некроманта хватом для нитей (R). Зона - шириной с землю притязания или меньше, по высоте - весь мир. Нажми «Запустить», и дух один раз обойдёт зону: предметы на земле, мешки с зомби и красные трупы - всё в ящик. Ящики зданий, аэродроп и рюкзаки погибших игроков не трогает. Кнопки с черепом на урне показывают состояние: жёлтая - ждёт, жёлтая мигает - ящик полон, зелёная - работает, красная - кровавый прилив. В лимит урн не входит.","Ganimeti toplayan bir ruh için küllük. İçine bir kafatası koy ve bir sandığı herhangi bir yönde hayalet iplikle bağla - iplik tutuşundaki Nekromansör Bıçağıyla (R). Bölgesi bir arazi talebi kadar geniş ya da daha dardır ve dünyanın tüm yüksekliğini kapsar. Başlat'a bas, ruh bölgeyi bir kez tarasın: yerdeki eşyalar, zombi ganimet torbaları ve kırmızı cesetler, hepsi sandığa. Binalardaki kapları, hava indirmelerini ve düşen oyuncuların sırt çantalarını ellemez. Küllükteki kafatası düğmeleri durumu gösterir: sarı - bekliyor, yanıp sönen sarı - sandık dolu, yeşil - çalışıyor, kırmızı - kan gelgiti. Küllük sınırına sayılmaz.",一座骨灰瓮,供收集战利品的灵魂栖身。放入一颗头骨,再用幽灵丝线把一个箱子连到它上,方向不限——用丝线握法的死灵法师之刃(R)。它的区域与领地一样宽或更小,并覆盖整个世界的高度。按下“启动”,灵魂会把区域扫一遍:地上的物品、僵尸战利品袋和红色尸体,统统放进箱子。建筑里的容器、空投和倒下玩家的背包它不碰。骨灰瓮上的头骨按钮显示状态:黄——等待,黄色闪烁——箱子已满,绿——工作中,红——血潮。不计入骨灰瓮上限。,一座骨灰甕,供收集戰利品的靈魂棲身。放入一顆頭骨,再用幽靈絲線把一個箱子連到它上,方向不限——用絲線握法的死靈法師之刃(R)。它的區域與領地一樣寬或更小,並覆蓋整個世界的高度。按下「啟動」,靈魂會把區域掃一遍:地上的物品、殭屍戰利品袋和紅色屍體,統統放進箱子。建築裡的容器、空投和倒下玩家的背包它不碰。骨灰甕上的頭骨按鈕顯示狀態:黃——等待,黃色閃爍——箱子已滿,綠——工作中,紅——血潮。不計入骨灰甕上限。
+blockcommand_zurn_run,UI,Text,,,,Start,,Starten,Iniciar,Démarrer,Avvia,開始,시작,Uruchom,Iniciar,Запустить,Başlat,启动,啟動
+blockcommand_zurn_stop,UI,Text,,,,Stop,,Anhalten,Detener,Arrêter,Ferma,止める,멈추기,Zatrzymaj,Parar,Остановить,Durdur,停止,停止
+blockcommand_zurn_size,UI,Text,,,,Zone size,,Zonengröße,Tamaño de zona,Taille de zone,Dimensione zona,ゾーンの広さ,구역 크기,Rozmiar strefy,Tamanho da zona,Размер зоны,Bölge boyutu,区域大小,區域大小
+necroZoneUrnWait,UI,Text,,,,waiting,,wartet,esperando,en attente,in attesa,待機中,대기 중,czeka,esperando,ждёт,bekliyor,等待中,等待中
+necroZoneUrnBusy,UI,Text,,,,at work,,arbeitet,trabajando,au travail,al lavoro,作業中,작업 중,pracuje,trabalhando,работает,çalışıyor,工作中,工作中
+necroZoneUrnNoMaterials,UI,Text,,,,out of materials,,kein Material,sin materiales,plus de matériaux,mancano materiali,材料切れ,재료 부족,brak materiałów,sem materiais,не хватает материалов,malzeme yok,材料不足,材料不足
+necroZoneUrnFull,UI,Text,,,,the chest is full,,Truhe voll,cofre lleno,coffre plein,forziere pieno,箱が満杯,상자 가득,skrzynia pełna,baú cheio,ящик полон,sandık dolu,箱子已满,箱子已滿
+necroZoneUrnTide,UI,Text,,,,blood tide - the spirit hides,,Blutflut - der Geist versteckt sich,marea de sangre: el espíritu se esconde,marée de sang - l'esprit se cache,marea di sangue: lo spirito si nasconde,血の潮――霊は隠れている,피의 조수 - 영혼이 숨었다,krwawy przypływ - duch się chowa,maré de sangue: o espírito se esconde,кровавый прилив - дух в урне,kan gelgiti - ruh saklanıyor,血潮——灵魂躲了起来,血潮——靈魂躲了起來
+necroZoneUrnSize,UI,Text,,,,zone {0}x{0},,Zone {0}x{0},zona {0}x{0},zone {0}x{0},zona {0}x{0},ゾーン {0}x{0},구역 {0}x{0},strefa {0}x{0},zona {0}x{0},зона {0}x{0},bölge {0}x{0},区域 {0}x{0},區域 {0}x{0}
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,丝线:骨灰瓮到箱子——灵会把成品放在这里,絲線:骨灰甕到箱子——靈會把成品放在這裡
diff --git a/Config/blocks.xml b/Config/blocks.xml
index dc48bba..854fe05 100644
--- a/Config/blocks.xml
+++ b/Config/blocks.xml
@@ -613,5 +613,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Config/progression.xml b/Config/progression.xml
index 5f20194..50888da 100644
--- a/Config/progression.xml
+++ b/Config/progression.xml
@@ -181,7 +181,7 @@
пользуется, вода сидит на necroNecromancyLvl20. -->
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HarmonySrc/Cinerarium.cs b/HarmonySrc/Cinerarium.cs
index 81e2d25..efdc3b8 100644
--- a/HarmonySrc/Cinerarium.cs
+++ b/HarmonySrc/Cinerarium.cs
@@ -27,7 +27,7 @@ namespace NecromancerTome
/// Имена команд не должны кончаться на zone_show/zone_hide/effect_on/effect_off: заплатка пирамиды на
/// BlockCompositeTileEntity.OnBlockActivated ловит их по EndsWith у ЛЮБОГО составного блока.
///
- public class TEFeatureCinerarium : TEFeatureAbs
+ public partial class TEFeatureCinerarium : TEFeatureAbs
{
public const string BlockName = "necroCinerarium";
public const string SkullBlockName = "necroCinerariumSkull";
@@ -62,8 +62,9 @@ namespace NecromancerTome
return n == WorkerBlockName || n == WorkerSkullBlockName;
}
- /// Урна без зоны (логист, рабочий): ставится вплотную к любым урнам, в лимит не входит, работает по нитям.
- public static bool IsZonelessBlock(Block _b) => IsLogistBlock(_b) || IsWorkerBlock(_b);
+ /// Урна без зоны 5x5 (логист, рабочий, ремонтник, лутер): ставится вплотную к любым урнам, в лимит не входит,
+ /// работает по нитям. У ремонтника и лутера своя большая зона (CinerariumZone.cs) - зоны таких урн могут пересекаться.
+ public static bool IsZonelessBlock(Block _b) => IsLogistBlock(_b) || IsWorkerBlock(_b) || IsRepairBlock(_b) || IsLootBlock(_b);
public const string SkullItemName = "necroSkull";
public const int HalfWidth = 2;
public const int Below = 1;
@@ -71,7 +72,7 @@ namespace NecromancerTome
/// Убийств на одну урну сборщика. 17.10: «лимит урн - ослабить/убрать» - нагрузку теперь держит общий бюджет
/// шагов (CinerariumLoad), лимит остался мягким игровым порогом: было 500, стало 200.
public const float KillsPerUrn = 200f;
- const byte SaveVersion = 5; // 2 - нити, 3 - фильтр логиста (16.10), 4 - нити рабочего «куда» и образец (18.10), 5 - черепа-ретрансляторы (18.10)
+ const byte SaveVersion = 6; // 2 - нити, 3 - фильтр логиста (16.10), 4 - нити рабочего «куда» и образец (18.10), 5 - черепа-ретрансляторы (18.10), 6 - зона и запуск ремонтника/лутера (18.10)
public bool HasSkull;
public bool ZoneShown;
@@ -102,7 +103,7 @@ namespace NecromancerTome
}
}
bool _worker;
- public bool IsZoneless => IsLogist || IsWorker;
+ public bool IsZoneless => IsLogist || IsWorker || IsBigZone;
/// Урна-логист - по блоку (necroCinerariumLogist / ...Skull); узнаётся при постановке или по миру.
public bool IsLogist
{
@@ -116,6 +117,8 @@ namespace NecromancerTome
{
_logist = IsLogistBlock(bv.Block);
_worker = IsWorkerBlock(bv.Block);
+ _repair = IsRepairBlock(bv.Block);
+ _loot = IsLootBlock(bv.Block);
_logistKnown = true;
}
}
@@ -163,15 +166,18 @@ namespace NecromancerTome
base.OnAdded(_blockPos, _blockValue);
_logist = IsLogistBlock(_blockValue.Block);
_worker = IsWorkerBlock(_blockValue.Block);
+ _repair = IsRepairBlock(_blockValue.Block);
+ _loot = IsLootBlock(_blockValue.Block);
_logistKnown = true;
string placed = _blockValue.Block?.GetBlockName();
- if (placed == SkullBlockName || placed == LogistSkullBlockName || placed == LogistWallSkullBlockName || placed == WorkerSkullBlockName)
+ if (placed == SkullBlockName || placed == LogistSkullBlockName || placed == LogistWallSkullBlockName || placed == WorkerSkullBlockName ||
+ placed == RepairSkullBlockName || placed == LootSkullBlockName)
{
HasSkull = true; // поставили урну, поднятую с черепом
}
if (ConnectionManager.Instance.IsServer)
{
- CinerariumRegistry.Add(_blockPos, Parent?.Owner, _logist || _worker);
+ CinerariumRegistry.Add(_blockPos, Parent?.Owner, _logist || _worker || _repair || _loot);
}
}
@@ -183,6 +189,7 @@ namespace NecromancerTome
RemoveThreadView();
RemoveGenie(false);
CinerariumContents.CloseIfShowing(this);
+ ZoneStop(false);
if (!ConnectionManager.Instance.IsServer)
{
return;
@@ -253,6 +260,8 @@ namespace NecromancerTome
AppendVia(via, l);
}
CinerariumWriteHelper.WriteInts(_bw, via.ToArray());
+ // v6: ремонтник/лутер - сторона зоны, идёт ли проход, где курсор обхода, кто нажал кнопку.
+ CinerariumWriteHelper.WriteInts(_bw, new[] { ZoneSide, ZoneRunning ? 1 : 0, ZoneCursor, ZonePressedBy });
}
static void AppendVia(List _dst, List _l)
@@ -389,6 +398,23 @@ namespace NecromancerTome
}
}
AlignVia();
+ ZoneSide = 0;
+ ZoneRunning = false;
+ ZoneCursor = 0;
+ ZonePressedBy = -1;
+ if (version >= 6)
+ {
+ int n = _br.ReadInt32();
+ int[] v = new int[n];
+ for (int i = 0; i < n; i++)
+ {
+ v[i] = _br.ReadInt32();
+ }
+ ZoneSide = n > 0 ? v[0] : 0;
+ ZoneRunning = n > 1 && v[1] != 0;
+ ZoneCursor = n > 2 ? v[2] : 0;
+ ZonePressedBy = n > 3 ? v[3] : -1;
+ }
_threadsDirty = true;
if (version > SaveVersion)
{
@@ -410,13 +436,18 @@ namespace NecromancerTome
_addCallback(new BlockActivationCommand("urn_repair", "wrench", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData);
_addCallback(new BlockActivationCommand("bounds_show", "frames", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData);
_addCallback(new BlockActivationCommand("bounds_hide", "frames", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData);
+ // Ремонтник, лутер (CinerariumZone.cs): запуск прохода кнопкой, остановка, размер зоны.
+ _addCallback(new BlockActivationCommand("zurn_run", "electric_switch", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData);
+ _addCallback(new BlockActivationCommand("zurn_stop", "electric_switch", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData);
+ _addCallback(new BlockActivationCommand("zurn_size", "map_cursor", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData);
}
public override string GetActivationText(WorldBase _world, Vector3i _blockPos, BlockValue _blockValue, EntityAlive _entityFocusing, string _activateHotkeyMarkup, string _focusedTileEntityName)
{
base.GetActivationText(_world, _blockPos, _blockValue, _entityFocusing, _activateHotkeyMarkup, _focusedTileEntityName);
- return _activateHotkeyMarkup + " " + _blockValue.Block.GetLocalizedBlockName() + " - " +
- Localization.Get(HasSkull ? "necroCinerariumStateSkull" : "necroCinerariumStateEmpty");
+ string s = _activateHotkeyMarkup + " " + _blockValue.Block.GetLocalizedBlockName() + " - " +
+ Localization.Get(HasSkull ? "necroCinerariumStateSkull" : "necroCinerariumStateEmpty");
+ return IsBigZone ? s + ZoneStateText() : s;
}
public override void UpdateTick(World _world)
@@ -716,6 +747,11 @@ namespace NecromancerTome
_pending = false;
return;
}
+ if (IsBigZone)
+ {
+ ZoneWork(_world); // ремонтник, лутер - CinerariumZone.cs
+ return;
+ }
CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent() : null;
if (_pending)
{
@@ -926,7 +962,7 @@ namespace NecromancerTome
foreach (Renderer r in t.GetComponentsInChildren(true))
{
if (_genie != null && r.transform.IsChildOf(_genie.transform) || _bounds != null && r.transform.IsChildOf(_bounds.transform) ||
- _threadView != null && r.transform.IsChildOf(_threadView.transform))
+ _threadView != null && r.transform.IsChildOf(_threadView.transform) || CinerariumButtons.IsButton(r))
{
continue;
}
@@ -1013,11 +1049,13 @@ namespace NecromancerTome
lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
lr.receiveShadows = false;
- // Углы по граням блоков: x и z от -2.5 до +2.5 от центра урны, y от низа блока под ней до верха +4.
+ // Углы по граням блоков: x и z от -2.5 до +2.5 от центра урны, y от низа блока под ней до верха +4. Большая зона
+ // (ремонтник, лутер) - сторона ZoneSideNow, по высоте весь столб мира.
Vector3 c = World.blockToTransformPos(ToWorldPos()) - Origin.position;
- float x0 = c.x - HalfWidth - 0.5f, x1 = c.x + HalfWidth + 0.5f;
- float z0 = c.z - HalfWidth - 0.5f, z1 = c.z + HalfWidth + 0.5f;
- float y0 = c.y - Below, y1 = c.y + Above + 1f;
+ int hw = IsBigZone ? ZoneSideNow / 2 : HalfWidth;
+ float x0 = c.x - hw - 0.5f, x1 = c.x + hw + 0.5f;
+ float z0 = c.z - hw - 0.5f, z1 = c.z + hw + 0.5f;
+ float y0 = IsBigZone ? -Origin.position.y + 1f : c.y - Below, y1 = IsBigZone ? -Origin.position.y + 254f : c.y + Above + 1f;
Vector3 b0 = new Vector3(x0, y0, z0), b1 = new Vector3(x1, y0, z0), b2 = new Vector3(x1, y0, z1), b3 = new Vector3(x0, y0, z1);
Vector3 t0 = new Vector3(x0, y1, z0), t1 = new Vector3(x1, y1, z0), t2 = new Vector3(x1, y1, z1), t3 = new Vector3(x0, y1, z1);
// Одна ломаная по всем 12 рёбрам (часть рёбер проходится дважды - на глаз не видно).
@@ -1046,7 +1084,8 @@ namespace NecromancerTome
{
string n = _b?.GetBlockName();
return n == BlockName || n == SkullBlockName || n == LogistBlockName || n == LogistSkullBlockName ||
- n == LogistWallBlockName || n == LogistWallSkullBlockName || n == WorkerBlockName || n == WorkerSkullBlockName;
+ n == LogistWallBlockName || n == LogistWallSkullBlockName || n == WorkerBlockName || n == WorkerSkullBlockName ||
+ IsRepairBlock(_b) || IsLootBlock(_b);
}
public static int UrnLimit(EntityPlayer _player)
@@ -1261,11 +1300,23 @@ namespace NecromancerTome
}
else if (text.EndsWith("bounds_show"))
{
- _commands[i].enabled = !urn.ZoneShown && !urn.IsZoneless;
+ _commands[i].enabled = !urn.ZoneShown && (!urn.IsZoneless || urn.IsBigZone);
}
else if (text.EndsWith("bounds_hide"))
{
- _commands[i].enabled = urn.ZoneShown && !urn.IsZoneless;
+ _commands[i].enabled = urn.ZoneShown && (!urn.IsZoneless || urn.IsBigZone);
+ }
+ else if (text.EndsWith("zurn_run"))
+ {
+ _commands[i].enabled = urn.IsBigZone && urn.HasSkull && !urn.ZoneRunning;
+ }
+ else if (text.EndsWith("zurn_stop"))
+ {
+ _commands[i].enabled = urn.IsBigZone && urn.ZoneRunning;
+ }
+ else if (text.EndsWith("zurn_size"))
+ {
+ _commands[i].enabled = urn.IsBigZone;
}
}
}
@@ -1284,7 +1335,8 @@ namespace NecromancerTome
bool repair = _commandName.EndsWith("urn_repair");
bool bag = _commandName.EndsWith("TEFeatureCinerarium:bag");
bool bounds = _commandName.EndsWith("bounds_show") || _commandName.EndsWith("bounds_hide");
- if (!skull && !bounds && !repair && !bag)
+ bool zone = _commandName.EndsWith("zurn_run") || _commandName.EndsWith("zurn_stop") || _commandName.EndsWith("zurn_size");
+ if (!skull && !bounds && !repair && !bag && !zone)
{
return true;
}
@@ -1299,6 +1351,12 @@ namespace NecromancerTome
__result = composite.OnBlockActivated(null, "TEFeatureStorage:Search", world, _blockPos, world.GetBlock(_blockPos), _player);
return false;
}
+ if (zone)
+ {
+ urn.ZoneCommand(_commandName, _player);
+ __result = true;
+ return false;
+ }
if (skull)
{
CinerariumContents.Open(_player, urn);
@@ -1408,7 +1466,13 @@ namespace NecromancerTome
bool logist = TEFeatureCinerarium.IsLogistBlock(bv.Block);
bool wall = TEFeatureCinerarium.IsLogistWallBlock(bv.Block);
bool worker = TEFeatureCinerarium.IsWorkerBlock(bv.Block);
- ItemValue iv = ItemClass.GetItem(wall
+ bool repairUrn = TEFeatureCinerarium.IsRepairBlock(bv.Block);
+ bool lootUrn = TEFeatureCinerarium.IsLootBlock(bv.Block);
+ ItemValue iv = ItemClass.GetItem(repairUrn
+ ? (urn.HasSkull ? TEFeatureCinerarium.RepairSkullBlockName : TEFeatureCinerarium.RepairBlockName)
+ : lootUrn
+ ? (urn.HasSkull ? TEFeatureCinerarium.LootSkullBlockName : TEFeatureCinerarium.LootBlockName)
+ : wall
? (urn.HasSkull ? TEFeatureCinerarium.LogistWallSkullBlockName : TEFeatureCinerarium.LogistWallBlockName)
: logist
? (urn.HasSkull ? TEFeatureCinerarium.LogistSkullBlockName : TEFeatureCinerarium.LogistBlockName)
diff --git a/HarmonySrc/CinerariumZone.cs b/HarmonySrc/CinerariumZone.cs
new file mode 100644
index 0000000..08083b6
--- /dev/null
+++ b/HarmonySrc/CinerariumZone.cs
@@ -0,0 +1,709 @@
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.Scripting;
+
+namespace NecromancerTome
+{
+ ///
+ /// Урны с большой зоной - ремонтник и лутер (решения 17.10 и 18.10, BACKLOG «Урна ремонтника», «Урна лутера»).
+ ///
+ /// Общее. Зона - квадрат со стороной блока притязания (LandClaimSize сервера, по умолчанию 41) или меньше (31/21/11, пункт
+ /// меню «Размер зоны»), по высоте - весь столб мира; зоны таких урн могут пересекаться. Работа - только по кнопке (17.10: «А
+ /// если сделать рубильник или кнопку. Включил, ремонтник начал работу»): «Запустить» - один проход по зоне, конец прохода -
+ /// снова ожидание; до нажатия урна ничего не считает. Кнопки - на самой урне с четырёх сторон («как у дверного звонка, но с
+ /// черепом»), их цвет - состояние (CinerariumButtons): не горят - нет черепа; жёлтая ровно - ожидание; жёлтая мигает - не
+ /// хватает материалов (ремонтник) или ящик полон (лутер); зелёная - работа; красная - прилив (работа стоит, дух в урне).
+ ///
+ /// Ремонтник. Обход - по столбам зоны, порциями из общего бюджета клеток на кадр (CellsPerFrame на все урны); в столбе - от
+ /// низа до высоты чанка, земля мира (форма terrain) и воздух пропускаются сразу. Найденный повреждённый блок с RepairItems
+ /// встаёт в очередь (не длиннее QueueMax - обход ждёт), блок берёт тот, кто нашёл первым (Claimed). Ремонт - неспешно: одно
+ /// действие в RepairSeconds, дух летит к блоку и машет руками; материалы - RepairItems блока, доля по повреждению (как молотком),
+ /// из ящиков с нитями к урне. Не хватает - кнопка мигает, урна ждёт (положил в ящик - продолжит сама). Разрушенное не
+ /// восстанавливает - только повреждённое (17.10: «Восстанавливаю порушенное, затем включаю ремонтника и ухожу по делам»).
+ ///
+ /// Лутер. За действие - одна цель в зоне: предмет на земле, затем мешок с зомби, затем красный труп (тем же путём, что игрок:
+ /// LootBagOpened + NecroSkull.Roll, стадия добычи - того, кто нажал кнопку); добыча - в ящик с любой нитью к урне. Ящики
+ /// зданий, аэродроп и рюкзаки погибших игроков не трогает (18.10 «Принимаю всё»). Целей нет - проход окончен.
+ ///
+ public partial class TEFeatureCinerarium
+ {
+ public const string RepairBlockName = "necroCinerariumRepair";
+ public const string RepairSkullBlockName = "necroCinerariumRepairSkull";
+ public const string LootBlockName = "necroCinerariumLoot";
+ public const string LootSkullBlockName = "necroCinerariumLootSkull";
+
+ public static bool IsRepairBlock(Block _b)
+ {
+ string n = _b?.GetBlockName();
+ return n == RepairBlockName || n == RepairSkullBlockName;
+ }
+
+ public static bool IsLootBlock(Block _b)
+ {
+ string n = _b?.GetBlockName();
+ return n == LootBlockName || n == LootSkullBlockName;
+ }
+
+ bool _repair, _loot;
+
+ public bool IsRepairer
+ {
+ get
+ {
+ _ = IsLogist;
+ return _repair;
+ }
+ }
+
+ public bool IsLooter
+ {
+ get
+ {
+ _ = IsLogist;
+ return _loot;
+ }
+ }
+
+ public bool IsBigZone => IsRepairer || IsLooter;
+
+ /// Сторона зоны, выбранная в меню (0 - по притязанию). Сохраняется (v6).
+ public int ZoneSide;
+ public bool ZoneRunning;
+ /// Обход ремонтника: номер следующего столба зоны (строка за строкой).
+ public int ZoneCursor;
+ /// Кто нажал «Запустить» - стадия добычи лутера.
+ public int ZonePressedBy = -1;
+ /// Не хватает материалов / ящик полон - кнопка мигает жёлтым.
+ public bool ZoneLacking;
+
+ public static float RepairSeconds = 2.5f;
+ public static float LootSeconds = 1.5f;
+ public static int CellsPerFrame = 4000;
+ public static int QueueMax = 32;
+
+ static int _cellsFrame = -1, _cellsUsed;
+ /// Блоки, взятые ремонтниками в очередь (зоны пересекаются - чинит взявший первым).
+ static readonly HashSet Claimed = new HashSet();
+
+ readonly List _queue = new List();
+ float _zoneNext;
+ bool _zonePending;
+ float _zoneActAt;
+ Vector3i _zoneTarget;
+ int _zoneTargetEntity = -1;
+
+ /// Стороны зоны: притязание сервера и меньшие нечётные (31, 21, 11), не больше притязания.
+ public static List ZoneSides()
+ {
+ int claim = Mathf.Max(11, GamePrefs.GetInt(EnumGamePrefs.LandClaimSize));
+ List l = new List { claim };
+ foreach (int s in new[] { 31, 21, 11 })
+ {
+ if (s < claim)
+ {
+ l.Add(s);
+ }
+ }
+ return l;
+ }
+
+ public int ZoneSideNow
+ {
+ get
+ {
+ List l = ZoneSides();
+ return ZoneSide > 0 && l.Contains(ZoneSide) ? ZoneSide : l[0];
+ }
+ }
+
+ public enum ButtonState
+ {
+ Off,
+ Wait,
+ Lacking,
+ Busy,
+ Tide
+ }
+
+ public ButtonState Buttons
+ {
+ get
+ {
+ if (!HasSkull)
+ {
+ return ButtonState.Off;
+ }
+ if (CinerariumLoad.Tide)
+ {
+ return ButtonState.Tide;
+ }
+ if (!ZoneRunning)
+ {
+ return ButtonState.Wait;
+ }
+ return ZoneLacking ? ButtonState.Lacking : ButtonState.Busy;
+ }
+ }
+
+ string ZoneStateText()
+ {
+ string key = Buttons switch
+ {
+ ButtonState.Tide => "necroZoneUrnTide",
+ ButtonState.Lacking => IsLooter ? "necroZoneUrnFull" : "necroZoneUrnNoMaterials",
+ ButtonState.Busy => "necroZoneUrnBusy",
+ ButtonState.Wait => "necroZoneUrnWait",
+ _ => null
+ };
+ string side = ZoneSideNow.ToString();
+ return (key != null ? ", " + Localization.Get(key) : "") + ", " + string.Format(Localization.Get("necroZoneUrnSize"), side);
+ }
+
+ /// Пункты меню на E: запустить, остановить, размер зоны (по кругу).
+ public void ZoneCommand(string _cmd, EntityPlayerLocal _player)
+ {
+ if (_cmd.EndsWith("zurn_run"))
+ {
+ if (!HasSkull)
+ {
+ return;
+ }
+ ZoneRunning = true;
+ ZoneCursor = 0;
+ ZoneLacking = false;
+ ZonePressedBy = _player != null ? _player.entityId : -1;
+ _queue.Clear();
+ Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), "button4");
+ Debug.Log("[NecromancerTome] Cinerarium: " + (IsLooter ? "лутер" : "ремонтник") + " " + ToWorldPos() + " запущен, зона " + ZoneSideNow);
+ }
+ else if (_cmd.EndsWith("zurn_stop"))
+ {
+ ZoneStop(true);
+ Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), "button4");
+ }
+ else if (_cmd.EndsWith("zurn_size"))
+ {
+ List l = ZoneSides();
+ int i = l.IndexOf(ZoneSideNow);
+ ZoneSide = l[(i + 1) % l.Count];
+ if (_player != null)
+ {
+ GameManager.ShowTooltip(_player, string.Format(Localization.Get("necroZoneUrnSize"), ZoneSide.ToString()));
+ }
+ if (ZoneShown)
+ {
+ RemoveBounds();
+ SpawnBounds();
+ }
+ }
+ SetModified();
+ }
+
+ /// Остановить проход (кнопка, урна убрана): очередь и взятые блоки отпускаются.
+ public void ZoneStop(bool _log)
+ {
+ foreach (Vector3i p in _queue)
+ {
+ Claimed.Remove(p);
+ }
+ _queue.Clear();
+ _zonePending = false;
+ if (ZoneRunning && _log)
+ {
+ Debug.Log("[NecromancerTome] Cinerarium: " + (IsLooter ? "лутер" : "ремонтник") + " " + ToWorldPos() + " остановлен");
+ }
+ ZoneRunning = false;
+ ZoneLacking = false;
+ }
+
+ void ZoneWork(World _world)
+ {
+ SyncButtons();
+ if (!ZoneRunning)
+ {
+ return;
+ }
+ CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent() : null;
+ if (_zonePending)
+ {
+ if (Time.time < _zoneActAt)
+ {
+ return;
+ }
+ _zonePending = false;
+ if (IsRepairer)
+ {
+ RepairNow(_world, _zoneTarget);
+ }
+ else
+ {
+ LootNow(_world, _zoneTargetEntity);
+ }
+ return;
+ }
+ if (IsRepairer)
+ {
+ ScanColumns(_world);
+ }
+ if (Time.time < _zoneNext || !CinerariumLoad.TryTakeStep())
+ {
+ return;
+ }
+ _zoneNext = Time.time + (IsRepairer ? RepairSeconds : LootSeconds);
+ if (IsRepairer)
+ {
+ RepairPick(_world, motion);
+ }
+ else
+ {
+ LootPick(_world, motion);
+ }
+ }
+
+ /// Слетать к месту и сделать дело по прилёте (как дух сборщика) или сразу, если духа нет.
+ void FlyThenAct(World _world, CinerariumGenieMotion _motion, Vector3i _at)
+ {
+ _zonePending = true;
+ _zoneActAt = Time.time;
+ if (_motion != null)
+ {
+ List<(Vector3, Vector3, bool)> stops = new List<(Vector3, Vector3, bool)>
+ {
+ (Stand(_world, _at, out bool beside) - Origin.position, World.blockToTransformPos(_at) - Origin.position, beside)
+ };
+ _zoneActAt = Time.time + _motion.Visit(stops);
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // Ремонтник.
+ // ------------------------------------------------------------------
+
+ void ScanColumns(World _world)
+ {
+ if (_queue.Count >= QueueMax)
+ {
+ return;
+ }
+ if (Time.frameCount != _cellsFrame)
+ {
+ _cellsFrame = Time.frameCount;
+ _cellsUsed = 0;
+ }
+ int side = ZoneSideNow, half = side / 2, total = side * side;
+ Vector3i c = ToWorldPos();
+ while (ZoneCursor < total && _cellsUsed < CellsPerFrame && _queue.Count < QueueMax)
+ {
+ int x = c.x - half + ZoneCursor % side, z = c.z - half + ZoneCursor / side;
+ ZoneCursor++;
+ if (!(_world.GetChunkFromWorldPos(x, 0, z) is Chunk ch))
+ {
+ continue; // не загружен - пропустить столб
+ }
+ int lx = World.toBlockXZ(x), lz = World.toBlockXZ(z);
+ int top = Mathf.Min(254, ch.GetHeight(lx, lz) + 1);
+ _cellsUsed += top;
+ for (int y = 1; y <= top; y++)
+ {
+ BlockValue bv = ch.GetBlock(lx, y, lz);
+ if (bv.isair || bv.ischild || bv.damage <= 0)
+ {
+ continue;
+ }
+ Block b = bv.Block;
+ if (b == null || b.shape.IsTerrain() || b.RepairItems == null || b.RepairItems.Count == 0)
+ {
+ continue;
+ }
+ Vector3i p = new Vector3i(x, y, z);
+ if (Claimed.Add(p))
+ {
+ _queue.Add(p);
+ }
+ }
+ }
+ if (ZoneCursor >= total && _queue.Count == 0 && !_zonePending)
+ {
+ Debug.Log("[NecromancerTome] Cinerarium: ремонтник " + c + " обошёл зону - ожидание");
+ ZoneStop(false);
+ SetModified();
+ }
+ }
+
+ void RepairPick(World _world, CinerariumGenieMotion _motion)
+ {
+ while (_queue.Count > 0)
+ {
+ Vector3i p = _queue[0];
+ BlockValue bv = _world.GetBlock(p);
+ if (bv.isair || bv.damage <= 0 || bv.Block?.RepairItems == null)
+ {
+ _queue.RemoveAt(0);
+ Claimed.Remove(p);
+ continue; // починили до нас или блок убран
+ }
+ if (!HasMaterials(_world, bv, false))
+ {
+ if (!ZoneLacking)
+ {
+ Debug.Log("[NecromancerTome] Cinerarium: ремонтнику " + ToWorldPos() + " не хватает материалов для " + bv.Block.GetBlockName() + " в " + p);
+ }
+ ZoneLacking = true;
+ return; // ждём: положат в ящик - продолжим с того же блока
+ }
+ ZoneLacking = false;
+ _zoneTarget = p;
+ FlyThenAct(_world, _motion, p);
+ return;
+ }
+ }
+
+ void RepairNow(World _world, Vector3i _p)
+ {
+ _queue.Remove(_p);
+ Claimed.Remove(_p);
+ BlockValue bv = _world.GetBlock(_p);
+ if (bv.isair || bv.damage <= 0 || bv.Block?.RepairItems == null)
+ {
+ return;
+ }
+ if (!HasMaterials(_world, bv, true))
+ {
+ ZoneLacking = true;
+ _queue.Insert(0, _p);
+ Claimed.Add(_p);
+ return;
+ }
+ int was = bv.damage;
+ bv.damage = 0;
+ _world.SetBlockRPC(_p, bv);
+ Debug.Log("[NecromancerTome] Cinerarium: ремонтник " + ToWorldPos() + " починил " + bv.Block.GetBlockName() + " в " + _p + " (было повреждение " + was + ")");
+ }
+
+ /// Хватает ли в ящиках с нитями к урне материалов на ремонт (RepairItems блока, доля по повреждению, не меньше
+ /// одного); _take - сразу и забрать.
+ bool HasMaterials(World _world, BlockValue _bv, bool _take)
+ {
+ Block b = _bv.Block;
+ float share = Mathf.Clamp01(_bv.damage / (float)Mathf.Max(1, b.MaxDamage));
+ List boxes = ThreadBoxes(_world);
+ foreach (Block.SItemNameCount need in b.RepairItems)
+ {
+ ItemValue iv = ItemClass.GetItem(need.ItemName);
+ if (iv == null || iv.type == 0)
+ {
+ continue;
+ }
+ int n = Mathf.Max(1, Mathf.CeilToInt(need.Count * share));
+ int have = 0;
+ foreach (TEFeatureStorage s in boxes)
+ {
+ have += Count(s, iv);
+ }
+ if (have < n)
+ {
+ return false;
+ }
+ }
+ if (!_take)
+ {
+ return true;
+ }
+ foreach (Block.SItemNameCount need in b.RepairItems)
+ {
+ ItemValue iv = ItemClass.GetItem(need.ItemName);
+ if (iv == null || iv.type == 0)
+ {
+ continue;
+ }
+ int n = Mathf.Max(1, Mathf.CeilToInt(need.Count * share));
+ foreach (TEFeatureStorage s in boxes)
+ {
+ n -= TakeFrom(s, iv, n);
+ if (n <= 0)
+ {
+ break;
+ }
+ }
+ }
+ return true;
+ }
+
+ static int Count(TEFeatureStorage _s, ItemValue _iv)
+ {
+ int n = 0;
+ for (int i = 0; i < _s.ItemGrid.Length; i++)
+ {
+ ItemStack st = _s.ItemGrid[i];
+ if (st != null && !st.IsEmpty() && st.itemValue.type == _iv.type && !CinerariumWork.Locked(_s, i))
+ {
+ n += st.count;
+ }
+ }
+ return n;
+ }
+
+ static int TakeFrom(TEFeatureStorage _s, ItemValue _iv, int _n)
+ {
+ int took = 0;
+ for (int i = 0; i < _s.ItemGrid.Length && took < _n; i++)
+ {
+ ItemStack st = _s.ItemGrid[i];
+ if (st == null || st.IsEmpty() || st.itemValue.type != _iv.type || CinerariumWork.Locked(_s, i))
+ {
+ continue;
+ }
+ int k = Mathf.Min(st.count, _n - took);
+ CinerariumWork.Take(_s, i, k);
+ took += k;
+ }
+ return took;
+ }
+
+ /// Ящики урны по нитям: у ремонтника - «откуда» (материалы), у лутера - любая нить (куда класть).
+ List ThreadBoxes(World _world)
+ {
+ List l = new List();
+ void Add(Vector3i _p)
+ {
+ TEFeatureStorage s = CinerariumWork.BoxAt(_world, _p);
+ if (s != null && !l.Contains(s))
+ {
+ l.Add(s);
+ }
+ }
+ foreach (Vector3i p in ThreadFrom)
+ {
+ Add(p);
+ }
+ if (IsLooter)
+ {
+ if (HasThreadTo)
+ {
+ Add(ThreadTo);
+ }
+ foreach (Vector3i p in ThreadOut)
+ {
+ Add(p);
+ }
+ }
+ return l;
+ }
+
+ // ------------------------------------------------------------------
+ // Лутер.
+ // ------------------------------------------------------------------
+
+ static readonly List _ents = new List();
+
+ /// Цель лутера в зоне: предмет на земле, потом мешок с зомби, потом красный труп. Ничего - проход окончен.
+ void LootPick(World _world, CinerariumGenieMotion _motion)
+ {
+ List boxes = ThreadBoxes(_world);
+ if (boxes.Count == 0)
+ {
+ ZoneLacking = true; // некуда класть - как полный ящик
+ return;
+ }
+ int side = ZoneSideNow;
+ Vector3 c = World.blockToTransformPos(ToWorldPos());
+ Bounds bb = new Bounds(new Vector3(c.x, 128f, c.z), new Vector3(side, 256f, side));
+ Entity best = null;
+ int bestRank = int.MaxValue;
+ _ents.Clear();
+ _world.GetEntitiesInBounds(typeof(Entity), bb, _ents);
+ foreach (Entity e in _ents)
+ {
+ int rank = LootRank(e);
+ if (rank < bestRank && !LockManager.Instance.IsLockedServer(e, 0))
+ {
+ best = e;
+ bestRank = rank;
+ }
+ }
+ _ents.Clear();
+ if (best == null)
+ {
+ Debug.Log("[NecromancerTome] Cinerarium: лутер " + ToWorldPos() + " собрал всё в зоне - ожидание");
+ ZoneStop(false);
+ SetModified();
+ return;
+ }
+ ZoneLacking = false;
+ _zoneTargetEntity = best.entityId;
+ FlyThenAct(_world, _motion, World.worldToBlockPos(best.GetPosition()));
+ }
+
+ /// Что собирать и в каком порядке (меньше - раньше); int.MaxValue - не трогать. Рюкзаки игроков (EntityBackpack) и
+ /// аэродроп (EntitySupplyCrate) - не трогать.
+ static int LootRank(Entity _e)
+ {
+ if (_e == null || _e.IsDespawned)
+ {
+ return int.MaxValue;
+ }
+ if (_e is EntityBackpack || _e is EntitySupplyCrate)
+ {
+ return int.MaxValue;
+ }
+ if (_e is EntityLootContainer lc)
+ {
+ return lc.bag != null ? 1 : int.MaxValue;
+ }
+ if (_e is EntityItem it && !(_e is EntityContainerAbs))
+ {
+ return it.itemStack != null && !it.itemStack.IsEmpty() ? 0 : int.MaxValue;
+ }
+ if (_e is EntityAlive a && a.IsDead() && CorpseLoot.IsSearchable(a) && a.bag != null)
+ {
+ return 2;
+ }
+ return int.MaxValue;
+ }
+
+ void LootNow(World _world, int _id)
+ {
+ Entity e = _world.GetEntity(_id);
+ if (e == null || LootRank(e) == int.MaxValue || LockManager.Instance.IsLockedServer(e, 0))
+ {
+ return;
+ }
+ List boxes = ThreadBoxes(_world);
+ if (e is EntityItem it && !(e is EntityContainerAbs))
+ {
+ ItemStack s = it.itemStack.Clone();
+ int before = s.count;
+ CinerariumWork.Place(s, boxes);
+ if (s.count == before)
+ {
+ ZoneLacking = true;
+ return;
+ }
+ if (s.count <= 0)
+ {
+ _world.RemoveEntity(e.entityId, EnumRemoveEntityReason.Despawned);
+ }
+ else
+ {
+ it.itemStack = s;
+ }
+ Debug.Log("[NecromancerTome] Cinerarium: лутер " + ToWorldPos() + " подобрал " + s.itemValue.ItemClass?.GetItemName() + " x" + (before - s.count));
+ return;
+ }
+ Bag bag = e is EntityAlive a ? a.bag : (e as EntityLootContainer)?.bag;
+ if (bag == null)
+ {
+ return;
+ }
+ if (!bag.Touched)
+ {
+ // Как игрок: первое открытие наполняет сумку по списку добычи (стадия - нажавшего кнопку); у красного трупа - и
+ // находка черепа (CorpseLootPatch: OnLockRequestServer).
+ int who = ZonePressedBy >= 0 ? ZonePressedBy : (_world.GetPrimaryPlayer()?.entityId ?? -1);
+ GameManager.Instance.lootManager.LootBagOpened(bag, e, who);
+ if (e is EntityAlive corpse)
+ {
+ NecroSkull.Roll(corpse, bag);
+ }
+ }
+ bool moved = false, left = false;
+ ItemStackGrid slots = bag.ItemGrid;
+ for (int i = 0; i < slots.Length; i++)
+ {
+ if (slots[i] == null || slots[i].IsEmpty())
+ {
+ continue;
+ }
+ ItemStack s = slots[i].Clone();
+ int before = s.count;
+ CinerariumWork.Place(s, boxes);
+ if (s.count != before)
+ {
+ moved = true;
+ }
+ if (s.count > 0)
+ {
+ left = true;
+ }
+ bag.SetSlot(i, s.count > 0 ? s : ItemStack.Empty.Clone());
+ }
+ ZoneLacking = left; // не всё влезло - ящик полон, кнопка мигает
+ Debug.Log("[NecromancerTome] Cinerarium: лутер " + ToWorldPos() + " обыскал " + e.GetType().Name + " " + e.entityId + (left ? " - не всё влезло" : ""));
+ }
+
+ // ------------------------------------------------------------------
+ // Кнопки на модели.
+ // ------------------------------------------------------------------
+
+ void SyncButtons()
+ {
+ Transform t = ModelTransform();
+ if (t == null)
+ {
+ return;
+ }
+ CinerariumButtons b = t.GetComponent();
+ if (b == null)
+ {
+ b = t.gameObject.AddComponent();
+ }
+ b.State = Buttons;
+ }
+ }
+
+ /// Кнопки-звонки с черепом на урне ремонтника/лутера: рендереры модели с «button» в имени. Цвет свечения - по
+ /// состоянию урны (TEFeatureCinerarium.Buttons), жёлтая мигает - нехватка.
+ [Preserve]
+ public class CinerariumButtons : MonoBehaviour
+ {
+ public TEFeatureCinerarium.ButtonState State;
+ List _buttons;
+ static readonly MaterialPropertyBlock _block = new MaterialPropertyBlock();
+ static readonly int EmissionColorId = Shader.PropertyToID("_EmissionColor");
+ public static Color Yellow = new Color(2.2f, 1.6f, 0.15f);
+ public static Color Green = new Color(0.3f, 2.2f, 0.4f);
+ public static Color Red = new Color(2.4f, 0.15f, 0.1f);
+ Color _last = new Color(-1f, 0f, 0f);
+
+ public static bool IsButton(Renderer _r) => _r != null && _r.name.ToLowerInvariant().Contains("button");
+
+ void Update()
+ {
+ if (_buttons == null)
+ {
+ _buttons = new List();
+ foreach (Renderer r in GetComponentsInChildren(true))
+ {
+ if (IsButton(r))
+ {
+ _buttons.Add(r);
+ }
+ }
+ }
+ Color c;
+ switch (State)
+ {
+ case TEFeatureCinerarium.ButtonState.Wait: c = Yellow; break;
+ case TEFeatureCinerarium.ButtonState.Lacking: c = Mathf.Repeat(Time.time, 0.8f) < 0.4f ? Yellow : Color.black; break;
+ case TEFeatureCinerarium.ButtonState.Busy: c = Green; break;
+ case TEFeatureCinerarium.ButtonState.Tide: c = Red; break;
+ default: c = Color.black; break;
+ }
+ if (c == _last)
+ {
+ return;
+ }
+ _last = c;
+ foreach (Renderer r in _buttons)
+ {
+ if (r == null)
+ {
+ continue;
+ }
+ r.GetPropertyBlock(_block);
+ _block.SetColor(EmissionColorId, c);
+ r.SetPropertyBlock(_block);
+ }
+ }
+ }
+}
diff --git a/HarmonySrc/NecroDevCommands.cs b/HarmonySrc/NecroDevCommands.cs
index 2ce55b9..3eb3cb9 100644
--- a/HarmonySrc/NecroDevCommands.cs
+++ b/HarmonySrc/NecroDevCommands.cs
@@ -46,6 +46,43 @@ namespace NecromancerTome
case "necrohold": Hold(pl, _params); break;
case "necrobox": Box(w, _params); break;
case "necrourn": Urn(w, _params); break;
+ case "necrodamage":
+ {
+ // necrodamage x y z урон - повредить блок (проверка ремонтника)
+ Vector3i dp = P(_params, 0);
+ BlockValue dbv = w.GetBlock(dp);
+ dbv.damage = Mathf.Min(dbv.Block.MaxDamage - 1, dbv.damage + int.Parse(_params[3]));
+ w.SetBlockRPC(dp, dbv);
+ Out("necrodamage: " + dbv.Block.GetBlockName() + " в " + dp + " повреждение " + dbv.damage + "/" + dbv.Block.MaxDamage);
+ break;
+ }
+ case "necrodrop":
+ {
+ // necrodrop предмет число x y z - бросить на землю (проверка лутера)
+ ItemValue div = ItemClass.GetItem(_params[0], true);
+ GameManager.Instance.ItemDropServer(new ItemStack(div, int.Parse(_params[1])), P(_params, 2).ToVector3() + new Vector3(0.5f, 0.5f, 0.5f), Vector3.zero);
+ Out("necrodrop: " + _params[0] + " x" + _params[1]);
+ break;
+ }
+ case "necrobag":
+ {
+ // necrobag x y z - мешок с зомби (EntityLootContainer со списком cntDropBag), нетронутый
+ Entity bag = EntityFactory.CreateEntity(EntityClass.FromString("DroppedLootContainer"), P(_params, 0).ToVector3() + new Vector3(0.5f, 0.3f, 0.5f));
+ bag.lootList = "cntDropBag";
+ w.SpawnEntityInWorld(bag);
+ Out("necrobag: " + bag.entityId);
+ break;
+ }
+ case "necrocorpse":
+ {
+ // necrocorpse x y z [класс] - зомби, убитый и помеченный красным трупом (как VictimPatch)
+ string cls = _params.Count > 3 ? _params[3] : "zombieArlene";
+ EntityAlive z = EntityFactory.CreateEntity(EntityClass.FromString(cls), P(_params, 0).ToVector3() + new Vector3(0.5f, 0.1f, 0.5f)) as EntityAlive;
+ w.SpawnEntityInWorld(z);
+ ThreadManager.StartCoroutine(KillAndMark(z));
+ Out("necrocorpse: " + z.entityId + " " + cls);
+ break;
+ }
case "necrogod": God(pl, _params.Count == 0 || _params[0] != "off"); break;
case "necroclick":
// necroclick 0|1 [x y z] - по блоку под прицелом или по указанной клетке (луч не всегда попадает в низкий череп).
@@ -99,6 +136,18 @@ namespace NecromancerTome
Out("necrogod: " + (_on ? "бог, ИИ не видит" : "обычный"));
}
+ static System.Collections.IEnumerator KillAndMark(EntityAlive _z)
+ {
+ yield return new WaitForSeconds(1f);
+ _z.DamageEntity(new DamageSource(EnumDamageSource.Internal, EnumDamageTypes.Suicide), 99999, false);
+ yield return new WaitForSeconds(1f);
+ if (_z.IsDead())
+ {
+ CorpseLoot.MarkCorpse(_z);
+ }
+ UnityEngine.Debug.Log("[NecromancerTome] mailbox: necrocorpse: " + _z.entityId + (_z.IsDead() ? " убит и помечен" : " НЕ умер"));
+ }
+
static void Aim(World _w, EntityPlayerLocal _pl)
{
StringBuilder sb = new StringBuilder("necroaim: игрок ").Append(_pl.position.ToString("F1")).Append(", взгляд ").Append(_pl.rotation.ToString("F0"));
@@ -247,6 +296,9 @@ namespace NecromancerTome
case "sample":
u.SetSample(_p.Count > 5 ? new ItemStack(ItemClass.GetItem(_p[4], true), int.Parse(_p[5])) : ItemStack.Empty.Clone());
break;
+ case "run": u.ZoneCommand("zurn_run", GameManager.Instance.World.GetPrimaryPlayer()); break;
+ case "stop": u.ZoneCommand("zurn_stop", null); break;
+ case "size": u.ZoneSide = int.Parse(_p[4]); u.SetModified(); break;
case "group":
u.GroupMask = _p[4] == "нет" || _p[4] == "none" ? 0u : CinerariumGroups.All;
u.SetModified();
@@ -262,7 +314,7 @@ namespace NecromancerTome
}
Out("necrourn: " + pos + " " + _w.GetBlock(pos).Block?.GetBlockName() + ", череп " + u.HasSkull + ", застрял " + u.Stuck + ", мигание " + blink +
", откуда " + 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));
+ ", зона " + (u.IsBigZone ? u.ZoneSideNow + (u.ZoneRunning ? " идёт" : " ждёт") + (u.ZoneLacking ? " нехватка" : "") + " курсор " + u.ZoneCursor : "-") + ", образец " + (u.Sample == null || u.Sample.IsEmpty() ? "нет" : u.Sample.itemValue.ItemClass?.GetItemName() + "x" + u.Sample.count));
}
}
@@ -337,4 +389,40 @@ namespace NecromancerTome
public override bool AllowedInMainMenu => false;
public override void Execute(List _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrogod", _params);
}
+
+ [Preserve]
+ public class ConsoleCmdNecroDamage : ConsoleCmdAbstract
+ {
+ public override string[] getCommands() => new[] { "necrodamage" };
+ public override string getDescription() => "NecromancerTome (разработка): повредить блок x y z урон";
+ public override bool AllowedInMainMenu => false;
+ public override void Execute(List _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrodamage", _params);
+ }
+
+ [Preserve]
+ public class ConsoleCmdNecroDrop : ConsoleCmdAbstract
+ {
+ public override string[] getCommands() => new[] { "necrodrop" };
+ public override string getDescription() => "NecromancerTome (разработка): бросить предмет: имя число x y z";
+ public override bool AllowedInMainMenu => false;
+ public override void Execute(List _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrodrop", _params);
+ }
+
+ [Preserve]
+ public class ConsoleCmdNecroBag : ConsoleCmdAbstract
+ {
+ public override string[] getCommands() => new[] { "necrobag" };
+ public override string getDescription() => "NecromancerTome (разработка): мешок с зомби x y z";
+ public override bool AllowedInMainMenu => false;
+ public override void Execute(List _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrobag", _params);
+ }
+
+ [Preserve]
+ public class ConsoleCmdNecroCorpse : ConsoleCmdAbstract
+ {
+ public override string[] getCommands() => new[] { "necrocorpse" };
+ public override string getDescription() => "NecromancerTome (разработка): красный труп x y z [класс]";
+ public override bool AllowedInMainMenu => false;
+ public override void Execute(List _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrocorpse", _params);
+ }
}
diff --git a/NecromancerHarmony.dll b/NecromancerHarmony.dll
index 8304254..0dd7ea2 100644
Binary files a/NecromancerHarmony.dll and b/NecromancerHarmony.dll differ
diff --git a/NecromancerHarmony.pdb b/NecromancerHarmony.pdb
index 4874e94..65bbb5a 100644
Binary files a/NecromancerHarmony.pdb and b/NecromancerHarmony.pdb differ