diff --git a/Config/Localization.csv b/Config/Localization.csv index fbc06c4..66803e7 100644 --- a/Config/Localization.csv +++ b/Config/Localization.csv @@ -99,6 +99,24 @@ blockcommand_effect_on,blocks,Block,,,,Enable Effect,,Effekt aktivieren,Activar blockcommand_effect_off,blocks,Block,,,,Disable Effect,,Effekt deaktivieren,Desactivar efecto,Désactiver l'effet,Disattiva effetto,効果をオフ,효과 끄기,Wyłącz efekt,Desativar efeito,Отключить эффект,Etkiyi Kapat,停用效果,停用效果 blockcommand_zone_show,blocks,Block,,,,Show Zone,,Zone anzeigen,Mostrar zona,Afficher la zone,Mostra zona,範囲を表示,구역 표시,Pokaż strefę,Mostrar zona,Показать зону,Bölgeyi Göster,显示范围,顯示範圍 blockcommand_zone_hide,blocks,Block,,,,Hide Zone,,Zone ausblenden,Ocultar zona,Masquer la zone,Nascondi zona,範囲を非表示,구역 숨기기,Ukryj strefę,Ocultar zona,Скрыть зону,Bölgeyi Gizle,隐藏范围,隱藏範圍 +necroCinerarium,blocks,Block,,,,Harvest Cinerarium,,Ernte-Cinerarium,Cinerario de cosecha,Cinéraire des moissons,Cinerario del raccolto,収穫の納骨壺,수확의 유골함,Cinerarium żniw,Cinerário da colheita,Цинерарий,Hasat Küllüğü,丰收骨灰瓮,豐收骨灰甕 +necroCinerariumDesc,blocks,Block,,,,"A funeral urn that houses a skull taken from a red corpse. Put the skull in its trunk and a spirit worker tends the garden around the urn: a 5 by 5 area, from one block below the urn to four above. The urn's own storage is the spirit's bag. Hold E on the urn for the trunk, the bag or the zone. Picked up, the urn keeps its skull. Zones of two urns cannot overlap, and you may keep one urn for every 500 dead you have put to rest.",,"Eine Grabesurne für einen Schädel aus einer roten Leiche. Leg den Schädel in ihren Kofferraum, und ein Geisterarbeiter pflegt den Garten um die Urne: eine Fläche von 5 mal 5, von einem Block unter der Urne bis vier darüber. Der Speicher der Urne ist die Tasche des Geistes. Halte E an der Urne für Kofferraum, Tasche oder Zone. Aufgehoben behält die Urne ihren Schädel. Die Zonen zweier Urnen dürfen sich nicht überschneiden, und du darfst je 500 zur Ruhe gebrachte Tote eine Urne halten.","Una urna funeraria que alberga un cráneo sacado de un cadáver rojo. Pon el cráneo en su maletero y un espíritu trabajador cuidará el huerto alrededor de la urna: un área de 5 por 5, desde un bloque bajo la urna hasta cuatro por encima. El almacén de la urna es la bolsa del espíritu. Mantén E sobre la urna para el maletero, la bolsa o la zona. Al recogerla, la urna conserva su cráneo. Las zonas de dos urnas no pueden solaparse, y puedes tener una urna por cada 500 muertos a los que hayas dado descanso.","Une urne funéraire qui abrite un crâne pris sur un cadavre rouge. Placez le crâne dans son coffre et un esprit ouvrier entretient le jardin autour de l'urne : une zone de 5 sur 5, d'un bloc sous l'urne à quatre au-dessus. Le stockage de l'urne est le sac de l'esprit. Maintenez E sur l'urne pour le coffre, le sac ou la zone. Ramassée, l'urne garde son crâne. Les zones de deux urnes ne peuvent pas se chevaucher, et vous pouvez garder une urne pour 500 morts que vous avez mis au repos.","Un'urna funeraria che custodisce un teschio preso da un cadavere rosso. Metti il teschio nel suo bagagliaio e uno spirito lavoratore curerà l'orto intorno all'urna: un'area di 5 per 5, da un blocco sotto l'urna fino a quattro sopra. Il deposito dell'urna è la borsa dello spirito. Tieni premuto E sull'urna per il bagagliaio, la borsa o la zona. Raccolta, l'urna conserva il teschio. Le zone di due urne non possono sovrapporsi, e puoi tenere un'urna ogni 500 morti a cui hai dato riposo.",赤い死体から取った頭蓋骨を納める葬送の壺。頭蓋骨をトランクに入れると、霊の働き手が壺の周りの畑を世話する。範囲は5×5、壺の1ブロック下から4ブロック上まで。壺の収納が霊の鞄になる。壺に向けてEを長押しすると、トランク・鞄・範囲表示を選べる。拾い上げても頭蓋骨は壺に残る。2つの壺の範囲は重ねられず、眠らせた死者500体ごとに壺を1つ持てる。,"붉은 시체에서 얻은 두개골을 모시는 장례용 항아리. 두개골을 트렁크에 넣으면 영혼 일꾼이 항아리 주변의 밭을 돌본다. 범위는 5×5, 항아리 한 칸 아래부터 네 칸 위까지. 항아리의 저장 공간이 영혼의 가방이다. 항아리에 E를 길게 누르면 트렁크, 가방, 구역 표시를 고를 수 있다. 주워도 두개골은 항아리에 남는다. 두 항아리의 구역은 겹칠 수 없고, 안식시킨 망자 500명마다 항아리를 하나 둘 수 있다.","Urna pogrzebowa, w której spoczywa czaszka wzięta z czerwonych zwłok. Włóż czaszkę do jej bagażnika, a duch-robotnik zadba o ogród wokół urny: obszar 5 na 5, od bloku pod urną do czterech nad nią. Schowek urny to torba ducha. Przytrzymaj E na urnie: bagażnik, torba lub strefa. Podniesiona urna zachowuje czaszkę. Strefy dwóch urn nie mogą na siebie zachodzić, a jedną urnę możesz mieć na każde 500 zmarłych, których ukoiłeś.","Uma urna funerária que abriga um crânio tirado de um cadáver vermelho. Coloque o crânio no porta-malas e um espírito trabalhador cuida da horta ao redor da urna: uma área de 5 por 5, de um bloco abaixo da urna até quatro acima. O armazenamento da urna é a bolsa do espírito. Segure E na urna para o porta-malas, a bolsa ou a zona. Recolhida, a urna mantém o crânio. As zonas de duas urnas não podem se sobrepor, e você pode ter uma urna para cada 500 mortos que pôs para descansar.","Погребальная урна для черепа из красного трупа. Положите череп в её багажник - и дух-работник будет ухаживать за грядками вокруг урны: участок 5 на 5, от блока под урной до четырёх над ней. Хранилище урны - сумка духа. Удерживайте E на урне: багажник, сумка или зона. Поднятая урна сохраняет череп. Зоны двух урн не пересекаются; одну урну можно держать на каждые 500 упокоенных мертвецов.","Kırmızı bir cesetten alınmış kafatasını barındıran bir cenaze küpü. Kafatasını bagajına koy; bir ruh işçi küpün çevresindeki bahçeye bakar: 5'e 5'lik bir alan, küpün bir blok altından dört blok üstüne kadar. Küpün deposu ruhun çantasıdır. Bagaj, çanta ya da bölge için küpe E'yi basılı tut. Toplanan küp kafatasını korur. İki küpün bölgeleri çakışamaz; huzura kavuşturduğun her 500 ölü için bir küp tutabilirsin.",安放红色尸体头骨的葬瓮。把头骨放进它的后备箱,灵魂工人会照料瓮周围的田地:5×5的范围,从瓮下方1格到上方4格。瓮的储物空间就是灵魂的背包。对着瓮长按E可选择后备箱、背包或显示区域。捡起瓮时头骨仍留在其中。两个瓮的区域不能重叠;每安息500个亡者可拥有一个瓮。,安放紅色屍體頭骨的葬甕。把頭骨放進它的後車廂,靈魂工人會照料甕周圍的田地:5×5的範圍,從甕下方1格到上方4格。甕的儲物空間就是靈魂的背包。對著甕長按E可選擇後車廂、背包或顯示區域。撿起甕時頭骨仍留在其中。兩個甕的區域不能重疊;每安息500個亡者可擁有一個甕。 +necroCinerariumSkull,blocks,Block,,,,Harvest Cinerarium (with skull),,Ernte-Cinerarium (mit Schädel),Cinerario de cosecha (con cráneo),Cinéraire des moissons (avec crâne),Cinerario del raccolto (con teschio),収穫の納骨壺(頭蓋骨入り),수확의 유골함 (두개골 포함),Cinerarium żniw (z czaszką),Cinerário da colheita (com crânio),Цинерарий с черепом,Hasat Küllüğü (kafatasıyla),丰收骨灰瓮(含头骨),豐收骨灰甕(含頭骨) +blockcommand_TEFeatureCinerarium:trunk,blocks,Block,,,,Trunk,,Kofferraum,Maletero,Coffre,Bagagliaio,トランク,트렁크,Bagażnik,Porta-malas,Багажник,Bagaj,后备箱,後車廂 +blockcommand_trunk,blocks,Block,,,,Trunk,,Kofferraum,Maletero,Coffre,Bagagliaio,トランク,트렁크,Bagażnik,Porta-malas,Багажник,Bagaj,后备箱,後車廂 +blockcommand_TEFeatureCinerarium:urn_repair,blocks,Block,,,,Repair,,Reparieren,Reparar,Réparer,Riparare,修理,수리,Napraw,Reparar,Починить,Onar,修理,修理 +blockcommand_urn_repair,blocks,Block,,,,Repair,,Reparieren,Reparar,Réparer,Riparare,修理,수리,Napraw,Reparar,Починить,Onar,修理,修理 +necroCinerariumNoBones,UI,Tooltip,,,,Repair needs 5 bones,,Zum Reparieren braucht es 5 Knochen,Reparar requiere 5 huesos,Réparer demande 5 os,Per riparare servono 5 ossa,修理には骨が5本必要,수리에는 뼈 5개가 필요하다,Naprawa wymaga 5 kości,Reparar exige 5 ossos,Для починки нужно 5 костей,Onarmak için 5 kemik gerekir,修理需要5根骨头,修理需要5根骨頭 +necroCinerariumBagNotEmpty,UI,Tooltip,,,,Empty the bag first,,Leere zuerst die Tasche,Vacía primero la bolsa,Videz d'abord le sac,Svuota prima la borsa,先に鞄を空にする,먼저 가방을 비워라,Najpierw opróżnij torbę,Esvazie a bolsa primeiro,Сначала освободите сумку,Önce çantayı boşalt,请先清空背包,請先清空背包 +necroCinerariumTrunk,UI,XUI,,,,Trunk,,Kofferraum,Maletero,Coffre,Bagagliaio,トランク,트렁크,Bagażnik,Porta-malas,Багажник,Bagaj,后备箱,後車廂 +blockcommand_TEFeatureCinerarium:bounds_show,blocks,Block,,,,Show Zone,,Zone anzeigen,Mostrar zona,Afficher la zone,Mostra zona,範囲を表示,구역 표시,Pokaż strefę,Mostrar zona,Показать зону,Bölgeyi göster,显示区域,顯示區域 +blockcommand_bounds_show,blocks,Block,,,,Show Zone,,Zone anzeigen,Mostrar zona,Afficher la zone,Mostra zona,範囲を表示,구역 표시,Pokaż strefę,Mostrar zona,Показать зону,Bölgeyi göster,显示区域,顯示區域 +blockcommand_TEFeatureCinerarium:bounds_hide,blocks,Block,,,,Hide Zone,,Zone ausblenden,Ocultar zona,Masquer la zone,Nascondi zona,範囲を非表示,구역 숨기기,Ukryj strefę,Ocultar zona,Скрыть зону,Bölgeyi gizle,隐藏区域,隱藏區域 +blockcommand_bounds_hide,blocks,Block,,,,Hide Zone,,Zone ausblenden,Ocultar zona,Masquer la zone,Nascondi zona,範囲を非表示,구역 숨기기,Ukryj strefę,Ocultar zona,Скрыть зону,Bölgeyi gizle,隐藏区域,隱藏區域 +necroCinerariumStateSkull,blocks,Block,,,,skull inside,,Schädel darin,con cráneo,crâne à l'intérieur,teschio dentro,頭蓋骨あり,두개골 있음,czaszka w środku,com crânio,череп внутри,kafatası içinde,已放头骨,已放頭骨 +necroCinerariumStateEmpty,blocks,Block,,,,no skull,,kein Schädel,sin cráneo,pas de crâne,nessun teschio,頭蓋骨なし,두개골 없음,brak czaszki,sem crânio,черепа нет,kafatası yok,没有头骨,沒有頭骨 +necroCinerariumOverlap,UI,Tooltip,,,,Too close to another cinerarium: their zones would overlap,,Zu nah an einem anderen Cinerarium: die Zonen würden sich überschneiden,Demasiado cerca de otro cinerario: sus zonas se solaparían,Trop près d'un autre cinéraire : leurs zones se chevaucheraient,Troppo vicino a un altro cinerario: le zone si sovrapporrebbero,別の納骨壺に近すぎる:範囲が重なる,다른 유골함과 너무 가깝다: 구역이 겹친다,Za blisko innego cinerarium: strefy by się nałożyły,Perto demais de outro cinerário: as zonas se sobreporiam,Слишком близко к другому цинерарию: зоны пересекутся,Başka bir küllüğe çok yakın: bölgeler çakışır,离另一个骨灰瓮太近:区域会重叠,離另一個骨灰甕太近:區域會重疊 +necroCinerariumLimit,UI,Tooltip,,,,No more cinerariums for now: one for every 500 dead put to rest,,Vorerst keine weiteren Cinerarien: eines je 500 zur Ruhe gebrachte Tote,No puedes tener más cinerarios por ahora: uno por cada 500 muertos en descanso,Pas d'autre cinéraire pour l'instant : un pour 500 morts mis au repos,Nessun altro cinerario per ora: uno ogni 500 morti messi a riposo,今はこれ以上置けない:眠らせた死者500体ごとに1つ,지금은 더 둘 수 없다: 안식시킨 망자 500명마다 하나,Na razie nie więcej cinerariów: jedno na 500 ukojonych zmarłych,Nada de mais cinerários por enquanto: um a cada 500 mortos em descanso,Больше цинерариев пока нельзя: один на каждые 500 упокоенных,Şimdilik daha fazla küllük yok: huzura kavuşan her 500 ölü için bir,暂时不能再放骨灰瓮:每安息500个亡者一个,暫時不能再放骨灰甕:每安息500個亡者一個 necroModKnifeTearsOfTheDead,item_modifiers,Item,,,,Tears of the Dead,,Tränen des Toten,Lágrimas del muerto,Larmes du mort,Lacrime del morto,死者の涙,죽은 자의 눈물,Łzy umarłego,Lágrimas do morto,Слёзы мертвеца,Ölünün Gözyaşları,亡者之泪,亡者之淚 necroModKnifeTearsOfTheDeadDesc,item_modifiers,Item,,,,"A modification for the Necromancer's Knife alone. Every zombie the blade kills gives up what little moisture it has left: 2 units of water, drawn straight out of the corpse. Ordinary knife mods will not fit this weapon, and this one will not fit any other.",,"Eine Modifikation ausschließlich für das Messer des Nekromanten. Jeder Zombie, den die Klinge tötet, gibt her, was ihm an Feuchtigkeit noch geblieben ist: 2 Einheiten Wasser, direkt aus der Leiche gezogen. Gewöhnliche Messermodifikationen passen nicht in diese Waffe, und diese hier passt in keine andere.","Una modificación exclusiva para el Cuchillo del nigromante. Cada zombi que la hoja mata entrega la poca humedad que le queda: 2 unidades de agua, extraídas directamente del cadáver. Las modificaciones de cuchillo corrientes no encajan en esta arma, y esta no encaja en ninguna otra.","Une modification réservée au seul Couteau du nécromancien. Chaque zombie tué par la lame rend le peu d'humidité qui lui restait : 2 unités d'eau, tirées droit du cadavre. Les modifications de couteau ordinaires n'entrent pas dans cette arme, et celle-ci n'entre dans aucune autre.","Una modifica esclusiva per il Coltello del negromante. Ogni zombie ucciso dalla lama cede quel poco di umidità che gli resta: 2 unità d'acqua, estratte direttamente dal cadavere. Le modifiche per coltelli comuni non entrano in quest'arma, e questa non entra in nessun'altra.",ネクロマンサーのナイフ専用の改造パーツ。この刃に倒されたゾンビは、残されたわずかな水分を差し出す。死体から直に引き出された水2単位だ。通常のナイフ用改造パーツはこの武器に取り付けられず、これも他のどんな武器にも取り付けられない。,"강령술사의 단검 전용 개조 부품입니다. 칼날에 쓰러진 좀비는 남아 있던 얼마 안 되는 수분을 내놓습니다. 시체에서 곧바로 뽑아낸 물 2단위입니다. 일반 단검용 부품은 이 무기에 장착되지 않으며, 이 부품도 다른 무기에는 장착되지 않습니다.","Modyfikacja wyłącznie do Noża nekromanty. Każde zombie zabite tym ostrzem oddaje tę odrobinę wilgoci, jaka mu jeszcze została: 2 jednostki wody, wyciągnięte prosto ze zwłok. Zwykłe modyfikacje do noży nie pasują do tej broni, a ta nie pasuje do żadnej innej.","Uma modificação exclusiva para a Faca do necromante. Cada zumbi que a lâmina mata entrega a pouca umidade que lhe resta: 2 unidades de água, tiradas direto do cadáver. Modificações comuns de faca não servem nesta arma, e esta não serve em nenhuma outra.","Модификация только для Ножа некроманта. Каждый убитый клинком зомби отдаёт ту каплю влаги, что в нём ещё осталась: 2 единицы воды, вытянутые прямо из трупа. Обычные моды для ножей в это оружие не встанут, а этот не встанет ни во что другое.","Yalnızca Nekromantın Bıçağı'na özel bir modifikasyon. Bıçağın öldürdüğü her zombi, kendisinde kalan az miktardaki nemi verir: doğrudan cesetten çekilmiş 2 birim su. Sıradan bıçak modifikasyonları bu silaha takılmaz, bu da başka hiçbir silaha takılmaz.",亡灵法师之刃专用的改装件。被此刃杀死的每一只僵尸都会交出它仅存的那点水分:2 单位水,直接自尸体中抽出。寻常的匕首改装件装不进这把武器,而这一件也装不进其他任何武器。,亡靈法師之刃專用的改裝件。被此刃殺死的每一隻殭屍都會交出牠僅存的那點水分:2 單位水,直接自屍體中抽出。尋常的匕首改裝件裝不進這把武器,而這一件也裝不進其他任何武器。 necroModKnifeScavengersFeast,item_modifiers,Item,,,,Scavenger's Feast,,Festmahl des Aasfressers,Festín del carroñero,Festin du charognard,Banchetto dello spazzino,腐肉喰らいの宴,시체 청소부의 만찬,Uczta padlinożercy,Banquete do carniceiro,Пир падальщика,Leşçinin Ziyafeti,食腐者的盛宴,食腐者的盛宴 diff --git a/Config/XUi_InGame/windows.xml b/Config/XUi_InGame/windows.xml index 1f688c4..673a8ab 100644 --- a/Config/XUi_InGame/windows.xml +++ b/Config/XUi_InGame/windows.xml @@ -208,4 +208,21 @@ + + + + + + + + + + + + + + diff --git a/Config/XUi_InGame/xui.xml b/Config/XUi_InGame/xui.xml index e6ac8a7..171b8f4 100644 --- a/Config/XUi_InGame/xui.xml +++ b/Config/XUi_InGame/xui.xml @@ -59,4 +59,10 @@ + + + + + + diff --git a/Config/blocks.xml b/Config/blocks.xml index ed3af8f..7de1efe 100644 --- a/Config/blocks.xml +++ b/Config/blocks.xml @@ -513,4 +513,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Config/loot.xml b/Config/loot.xml index 46e6ada..d8d1f4c 100644 --- a/Config/loot.xml +++ b/Config/loot.xml @@ -45,4 +45,11 @@ + + + + + + + diff --git a/Config/progression.xml b/Config/progression.xml index 4ac732e..2cb85f2 100644 --- a/Config/progression.xml +++ b/Config/progression.xml @@ -181,7 +181,7 @@ пользуется, вода сидит на necroNecromancyLvl20. --> - + + + + + + + + diff --git a/HarmonySrc/Cinerarium.cs b/HarmonySrc/Cinerarium.cs new file mode 100644 index 0000000..c206c04 --- /dev/null +++ b/HarmonySrc/Cinerarium.cs @@ -0,0 +1,606 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using HarmonyLib; +using Platform; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// Урна-цинерарий (Harvest Cinerarium), этап 2 некротехнолога (BACKLOG «Некротехнолог v2», решения 06.10, + /// 15.10, 16.10). Составной блок игры, как пирамида и земельный камень: TEFeatureStorage игры - сумка духа + /// (обычное окно хранилища), эта деталь - череп, зона и меню на E. Решение 16.10 «урна как транспорт»: на E + /// круговое меню, как у велосипеда - «Багажник» (ячейка черепа, CinerariumTrunk.cs), сумка (хранилище игры), + /// зона. Есть ли в урне череп - поле детали с сохранением (CinerariumWriteHelper, версия первым байтом). + /// + /// Поднятая урна держит череп, как транспорт - модификации: игра при подъёме данных блока в предмет не + /// переносит (Block.TakeItemWithTimerDone -> BlockValue.ToItemValue, только тип), поэтому два блока - + /// necroCinerarium (пустая) и necroCinerariumSkull (с черепом); подъём отдаёт тот, что соответствует черепу, + /// постановка второго кладёт череп сразу. + /// + /// Зона работника - 5x5 по горизонтали с урной в центре, по высоте от -1 до +4 блока (06.10). Зоны урн не + /// пересекаются, урн у игрока не больше ceil(убийств / 500) - обе проверки при постановке + /// (CanPlaceBlockAt), по реестру урн сейва (CinerariumRegistry). + /// + /// Имена команд не должны кончаться на zone_show/zone_hide/effect_on/effect_off: заплатка пирамиды на + /// BlockCompositeTileEntity.OnBlockActivated ловит их по EndsWith у ЛЮБОГО составного блока. + /// + public class TEFeatureCinerarium : TEFeatureAbs + { + public const string BlockName = "necroCinerarium"; + public const string SkullBlockName = "necroCinerariumSkull"; + public const string SkullItemName = "necroSkull"; + public const int HalfWidth = 2; + public const int Below = 1; + public const int Above = 4; + public const float KillsPerUrn = 500f; + const byte SaveVersion = 1; + + public bool HasSkull; + public bool ZoneShown; + + public override void CopyFromInternal(TileEntityComposite _other) + { + if (_other.TryGetSelfOrFeature(out TEFeatureCinerarium other)) + { + HasSkull = other.HasSkull; + ZoneShown = other.ZoneShown; + } + } + + /// Блок поставлен (OnBlockAdded составного блока; на загрузке чанка не зовётся). + public override void OnAdded(Vector3i _blockPos, BlockValue _blockValue) + { + base.OnAdded(_blockPos, _blockValue); + if (_blockValue.Block?.GetBlockName() == SkullBlockName) + { + HasSkull = true; // поставили урну, поднятую с черепом + } + if (ConnectionManager.Instance.IsServer) + { + CinerariumRegistry.Add(_blockPos, Parent?.Owner); + } + } + + /// Блок убран (подобран или разрушен; выгрузка чанка - OnUnload). Череп не пропадает. + public override void OnRemove(World _world) + { + base.OnRemove(_world); + RemoveBounds(); + XUiC_NecroCinerariumSkullGrid.CloseIfShowing(this); + if (!ConnectionManager.Instance.IsServer) + { + return; + } + Vector3i pos = ToWorldPos(); + CinerariumRegistry.Remove(pos); + // Подняли целиком - череп уехал вместе с урной (necroCinerariumSkull), не выбрасывать. + bool pickedUp = Patch_Block_TakeItemWithTimerDone_Cinerarium.WasPickedUp(pos); + if (HasSkull && _world != null && !pickedUp) + { + HasSkull = false; + ItemValue skull = ItemClass.GetItem(SkullItemName); + if (skull != null && skull.type > 0) + { + _world.gameManager.ItemDropServer(new ItemStack(skull, 1), World.blockToTransformPos(pos) + Vector3.up * 0.7f, Vector3.zero); + Debug.Log("[NecromancerTome] Cinerarium: урна " + pos + " убрана - череп выпал"); + } + } + } + + public override void OnUnload(World _world) + { + base.OnUnload(_world); + RemoveBounds(); + XUiC_NecroCinerariumSkullGrid.CloseIfShowing(this); + } + + public override void Write(PooledBinaryWriter _bw, StreamModeWrite _eStreamMode) + { + base.Write(_bw, _eStreamMode); + CinerariumWriteHelper.Write(_bw, SaveVersion, HasSkull, ZoneShown); + } + + public override void Read(PooledBinaryReader _br, StreamModeRead _eStreamMode) + { + base.Read(_br, _eStreamMode); + byte version = _br.ReadByte(); + HasSkull = _br.ReadBoolean(); + ZoneShown = _br.ReadBoolean(); + if (version > SaveVersion) + { + Debug.LogWarning("[NecromancerTome] Cinerarium: урна из более новой версии мода (" + version + ") - прочитано только известное"); + } + } + + /// Меню как у велосипеда (EntityVehicle.InitLocalActivationCommands): «service» - багажник (у транспорта + /// «Обслуживание», окно модификаций), «wrench» - починить, «frames» - границы, как у земельного камня. Сумку даёт + /// TEFeatureStorage («Search»), подъём - TEFeaturePickup («Take»), обе детали игры. + public override void InitBlockActivationCommands(Action _addCallback) + { + base.InitBlockActivationCommands(_addCallback); + _addCallback(new BlockActivationCommand("trunk", "service", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData); + _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); + } + + 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"); + } + + public override void UpdateTick(World _world) + { + base.UpdateTick(_world); + if (ZoneShown && _bounds == null) + { + SpawnBounds(); + } + } + + // ------------------------------------------------------------------ + // Череп. + // ------------------------------------------------------------------ + + /// Череп положен в ячейку багажника или вынут из неё. + public void SetSkull(bool _has) + { + if (HasSkull == _has) + { + return; + } + HasSkull = _has; + SetModified(); + Debug.Log("[NecromancerTome] Cinerarium: урна " + ToWorldPos() + (_has ? " - череп положен" : " - череп вынут")); + } + + /// «Починить», как у транспорта: целиком, за RepairBones костей из рюкзака (сколько берёт и молоток - + /// RepairItems в blocks.xml). + public const string RepairItemName = "resourceBone"; + public const int RepairBones = 5; + + public static int BonesCarried(EntityPlayerLocal _player) + { + ItemValue bone = ItemClass.GetItem(RepairItemName); + if (_player == null || bone == null || bone.type <= 0) + { + return 0; + } + return (_player.inventory?.GetItemCount(bone) ?? 0) + (_player.bag?.GetItemCount(bone) ?? 0); + } + + public static bool Repair(World _world, Vector3i _pos, EntityPlayerLocal _player) + { + BlockValue bv = _world.GetBlock(_pos); + ItemValue bone = ItemClass.GetItem(RepairItemName); + if (bv.damage <= 0 || bone == null || bone.type <= 0 || BonesCarried(_player) < RepairBones) + { + return false; + } + int need = RepairBones; + if (_player.bag != null) + { + need -= _player.bag.DecItem(bone, need); + } + if (need > 0 && _player.inventory != null) + { + need -= _player.inventory.DecItem(bone, need); + } + bv.damage = 0; + _world.SetBlockRPC(_pos, bv); + _player.PlayOneShot("crafting/craft_repair_item", true); + Debug.Log("[NecromancerTome] Cinerarium: урна " + _pos + " починена"); + return true; + } + + // ------------------------------------------------------------------ + // Границы зоны: каркас параллелепипеда, как у земельного камня. Дочерний объект модели блока - сдвиги + // Origin и выгрузка чанка его не теряют (как кольцо пирамиды); материал один на все урны. + // ------------------------------------------------------------------ + + GameObject _bounds; + static Material _boundsMat; + + Transform ModelTransform() + { + World world = GameManager.Instance?.World; + if (world == null) + { + return null; + } + Vector3i pos = ToWorldPos(); + Chunk chunk = world.GetChunkFromWorldPos(pos) as Chunk; + BlockEntityData be = chunk?.GetBlockEntity(pos); + return be != null && be.bHasTransform ? be.transform : null; + } + + public void SpawnBounds() + { + if (_bounds != null) + { + return; + } + Transform t = ModelTransform(); + if (t == null) + { + return; + } + _bounds = new GameObject("NecroCinerariumBounds"); + _bounds.transform.SetParent(t, false); + LineRenderer lr = _bounds.AddComponent(); + lr.useWorldSpace = false; + lr.widthMultiplier = 0.06f; + if (_boundsMat == null) + { + _boundsMat = new Material(Shader.Find("Sprites/Default")) { name = "NecroCinerariumBounds" }; + } + lr.sharedMaterial = _boundsMat; + Color col = new Color(0.55f, 0.9f, 0.75f, 0.85f); + lr.startColor = col; + lr.endColor = col; + lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; + lr.receiveShadows = false; + + // Углы по граням блоков: x и z от -2.5 до +2.5 от центра урны, y от низа блока под ней до верха +4. + 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; + 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 рёбрам (часть рёбер проходится дважды - на глаз не видно). + Vector3[] path = { b0, b1, b2, b3, b0, t0, t1, b1, t1, t2, b2, t2, t3, b3, t3, t0 }; + lr.positionCount = path.Length; + for (int i = 0; i < path.Length; i++) + { + lr.SetPosition(i, t.InverseTransformPoint(path[i])); + } + } + + public void RemoveBounds() + { + if (_bounds != null) + { + UnityEngine.Object.Destroy(_bounds); + _bounds = null; + } + } + + // ------------------------------------------------------------------ + // Постановка: лимит и пересечение зон. + // ------------------------------------------------------------------ + + public static bool IsUrn(Block _b) + { + string n = _b?.GetBlockName(); + return n == BlockName || n == SkullBlockName; + } + + public static int UrnLimit(EntityPlayer _player) + { + float kills = _player?.Buffs != null ? _player.Buffs.GetCustomVar(Patch_EntityPlayer_AddKillXP_NecromancyCount.KillsCVarName) : 0f; + return Mathf.CeilToInt(kills / KillsPerUrn); + } + + /// Ключ подсказки, почему урну сюда нельзя, или null. + public static string PlacementRefusal(Vector3i _pos) + { + if (CinerariumRegistry.Overlaps(_pos)) + { + return "necroCinerariumOverlap"; + } + World world = GameManager.Instance?.World; + EntityPlayerLocal player = world?.GetPrimaryPlayer(); + // Реестр живёт на сервере; клиент в сети лимит не проверяет (мультиплеер поддержан не полностью). + if (player != null && world != null && !world.IsRemote()) + { + PlatformUserIdentifierAbs me = PlatformManager.InternalLocalUserIdentifier; + if (CinerariumRegistry.CountFor(me) >= UrnLimit(player)) + { + return "necroCinerariumLimit"; + } + } + return null; + } + } + + /// Все урны сейва: позиция и владелец, файл в папке сейва (как сумка волка). Сервер. + public static class CinerariumRegistry + { + const string FileName = "NecroCinerariums.txt"; + static Dictionary _urns; + static string _dir; + + static string FilePath => Path.Combine(GameIO.GetSaveGameDir(), FileName); + + static Dictionary Urns() + { + string dir = GameIO.GetSaveGameDir(); + if (_urns != null && _dir == dir) + { + return _urns; + } + _dir = dir; + _urns = new Dictionary(); + try + { + if (File.Exists(FilePath)) + { + foreach (string line in File.ReadAllLines(FilePath)) + { + string[] parts = line.Split('|'); + string[] xyz = parts[0].Split(','); + if (xyz.Length == 3 && int.TryParse(xyz[0], out int x) && int.TryParse(xyz[1], out int y) && int.TryParse(xyz[2], out int z)) + { + _urns[new Vector3i(x, y, z)] = parts.Length > 1 ? parts[1] : ""; + } + } + } + } + catch (Exception e) + { + Debug.LogWarning("[NecromancerTome] Cinerarium: реестр урн не прочитан (" + e.Message + ")"); + } + return _urns; + } + + static void Save() + { + try + { + StringBuilder sb = new StringBuilder(); + foreach (KeyValuePair kv in Urns()) + { + sb.Append(kv.Key.x).Append(',').Append(kv.Key.y).Append(',').Append(kv.Key.z).Append('|').Append(kv.Value).Append('\n'); + } + File.WriteAllText(FilePath, sb.ToString()); + } + catch (Exception e) + { + Debug.LogWarning("[NecromancerTome] Cinerarium: реестр урн не записан (" + e.Message + ")"); + } + } + + public static void Add(Vector3i _pos, PlatformUserIdentifierAbs _owner) + { + Urns()[_pos] = _owner != null ? _owner.CombinedString : ""; + Save(); + Debug.Log("[NecromancerTome] Cinerarium: урна поставлена " + _pos + ", всего в сейве " + _urns.Count); + } + + public static void Remove(Vector3i _pos) + { + if (Urns().Remove(_pos)) + { + Save(); + Debug.Log("[NecromancerTome] Cinerarium: урна убрана " + _pos + ", всего в сейве " + _urns.Count); + } + } + + public static int CountFor(PlatformUserIdentifierAbs _owner) + { + string id = _owner != null ? _owner.CombinedString : ""; + int n = 0; + foreach (string v in Urns().Values) + { + if (v == id) + { + n++; + } + } + return n; + } + + /// Пересекутся ли зоны урны в _pos и уже стоящей: по горизонтали ближе 5 блоков по обеим осям, + /// по высоте ближе 6 (зона от -1 до +4). + public static bool Overlaps(Vector3i _pos) + { + int w = TEFeatureCinerarium.HalfWidth * 2; + int h = TEFeatureCinerarium.Below + TEFeatureCinerarium.Above; + foreach (Vector3i p in Urns().Keys) + { + if (p != _pos && Math.Abs(p.x - _pos.x) <= w && Math.Abs(p.z - _pos.z) <= w && Math.Abs(p.y - _pos.y) <= h) + { + return true; + } + } + return false; + } + } + + /// Какие команды меню урны доступны сейчас (текст команд общий на тип блока - см. TEFeaturePyramidWard). + [HarmonyPatch(typeof(TileEntityComposite), "UpdateBlockActivationCommands")] + public static class Patch_TileEntityComposite_UpdateBlockActivationCommands_Cinerarium + { + public static void Postfix(TileEntityComposite __instance, BlockActivationCommand[] _commands, WorldBase _world, Vector3i _blockPos) + { + if (__instance == null || _commands == null || !__instance.TryGetSelfOrFeature(out TEFeatureCinerarium urn)) + { + return; + } + for (int i = 0; i < _commands.Length; i++) + { + string text = _commands[i].text; + if (string.IsNullOrEmpty(text)) + { + continue; + } + if (text.EndsWith(":trunk") || text == "trunk") + { + _commands[i].enabled = true; + } + else if (text.EndsWith("urn_repair")) + { + _commands[i].enabled = _world != null && _world.GetBlock(_blockPos).damage > 0; + } + else if (text.EndsWith(":Take")) + { + // Как у транспорта: поднять можно, только вынув вещи из сумки; череп (модификация) остаётся в урне. + if (__instance.TryGetSelfOrFeature(out TEFeatureStorage bag) && !bag.IsEmpty()) + { + _commands[i].enabled = false; + } + } + else if (text.EndsWith("bounds_show")) + { + _commands[i].enabled = !urn.ZoneShown; + } + else if (text.EndsWith("bounds_hide")) + { + _commands[i].enabled = urn.ZoneShown; + } + } + } + } + + [HarmonyPatch(typeof(BlockCompositeTileEntity), "OnBlockActivated", new Type[] { typeof(string), typeof(WorldBase), typeof(Vector3i), typeof(BlockValue), typeof(EntityPlayerLocal) })] + public static class Patch_BlockCompositeTileEntity_OnBlockActivated_Cinerarium + { + public static bool Prefix(string _commandName, Vector3i _blockPos, EntityPlayerLocal _player, ref bool __result) + { + if (string.IsNullOrEmpty(_commandName)) + { + return true; + } + bool skull = _commandName.EndsWith(":trunk") || _commandName == "trunk"; + bool repair = _commandName.EndsWith("urn_repair"); + bool bounds = _commandName.EndsWith("bounds_show") || _commandName.EndsWith("bounds_hide"); + if (!skull && !bounds && !repair) + { + return true; + } + World world = GameManager.Instance?.World; + if (!(world?.GetTileEntity(_blockPos) is TileEntityComposite composite) || + !composite.TryGetSelfOrFeature(out TEFeatureCinerarium urn)) + { + return true; + } + if (skull) + { + XUiC_NecroCinerariumSkullGrid.Open(_player, urn); + } + else if (repair) + { + if (!TEFeatureCinerarium.Repair(world, _blockPos, _player)) + { + GameManager.ShowTooltip(_player, "necroCinerariumNoBones"); + } + } + else + { + urn.ZoneShown = !urn.ZoneShown; + if (urn.ZoneShown) + { + urn.SpawnBounds(); + } + else + { + urn.RemoveBounds(); + } + urn.SetModified(); + } + __result = true; + return false; + } + } + + /// Лимит урн и пересечение зон. Составной блок CanPlaceBlockAt не переопределяет - заплатка на Block, + /// с отбором по имени блока. Зовётся и каждый кадр для призрака постановки (RenderDisplacedCube) - он + /// краснеет; причину показывает подмена подсказки «нельзя поставить» при щелчке (ниже). + [HarmonyPatch(typeof(Block), nameof(Block.CanPlaceBlockAt))] + public static class Patch_Block_CanPlaceBlockAt_Cinerarium + { + public static string LastRefusal; + public static float LastRefusalTime; + + public static void Postfix(Block __instance, Vector3i _blockPos, ref bool __result) + { + if (!__result || __instance == null || !TEFeatureCinerarium.IsUrn(__instance)) + { + return; + } + string why = TEFeatureCinerarium.PlacementRefusal(_blockPos); + if (why != null) + { + __result = false; + LastRefusal = why; + LastRefusalTime = Time.time; + } + } + } + + [HarmonyPatch(typeof(GameManager), nameof(GameManager.ShowTooltip), new Type[] { typeof(EntityPlayerLocal), typeof(string), typeof(bool), typeof(bool), typeof(float) })] + public static class Patch_GameManager_ShowTooltip_Cinerarium + { + public static void Prefix(ref string _text) + { + if (_text == "blockCantPlaced" && Patch_Block_CanPlaceBlockAt_Cinerarium.LastRefusal != null && + Time.time - Patch_Block_CanPlaceBlockAt_Cinerarium.LastRefusalTime < 0.1f) + { + _text = Patch_Block_CanPlaceBlockAt_Cinerarium.LastRefusal; + Patch_Block_CanPlaceBlockAt_Cinerarium.LastRefusal = null; + } + } + } + + /// Подъём урны («Take» детали TEFeaturePickup -> Block.TakeItemWithTimer): по окончании таймера игра отдаёт + /// предмет по типу блока (BlockValue.ToItemValue), OnBlockPickedUp не зовёт. Для урны - своя копия того же метода: + /// с черепом - предмет necroCinerariumSkull, без - necroCinerarium; сумка за время таймера не должна наполниться. + /// Позиция запоминается - OnRemove урны по ней понимает, что её подняли, а не разбили (череп не выбрасывать). + [HarmonyPatch(typeof(Block), nameof(Block.TakeItemWithTimerDone))] + public static class Patch_Block_TakeItemWithTimerDone_Cinerarium + { + static Vector3i _pos; + static float _time = -100f; + + public static bool WasPickedUp(Vector3i _p) => _p == _pos && Time.time - _time < 5f; + + public static bool Prefix(TimerEventData _timerData) + { + World world = GameManager.Instance?.World; + if (world == null || !(_timerData?.Data is object[] obj) || obj.Length < 3 || !(obj[1] is Vector3i pos) || + !(obj[0] is BlockValue started) || !(obj[2] is EntityPlayerLocal player)) + { + return true; + } + BlockValue bv = world.GetBlock(pos); + if (!TEFeatureCinerarium.IsUrn(bv.Block) || bv.type != started.type || bv.damage > 0 || + !(world.GetTileEntity(pos) is TileEntityComposite composite) || + !composite.TryGetSelfOrFeature(out TEFeatureCinerarium urn)) + { + return true; // чужой блок или отказ игры (повреждён, подменён) - пусть игра сама скажет почему + } + if (composite.IsUserAccessing()) + { + GameManager.ShowTooltip(player, Localization.Get("ttCantPickupInUse"), string.Empty, "ui_denied"); + return false; + } + if (composite.TryGetSelfOrFeature(out TEFeatureStorage bag) && !bag.IsEmpty()) + { + GameManager.ShowTooltip(player, Localization.Get("necroCinerariumBagNotEmpty"), string.Empty, "ui_denied"); + return false; + } + ItemValue iv = ItemClass.GetItem(urn.HasSkull ? TEFeatureCinerarium.SkullBlockName : TEFeatureCinerarium.BlockName); + if (iv == null || iv.type <= 0) + { + return true; + } + _pos = pos; + _time = Time.time; + ItemStack stack = new ItemStack(iv, 1); + LocalPlayerUI ui = LocalPlayerUI.GetUIForPlayer(player); + if (!ui.xui.PlayerInventory.AddItem(stack)) + { + ui.xui.PlayerInventory.DropItem(stack); + } + world.SetBlockRPC(pos, BlockValue.Air); + Debug.Log("[NecromancerTome] Cinerarium: урна " + pos + " поднята" + (urn.HasSkull ? " с черепом" : "")); + return false; + } + } +} diff --git a/HarmonySrc/CinerariumTrunk.cs b/HarmonySrc/CinerariumTrunk.cs new file mode 100644 index 0000000..d7f9633 --- /dev/null +++ b/HarmonySrc/CinerariumTrunk.cs @@ -0,0 +1,85 @@ +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// «Багажник» урны-цинерария (решение 16.10: «урна как транспорт» - на E круговое меню, как у велосипеда: + /// багажник с ячейкой черепа и сумка духа). Окно necroCinerariumTrunk (Config/XUi_InGame) - панель с одной + /// ячейкой игры для обязательного предмета (XUiC_RequiredItemStack, как в панели модификаций коллектора + /// росы), рядом рюкзак (open_backpack_on_open). Ячейка принимает только череп и не больше одного + /// (override_stack_count="1" в XML). Череп - обычный предмет (ItemClass): в сейвах он уже есть, менять ему + /// класс на модификацию нельзя (см. память про сейвы), поэтому ячейка, а не сборка как у робомолота. + /// + public class XUiC_NecroCinerariumSkullGrid : XUiC_ItemStackGrid + { + public const string WindowGroup = "necroCinerariumTrunk"; + + /// Урна, чей багажник открыт (ставится перед открытием окна). + public static TEFeatureCinerarium Urn; + + public static void Open(EntityPlayerLocal _player, TEFeatureCinerarium _urn) + { + LocalPlayerUI ui = _player?.PlayerUI; + if (ui == null || _urn == null) + { + return; + } + Urn = _urn; + ui.windowManager.Open(WindowGroup, true); + } + + public override void OnOpen() + { + base.OnOpen(); + ItemValue skull = ItemClass.GetItem(TEFeatureCinerarium.SkullItemName); + for (int i = 0; i < itemControllers.Length; i++) + { + if (itemControllers[i] is XUiC_RequiredItemStack slot) + { + slot.SetAllowedItemClassSingle(TEFeatureCinerarium.SkullItemName); + slot.RequiredItemOnly = true; + slot.StackLocation = XUiC_ItemStack.StackLocationTypes.Workstation; + } + } + ItemStack[] stacks = new ItemStack[itemControllers.Length]; + for (int i = 0; i < stacks.Length; i++) + { + stacks[i] = ItemStack.Empty.Clone(); + } + if (stacks.Length > 0 && Urn != null && Urn.HasSkull && skull != null && skull.type > 0) + { + stacks[0] = new ItemStack(skull, 1); + } + SetStacks(stacks); + } + + public override void OnClose() + { + base.OnClose(); + Urn = null; + } + + public override void UpdateBackend(ItemStack[] stackList) + { + base.UpdateBackend(stackList); + bool has = stackList != null && stackList.Length > 0 && stackList[0] != null && !stackList[0].IsEmpty(); + Urn?.SetSkull(has); + } + + /// Урна убрана или выгружена, пока открыт её багажник, - закрыть окно (иначе череп в ячейке + /// уже ни к чему не привязан). + public static void CloseIfShowing(TEFeatureCinerarium _urn) + { + if (Urn == null || Urn != _urn) + { + return; + } + LocalPlayerUI ui = GameManager.Instance?.World?.GetPrimaryPlayer()?.PlayerUI; + if (ui != null && ui.windowManager.IsWindowOpen(WindowGroup)) + { + ui.windowManager.Close(WindowGroup); + } + Urn = null; + } + } +} diff --git a/NecromancerHarmony.dll b/NecromancerHarmony.dll index 26d8594..9ecb36c 100644 Binary files a/NecromancerHarmony.dll and b/NecromancerHarmony.dll differ diff --git a/NecromancerTEPersistence.dll b/NecromancerTEPersistence.dll index 3b015ca..a3f7139 100644 Binary files a/NecromancerTEPersistence.dll and b/NecromancerTEPersistence.dll differ diff --git a/TEPersistenceSrc/CinerariumWriteHelper.cs b/TEPersistenceSrc/CinerariumWriteHelper.cs new file mode 100644 index 0000000..3be79f1 --- /dev/null +++ b/TEPersistenceSrc/CinerariumWriteHelper.cs @@ -0,0 +1,18 @@ +namespace NecromancerTome +{ + /// + /// Запись состояния урны-цинерария (TEFeatureCinerarium, HarmonySrc/Cinerarium.cs). Здесь, а не в + /// основном проекте, по той же причине, что и PyramidWardWriteHelper: PooledBinaryWriter.Write + /// оттуда не компилируется (см. NecromancerTEPersistence.csproj). Первым байтом - версия, чтобы + /// поля урны (дух, его работа) можно было дописывать, не ломая старые сейвы. + /// + public static class CinerariumWriteHelper + { + public static void Write(PooledBinaryWriter _bw, byte _version, bool _hasSkull, bool _zoneShown) + { + _bw.Write(_version); + _bw.Write(_hasSkull); + _bw.Write(_zoneShown); + } + } +}