diff --git a/Config/Localization.csv b/Config/Localization.csv
index e2ce0b5..852f23d 100644
--- a/Config/Localization.csv
+++ b/Config/Localization.csv
@@ -31,7 +31,19 @@ tinCanBoiledWaterDesc,items,Item,,,,Purified boiled water in a tin can. Safe to
necroZombieDog,entityclasses,Entity,,,,Zombie Dog,,Zombiehund,Perro Zombi,Chien Zombie,Cane Zombie,ゾンビ犬,좀비 개,Pies Zombie,Cão Zumbi,Зомбособака,Zombi Köpek,僵尸犬,殭屍犬
necroInsectSwarm,entityclasses,Entity,,,,Pharaoh's Swarm,,Schwarm des Pharao,Enjambre del faraón,Essaim du pharaon,Sciame del faraone,ファラオの群れ,파라오의 무리,Rój faraona,Enxame do faraó,Рой фараона,Firavun Sürüsü,法老之群,法老之群
necroWpnBladeNecroKnife,items,Item,,,,Necromancer's Knife,,Messer des Nekromanten,Cuchillo del nigromante,Couteau du nécromancien,Coltello del negromante,ネクロマンサーのナイフ,강령술사의 단검,Nóż nekromanty,Faca do necromante,Нож некроманта,Nekromantın Bıçağı,亡灵法师之刃,亡靈法師之刃
-necroWpnBladeNecroKnifeDesc,items,Item,,,,"A blackened bone knife bound to your necromantic power. Its damage scales with your Necromancy skill - worthless in untrained hands, deadly in an experienced necromancer's. Its ordinary swing heals you for half the damage it deals, and any zombie it wounds is marked as a Victim, and its corpse can be searched for special loot. Its power attack deals no extra damage and returns nothing to you - instead it always knocks its target off its feet, and takes 20 health from you as the price. Without that much health to spare, it strikes like an ordinary swing.",,"Ein geschwärztes Knochenmesser, an deine nekromantische Macht gebunden. Sein Schaden wächst mit deiner Nekromantie-Fertigkeit – in ungeübten Händen wertlos, in denen eines erfahrenen Nekromanten tödlich. Der normale Hieb heilt dich um die Hälfte des angerichteten Schadens, und jeder verwundete Zombie wird als Opfer markiert, und seine Leiche lässt sich nach besonderer Beute durchsuchen. Der Powerangriff richtet keinen zusätzlichen Schaden an und gibt dir nichts zurück – stattdessen reißt er sein Ziel immer von den Beinen und nimmt dir dafür 20 Lebenspunkte. Reicht deine Gesundheit dafür nicht, schlägt es wie ein normaler Hieb zu.","Un cuchillo de hueso ennegrecido, ligado a tu poder nigromántico. Su daño crece con tu habilidad de Nigromancia: inútil en manos inexpertas, letal en las de un nigromante veterano. Su golpe normal te cura la mitad del daño infligido, y todo zombi al que hiera queda marcado como Víctima, y su cadáver se puede registrar en busca de botín especial. Su ataque potente no hace daño adicional ni te devuelve nada: en cambio, siempre derriba a su objetivo y te cobra 20 puntos de salud. Si no tienes tanta salud, golpea como un ataque normal.","Un couteau en os noirci, lié à ton pouvoir nécromantique. Ses dégâts croissent avec ta compétence de Nécromancie : dérisoire en main inexpérimentée, mortel dans celle d'un nécromancien aguerri. Son coup normal te soigne de la moitié des dégâts infligés, et tout zombie qu'il blesse est marqué comme Victime, et son cadavre peut être fouillé pour un butin spécial. Son attaque puissante n'inflige aucun dégât supplémentaire et ne te rend rien : en revanche, elle met toujours sa cible à terre et te prend 20 points de vie en paiement. S'il ne te reste pas assez de vie, il frappe comme un coup normal.","Un coltello d'osso annerito, legato al tuo potere negromantico. Il suo danno cresce con la tua abilità di Negromanzia: inutile in mani inesperte, letale in quelle di un negromante navigato. Il colpo normale ti cura per metà del danno inflitto, e ogni zombie che ferisce viene marchiato come Vittima, e il suo cadavere può essere perquisito per un bottino speciale. L'attacco potente non infligge danno extra e non ti restituisce nulla: in compenso atterra sempre il bersaglio e ti costa 20 punti salute. Se non hai abbastanza salute, colpisce come un colpo normale.",あなたの降霊術の力に結びついた、黒ずんだ骨のナイフ。その威力は降霊術スキルとともに伸びる。未熟な手では役に立たず、熟練したネクロマンサーの手では致命的だ。通常の一撃は与えたダメージの半分をあなたに回復させ、傷を負ったゾンビは「犠牲者」として刻まれ、その死体を漁れば特別な戦利品が手に入る。強攻撃は追加ダメージを与えず、あなたに何も返さない。その代わり、必ず相手を打ち倒し、代償としてあなたの体力を20奪う。体力が足りなければ、通常の一撃になる。,"당신의 강령술 힘에 묶인 검게 그을린 뼈 단검. 위력은 강령술 기술에 따라 커집니다. 미숙한 손에서는 쓸모없고, 숙련된 강령술사의 손에서는 치명적입니다. 평범한 일격은 입힌 피해의 절반만큼 당신을 치유하고, 상처 입은 좀비는 '희생자'로 표시되어 그 시체를 뒤져 특별한 전리품을 얻을 수 있습니다. 강공격은 추가 피해를 주지 않고 당신에게 아무것도 돌려주지 않습니다. 대신 언제나 대상을 넘어뜨리며, 그 대가로 당신의 체력 20을 가져갑니다. 체력이 부족하면 평범한 일격이 됩니다.","Poczerniały kościany nóż związany z twoją nekromancką mocą. Jego obrażenia rosną wraz z umiejętnością Nekromancji – bezużyteczny w niewprawnych rękach, śmiercionośny w dłoni doświadczonego nekromanty. Zwykłe cięcie leczy cię o połowę zadanych obrażeń, a każde zranione zombie zostaje oznaczone jako Ofiara, a jego zwłoki można przeszukać w poszukiwaniu specjalnego łupu. Atak specjalny nie zadaje dodatkowych obrażeń i niczego ci nie zwraca – za to zawsze zwala cel z nóg, a w zamian zabiera ci 20 punktów zdrowia. Gdy brakuje ci zdrowia, uderza jak zwykłe cięcie.","Uma faca de osso enegrecida, ligada ao seu poder necromântico. Seu dano cresce com a habilidade de Necromancia: inútil em mãos inexperientes, letal nas de um necromante experiente. O golpe comum cura você em metade do dano causado, e todo zumbi ferido é marcado como Vítima, e seu cadáver pode ser revistado em busca de espólio especial. O ataque forte não causa dano extra e não devolve nada a você: em vez disso, sempre derruba o alvo e cobra 20 pontos da sua vida. Se não houver vida suficiente, golpeia como um ataque comum.","Почерневший костяной нож, подчинённый вашей некромантской силе. Его урон растёт вместе с навыком Некромантии - бесполезен в неопытных руках, смертоносен в руках опытного некроманта. Обычный удар лечит вас на половину нанесённого урона, а любой раненый им зомби помечен как Жертва, и его труп можно обыскать ради особой добычи. Силовая атака не бьёт сильнее и ничего вам не возвращает - вместо этого она всегда сбивает цель с ног, забирая у вас 20 единиц здоровья как плату. Если столько здоровья нет, нож бьёт обычным ударом.","Nekromansi gücüne bağlanmış, kararmış bir kemik bıçak. Verdiği hasar Nekromansi yeteneğinle birlikte büyür: acemi ellerde işe yaramaz, deneyimli bir nekromantın elinde ölümcül. Sıradan darbesi verdiği hasarın yarısı kadar seni iyileştirir ve yaraladığı her zombi Kurban olarak işaretlenir; cesedi özel ganimet için aranabilir. Güçlü saldırı ek hasar vermez ve sana hiçbir şey döndürmez; bunun yerine hedefi daima yere serer ve bedel olarak senden 20 can alır. Yeterli canın yoksa sıradan bir darbe gibi vurur.",一把与你的亡灵之力相连的漆黑骨刃。其伤害随亡灵法术技能增长——在生手手中一文不值,在老练的亡灵法师手中则致命。普通挥击可为你回复所造成伤害的一半,被它所伤的僵尸都会被标记为「祭品」,其尸体可被搜刮以获得特殊战利品。强力攻击不会造成额外伤害,也不会回报给你任何东西——但它必定将目标掀翻在地,并以你 20 点生命为代价。生命不足时,它只会像普通挥击一样出手。,一把與你的亡靈之力相連的漆黑骨刃。其傷害隨亡靈法術技能增長——在生手手中一文不值,在老練的亡靈法師手中則致命。普通揮擊可為你回復所造成傷害的一半,被它所傷的殭屍都會被標記為「祭品」,其屍體可被搜刮以獲得特殊戰利品。強力攻擊不會造成額外傷害,也不會回報給你任何東西——但它必定將目標掀翻在地,並以你 20 點生命為代價。生命不足時,它只會像普通揮擊一樣出手。
+necroWpnBladeNecroKnifeDesc,items,Item,,,,"A blackened bone knife bound to your necromantic power. Its damage scales with your Necromancy skill - worthless in untrained hands, deadly in an experienced necromancer's. Its ordinary swing heals you for half the damage it deals, and any zombie it wounds is marked as a Victim, and its corpse can be searched for special loot. Its power attack deals no extra damage and returns nothing to you - instead it always knocks its target off its feet, and takes 20 health from you as the price. Without that much health to spare, it strikes like an ordinary swing. Press R to change the grip: the thread grip links a cinerarium with chests.",,"Ein geschwärztes Knochenmesser, an deine nekromantische Macht gebunden. Sein Schaden wächst mit deiner Nekromantie-Fertigkeit – in ungeübten Händen wertlos, in denen eines erfahrenen Nekromanten tödlich. Der normale Hieb heilt dich um die Hälfte des angerichteten Schadens, und jeder verwundete Zombie wird als Opfer markiert, und seine Leiche lässt sich nach besonderer Beute durchsuchen. Der Powerangriff richtet keinen zusätzlichen Schaden an und gibt dir nichts zurück – stattdessen reißt er sein Ziel immer von den Beinen und nimmt dir dafür 20 Lebenspunkte. Reicht deine Gesundheit dafür nicht, schlägt es wie ein normaler Hieb zu. R wechselt den Griff: Mit dem Fadengriff verbindest du ein Cinerarium mit Truhen.","Un cuchillo de hueso ennegrecido, ligado a tu poder nigromántico. Su daño crece con tu habilidad de Nigromancia: inútil en manos inexpertas, letal en las de un nigromante veterano. Su golpe normal te cura la mitad del daño infligido, y todo zombi al que hiera queda marcado como Víctima, y su cadáver se puede registrar en busca de botín especial. Su ataque potente no hace daño adicional ni te devuelve nada: en cambio, siempre derriba a su objetivo y te cobra 20 puntos de salud. Si no tienes tanta salud, golpea como un ataque normal. R cambia el agarre: el agarre de hilos une un cinerario con cofres.","Un couteau en os noirci, lié à ton pouvoir nécromantique. Ses dégâts croissent avec ta compétence de Nécromancie : dérisoire en main inexpérimentée, mortel dans celle d'un nécromancien aguerri. Son coup normal te soigne de la moitié des dégâts infligés, et tout zombie qu'il blesse est marqué comme Victime, et son cadavre peut être fouillé pour un butin spécial. Son attaque puissante n'inflige aucun dégât supplémentaire et ne te rend rien : en revanche, elle met toujours sa cible à terre et te prend 20 points de vie en paiement. S'il ne te reste pas assez de vie, il frappe comme un coup normal. R change de prise : la prise des fils relie un cinéraire à des coffres.","Un coltello d'osso annerito, legato al tuo potere negromantico. Il suo danno cresce con la tua abilità di Negromanzia: inutile in mani inesperte, letale in quelle di un negromante navigato. Il colpo normale ti cura per metà del danno inflitto, e ogni zombie che ferisce viene marchiato come Vittima, e il suo cadavere può essere perquisito per un bottino speciale. L'attacco potente non infligge danno extra e non ti restituisce nulla: in compenso atterra sempre il bersaglio e ti costa 20 punti salute. Se non hai abbastanza salute, colpisce come un colpo normale. R cambia la presa: la presa dei fili collega un cinerario ai forzieri.",あなたの降霊術の力に結びついた、黒ずんだ骨のナイフ。その威力は降霊術スキルとともに伸びる。未熟な手では役に立たず、熟練したネクロマンサーの手では致命的だ。通常の一撃は与えたダメージの半分をあなたに回復させ、傷を負ったゾンビは「犠牲者」として刻まれ、その死体を漁れば特別な戦利品が手に入る。強攻撃は追加ダメージを与えず、あなたに何も返さない。その代わり、必ず相手を打ち倒し、代償としてあなたの体力を20奪う。体力が足りなければ、通常の一撃になる。Rで握りを変える。糸の握りで骨壺と箱をつなぐ。,"당신의 강령술 힘에 묶인 검게 그을린 뼈 단검. 위력은 강령술 기술에 따라 커집니다. 미숙한 손에서는 쓸모없고, 숙련된 강령술사의 손에서는 치명적입니다. 평범한 일격은 입힌 피해의 절반만큼 당신을 치유하고, 상처 입은 좀비는 '희생자'로 표시되어 그 시체를 뒤져 특별한 전리품을 얻을 수 있습니다. 강공격은 추가 피해를 주지 않고 당신에게 아무것도 돌려주지 않습니다. 대신 언제나 대상을 넘어뜨리며, 그 대가로 당신의 체력 20을 가져갑니다. 체력이 부족하면 평범한 일격이 됩니다. R로 쥐는 법을 바꿉니다. 실 쥐기로 유골함과 상자를 잇습니다.","Poczerniały kościany nóż związany z twoją nekromancką mocą. Jego obrażenia rosną wraz z umiejętnością Nekromancji – bezużyteczny w niewprawnych rękach, śmiercionośny w dłoni doświadczonego nekromanty. Zwykłe cięcie leczy cię o połowę zadanych obrażeń, a każde zranione zombie zostaje oznaczone jako Ofiara, a jego zwłoki można przeszukać w poszukiwaniu specjalnego łupu. Atak specjalny nie zadaje dodatkowych obrażeń i niczego ci nie zwraca – za to zawsze zwala cel z nóg, a w zamian zabiera ci 20 punktów zdrowia. Gdy brakuje ci zdrowia, uderza jak zwykłe cięcie. R zmienia chwyt: chwyt do nici łączy cinerarium ze skrzyniami.","Uma faca de osso enegrecida, ligada ao seu poder necromântico. Seu dano cresce com a habilidade de Necromancia: inútil em mãos inexperientes, letal nas de um necromante experiente. O golpe comum cura você em metade do dano causado, e todo zumbi ferido é marcado como Vítima, e seu cadáver pode ser revistado em busca de espólio especial. O ataque forte não causa dano extra e não devolve nada a você: em vez disso, sempre derruba o alvo e cobra 20 pontos da sua vida. Se não houver vida suficiente, golpeia como um ataque comum. R troca a pegada: a pegada de fios liga um cinerário a baús.","Почерневший костяной нож, подчинённый вашей некромантской силе. Его урон растёт вместе с навыком Некромантии - бесполезен в неопытных руках, смертоносен в руках опытного некроманта. Обычный удар лечит вас на половину нанесённого урона, а любой раненый им зомби помечен как Жертва, и его труп можно обыскать ради особой добычи. Силовая атака не бьёт сильнее и ничего вам не возвращает - вместо этого она всегда сбивает цель с ног, забирая у вас 20 единиц здоровья как плату. Если столько здоровья нет, нож бьёт обычным ударом. R - сменить хват: хватом для нитей цинерарий связывается с ящиками.","Nekromansi gücüne bağlanmış, kararmış bir kemik bıçak. Verdiği hasar Nekromansi yeteneğinle birlikte büyür: acemi ellerde işe yaramaz, deneyimli bir nekromantın elinde ölümcül. Sıradan darbesi verdiği hasarın yarısı kadar seni iyileştirir ve yaraladığı her zombi Kurban olarak işaretlenir; cesedi özel ganimet için aranabilir. Güçlü saldırı ek hasar vermez ve sana hiçbir şey döndürmez; bunun yerine hedefi daima yere serer ve bedel olarak senden 20 can alır. Yeterli canın yoksa sıradan bir darbe gibi vurur. R tutuşu değiştirir: iplik tutuşu bir küllüğü sandıklara bağlar.",一把与你的亡灵之力相连的漆黑骨刃。其伤害随亡灵法术技能增长——在生手手中一文不值,在老练的亡灵法师手中则致命。普通挥击可为你回复所造成伤害的一半,被它所伤的僵尸都会被标记为「祭品」,其尸体可被搜刮以获得特殊战利品。强力攻击不会造成额外伤害,也不会回报给你任何东西——但它必定将目标掀翻在地,并以你 20 点生命为代价。生命不足时,它只会像普通挥击一样出手。按R换握法:牵线握法可将骨灰瓮与箱子相连。,一把與你的亡靈之力相連的漆黑骨刃。其傷害隨亡靈法術技能增長——在生手手中一文不值,在老練的亡靈法師手中則致命。普通揮擊可為你回復所造成傷害的一半,被它所傷的殭屍都會被標記為「祭品」,其屍體可被搜刮以獲得特殊戰利品。強力攻擊不會造成額外傷害,也不會回報給你任何東西——但它必定將目標掀翻在地,並以你 20 點生命為代價。生命不足時,它只會像普通揮擊一樣出手。按R換握法:牽線握法可將骨灰甕與箱子相連。
+necroWpnBladeNecroKnifeThread,items,Item,,,,Necromancer's Knife (thread grip),,Messer des Nekromanten (Fadengriff),Cuchillo del nigromante (agarre de hilos),Couteau du nécromancien (prise des fils),Coltello del negromante (presa dei fili),ネクロマンサーのナイフ(糸の握り),강령술사의 단검 (실 쥐기),Nóż nekromanty (chwyt do nici),Faca do necromante (pegada de fios),Нож некроманта (хват для нитей),Nekromantın Bıçağı (iplik tutuşu),亡灵法师之刃(牵线握法),亡靈法師之刃(牽線握法)
+necroWpnBladeNecroKnifeThreadDesc,items,Item,,,,"The same knife, held by the blade like a stylus. It does not strike - it draws ghostly threads between a cinerarium and chests, up to 15 m long. Right-click a chest, then the urn: the spirit takes from that chest. Right-click the urn, then a chest: the spirit brings things there - only what that chest already holds. Left-click an urn or a chest to cut its threads. An urn with threads only carries things between chests. Press R to change the grip.",,"Dasselbe Messer, an der Klinge gehalten wie ein Griffel. Es schlägt nicht zu - es zieht geisterhafte Fäden zwischen einem Cinerarium und Truhen, bis zu 15 m lang. Rechtsklick auf eine Truhe, dann auf die Urne: Der Geist nimmt aus dieser Truhe. Rechtsklick auf die Urne, dann auf eine Truhe: Der Geist bringt Dinge dorthin - nur das, was schon darin liegt. Linksklick auf Urne oder Truhe trennt ihre Fäden. Eine Urne mit Fäden trägt nur noch Dinge zwischen Truhen. R wechselt den Griff.","El mismo cuchillo, sujeto por la hoja como un punzón. No golpea: tiende hilos fantasmales entre un cinerario y cofres, de hasta 15 m. Clic derecho en un cofre y luego en la urna: el espíritu toma de ese cofre. Clic derecho en la urna y luego en un cofre: el espíritu lleva cosas allí, solo lo que ese cofre ya guarda. Clic izquierdo en una urna o un cofre corta sus hilos. Una urna con hilos solo transporta cosas entre cofres. R cambia el agarre.","Le même couteau, tenu par la lame comme un stylet. Il ne frappe pas : il tend des fils spectraux entre un cinéraire et des coffres, jusqu'à 15 m. Clic droit sur un coffre puis sur l'urne : l'esprit prend dans ce coffre. Clic droit sur l'urne puis sur un coffre : l'esprit y apporte des objets, seulement ce que ce coffre contient déjà. Clic gauche sur une urne ou un coffre coupe ses fils. Une urne reliée ne fait plus que transporter entre coffres. R change de prise.","Lo stesso coltello, tenuto per la lama come uno stilo. Non colpisce: tende fili spettrali tra un cinerario e dei forzieri, lunghi fino a 15 m. Clic destro su un forziere, poi sull'urna: lo spirito prende da quel forziere. Clic destro sull'urna, poi su un forziere: lo spirito vi porta oggetti, solo ciò che quel forziere già contiene. Clic sinistro su un'urna o un forziere ne taglia i fili. Un'urna con fili trasporta soltanto tra forzieri. R cambia la presa.",刃を持ち、筆のように握った同じナイフ。攻撃はせず、骨壺と箱のあいだに最長15mの霊の糸を張る。箱を右クリックしてから骨壺を右クリックすると、霊はその箱から取る。骨壺を右クリックしてから箱を右クリックすると、霊はそこへ運ぶ――その箱にすでにある物だけを。骨壺か箱を左クリックすると糸を断つ。糸のある骨壺は箱から箱への運搬だけをする。Rで握りを変える。,"칼날을 쥐고 붓처럼 잡은 같은 단검. 공격하지 않고, 유골함과 상자 사이에 최대 15m의 유령 실을 잇습니다. 상자를 우클릭한 뒤 유골함을 우클릭하면 영혼이 그 상자에서 가져갑니다. 유골함을 우클릭한 뒤 상자를 우클릭하면 영혼이 그곳으로 옮깁니다 - 그 상자에 이미 있는 것만. 유골함이나 상자를 좌클릭하면 실이 끊어집니다. 실이 이어진 유골함은 상자 사이 운반만 합니다. R로 쥐는 법을 바꿉니다.","Ten sam nóż, trzymany za ostrze jak rysik. Nie zadaje ciosów - rozpina widmowe nici między cinerarium a skrzyniami, do 15 m. PPM na skrzyni, potem na urnie: duch bierze z tej skrzyni. PPM na urnie, potem na skrzyni: duch zanosi tam rzeczy - tylko to, co już w niej leży. LPM na urnie lub skrzyni przecina jej nici. Urna z niciami tylko przenosi rzeczy między skrzyniami. R zmienia chwyt.","A mesma faca, segurada pela lâmina como um estilete. Não golpeia: estende fios fantasmagóricos entre um cinerário e baús, de até 15 m. Clique direito num baú e depois na urna: o espírito tira desse baú. Clique direito na urna e depois num baú: o espírito leva coisas para lá, só o que esse baú já guarda. Clique esquerdo numa urna ou num baú corta seus fios. Uma urna com fios só transporta entre baús. R troca a pegada.","Тот же нож, взятый за клинок, как стилус. Он не бьёт, а тянет призрачные нити между цинерарием и ящиками, длиной до 15 м. ПКМ по ящику, затем по урне - дух берёт из этого ящика. ПКМ по урне, затем по ящику - дух носит туда, и только то, что в этом ящике уже лежит. ЛКМ по урне или ящику обрезает его нити. Урна с нитями только перевозит вещи между ящиками. R - сменить хват.","Aynı bıçak, bir kalem gibi bıçağından tutulmuş. Vurmaz; bir küllük ile sandıklar arasında 15 m'ye kadar hayalet iplikler gerer. Önce bir sandığa, sonra küllüğe sağ tıkla: ruh o sandıktan alır. Önce küllüğe, sonra bir sandığa sağ tıkla: ruh oraya taşır - yalnızca o sandıkta zaten olanı. Bir küllüğe ya da sandığa sol tıklamak ipliklerini keser. İplikli bir küllük yalnızca sandıklar arasında taşır. R tutuşu değiştirir.",同一把刀,握住刀刃,像握笔一样。它不攻击,而是在骨灰瓮与箱子之间牵起最长15米的幽灵丝线。先右键箱子、再右键骨灰瓮:灵从那个箱子取物。先右键骨灰瓮、再右键箱子:灵把东西送到那里——只送那个箱子里已有的东西。左键骨灰瓮或箱子可剪断它的丝线。连了丝线的骨灰瓮只在箱子之间搬运。按R换握法。,同一把刀,握住刀刃,像握筆一樣。它不攻擊,而是在骨灰甕與箱子之間牽起最長15公尺的幽靈絲線。先右鍵箱子、再右鍵骨灰甕:靈從那個箱子取物。先右鍵骨灰甕、再右鍵箱子:靈把東西送到那裡——只送那個箱子裡已有的東西。左鍵骨灰甕或箱子可剪斷它的絲線。連了絲線的骨灰甕只在箱子之間搬運。按R換握法。
+necroKnifeGripNormal,UI,Tooltip,,,,Ordinary grip,,Normaler Griff,Agarre normal,Prise normale,Presa normale,通常の握り,보통 쥐기,Zwykły chwyt,Pegada normal,Обычный хват,Normal tutuş,普通握法,普通握法
+necroKnifeGripThread,UI,Tooltip,,,,Thread grip,,Fadengriff,Agarre de hilos,Prise des fils,Presa dei fili,糸の握り,실 쥐기,Chwyt do nici,Pegada de fios,Хват для нитей,İplik tutuşu,牵线握法,牽線握法
+necroThreadStart,UI,Tooltip,,,,Thread started - now right-click the other end (urn or chest),,Faden begonnen - jetzt das andere Ende rechtsklicken (Urne oder Truhe),Hilo iniciado: ahora clic derecho en el otro extremo (urna o cofre),Fil commencé - clic droit sur l'autre bout (urne ou coffre),Filo iniziato: ora clic destro sull'altro capo (urna o forziere),糸を張り始めた――もう一方の端(骨壺か箱)を右クリック,실을 잇기 시작했다 - 다른 끝(유골함이나 상자)을 우클릭,Nić zaczęta - teraz PPM na drugim końcu (urna lub skrzynia),Fio iniciado: agora clique direito na outra ponta (urna ou baú),Нить начата - теперь ПКМ по другому концу (урна или ящик),İplik başladı - şimdi diğer uca sağ tıkla (küllük ya da sandık),丝线已起头——右键另一端(骨灰瓮或箱子),絲線已起頭——右鍵另一端(骨灰甕或箱子)
+necroThreadNeedPair,UI,Tooltip,,,,A thread links a cinerarium and a chest,,Ein Faden verbindet ein Cinerarium und eine Truhe,Un hilo une un cinerario y un cofre,Un fil relie un cinéraire et un coffre,Un filo unisce un cinerario e un forziere,糸は骨壺と箱をつなぐ,실은 유골함과 상자를 잇는다,Nić łączy cinerarium ze skrzynią,Um fio liga um cinerário e um baú,Нить связывает цинерарий и ящик,İplik bir küllük ile bir sandığı bağlar,丝线连接骨灰瓮与箱子,絲線連接骨灰甕與箱子
+necroThreadTooFar,UI,Tooltip,,,,Too far for a thread (15 m),,Zu weit für einen Faden (15 m),Demasiado lejos para un hilo (15 m),Trop loin pour un fil (15 m),Troppo lontano per un filo (15 m),糸には遠すぎる(15m),실을 잇기엔 너무 멀다 (15m),Za daleko na nić (15 m),Longe demais para um fio (15 m),Слишком далеко для нити (15 м),İplik için çok uzak (15 m),太远了,丝线够不着(15米),太遠了,絲線夠不著(15公尺)
+necroThreadFrom,UI,Tooltip,,,,Thread: chest to urn - the spirit will take from here,,Faden: Truhe zur Urne - der Geist nimmt von hier,Hilo: cofre a urna; el espíritu tomará de aquí,Fil : coffre vers urne - l'esprit prendra ici,Filo: forziere verso urna - lo spirito prenderà da qui,糸:箱から骨壺へ――霊はここから取る,실: 상자에서 유골함으로 - 영혼이 여기서 가져간다,Nić: skrzynia do urny - duch będzie stąd brał,Fio: baú para urna - o espírito vai tirar daqui,Нить: ящик - урна. Дух будет брать отсюда,İplik: sandıktan küllüğe - ruh buradan alacak,丝线:箱子到骨灰瓮——灵会从这里取,絲線:箱子到骨灰甕——靈會從這裡取
+necroThreadTo,UI,Tooltip,,,,Thread: urn to chest - the spirit will bring things here,,Faden: Urne zur Truhe - der Geist bringt Dinge hierher,Hilo: urna a cofre; el espíritu traerá cosas aquí,Fil : urne vers coffre - l'esprit apportera ici,Filo: urna verso forziere - lo spirito porterà qui,糸:骨壺から箱へ――霊はここへ運ぶ,실: 유골함에서 상자로 - 영혼이 여기로 옮긴다,Nić: urna do skrzyni - duch będzie tu znosił,Fio: urna para baú - o espírito vai trazer para cá,Нить: урна - ящик. Дух будет носить сюда,İplik: küllükten sandığa - ruh buraya taşıyacak,丝线:骨灰瓮到箱子——灵会把东西送到这里,絲線:骨灰甕到箱子——靈會把東西送到這裡
+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ı,丝线已放下,絲線已放下
necroKnifeNoHealth,items,Item,,,,Not enough health,,Nicht genug Gesundheit,Salud insuficiente,Pas assez de vie,Salute insufficiente,体力が足りない,체력이 부족합니다,Za mało zdrowia,Vida insuficiente,Недостаточно здоровья,Yetersiz can,生命不足,生命不足
resourceVictimSkin,items,Item,,,,Victim's Skin,,Haut des Opfers,Piel de la víctima,Peau de la victime,Pelle della vittima,犠牲者の皮,희생자의 가죽,Skóra ofiary,Pele da vítima,Кожа жертвы,Kurbanın Derisi,祭品之皮,祭品之皮
resourceVictimSkinDesc,items,Item,,,,Skin taken from a zombie marked as a Victim by the Necromancer's Knife. An ingredient for necromantic summoning tomes.,,"Haut, abgezogen von einem Zombie, den das Messer des Nekromanten als Opfer markiert hat. Eine Zutat für nekromantische Beschwörungsbücher.",Piel tomada de un zombi marcado como Víctima por el Cuchillo del nigromante. Ingrediente para los tomos de invocación nigrománticos.,Peau prélevée sur un zombie marqué comme Victime par le Couteau du nécromancien. Ingrédient des ouvrages d'invocation nécromantiques.,Pelle presa da uno zombie marchiato come Vittima dal Coltello del negromante. Ingrediente per i tomi di evocazione negromantici.,ネクロマンサーのナイフに「犠牲者」として刻まれたゾンビから剥いだ皮。降霊術の召喚書の材料。,강령술사의 단검이 '희생자'로 표시한 좀비에게서 벗겨낸 가죽. 강령술 소환 서적의 재료입니다.,Skóra zdjęta z zombie oznaczonego jako Ofiara przez Nóż nekromanty. Składnik nekromanckich ksiąg przyzwania.,Pele retirada de um zumbi marcado como Vítima pela Faca do necromante. Ingrediente para os tomos de invocação necromânticos.,"Кожа, снятая с зомби, помеченного Ножом некроманта как Жертва. Ингредиент для некромантских книг призыва.",Nekromantın Bıçağı tarafından Kurban olarak işaretlenmiş bir zombiden yüzülmüş deri. Nekromansi çağırma kitaplarının malzemesi.,从被亡灵法师之刃标记为「祭品」的僵尸身上剥下的皮。亡灵召唤典籍的材料。,從被亡靈法師之刃標記為「祭品」的殭屍身上剝下的皮。亡靈召喚典籍的材料。
diff --git a/Config/items.xml b/Config/items.xml
index 0812eed..1a082ea 100644
--- a/Config/items.xml
+++ b/Config/items.xml
@@ -2018,4 +2018,19 @@
+
+
+ -
+
+
+
+
+
+
+
diff --git a/HarmonySrc/Cinerarium.cs b/HarmonySrc/Cinerarium.cs
index 38eac46..1291d6b 100644
--- a/HarmonySrc/Cinerarium.cs
+++ b/HarmonySrc/Cinerarium.cs
@@ -36,10 +36,15 @@ namespace NecromancerTome
public const int Below = 1;
public const int Above = 4;
public const float KillsPerUrn = 500f;
- const byte SaveVersion = 1;
+ const byte SaveVersion = 2; // 2 - нити (16.10)
public bool HasSkull;
public bool ZoneShown;
+ /// Нити (CinerariumThreads.cs): ящики «откуда» и ящик «куда», главные клетки блоков. Есть нити - урна-логист.
+ public readonly List ThreadFrom = new List();
+ public bool HasThreadTo;
+ public Vector3i ThreadTo;
+ public bool IsLogist => HasThreadTo || ThreadFrom.Count > 0;
public override void CopyFromInternal(TileEntityComposite _other)
{
@@ -47,6 +52,11 @@ namespace NecromancerTome
{
HasSkull = other.HasSkull;
ZoneShown = other.ZoneShown;
+ ThreadFrom.Clear();
+ ThreadFrom.AddRange(other.ThreadFrom);
+ HasThreadTo = other.HasThreadTo;
+ ThreadTo = other.ThreadTo;
+ _threadsDirty = true;
}
}
@@ -69,6 +79,7 @@ namespace NecromancerTome
{
base.OnRemove(_world);
RemoveBounds();
+ RemoveThreadView();
RemoveGenie(false);
CinerariumContents.CloseIfShowing(this);
if (!ConnectionManager.Instance.IsServer)
@@ -95,6 +106,8 @@ namespace NecromancerTome
{
base.OnUnload(_world);
RemoveBounds();
+ RemoveThreadView();
+ _threadsDirty = true; // чанк вернётся - нити построить заново
RemoveGenie(false);
CinerariumContents.CloseIfShowing(this);
}
@@ -103,6 +116,14 @@ namespace NecromancerTome
{
base.Write(_bw, _eStreamMode);
CinerariumWriteHelper.Write(_bw, SaveVersion, HasSkull, ZoneShown);
+ List t = new List { ThreadFrom.Count };
+ foreach (Vector3i p in ThreadFrom)
+ {
+ t.Add(p.x); t.Add(p.y); t.Add(p.z);
+ }
+ t.Add(HasThreadTo ? 1 : 0);
+ t.Add(ThreadTo.x); t.Add(ThreadTo.y); t.Add(ThreadTo.z);
+ CinerariumWriteHelper.WriteInts(_bw, t.ToArray());
}
public override void Read(PooledBinaryReader _br, StreamModeRead _eStreamMode)
@@ -111,6 +132,29 @@ namespace NecromancerTome
byte version = _br.ReadByte();
HasSkull = _br.ReadBoolean();
ZoneShown = _br.ReadBoolean();
+ ThreadFrom.Clear();
+ HasThreadTo = false;
+ if (version >= 2)
+ {
+ int n = _br.ReadInt32();
+ int[] v = new int[n];
+ for (int i = 0; i < n; i++)
+ {
+ v[i] = _br.ReadInt32();
+ }
+ int k = 0;
+ int from = n > 0 ? v[k++] : 0;
+ for (int i = 0; i < from && k + 2 < n; i++, k += 3)
+ {
+ ThreadFrom.Add(new Vector3i(v[k], v[k + 1], v[k + 2]));
+ }
+ if (k + 3 < n)
+ {
+ HasThreadTo = v[k] != 0;
+ ThreadTo = new Vector3i(v[k + 1], v[k + 2], v[k + 3]);
+ }
+ }
+ _threadsDirty = true;
if (version > SaveVersion)
{
Debug.LogWarning("[NecromancerTome] Cinerarium: урна из более новой версии мода (" + version + ") - прочитано только известное");
@@ -148,9 +192,148 @@ namespace NecromancerTome
SpawnBounds();
}
SyncGenie(false);
+ SyncThreads(_world);
Work(_world);
}
+ // ------------------------------------------------------------------
+ // Нити (CinerariumThreads.cs).
+ // ------------------------------------------------------------------
+
+ bool _threadsDirty = true;
+ GameObject _threadView;
+ float _nextThreadCheck;
+
+ public bool AddThreadFrom(Vector3i _box)
+ {
+ if (ThreadFrom.Contains(_box))
+ {
+ return true;
+ }
+ if (ThreadFrom.Count >= CinerariumThreads.MaxFrom)
+ {
+ return false;
+ }
+ ThreadFrom.Add(_box);
+ ThreadsChanged();
+ return true;
+ }
+
+ public void SetThreadTo(Vector3i _box)
+ {
+ HasThreadTo = true;
+ ThreadTo = _box;
+ ThreadsChanged();
+ }
+
+ public int ClearThreads()
+ {
+ int n = ThreadFrom.Count + (HasThreadTo ? 1 : 0);
+ ThreadFrom.Clear();
+ HasThreadTo = false;
+ if (n > 0)
+ {
+ ThreadsChanged();
+ }
+ return n;
+ }
+
+ public int RemoveThreadsTo(Vector3i _box)
+ {
+ int n = ThreadFrom.RemoveAll(p => p == _box);
+ if (HasThreadTo && ThreadTo == _box)
+ {
+ HasThreadTo = false;
+ n++;
+ }
+ if (n > 0)
+ {
+ ThreadsChanged();
+ }
+ return n;
+ }
+
+ void ThreadsChanged()
+ {
+ _threadsDirty = true;
+ _glowState = -1; // логист - другие глаза
+ SetModified();
+ Debug.Log("[NecromancerTome] Cinerarium: нити урны " + ToWorldPos() + " - откуда " + ThreadFrom.Count + ", куда " + (HasThreadTo ? ThreadTo.ToString() : "нет"));
+ }
+
+ /// Нити к убранным ящикам снимаются (раз в 2 с); вид перестраивается после изменений.
+ void SyncThreads(World _world)
+ {
+ if (_world != null && ConnectionManager.Instance.IsServer && IsLogist && Time.time >= _nextThreadCheck)
+ {
+ _nextThreadCheck = Time.time + 2f;
+ bool gone = false;
+ foreach (Vector3i p in ThreadFrom.ToArray())
+ {
+ if (!CinerariumThreads.Resolve(_world, p, out Vector3i _, out TEFeatureCinerarium u) || u != null)
+ {
+ if (_world.GetChunkFromWorldPos(p) != null) // чанк не загружен - не снимать
+ {
+ ThreadFrom.Remove(p);
+ gone = true;
+ }
+ }
+ }
+ if (HasThreadTo && (!CinerariumThreads.Resolve(_world, ThreadTo, out Vector3i _, out TEFeatureCinerarium u2) || u2 != null) &&
+ _world.GetChunkFromWorldPos(ThreadTo) != null)
+ {
+ HasThreadTo = false;
+ gone = true;
+ }
+ if (gone)
+ {
+ ThreadsChanged();
+ }
+ }
+ if (!_threadsDirty && (_threadView != null || !IsLogist))
+ {
+ return;
+ }
+ Transform t = ModelTransform();
+ if (t == null)
+ {
+ return;
+ }
+ _threadsDirty = false;
+ SyncGlow();
+ if (!IsLogist)
+ {
+ RemoveThreadView();
+ return;
+ }
+ if (_threadView == null)
+ {
+ _threadView = new GameObject("NecroCinerariumThreads");
+ _threadView.transform.SetParent(t, false);
+ _threadView.AddComponent();
+ }
+ Vector3i me = ToWorldPos();
+ List<(Vector3 a, Vector3 b)> pairs = new List<(Vector3, Vector3)>();
+ foreach (Vector3i p in ThreadFrom)
+ {
+ pairs.Add((CinerariumThreads.BoxPoint(p, me), CinerariumThreads.UrnPoint(me)));
+ }
+ if (HasThreadTo)
+ {
+ pairs.Add((CinerariumThreads.UrnPoint(me), CinerariumThreads.BoxPoint(ThreadTo, me)));
+ }
+ _threadView.GetComponent().Build(pairs);
+ }
+
+ void RemoveThreadView()
+ {
+ if (_threadView != null)
+ {
+ UnityEngine.Object.Destroy(_threadView);
+ _threadView = null;
+ }
+ }
+
float _nextWork;
public bool Stuck;
/// Дело найдено пробным шагом, дух полетел; выполнить в _actAt (16.10: «делал действие не сразу»).
@@ -197,11 +380,17 @@ namespace NecromancerTome
if (at.HasValue)
{
_pending = true;
- _actAt = Time.time + (motion != null ? CinerariumGenieMotion.ActDelay : 0f);
+ _actAt = Time.time;
if (motion != null)
{
- Vector3 look = World.blockToTransformPos(at.Value) - Origin.position;
- motion.Visit(Stand(_world, at.Value, out bool beside) - Origin.position, look, beside);
+ Vector3i? from = CinerariumWork.LastFrom;
+ List<(Vector3, Vector3, bool)> stops = new List<(Vector3, Vector3, bool)>();
+ if (from.HasValue)
+ {
+ stops.Add((Stand(_world, from.Value, out bool b0) - Origin.position, World.blockToTransformPos(from.Value) - Origin.position, b0));
+ }
+ stops.Add((Stand(_world, at.Value, out bool beside) - Origin.position, World.blockToTransformPos(at.Value) - Origin.position, beside));
+ _actAt = Time.time + motion.Visit(stops);
}
}
}
@@ -330,23 +519,67 @@ namespace NecromancerTome
void SyncGlow()
{
Transform t = ModelTransform();
- int want = HasSkull ? 1 : 0;
+ int want = HasSkull ? (IsLogist ? 2 : 1) : 0;
+ Texture logist = want == 2 ? LogistEmission() : null;
if (t == null || (_glowState == want && _glowModel == t))
{
return;
}
foreach (Renderer r in t.GetComponentsInChildren(true))
{
- if (_genie != null && r.transform.IsChildOf(_genie.transform) || _bounds != null && r.transform.IsChildOf(_bounds.transform))
+ if (_genie != null && r.transform.IsChildOf(_genie.transform) || _bounds != null && r.transform.IsChildOf(_bounds.transform) ||
+ _threadView != null && r.transform.IsChildOf(_threadView.transform))
{
continue;
}
r.GetPropertyBlock(_glowBlock);
_glowBlock.SetColor(EmissionColorId, HasSkull ? GlowColor : Color.black);
+ if (logist != null)
+ {
+ _glowBlock.SetTexture(EmissionMapId, logist);
+ }
+ else if (_hadLogistTex)
+ {
+ _glowBlock.Clear(); // вернуть текстуру материала
+ _glowBlock.SetColor(EmissionColorId, HasSkull ? GlowColor : Color.black);
+ }
r.SetPropertyBlock(_glowBlock);
}
_glowState = want;
_glowModel = t;
+ _hadLogistTex = logist != null;
+ }
+
+ bool _hadLogistTex;
+ static readonly int EmissionMapId = Shader.PropertyToID("_EmissionMap");
+ static Texture2D _logistTex;
+ static bool _logistTried;
+ public const string LogistTextureFile = "necroCinerarium_e_logist.png";
+
+ /// Свечение урны-логиста (16.10: «пусть глаза черепов светятся другим цветом. Зелёным»): копия запечённой
+ /// necroCinerarium_e с зелёными глазницами (руны - те же), из папки Resources мода - бандл не пересобирается.
+ static Texture2D LogistEmission()
+ {
+ if (!_logistTried)
+ {
+ _logistTried = true;
+ try
+ {
+ string path = Path.Combine(ModEntry.Instance.Path, "Resources", LogistTextureFile);
+ Texture2D tex = new Texture2D(2, 2, TextureFormat.RGBA32, true);
+ if (tex.LoadImage(File.ReadAllBytes(path)))
+ {
+ tex.name = "necroCinerarium_e_logist";
+ tex.anisoLevel = 4;
+ _logistTex = tex;
+ }
+ }
+ catch (Exception e)
+ {
+ Debug.LogWarning("[NecromancerTome] Cinerarium: свечение логиста не загружено (" + e.Message + ")");
+ }
+ }
+ return _logistTex;
}
Transform _glowModel;
@@ -563,6 +796,11 @@ namespace NecromancerTome
return n;
}
+ public static List All()
+ {
+ return new List(Urns().Keys);
+ }
+
/// Пересекутся ли зоны урны в _pos и уже стоящей: по горизонтали ближе 5 блоков по обеим осям,
/// по высоте ближе 6 (зона от -1 до +4).
public static bool Overlaps(Vector3i _pos)
diff --git a/HarmonySrc/CinerariumGenie.cs b/HarmonySrc/CinerariumGenie.cs
index 055609b..1ef0d70 100644
--- a/HarmonySrc/CinerariumGenie.cs
+++ b/HarmonySrc/CinerariumGenie.cs
@@ -227,15 +227,22 @@ namespace NecromancerTome
float _yaw;
float _phase;
/// Полёт к месту работы и обратно (16.10: «возвращается задом», «делал действие не сразу, а хоть руками
- /// пошевелив»): туда - лицом к месту, над местом - жест руками (дело делается в его середине, см. ActDelay),
+ /// пошевелив»): туда - лицом к месту, над местом - жест руками (дело делается в его середине, см. Visit),
/// обратно - развернувшись лицом к урне. Точка - в координатах модели урны; _t - время с начала полёта, -1 - дома.
- Vector3 _visit;
+ struct Stop
+ {
+ public Vector3 Hover, Look; // в осях модели урны
+ public float Fly;
+ }
+
+ readonly List _stops = new List();
+ float _backFly;
float _t = -1f;
+ /// Скорость полёта, м/с: время перелёта - не меньше OutSeconds/BackSeconds, дальше - по расстоянию (нити до 15 м).
+ public static float FlySpeed = 4f;
public static float OutSeconds = 1.3f;
public static float WorkSeconds = 1.6f;
public static float BackSeconds = 1.3f;
- /// Через сколько после начала полёта дело выполняется - в середине жеста.
- public static float ActDelay => OutSeconds + WorkSeconds * 0.5f;
/// Жест: плечи вперёд до GestureFwd градусов, локти ходят на ±GestureWave вразнобой.
public static float GestureFwd = 55f;
public static float GestureWave = 30f;
@@ -309,21 +316,63 @@ namespace NecromancerTome
/// Светящиеся материалы рендереров - вернуть после почернения.
readonly Dictionary _glowing = new Dictionary();
- /// Куда смотреть за работой (в координатах модели урны); _beside - дух стоит сбоку от места, а не над ним.
- Vector3 _look;
- bool _beside;
-
- /// Точки - в координатах сцены (без Origin): _stand - где встать (низ клетки), _look - низ клетки места работы.
- public void Visit(Vector3 _stand, Vector3 _lookAt, bool _besideIt)
+ /// Облёт мест работы по порядку (у перевозки по нити - ящик «откуда», потом «куда»), у каждого - жест, затем
+ /// назад к урне. Точки - в координатах сцены (без Origin): stand - где встать (низ клетки), look - низ клетки места,
+ /// beside - встал сбоку, а не над местом. Возвращает, через сколько секунд дело (середина последнего жеста).
+ public float Visit(List<(Vector3 stand, Vector3 look, bool beside)> _list)
{
- if (transform.parent == null)
+ if (transform.parent == null || _list == null || _list.Count == 0)
{
- return;
+ return 0f;
}
- _visit = transform.parent.InverseTransformPoint(_stand);
- _look = transform.parent.InverseTransformPoint(_lookAt);
- _beside = _besideIt;
+ _stops.Clear();
+ Vector3 home = Home();
+ Vector3 prev = home;
+ float delay = 0f;
+ foreach ((Vector3 stand, Vector3 look, bool beside) in _list)
+ {
+ Stop st = new Stop
+ {
+ Hover = transform.parent.InverseTransformPoint(stand) + new Vector3(0f, CinerariumGenie.OffsetY + (beside ? 0.15f : 0.8f), 0f),
+ Look = transform.parent.InverseTransformPoint(look)
+ };
+ st.Fly = Mathf.Max(OutSeconds, Vector3.Distance(prev, st.Hover) / FlySpeed);
+ delay += st.Fly + WorkSeconds;
+ prev = st.Hover;
+ _stops.Add(st);
+ }
+ _backFly = Mathf.Max(BackSeconds, Vector3.Distance(prev, home) / FlySpeed);
_t = 0f;
+ return delay - WorkSeconds * 0.5f;
+ }
+
+ Vector3 Home()
+ {
+ return new Vector3(0f, CinerariumGenie.OffsetY, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward);
+ }
+
+ /// Повернуться лицом по направлению _dir (горизонтально), не быстрее 300°/с.
+ void Face(Vector3 _dir)
+ {
+ if (_dir.x * _dir.x + _dir.z * _dir.z > 0.01f)
+ {
+ _yaw = Mathf.MoveTowardsAngle(_yaw, Mathf.Atan2(_dir.x, _dir.z) * Mathf.Rad2Deg, 300f * Time.deltaTime) % 360f;
+ }
+ }
+
+ void Gesture(float _w)
+ {
+ if (_w > 0f)
+ {
+ float wave = Mathf.Sin(_t * Mathf.PI * 2f * 1.5f) * GestureWave * _w;
+ Pose(Mathf.Lerp(CinerariumGenie.ArmFwd, GestureFwd, _w), CinerariumGenie.Elbow + 15f * _w + wave, CinerariumGenie.Elbow + 15f * _w - wave);
+ _gesturing = true;
+ }
+ else if (_gesturing)
+ {
+ Pose();
+ _gesturing = false;
+ }
}
public void SetStuck(bool _on)
@@ -379,58 +428,44 @@ namespace NecromancerTome
return; // замер как был
}
float yawTarget = _yaw + CinerariumGenie.TurnSpeed * Time.deltaTime;
- Vector3 home = new Vector3(0f, CinerariumGenie.OffsetY, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward);
- if (_t >= 0f)
+ Vector3 home = Home();
+ if (_t >= 0f && _stops.Count > 0)
{
_t += Time.deltaTime;
- if (_t < OutSeconds + WorkSeconds + BackSeconds)
+ float acc = 0f;
+ Vector3 from = home;
+ foreach (Stop st in _stops)
{
- // Сбоку - на уровне места (тело вровень со станцией), над местом - выше него.
- Vector3 over = _visit + new Vector3(0f, CinerariumGenie.OffsetY + (_beside ? 0.15f : 0.8f), 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward);
- Vector3 d = _visit - new Vector3(0f, CinerariumGenie.OffsetY, 0f);
- bool far = d.x * d.x + d.z * d.z > 0.01f;
- float toPlace = far ? Mathf.Atan2(d.x, d.z) * Mathf.Rad2Deg : _yaw;
- Vector3 dl = _look - _visit;
- float toLook = dl.x * dl.x + dl.z * dl.z > 0.01f ? Mathf.Atan2(dl.x, dl.z) * Mathf.Rad2Deg : toPlace;
- float k, face, w = 0f;
- if (_t < OutSeconds)
+ if (_t < acc + st.Fly)
{
- k = Mathf.SmoothStep(0f, 1f, _t / OutSeconds);
- face = toPlace;
+ transform.localPosition = Vector3.Lerp(from, st.Hover, Mathf.SmoothStep(0f, 1f, (_t - acc) / st.Fly));
+ Face(st.Hover - from);
+ Gesture(0f);
+ transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
+ return;
}
- else if (_t < OutSeconds + WorkSeconds)
+ acc += st.Fly;
+ if (_t < acc + WorkSeconds)
{
- k = 1f;
- face = toLook; // за работой - лицом к станции
- w = Mathf.Sin((_t - OutSeconds) / WorkSeconds * Mathf.PI);
+ transform.localPosition = st.Hover;
+ Face(st.Look - st.Hover); // за работой - лицом к месту
+ Gesture(Mathf.Sin((_t - acc) / WorkSeconds * Mathf.PI));
+ transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
+ return;
}
- else
- {
- k = 1f - Mathf.SmoothStep(0f, 1f, (_t - OutSeconds - WorkSeconds) / BackSeconds);
- face = far ? toPlace + 180f : _yaw; // назад - лицом к урне
- }
- _yaw = Mathf.MoveTowardsAngle(_yaw, face, 300f * Time.deltaTime) % 360f;
- transform.localPosition = Vector3.Lerp(home, over, k);
+ acc += WorkSeconds;
+ from = st.Hover;
+ }
+ if (_t < acc + _backFly)
+ {
+ transform.localPosition = Vector3.Lerp(from, home, Mathf.SmoothStep(0f, 1f, (_t - acc) / _backFly));
+ Face(home - from); // назад - лицом к урне
+ Gesture(0f);
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
- if (w > 0f)
- {
- float wave = Mathf.Sin(_t * Mathf.PI * 2f * 1.5f) * GestureWave * w;
- Pose(Mathf.Lerp(CinerariumGenie.ArmFwd, GestureFwd, w), CinerariumGenie.Elbow + 15f * w + wave, CinerariumGenie.Elbow + 15f * w - wave);
- _gesturing = true;
- }
- else if (_gesturing)
- {
- Pose();
- _gesturing = false;
- }
return;
}
_t = -1f;
- if (_gesturing)
- {
- Pose();
- _gesturing = false;
- }
+ Gesture(0f);
}
_yaw = yawTarget % 360f;
float bob = CinerariumGenie.BobPeriod > 0.01f
diff --git a/HarmonySrc/CinerariumThreads.cs b/HarmonySrc/CinerariumThreads.cs
new file mode 100644
index 0000000..928d826
--- /dev/null
+++ b/HarmonySrc/CinerariumThreads.cs
@@ -0,0 +1,429 @@
+using System.Collections.Generic;
+using GUI_2;
+using HarmonyLib;
+using UnityEngine;
+using UnityEngine.Scripting;
+
+namespace NecromancerTome
+{
+ ///
+ /// Нити цинерария (16.10.2026, BACKLOG «Нити цинерария»): перевозка между ящиками, как провода электрики. «Ящик на
+ /// территории одного цинерария и присоединяю к нему другой цинерарий проводом. А затем от цинерария тяну провод до другого
+ /// ящика. Дух в цинерарии будет забирать всё что лежит в ящике из которого в него тянется провод, и тащить в ящик, в
+ /// который тянется провод.» Решения: у урны несколько нитей «откуда» и одна «куда»; везётся по образцу (только то, что уже
+ /// лежит в ящике «куда»; пустой - всё подряд); «куда» полон - дух ждёт; нить призрачная; длина - как у провода (15 м);
+ /// ящики - любые. Урна с нитями - «логист»: её дух только перевозит, глазницы черепов светятся зелёным.
+ ///
+ /// Инструмент - нож некроманта «хватом для нитей»: отдельный предмет necroWpnBladeNecroKnifeThread (класс ножа в сейве
+ /// менять нельзя), переключение - меню по зажатому R, как у черепа (SkullDecorPlace); качество и моды переносятся
+ /// (ItemValue.Clone со сменой типа). Как у стриппера: ПКМ - начать нить и закончить её (порядок = направление: ящик, потом
+ /// урна - «откуда»; урна, потом ящик - «куда»), ЛКМ по урне или ящику - снять его нити.
+ /// Первая версия - одиночная игра и хост (связи меняются на месте, клиентам сети - позже).
+ ///
+ public static class CinerariumThreads
+ {
+ public const string KnifeName = Patch_ItemClass_ExecuteAction_KnifePowerCost.KnifeName;
+ public const string ThreadKnifeName = "necroWpnBladeNecroKnifeThread";
+ /// Как у провода электрики: ItemActionConnectPower.maxWireLength = 15.
+ public const float MaxLength = 15f;
+ public const int MaxFrom = 8;
+ public const int GripNormal = 0, GripThread = 1;
+ /// Цвет нити - в цвет духа (CinerariumGenie.Tint).
+ public static readonly Color ThreadColor = new Color(0.55f, 0.8f, 1f, 0.45f);
+ public static readonly Color SparkColor = new Color(0.8f, 0.95f, 1f, 1f);
+
+ public static bool IsKnife(ItemClass _i) => _i != null && (_i.Name == KnifeName || _i.Name == ThreadKnifeName);
+ public static bool IsThreadKnife(ItemClass _i) => _i != null && _i.Name == ThreadKnifeName;
+
+ // ------------------------------------------------------------------
+ // Хват.
+ // ------------------------------------------------------------------
+
+ /// Нож в руке меняется на _name: тот же ItemValue (качество, износ, моды), другой тип.
+ public static void Convert(EntityPlayerLocal _pl, string _name)
+ {
+ ItemStack cur = _pl?.inventory?.holdingItemStack;
+ if (cur == null || cur.IsEmpty() || !IsKnife(cur.itemValue.ItemClass) || cur.itemValue.ItemClass.Name == _name)
+ {
+ return;
+ }
+ ItemValue target = ItemClass.GetItem(_name);
+ if (target == null || target.IsEmpty())
+ {
+ return;
+ }
+ ItemValue iv = cur.itemValue.Clone();
+ iv.type = target.type;
+ _pl.inventory.SetItem(_pl.inventory.holdingItemIdx, new ItemStack(iv, cur.count));
+ Cancel();
+ }
+
+ public static void OnMenu(XUiC_Radial _sender, int _commandIndex, XUiC_Radial.RadialContextAbs _context)
+ {
+ EntityPlayerLocal pl = _sender?.xui?.playerUI?.entityPlayer;
+ if (_commandIndex == GripNormal)
+ {
+ Convert(pl, KnifeName);
+ }
+ else if (_commandIndex == GripThread)
+ {
+ Convert(pl, ThreadKnifeName);
+ }
+ }
+
+ public static void OpenMenu(PlayerMoveController _pmc, EntityPlayerLocal _pl)
+ {
+ XUiC_Radial r = _pmc.playerUI?.xui?.RadialWindow;
+ if (r == null)
+ {
+ return;
+ }
+ bool thread = IsThreadKnife(_pl.inventory.holdingItem);
+ r.Open();
+ r.ResetRadialEntries();
+ r.CreateRadialEntry(GripNormal, "NecromancersKnife", "ItemIconAtlas", "", Localization.Get("necroKnifeGripNormal"), !thread);
+ // 16.10: «Есть в игре пиктограмма ниток, катушки?» - набор для шитья (катушка с иглой).
+ r.CreateRadialEntry(GripThread, "resourceSewingKit", "ItemIconAtlas", "", Localization.Get("necroKnifeGripThread"), thread);
+ r.SetCommonData(UIUtils.GetButtonIconForAction(_pmc.playerInput.Reload), OnMenu,
+ new XUiC_Radial.RadialContextHoldingSlotIndex(_pl.inventory.holdingItemIdx), thread ? GripThread : GripNormal, false,
+ XUiC_Radial.RadialValidSameHoldingSlotIndex);
+ }
+
+ // ------------------------------------------------------------------
+ // Соединение.
+ // ------------------------------------------------------------------
+
+ static Vector3i? _start;
+ static bool _startUrn;
+ static GameObject _preview;
+
+ public static Vector3i? Start => _start;
+ public static bool StartIsUrn => _startUrn;
+
+ public static void Cancel()
+ {
+ _start = null;
+ if (_preview != null)
+ {
+ Object.Destroy(_preview);
+ _preview = null;
+ }
+ }
+
+ /// Урна или ящик (составной блок игрока с хранилищем) в клетке _p; _owner - главная клетка блока.
+ public static bool Resolve(World _w, Vector3i _p, out Vector3i _owner, out TEFeatureCinerarium _urn)
+ {
+ BlockValue bv = _w.GetBlock(_p);
+ _owner = bv.ischild ? _p + bv.parent : _p;
+ _urn = null;
+ if (!(_w.GetTileEntity(_owner) is TileEntityComposite comp))
+ {
+ return false;
+ }
+ if (comp.TryGetSelfOrFeature(out _urn))
+ {
+ return true;
+ }
+ return comp.PlayerPlaced && comp.TryGetSelfOrFeature(out TEFeatureStorage _);
+ }
+
+ public static TEFeatureCinerarium UrnAt(World _w, Vector3i _p)
+ {
+ return _w.GetTileEntity(_p) is TileEntityComposite c && c.TryGetSelfOrFeature(out TEFeatureCinerarium u) ? u : null;
+ }
+
+ /// Отпущена кнопка с ножом-для-нитей: _connect - ПКМ (тянуть нить), иначе ЛКМ (снять нити).
+ public static void Click(EntityPlayerLocal _pl, bool _connect)
+ {
+ World w = GameManager.Instance?.World;
+ WorldRayHitInfo hi = _pl?.HitInfo;
+ if (w == null || hi == null || !hi.bHitValid || !GameUtils.IsBlockOrTerrain(hi.tag) || hi.hit.distanceSq > 36f)
+ {
+ return;
+ }
+ bool ok = Resolve(w, hi.hit.blockPos, out Vector3i owner, out TEFeatureCinerarium urn);
+ if (!_connect)
+ {
+ if (_start.HasValue)
+ {
+ Cancel();
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadCancel"));
+ return;
+ }
+ if (ok && RemoveAll(w, owner, urn) > 0)
+ {
+ _pl.PlayOneShot("wiretool_grab");
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadRemoved"));
+ }
+ return;
+ }
+ if (!ok)
+ {
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedPair"));
+ return;
+ }
+ if (!_start.HasValue)
+ {
+ _start = owner;
+ _startUrn = urn != null;
+ _preview = new GameObject("NecroThreadPreview");
+ _preview.AddComponent();
+ _pl.PlayOneShot("wiretool_grab");
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadStart"));
+ return;
+ }
+ Vector3i s = _start.Value;
+ bool sUrn = _startUrn;
+ Cancel();
+ if (s == owner)
+ {
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadCancel"));
+ return;
+ }
+ if (sUrn == (urn != null))
+ {
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedPair"));
+ return;
+ }
+ if (Vector3.Distance(s.ToVector3(), owner.ToVector3()) > MaxLength)
+ {
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTooFar"));
+ return;
+ }
+ if (sUrn)
+ {
+ TEFeatureCinerarium u = UrnAt(w, s);
+ if (u == null)
+ {
+ return;
+ }
+ u.SetThreadTo(owner);
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTo"));
+ }
+ else
+ {
+ if (!urn.AddThreadFrom(s))
+ {
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadLimit"));
+ return;
+ }
+ GameManager.ShowTooltip(_pl, Localization.Get("necroThreadFrom"));
+ }
+ _pl.PlayOneShot("wiretool_place");
+ }
+
+ /// Снять все нити урны или все нити, идущие к ящику (у всех урн сейва). Сколько снято.
+ static int RemoveAll(World _w, Vector3i _owner, TEFeatureCinerarium _urn)
+ {
+ if (_urn != null)
+ {
+ return _urn.ClearThreads();
+ }
+ int n = 0;
+ foreach (Vector3i p in CinerariumRegistry.All())
+ {
+ TEFeatureCinerarium u = UrnAt(_w, p);
+ if (u != null)
+ {
+ n += u.RemoveThreadsTo(_owner);
+ }
+ }
+ return n;
+ }
+
+ // ------------------------------------------------------------------
+ // Вид нитей.
+ // ------------------------------------------------------------------
+
+ static Material _mat;
+
+ public static Material Mat()
+ {
+ if (_mat == null)
+ {
+ _mat = new Material(Shader.Find("Sprites/Default")) { name = "NecroThread" };
+ }
+ return _mat;
+ }
+
+ /// Где нить крепится: у урны - горлышко, у ящика - край со стороны урны, чуть выше середины. Сцена (без Origin).
+ public static Vector3 UrnPoint(Vector3i _urn) => World.blockToTransformPos(_urn) - Origin.position + Vector3.up * 0.5f;
+
+ public static Vector3 BoxPoint(Vector3i _box, Vector3i _urn)
+ {
+ Vector3 c = World.blockToTransformPos(_box) - Origin.position + Vector3.up * 0.6f;
+ Vector3 d = (World.blockToTransformPos(_urn) - World.blockToTransformPos(_box));
+ d.y = 0f;
+ if (d.sqrMagnitude > 0.01f)
+ {
+ d.Normalize();
+ c += d * 0.5f;
+ }
+ return c;
+ }
+
+ /// Провисающая нить: точка по доле пути _u.
+ public static Vector3 Curve(Vector3 _a, Vector3 _b, float _u)
+ {
+ float sag = Mathf.Min(0.6f, Vector3.Distance(_a, _b) * 0.05f);
+ return Vector3.Lerp(_a, _b, _u) + Vector3.down * (sag * 4f * _u * (1f - _u));
+ }
+
+ public static LineRenderer NewLine(Transform _parent, string _name, float _width, Color _c)
+ {
+ GameObject g = new GameObject(_name);
+ g.transform.SetParent(_parent, false);
+ LineRenderer lr = g.AddComponent();
+ lr.useWorldSpace = false;
+ lr.widthMultiplier = _width;
+ lr.sharedMaterial = Mat();
+ lr.startColor = _c;
+ lr.endColor = _c;
+ lr.numCapVertices = 2;
+ lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
+ lr.receiveShadows = false;
+ return lr;
+ }
+ }
+
+ /// Нити одной урны: провисающие линии и бегущие по ним огоньки (направление). Дочерний объект модели урны -
+ /// сдвиги Origin и выгрузка чанка его не теряют; точки - в осях модели.
+ [Preserve]
+ public class CinerariumThreadView : MonoBehaviour
+ {
+ struct Thread
+ {
+ public Vector3 A, B; // в осях модели урны, огоньки бегут от A к B
+ public LineRenderer[] Sparks;
+ }
+
+ readonly List _threads = new List();
+ const int Segments = 16;
+ const int SparksPerThread = 3;
+ public static float SparkSpeed = 1.2f; // м/с
+
+ /// _pairs - концы в координатах сцены (без Origin).
+ public void Build(List<(Vector3 a, Vector3 b)> _pairs)
+ {
+ foreach (Transform c in transform)
+ {
+ Destroy(c.gameObject);
+ }
+ _threads.Clear();
+ Transform t = transform;
+ foreach ((Vector3 a, Vector3 b) in _pairs)
+ {
+ Thread th = new Thread { A = t.InverseTransformPoint(a), B = t.InverseTransformPoint(b), Sparks = new LineRenderer[SparksPerThread] };
+ LineRenderer lr = CinerariumThreads.NewLine(t, "thread", 0.025f, CinerariumThreads.ThreadColor);
+ lr.positionCount = Segments + 1;
+ for (int i = 0; i <= Segments; i++)
+ {
+ lr.SetPosition(i, CinerariumThreads.Curve(th.A, th.B, i / (float)Segments));
+ }
+ for (int k = 0; k < SparksPerThread; k++)
+ {
+ th.Sparks[k] = CinerariumThreads.NewLine(t, "spark", 0.06f, CinerariumThreads.SparkColor);
+ th.Sparks[k].positionCount = 2;
+ }
+ _threads.Add(th);
+ }
+ }
+
+ void Update()
+ {
+ foreach (Thread th in _threads)
+ {
+ float len = Mathf.Max(0.5f, Vector3.Distance(th.A, th.B));
+ float step = 0.2f / len;
+ for (int k = 0; k < th.Sparks.Length; k++)
+ {
+ float u = Mathf.Repeat(Time.time * SparkSpeed / len + k / (float)th.Sparks.Length, 1f);
+ th.Sparks[k].SetPosition(0, CinerariumThreads.Curve(th.A, th.B, Mathf.Max(0f, u - step)));
+ th.Sparks[k].SetPosition(1, CinerariumThreads.Curve(th.A, th.B, u));
+ }
+ }
+ }
+ }
+
+ /// Нить, которую тянут: от начала до руки игрока; дальше 15 м - красная. Нож убрали - нить брошена.
+ [Preserve]
+ public class CinerariumThreadPreview : MonoBehaviour
+ {
+ LineRenderer _lr;
+
+ void Start()
+ {
+ _lr = CinerariumThreads.NewLine(transform, "line", 0.025f, CinerariumThreads.ThreadColor);
+ _lr.useWorldSpace = true;
+ _lr.positionCount = 17;
+ }
+
+ void Update()
+ {
+ EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
+ if (pl == null || !CinerariumThreads.Start.HasValue || !CinerariumThreads.IsThreadKnife(pl.inventory?.holdingItem))
+ {
+ CinerariumThreads.Cancel();
+ return;
+ }
+ if (_lr == null)
+ {
+ return;
+ }
+ Vector3i s = CinerariumThreads.Start.Value;
+ Vector3 a = World.blockToTransformPos(s) - Origin.position + Vector3.up * (CinerariumThreads.StartIsUrn ? 0.5f : 0.6f);
+ Transform cam = pl.playerCamera != null ? pl.playerCamera.transform : null;
+ Vector3 b = cam != null ? cam.position + cam.forward * 0.6f + cam.right * 0.2f - cam.up * 0.25f : pl.GetPosition() - Origin.position + Vector3.up * 1.2f;
+ bool far = Vector3.Distance(s.ToVector3(), pl.GetPosition()) > CinerariumThreads.MaxLength;
+ Color c = far ? new Color(1f, 0.3f, 0.3f, 0.7f) : CinerariumThreads.ThreadColor;
+ _lr.startColor = c;
+ _lr.endColor = c;
+ for (int i = 0; i <= 16; i++)
+ {
+ _lr.SetPosition(i, CinerariumThreads.Curve(a, b, i / 16f));
+ }
+ }
+ }
+
+ /// Нож для нитей не бьёт: ЛКМ и ПКМ - нити (на отпускание кнопки).
+ [HarmonyPatch(typeof(ItemClass), nameof(ItemClass.ExecuteAction))]
+ public static class Patch_ItemClass_ExecuteAction_ThreadKnife
+ {
+ public static bool Prefix(ItemClass __instance, int _actionIdx, ItemInventoryData _data, bool _bReleased)
+ {
+ if (!CinerariumThreads.IsThreadKnife(__instance))
+ {
+ return true;
+ }
+ if (_bReleased && _data?.holdingEntity is EntityPlayerLocal pl)
+ {
+ CinerariumThreads.Click(pl, _actionIdx == 1);
+ }
+ return false;
+ }
+ }
+
+ /// R с ножом некроманта (любым хватом) - меню хвата. Игра ножу по R ничего не открывает.
+ [HarmonyPatch(typeof(PlayerMoveController), nameof(PlayerMoveController.Update))]
+ public static class Patch_PlayerMoveController_Update_KnifeGrip
+ {
+ public static void Postfix(PlayerMoveController __instance)
+ {
+ EntityPlayerLocal pl = __instance.entityPlayerLocal;
+ if (pl == null || pl.inventory == null || !CinerariumThreads.IsKnife(pl.inventory.holdingItem) || pl.IsDead())
+ {
+ return;
+ }
+ PlayerActionsLocal inp = __instance.playerInput;
+ if (inp == null || !(inp.Reload.WasPressed || inp.PermanentActions.Reload.WasPressed) || pl.inventory.IsHandSwitching())
+ {
+ return;
+ }
+ if (__instance.playerUI == null || __instance.playerUI.windowManager.IsModalWindowOpen())
+ {
+ return;
+ }
+ CinerariumThreads.OpenMenu(__instance, pl);
+ }
+ }
+}
diff --git a/HarmonySrc/CinerariumWork.cs b/HarmonySrc/CinerariumWork.cs
index d659e4c..29f16a8 100644
--- a/HarmonySrc/CinerariumWork.cs
+++ b/HarmonySrc/CinerariumWork.cs
@@ -59,9 +59,18 @@ namespace NecromancerTome
}
}
+ /// У перевозки по нити - ящик «откуда» последнего шага (дух летит сначала к нему), иначе null.
+ public static Vector3i? LastFrom;
+
static Vector3i? DoStep(World _world, TEFeatureCinerarium _urn, out bool _stuck)
{
_stuck = false;
+ LastFrom = null;
+ // Урна с нитями - логист (16.10: «занимается только перетаскиванием из ящиков в ящики»).
+ if (_urn.IsLogist)
+ {
+ return Ferry(_world, _urn);
+ }
Zone z = Scan(_world, _urn);
// 1. Расходники.
foreach (TileEntityCollector c in z.Collectors)
@@ -122,6 +131,80 @@ namespace NecromancerTome
return null;
}
+ /// Перевозка по нитям: одна стопка из ящика «откуда» в ящик «куда». По образцу - только то, что в «куда» уже
+ /// есть (пустой «куда» - что угодно); «куда» полон - ждём (дух не чернеет). Открытые у игрока ящики и закреплённые
+ /// ячейки не трогаются. Возвращает ящик «куда», LastFrom - откуда взято.
+ static Vector3i? Ferry(World _world, TEFeatureCinerarium _urn)
+ {
+ if (!_urn.HasThreadTo)
+ {
+ return null;
+ }
+ TEFeatureStorage dst = BoxAt(_world, _urn.ThreadTo);
+ if (dst == null)
+ {
+ return null;
+ }
+ bool any = dst.IsEmpty();
+ foreach (Vector3i f in _urn.ThreadFrom)
+ {
+ if (f == _urn.ThreadTo)
+ {
+ continue;
+ }
+ TEFeatureStorage src = BoxAt(_world, f);
+ if (src == null)
+ {
+ continue;
+ }
+ for (int k = 0; k < src.ItemGrid.Length; k++)
+ {
+ ItemStack s = src.ItemGrid[k];
+ if (s == null || s.IsEmpty() || Locked(src, k) || (!any && !Holds(dst, s.itemValue.type)) || !Fits(dst, s))
+ {
+ continue;
+ }
+ LastFrom = f;
+ if (_dry)
+ {
+ return _urn.ThreadTo;
+ }
+ ItemStack moving = s.Clone();
+ Place(moving, new[] { dst });
+ int moved = s.count - moving.count;
+ if (moved <= 0)
+ {
+ continue;
+ }
+ Take(src, k, moved);
+ dst.SetModified();
+ Debug.Log("[NecromancerTome] Cinerarium: дух перенёс " + moved + " " + s.itemValue.ItemClass?.GetItemName() + " из " + f + " в " + _urn.ThreadTo);
+ return _urn.ThreadTo;
+ }
+ }
+ return null;
+ }
+
+ static TEFeatureStorage BoxAt(World _world, Vector3i _p)
+ {
+ return _world.GetTileEntity(_p) is TileEntityComposite c && !c.IsUserAccessing() &&
+ !c.TryGetSelfOrFeature(out TEFeatureCinerarium _) &&
+ c.TryGetSelfOrFeature(out TEFeatureStorage s) ? s : null;
+ }
+
+ static bool Holds(TEFeatureStorage _t, int _type)
+ {
+ for (int i = 0; i < _t.ItemGrid.Length; i++)
+ {
+ ItemStack x = _t.ItemGrid[i];
+ if (x != null && !x.IsEmpty() && x.itemValue.type == _type)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
static bool IsRipe(Block _b)
{
return _b != null && !(_b is BlockPlantGrowing) && _b.HasAnyFastTags(CropTag) &&
diff --git a/NecromancerHarmony.dll b/NecromancerHarmony.dll
index faeacb3..ed18dde 100644
Binary files a/NecromancerHarmony.dll and b/NecromancerHarmony.dll differ
diff --git a/NecromancerTEPersistence.dll b/NecromancerTEPersistence.dll
index a3f7139..683f9eb 100644
Binary files a/NecromancerTEPersistence.dll and b/NecromancerTEPersistence.dll differ
diff --git a/Resources/necroCinerarium_e_logist.png b/Resources/necroCinerarium_e_logist.png
new file mode 100644
index 0000000..ab59367
Binary files /dev/null and b/Resources/necroCinerarium_e_logist.png differ
diff --git a/TEPersistenceSrc/CinerariumWriteHelper.cs b/TEPersistenceSrc/CinerariumWriteHelper.cs
index 3be79f1..3a2ef88 100644
--- a/TEPersistenceSrc/CinerariumWriteHelper.cs
+++ b/TEPersistenceSrc/CinerariumWriteHelper.cs
@@ -14,5 +14,15 @@ namespace NecromancerTome
_bw.Write(_hasSkull);
_bw.Write(_zoneShown);
}
+
+ /// Версия 2 (16.10, нити): число, затем сами числа - координаты нитей (см. TEFeatureCinerarium.Write).
+ public static void WriteInts(PooledBinaryWriter _bw, int[] _values)
+ {
+ _bw.Write(_values.Length);
+ foreach (int v in _values)
+ {
+ _bw.Write(v);
+ }
+ }
}
}