diff --git a/Config/Localization.csv b/Config/Localization.csv index 64d2e95..b606645 100644 --- a/Config/Localization.csv +++ b/Config/Localization.csv @@ -73,10 +73,16 @@ EntityLootContainerVictim,entityclasses,Entity Info,,,,Victim's Bag,,Beutel des noteDuke01,items,Quest - Note,,,,Note,,Notiz,Nota,Mot,Biglietto,書き置き,쪽지,List,Bilhete,Записка,Not,纸条,紙條 noteDuke01Desc,items,Quest - Note,,,,"You don't deserve death by my hand. You deserve death by a thousand hands. Every seven days, this world will tear you apart - and every time, it will hurt more, terrify more. You will build, only to lose it all. You will hope, only to despair. And when you tire, when you finally break, you will simply cease to be. You will become one of them. And even then, I won't remember your name. Let the dead make you dead. Let death bring you no peace. Let the living forget you forever. Goodbye, mistake.",,"Du verdienst keinen Tod durch meine Hand. Du verdienst den Tod durch tausend Hände. Alle sieben Tage wird diese Welt dich zerreißen – und jedes Mal wird es mehr wehtun, mehr Angst machen. Du wirst bauen, nur um alles zu verlieren. Du wirst hoffen, nur um zu verzweifeln. Und wenn du müde wirst, wenn du endlich zerbrichst, wirst du einfach aufhören zu sein. Du wirst einer von ihnen. Und selbst dann werde ich mich nicht an deinen Namen erinnern. Sollen die Toten dich tot machen. Soll der Tod dir keinen Frieden bringen. Sollen die Lebenden dich für immer vergessen. Leb wohl, Fehler.","No mereces morir a mi mano. Mereces morir a manos de mil. Cada siete días, este mundo te hará pedazos, y cada vez dolerá más y aterrará más. Construirás solo para perderlo todo. Tendrás esperanza solo para desesperar. Y cuando te canses, cuando por fin te quiebres, sencillamente dejarás de existir. Te convertirás en uno de ellos. Y ni siquiera entonces recordaré tu nombre. Que los muertos te hagan morir. Que la muerte no te traiga paz. Que los vivos te olviden para siempre. Adiós, error.","Tu ne mérites pas de mourir de ma main. Tu mérites de mourir de mille mains. Tous les sept jours, ce monde te mettra en pièces – et chaque fois, la douleur sera plus vive, la terreur plus grande. Tu bâtiras pour tout perdre. Tu espéreras pour désespérer. Et quand tu seras las, quand tu finiras par te briser, tu cesseras simplement d'être. Tu deviendras l'un d'eux. Et même alors, je ne me souviendrai pas de ton nom. Que les morts te rendent mort. Que la mort ne t'apporte aucune paix. Que les vivants t'oublient à jamais. Adieu, erreur.","Non meriti di morire per mano mia. Meriti di morire per mille mani. Ogni sette giorni questo mondo ti farà a pezzi, e ogni volta farà più male, spaventerà di più. Costruirai solo per perdere tutto. Spererai solo per disperare. E quando sarai stanco, quando alla fine ti spezzerai, semplicemente cesserai di essere. Diventerai uno di loro. E nemmeno allora ricorderò il tuo nome. Che i morti ti rendano morto. Che la morte non ti porti pace. Che i vivi ti dimentichino per sempre. Addio, errore.",お前は私の手にかかって死ぬに値しない。千の手にかかって死ぬに値する。七日ごとに、この世界はお前を引き裂く。そのたびに痛みは増し、恐怖は深まる。築いては、すべて失う。望んでは、絶望する。そして疲れ果て、ついに折れたとき、お前はただ在ることをやめる。お前は奴らの一人になる。そのときでさえ、私はお前の名を覚えていない。死者にお前を死者たらしめよ。死がお前に安らぎを与えぬように。生者が永久にお前を忘れるように。さらばだ、過ち。,"너는 내 손에 죽을 자격이 없다. 너는 천 개의 손에 죽을 자격이 있다. 이레마다 이 세계가 너를 찢어놓을 것이다. 그리고 그때마다 더 아프고, 더 두려울 것이다. 너는 세울 것이고, 모두 잃을 것이다. 너는 바랄 것이고, 절망할 것이다. 그러다 지치고, 마침내 부러지면, 너는 그저 존재하기를 그친다. 너는 그들 중 하나가 된다. 그때조차 나는 네 이름을 기억하지 못할 것이다. 죽은 자들이 너를 죽게 하리라. 죽음이 네게 안식을 주지 않기를. 산 자들이 너를 영원히 잊기를. 잘 가라, 실수여.","Nie zasługujesz na śmierć z mojej ręki. Zasługujesz na śmierć z tysiąca rąk. Co siedem dni ten świat będzie cię rozdzierał – i za każdym razem będzie bolało bardziej, przerażało bardziej. Będziesz budował, by wszystko stracić. Będziesz miał nadzieję, by rozpaczać. A gdy się zmęczysz, gdy w końcu pękniesz, po prostu przestaniesz być. Staniesz się jednym z nich. I nawet wtedy nie przypomnę sobie twojego imienia. Niech umarli uczynią cię umarłym. Niech śmierć nie przyniesie ci spokoju. Niech żywi zapomną o tobie na zawsze. Żegnaj, pomyłko.","Você não merece morrer pela minha mão. Merece morrer por mil mãos. A cada sete dias, este mundo vai te despedaçar - e a cada vez doerá mais, apavorará mais. Você vai construir, só para perder tudo. Vai esperar, só para se desesperar. E quando se cansar, quando finalmente se quebrar, simplesmente deixará de existir. Você se tornará um deles. E nem mesmo então eu lembrarei o seu nome. Que os mortos te façam morto. Que a morte não te traga paz. Que os vivos te esqueçam para sempre. Adeus, erro.","Ты не заслуживаешь смерти от моей руки. Ты заслуживаешь смерти от тысяч рук. Каждые семь дней этот мир будет рвать тебя на части, и каждый раз — всё больнее, всё страшнее. Ты будешь строить, чтобы терять. Ты будешь надеяться, чтобы отчаиваться. И когда ты устанешь, когда сломаешься — ты просто перестанешь быть. Ты превратишься в одного из них. И даже тогда — я не запомню твоё имя. Пусть мёртвые сделают тебя мёртвым. Пусть смерть не принесёт тебе покоя. Пусть живые навсегда забудут тебя. Прощай, ошибка.","Benim elimden ölmeyi hak etmiyorsun. Bin elin altında ölmeyi hak ediyorsun. Her yedi günde bir bu dünya seni paramparça edecek - ve her seferinde daha çok acıtacak, daha çok korkutacak. İnşa edeceksin, sonra hepsini kaybedeceksin. Umut edeceksin, sonra umutsuzluğa düşeceksin. Yorulduğunda, sonunda kırıldığında ise yalnızca var olmaktan çıkacaksın. Onlardan biri olacaksın. O zaman bile adını hatırlamayacağım. Bırak ölüler seni öldürsün. Bırak ölüm sana huzur getirmesin. Bırak yaşayanlar seni sonsuza dek unutsun. Elveda, hata.",你不配死在我手上。你配死在一千双手下。每隔七天,这个世界都会把你撕碎——而且一次比一次更痛,一次比一次更恐怖。你会建起一切,只为失去一切。你会心存希望,只为坠入绝望。等你倦了,等你终于崩溃,你便会干脆不再存在。你会成为它们中的一员。即便到那时,我也不会记得你的名字。让死者置你于死地。愿死亡不予你安宁。愿生者永远将你遗忘。永别了,错误。,你不配死在我手上。你配死在一千雙手下。每隔七天,這個世界都會把你撕碎——而且一次比一次更痛,一次比一次更恐怖。你會建起一切,只為失去一切。你會心存希望,只為墜入絕望。等你倦了,等你終於崩潰,你便會乾脆不再存在。你會成為牠們中的一員。即便到那時,我也不會記得你的名字。讓死者置你於死地。願死亡不予你安寧。願生者永遠將你遺忘。永別了,錯誤。 braceletSpatialVault,items,Item,,,,Spatial Bracelet,,Raumarmband,Brazalete espacial,Bracelet spatial,Bracciale spaziale,空間の腕輪,공간의 팔찌,Bransoleta przestrzenna,Bracelete espacial,Пространственный браслет,Uzam Bileziği,空间手环,空間手環 -braceletSpatialVaultDesc,items,Item,,,,A necromantic bracelet. Power attack to tear open a personal storage rift - its size grows with your Necromancy skill. The regular attack does nothing for now.,,Ein nekromantisches Armband. Mit dem Powerangriff reißt du einen persönlichen Lagerriss auf – seine Größe wächst mit deiner Nekromantie-Fertigkeit. Der normale Angriff tut vorerst nichts.,Un brazalete nigromántico. Usa el ataque potente para abrir una grieta de almacenamiento personal: su tamaño crece con tu habilidad de Nigromancia. El ataque normal no hace nada por ahora.,Un bracelet nécromantique. L'attaque puissante déchire une faille de stockage personnelle – sa taille croît avec ta compétence de Nécromancie. L'attaque normale ne fait rien pour l'instant.,Un bracciale negromantico. Con l'attacco potente squarci una fessura di stoccaggio personale: la sua capienza cresce con la tua abilità di Negromanzia. L'attacco normale per ora non fa nulla.,降霊術の腕輪。強攻撃で個人用の収納の裂け目を切り開く。その大きさは降霊術スキルとともに広がる。通常攻撃は今のところ何も起こさない。,강령술 팔찌. 강공격으로 개인 보관용 균열을 찢어 엽니다. 그 크기는 강령술 기술에 따라 커집니다. 일반 공격은 아직 아무 일도 하지 않습니다.,Nekromancka bransoleta. Atakiem specjalnym rozdzierasz osobistą szczelinę na zapasy – jej rozmiar rośnie wraz z umiejętnością Nekromancji. Zwykły atak na razie nic nie robi.,Um bracelete necromântico. Use o ataque forte para rasgar uma fenda de armazenamento pessoal: seu tamanho cresce com a habilidade de Necromancia. O ataque comum não faz nada por enquanto.,Некромантский браслет. Силовая атака открывает личный разлом-хранилище - его размер растёт вместе с навыком Некромантии. Обычная атака пока ничего не делает.,Nekromansi bileziği. Güçlü saldırıyla kişisel bir depo yarığı açarsın; boyutu Nekromansi yeteneğinle birlikte büyür. Normal saldırı şimdilik hiçbir şey yapmaz.,一只亡灵法术手环。使用强力攻击可撕开一道私人储物裂隙——其容量随亡灵法术技能增长。普通攻击目前没有任何作用。,一只亡靈法術手環。使用強力攻擊可撕開一道私人儲物裂隙——其容量隨亡靈法術技能增長。普通攻擊目前沒有任何作用。 +braceletSpatialVaultDesc,items,Item,,,,"A necromantic bracelet. Power attack to tear open a personal storage rift - its size grows with your Necromancy skill. Hold the regular attack on a block and it is pulled into the rift: ten seconds up close, one more for every block of distance. The power attack interrupts that and opens the rift instead.",,Ein nekromantisches Armband. Ein Machtangriff reißt einen persönlichen Lagerriss auf - seine Größe wächst mit deiner Nekromantie-Fertigkeit. Halte den normalen Angriff auf einem Block und er wird in den Riss gezogen: zehn Sekunden aus der Nähe und eine weitere pro Block Entfernung. Der Machtangriff bricht das ab und öffnet stattdessen den Riss.,Un brazalete nigromántico. El ataque potente abre una grieta de almacenamiento personal; su tamaño crece con tu habilidad de Nigromancia. Mantén el ataque normal sobre un bloque y este será atraído a la grieta: diez segundos de cerca y uno más por cada bloque de distancia. El ataque potente lo interrumpe y abre la grieta.,Un bracelet nécromantique. L'attaque puissante ouvre une faille de stockage personnelle - sa taille augmente avec votre compétence de Nécromancie. Maintenez l'attaque normale sur un bloc et il est aspiré dans la faille : dix secondes de près et une de plus par bloc de distance. L'attaque puissante interrompt cela et ouvre la faille.,Un bracciale negromantico. L'attacco potente apre una faglia di stoccaggio personale - la sua dimensione cresce con l'abilità Negromanzia. Tieni premuto l'attacco normale su un blocco e verrà attirato nella faglia: dieci secondi da vicino e uno in più per ogni blocco di distanza. L'attacco potente lo interrompe e apre la faglia.,ネクロマンサーの腕輪。パワーアタックで個人用の収納亀裂を開きます。容量はネクロマンシーのスキルと共に増えます。ブロックに照準して通常攻撃を押し続けると亀裂に引き込まれます。密着で十秒、距離が1ブロック増えるごとに1秒追加されます。パワーアタックで中断し、亀裂を開きます。,강령술사의 팔지. 강타로 개인 보관 균열을 열며 크기는 강령술 스킬에 따라 커집니다. 블록을 조준한 채 일반 공격을 누르고 있으면 그 블록이 균열로 빨려들어갑니다. 밀착 시 10초이며 거리 1블록마다 1초씩 늘어납니다. 강타는 이를 중단하고 균열을 엽니다.,Nekromancka bransoleta. Silny atak rozrywa osobistą szczelinę magazynową - jej rozmiar rośnie wraz z umiejętnością Nekromancji. Przytrzymaj zwykły atak na bloku a zostanie on wciągnięty do szczeliny: dziesięć sekund z bliska i jedna więcej za każdy blok odległości. Silny atak przerywa to i otwiera szczelinę.,Uma bracelete necromântica. O ataque forte abre uma fenda de armazenamento pessoal - seu tamanho cresce com a sua habilidade de Necromancia. Segure o ataque normal sobre um bloco e ele será puxado para a fenda: dez segundos de perto e mais um por bloco de distância. O ataque forte interrompe isso e abre a fenda.,Некромантский браслет. Силовая атака вскрывает личный разлом-хранилище - его размер растёт вместе с навыком Некромантии. Зажмите обычную атаку на блоке - и он втянется в разлом: десять секунд вплотную и ещё по секунде за каждый блок расстояния. Силовая атака прерывает это и открывает хранилище.,Nekromantik bir bileklik. Güçlü saldırı kişisel bir depo yarığı açar; boyutu Nekromantik becerinle birlikte büyür. Normal saldırıyı bir bloka basılı tut ve blok yarığa çekilsin: yakından on saniye ve her blokluk mesafe için bir saniye daha. Güçlü saldırı bunu keser ve yarığı açar.,亡灵法师的手镯。重击可撕开个人储物裂隙,其容量随亡灵法术技能增长。对准方块按住普通攻击,方块便会被拉入裂隙:贴近时为十秒,每远一格再加一秒。重击可中断并打开裂隙。,亡靈法師的手鐠。重擊可撕開個人儲物裂隙,其容量隨亡靈法術技能增長。對準方塊按住普通攻擊,方塊便會被拉入裂隙:貼近時為十秒,每遠一格再加一秒。重擊可中斷並打開裂隙。 braceletSpatialVaultWindowTitle,items,Item,,,,Spatial Storage,,Raumlager,Almacén espacial,Stockage spatial,Deposito spaziale,空間収納,공간 보관함,Schowek przestrzenny,Armazenamento espacial,Пространственное хранилище,Uzam Deposu,空间储物,空間儲物 braceletSpatialVaultTooWeak,items,Item,,,,Your Necromancy skill isn't strong enough yet to open a storage rift,,"Deine Nekromantie-Fertigkeit reicht noch nicht, um einen Lagerriss zu öffnen",Tu habilidad de Nigromancia aún no es lo bastante fuerte para abrir una grieta de almacenamiento,Ta compétence de Nécromancie n'est pas encore assez forte pour ouvrir une faille de stockage,La tua abilità di Negromanzia non è ancora abbastanza forte per aprire una fessura di stoccaggio,降霊術スキルがまだ足りず、収納の裂け目を開けません,강령술 기술이 아직 부족해 보관 균열을 열 수 없습니다,"Twoja Nekromancja jest jeszcze za słaba, by otworzyć szczelinę na zapasy",Sua habilidade de Necromancia ainda não é forte o bastante para abrir uma fenda de armazenamento,"Ваш навык Некромантии пока недостаточен, чтобы открыть разлом-хранилище",Nekromansi yeteneğin bir depo yarığı açacak kadar güçlü değil,你的亡灵法术技能尚不足以开启储物裂隙,你的亡靈法術技能尚不足以開啟儲物裂隙 braceletSpatialVaultNoTarget,items,Item,,,,There's no zombie in your sight,,Kein Zombie in Sicht,No hay ningún zombi a la vista,Aucun zombie en vue,Non c'è nessuno zombie nel mirino,視界にゾンビがいません,시야에 좀비가 없습니다,W polu widzenia nie ma zombie,Não há nenhum zumbi à vista,В поле зрения нет зомби,Görüş alanında zombi yok,视野中没有僵尸,視野中沒有殭屍 +braceletSpatialVaultNoBlock,items,Item,,,,There's no block in your sight,,Kein Block in Sicht,No hay ningún bloque a la vista,Aucun bloc en vue,Non c'è nessun blocco nel mirino,視界にブロックがありません,시야에 블록이 없습니다,W polu widzenia nie ma bloku,Não há nenhum bloco à vista,В поле зрения нет блока,Görüş alanında blok yok,视野中没有方块,視野中沒有方塊 +braceletSpatialVaultNoItemForm,items,Item,,,,This block can't be taken into the rift,,Dieser Block kann nicht in den Riss aufgenommen werden,Este bloque no puede guardarse en la grieta,Ce bloc ne peut pas être rangé dans la faille,Questo blocco non può essere riposto nella faglia,このブロックは亀裂に収納できません,이 블록은 균열에 보관할 수 없습니다,Tego bloku nie można schować w szczelinie,Este bloco não pode ser guardado na fenda,Этот блок нельзя убрать в разлом,Bu blok yarığa konulamaz,该方块无法存入裂隙,該方塊無法存入裂隙 +braceletSpatialVaultFull,items,Item,,,,The storage rift is full,,Der Lagerriss ist voll,La grieta de almacenamiento está llena,La faille de stockage est pleine,La faglia di stoccaggio è piena,収納の亀裂が満杯です,보관 균열이 가득 찼습니다,Szczelina magazynowa jest pełna,A fenda de armazenamento está cheia,В пространственном хранилище нет места,Depo yarığı dolu,空间储物已满,空間儲物已滿 +braceletSpatialVaultPickupChanneling,items,Item,,,,Pulling the block into the rift,,Block wird in den Riss gezogen,Atrayendo el bloque a la grieta,Attraction du bloc dans la faille,Il blocco viene attirato nella faglia,ブロックを亀裂に引き込んでいます,블록을 균열로 끌어들이는 중,Wciąganie bloku do szczeliny,Puxando o bloco para a fenda,Блок втягивается в разлом,Blok yarığa çekiliyor,正在将方块拉入裂隙,正在將方塊拉入裂隙 +braceletSpatialVaultTraderArea,items,Item,,,,You can't take blocks on a trader's ground,,Auf dem Gelände eines Händlers kannst du keine Blöcke aufnehmen,No puedes llevarte bloques en el recinto de un comerciante,Impossible de prendre des blocs sur le terrain d'un marchand,Non puoi prendere blocchi nel recinto di un mercante,商人の敷地内ではブロックを回収できません,상인의 부지에서는 블록을 가져갈 수 없습니다,Na terenie handlarza nie możesz zabierać bloków,Você não pode levar blocos no terreno de um comerciante,На территории торговца блоки забирать нельзя,Tüccarın arazisinde blok alamazsın,无法在商人的地盘上收取方块,無法在商人的地盤上收取方塊 +braceletSpatialVaultIndestructible,items,Item,,,,This block can't be destroyed or taken,,Dieser Block kann weder zerstört noch aufgenommen werden,Este bloque no puede destruirse ni llevarse,Ce bloc ne peut être ni détruit ni emporté,Questo blocco non può essere distrutto né preso,このブロックは破壊も回収もできません,이 블록은 파괴하거나 가져갈 수 없습니다,Tego bloku nie można zniszczyć ani zabrać,Este bloco não pode ser destruído nem levado,Этот блок нельзя разрушить или забрать,Bu blok ne yıkılabilir ne de alınabilir,该方块无法被破坏或收取,該方塊無法被破壞或收取 necroHeresyPyramid,blocks,Block,,,,Pyramid of Spirits,,Pyramide der Geister,Pirámide de los espíritus,Pyramide des esprits,Piramide degli spiriti,精霊のピラミッド,영혼의 피라미드,Piramida duchów,Pirâmide dos espíritos,Пирамида духов,Ruhlar Piramidi,灵魂金字塔,靈魂金字塔 necroHeresyPyramidDesc,blocks,Block,,,,"A necromantic ward shaped like a pyramid, wreathed in cold purple flame. While you stand within its radius, it charms any un-deviated zombie nearby (see the Deviator) and sets it alight, turning it against other zombies instead of you or your base. Its menu lets you toggle the effect and show or hide the boundary of its radius.",,"Ein nekromantischer Bannstein in Pyramidenform, umkränzt von kaltem violettem Feuer. Solange du in seinem Radius stehst, bezaubert er jeden nicht abgewichenen Zombie in der Nähe (siehe Abweichung) und setzt ihn in Brand, sodass er sich gegen andere Zombies wendet statt gegen dich oder deine Basis. Über sein Menü lässt sich der Effekt umschalten und die Grenze des Radius ein- oder ausblenden.","Un guardián nigromántico con forma de pirámide, envuelto en llamas moradas y frías. Mientras te encuentres dentro de su radio, hechiza a todo zombi cercano que no haya sido desviado (véase la desviación) y lo prende fuego, volviéndolo contra otros zombis en vez de contra ti o tu base. Su menú permite activar o desactivar el efecto y mostrar u ocultar el límite del radio.","Une protection nécromantique en forme de pyramide, ceinte de flammes violettes et froides. Tant que tu te tiens dans son rayon, elle charme tout zombie proche non dévié (voir la déviation) et l'embrase, le retournant contre les autres zombies plutôt que contre toi ou ta base. Son menu permet d'activer ou de couper l'effet et d'afficher ou masquer la limite du rayon.","Una protezione negromantica a forma di piramide, avvolta da una fredda fiamma viola. Finché resti nel suo raggio, incanta ogni zombie vicino non deviato (vedi la deviazione) e lo incendia, rivoltandolo contro gli altri zombie invece che contro te o la tua base. Dal suo menu si può accendere o spegnere l'effetto e mostrare o nascondere il confine del raggio.",冷たい紫の炎をまとった、ピラミッド型の降霊術の守り。あなたがその半径内に立っているあいだ、近くの未逸脱のゾンビを魅了し(逸脱を参照)、燃え上がらせて、あなたや拠点ではなく他のゾンビへ差し向ける。メニューから効果の入切と、半径の境界の表示・非表示を切り替えられる。,"차가운 보랏빛 불꽃에 휩싸인 피라미드 형태의 강령술 수호물. 당신이 그 반경 안에 있는 동안, 근처의 아직 일탈하지 않은 좀비를 매혹하고(일탈 참조) 불붙여, 당신이나 당신의 거점 대신 다른 좀비를 공격하게 만듭니다. 메뉴에서 효과를 켜고 끄거나 반경 경계를 표시/숨길 수 있습니다.","Nekromancki strażnik w kształcie piramidy, spowity zimnym fioletowym płomieniem. Dopóki stoisz w jego promieniu, oczarowuje każde pobliskie zombie, które nie uległo dewiacji (patrz: dewiacja), i podpala je, obracając przeciwko innym zombie zamiast przeciw tobie czy twojej bazie. Jego menu pozwala włączyć lub wyłączyć efekt oraz pokazać lub ukryć granicę promienia.","Uma proteção necromântica em forma de pirâmide, envolta em chamas roxas e frias. Enquanto você estiver dentro do seu raio, ela encanta qualquer zumbi próximo que ainda não tenha sido desviado (veja o desvio) e o incendeia, voltando-o contra outros zumbis em vez de contra você ou sua base. O menu permite ligar ou desligar o efeito e mostrar ou ocultar o limite do raio.","Некромантический оберег в форме пирамиды, окутанный холодным фиолетовым пламенем. Пока вы находитесь в радиусе его действия, он накладывает девиацию на не заряженных зомби поблизости (см. Девиатор) и поджигает их, обращая против других зомби, а не против вас или вашей базы. Меню блока позволяет включать эффект и показывать границу радиуса действия.","Soğuk mor alevlerle çevrili, piramit biçiminde bir nekromansi koruması. Yarıçapı içinde durduğun sürece, yakındaki henüz sapmamış her zombiyi büyüler (bkz. sapma) ve tutuşturur; böylece zombi sana ya da üssüne değil, diğer zombilere saldırır. Menüsünden etkiyi açıp kapatabilir, yarıçapın sınırını gösterip gizleyebilirsin.",一座亡灵法术守护物,形如金字塔,缭绕着冰冷的紫焰。只要你站在其半径之内,它便会魅惑附近尚未悖逆的僵尸(参见悖逆),并将其点燃,使其转而攻击其他僵尸,而非你或你的基地。其菜单可开关此效果,并显示或隐藏半径边界。,一座亡靈法術守護物,形如金字塔,繚繞著冰冷的紫焰。只要你站在其半徑之內,牠便會魅惑附近尚未悖逆的殭屍(參見悖逆),並將其點燃,使其轉而攻擊其他殭屍,而非你或你的基地。其選單可開關此效果,並顯示或隱藏半徑邊界。 blockcommand_TEFeaturePyramidWard:effect_on,blocks,Block,,,,Enable Effect,,Effekt aktivieren,Activar efecto,Activer l'effet,Attiva effetto,効果をオン,효과 켜기,Włącz efekt,Ativar efeito,Включить эффект,Etkiyi Aç,启用效果,啟用效果 diff --git a/HarmonySrc/ChannelVision.cs b/HarmonySrc/ChannelVision.cs new file mode 100644 index 0000000..2a232cb --- /dev/null +++ b/HarmonySrc/ChannelVision.cs @@ -0,0 +1,84 @@ +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// Drains the colour out of the world while the necromancer is channelling something, and + /// lets it back in when he stops (user request 2026-09-14: "эффект, когда мир становится + /// тёмным и чёрнобелым... повесить его на момент ожидания применения порталов и на момент + /// ожидания утаскивания блока", with "желательно плавно... секунды за 3" in both directions). + /// Shared by both channels so the look, the timing and the name of the effect live in one + /// place rather than drifting apart in two files. + /// + /// IT IS THE GAME'S OWN POST-PROCESS, not a reimplementation. EntityPlayerLocal carries a + /// ScreenEffects component - ScreenEffectManager - whose SetScreenEffect(name, intensity, + /// fadeTime) is what the engine itself calls for dying ("Dying"), for spawning in + /// ("VibrantDeSat") and for every buff in the game that tints the screen. THE FADE IS THAT + /// THIRD ARGUMENT: three seconds in and three seconds out cost nothing to implement, because + /// the ramp is the effect system's own. + /// + /// THE EFFECT IS "Greyscale", AND THE CHOICE IS ABOUT WHO ELSE TOUCHES IT. These effects are + /// a flat namespace of materials loaded from Resources/ScreenEffects - anyone writing to a + /// name overwrites whatever was there, so picking one is mostly picking a fight to avoid: + /// + /// - "Greyscale" is written by exactly two things in the whole game, twitch_buffMonochrome + /// and sandbox_blackandwhite - a Twitch-integration reward and a game-mode toggle. Neither + /// happens in an ordinary session, so the channel owns it in practice. + /// - "Dying" and "Dead" are the death visuals the user was describing, and they are exactly + /// the ones NOT to borrow: EntityPlayerLocal.Update writes "Dying" from the player's own + /// health every time it changes, so any damage mid-channel would take the effect over - + /// and being hit mid-channel is a thing that happens. + /// - "Dark" would have supplied the darkening half. It belongs to buffCrouching, which + /// fires on every crouch with a 0.2s fade and would stamp on this one. + /// + /// SO THE DARKENING HALF IS DELIBERATELY NOT DONE. Both effects that dim the screen are owned + /// by something that fights for them - crouching, and dying - and losing that fight looks like + /// a bug in this mod rather than in the effect system. Greyscale alone reads as the world + /// going wrong, which is what was actually asked for; if it wants to be darker too, the list + /// below takes a second entry and nothing else changes. + /// + /// NOTHING HERE TOUCHES INPUT. The effect is a camera post-process and outlives the timer + /// window on purpose: the three-second fade back keeps running while the player walks away, + /// which is the point of asking for a fade rather than a switch. + /// + public static class ChannelVision + { + /// Seconds to fade in, and to fade back out. + public const float FadeSeconds = 3f; + + /// What to fade, and how far. A list rather than a single name so a second layer + /// is one entry and not a rewrite - see the class comment on the darkening half. + public static readonly string[] EffectNames = { "Greyscale" }; + + /// Full strength per effect, in the same order as EffectNames. + public static readonly float[] EffectIntensities = { 1f }; + + /// Colour drains out over FadeSeconds. + public static void Begin(EntityPlayerLocal _player) + { + Apply(_player, _fullStrength: true); + } + + /// Colour comes back over FadeSeconds. Safe to call when nothing is running - + /// fading an effect that is already at zero to zero does nothing - which is what lets + /// every exit path call it without first working out whether it is the one that has to. + /// + public static void End(EntityPlayerLocal _player) + { + Apply(_player, _fullStrength: false); + } + + public static void Apply(EntityPlayerLocal _player, bool _fullStrength) + { + if (_player == null || _player.ScreenEffectManager == null) + { + return; + } + for (int i = 0; i < EffectNames.Length; i++) + { + float intensity = _fullStrength ? EffectIntensities[i] : 0f; + _player.ScreenEffectManager.SetScreenEffect(EffectNames[i], intensity, FadeSeconds); + } + } + } +} diff --git a/HarmonySrc/PortalStonePatch.cs b/HarmonySrc/PortalStonePatch.cs index 5f74cf6..590a5ae 100644 --- a/HarmonySrc/PortalStonePatch.cs +++ b/HarmonySrc/PortalStonePatch.cs @@ -114,6 +114,10 @@ namespace NecromancerTome // buff-trigger vocabulary, since there's no "for as long as this XUiC_Timer is open" // trigger to hang it off - this IS that lifecycle. player.Buffs.AddBuff(ChannelBuffName); + // The world drains to black and white for the length of the channel - see + // ChannelVision.cs. Started here rather than from the buff so both of this mod's + // channels share one definition of what channelling looks like. + ChannelVision.Begin(player); TimerEventData timerData = new TimerEventData { @@ -136,6 +140,7 @@ namespace NecromancerTome { Debug.Log("[NecromancerTome] PortalStonePatch: channel cancelled for " + itemName + ", owner=" + player.entityId); player.Buffs.RemoveBuff(ChannelBuffName); + ChannelVision.End(player); }; string labelKey = (itemName == BlueStoneName) ? "thrownStonePortalBlueChanneling" : "thrownStonePortalBlackChanneling"; @@ -151,6 +156,9 @@ namespace NecromancerTome { Debug.Log("[NecromancerTome] PortalStonePatch: channel completed for " + itemName + ", owner=" + player.entityId); player.Buffs.RemoveBuff(ChannelBuffName); + // Before the teleport rather than after: the colour is already on its way back while + // the player arrives, instead of starting to return only once he is standing there. + ChannelVision.End(player); if (itemName == BlackStoneName) { ShowBlackPortalConfirmation(player); diff --git a/HarmonySrc/SpatialVaultPatch.cs b/HarmonySrc/SpatialVaultPatch.cs index 0f73a28..4d1d166 100644 --- a/HarmonySrc/SpatialVaultPatch.cs +++ b/HarmonySrc/SpatialVaultPatch.cs @@ -90,13 +90,23 @@ namespace NecromancerTome if (_actionData.indexInEntityOfAction == 1) { - OpenVault(player); + // Not when this very press just cancelled a block pickup and opened the vault on + // the way - see SpatialVaultPickup.ConsumeCancelOpen. Down and up are one press. + if (!SpatialVaultPickup.ConsumeCancelOpen()) + { + OpenVault(player); + } + } + else + { + // The regular attack takes the block under the crosshair into the vault from + // 2026-09-14 - see SpatialVaultPickupPatch.cs. Before that it did nothing at all, + // by direct user request of 2026-08-30 ("пусть тогда обычная атака у + // пространственного браслета не делает ничего"), after the knockback+slow version did not + // visibly do anything in testing. ShoveZombieAtCrosshair is kept below, unused, + // because that abandoned version was never shown to be WRONG - only invisible. + SpatialVaultPickup.Begin(player); } - // else: regular attack (index 0) deliberately does nothing, per direct user request - // 2026-08-30 ("пусть тогда обычная атака у пространственного браслета не делает - // ничего") after the knockback+slow version didn't visibly do anything in testing - - // rather than debug ShoveZombieAtCrosshair blind (kept below, unused, in case this - // gets revisited), just absorb the click silently. // Skip ItemActionEat's own logic entirely - the click has been fully handled here. return false; diff --git a/HarmonySrc/SpatialVaultPickupPatch.cs b/HarmonySrc/SpatialVaultPickupPatch.cs new file mode 100644 index 0000000..0c7e25b --- /dev/null +++ b/HarmonySrc/SpatialVaultPickupPatch.cs @@ -0,0 +1,508 @@ +using HarmonyLib; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// Holding the Spatial Bracelet's REGULAR attack on a block takes that block into the vault + /// after a ten-second timer - the same circular indicator a workbench shows when you take it + /// (user request 2026-09-14: "при зажатии обычной атаки игрок видел индикатор как при + /// демонтаже верстака... блок должен исчезнуть и появиться в пространственном хранилище"). + /// Entry point is SpatialVaultPatch's existing Prefix, index 0, which until now deliberately + /// swallowed that click and did nothing. + /// + /// THE WHOLE RECIPE IS VANILLA'S, not an imitation of it. Block.TakeItemWithTimer and its + /// TakeItemWithTimerDone are short enough to read in one sitting, and they are the workbench + /// pickup; what follows is the same sequence with two substitutions - ten seconds instead of + /// the block's own TakeDelay, and the vault's Bag instead of the player's backpack. Even the + /// refusal messages are vanilla's own keys, which means they are already translated into every + /// language the game ships, and a player who has ever taken a workbench has already been + /// taught what they mean. + /// + /// A DAMAGED BLOCK IS REFUSED BEFORE THE TIMER EVER OPENS. That is vanilla's first line: + /// + /// if (_blockValue.damage > 0) + /// GameManager.ShowTooltip(_player, Localization.Get("ttRepairBeforePickup"), "", "ui_denied"); + /// else if (canTake(...)) + /// XUiC_Timer.OpenTimer(...); + /// + /// - and it is exactly what the user asked for: a message, and no indicator at all. + /// + /// EVERY GUARD IS CHECKED TWICE, ONCE TO OPEN THE TIMER AND ONCE TO FINISH IT, because ten + /// seconds is a long time in this game. Vanilla does the same for its own two seconds: the + /// block can be shot, mined, replaced, or opened by someone else while the circle fills, and + /// each of those has its own message rather than a silent failure or, worse, a block quietly + /// deleted from the world with nothing to show for it. + /// + /// THE TARGET IS ANY BLOCK UNDER THE CROSSHAIR (user's choice of 2026-09-14, over the + /// narrower "only what vanilla already lets you take"). That is a wider promise than vanilla + /// ever makes, and two things follow from it that the narrow version would never have had to + /// face: + /// + /// - MULTIBLOCKS. A door or a bed occupies several cells, and the crosshair usually lands on + /// a child rather than on the parent. Setting that one cell to air would leave the other + /// half standing as debris. The child is resolved to its parent first, with the engine's + /// own idiom - `isMultiBlock && ischild -> multiBlockPos.GetParentPos(...)` - which is + /// what Block's own methods do a dozen times over, and the parent is what gets removed. + /// - BLOCKS WITH NO ITEM FORM. Not everything placed in the world converts to something a + /// player can hold; ToItemValue comes back empty for those. They are refused up front, + /// because the alternative is deleting a block and handing back nothing. + /// + /// THE CHANNEL GETS LONGER WITH REACH - ten seconds against the block, one more per full + /// block of distance. The measurement is not computed from the player's position and the + /// block's position, which would mean picking a point in the player (feet? eyes?) and a point + /// in the block (centre? face?) and being wrong about one of them: the engine already fills in + /// HitInfoDetails.distanceSq for the very ray that chose this block, so the number used is the + /// length of that ray. It is also the honest one - it measures to the surface being looked at, + /// which is what "вплотную" means to a player standing against a wall. + /// + /// FLOOR, NOT ROUND, and that is what makes the two anchors in the request both come out + /// right: flush against a block the ray is well under a metre, floors to zero, and the channel + /// is the plain ten seconds; a block five away floors to five and costs fifteen. + /// + /// THE POWER ATTACK CANCELS THE CHANNEL AND OPENS THE VAULT (user request 2026-09-14, after + /// the feature was confirmed working: "можно случайно нажать и не иметь возможности прервать"). + /// Ten seconds of standing still after a misclick is a long time, and the vanilla escapes are + /// both poor here: getting hit is not something the player chooses, and the activate key is + /// not the button a hand is already on. The bracelet's other button is - and it lands on the + /// thing the player most likely wanted in the first place. + /// + /// WHAT A PICKAXE CANNOT BREAK, THE BRACELET CANNOT TAKE (user report 2026-09-14: it would + /// happily take a trader's compound apart, and bedrock with it). TWO SEPARATE ENGINE RULES + /// stand behind that one sentence, and they are worth keeping apart because they look + /// identical from inside the game and are nothing alike in the code: + /// + /// - A TRADER'S GROUND. The blocks there are ordinary; it is the AREA that is protected. + /// Vanilla simply skips DamageBlock inside it, which is why a pickaxe does nothing while + /// this bracelet - asking about the block rather than about the place - saw nothing wrong. + /// The test is the same predicate that suppression uses, with its condition copied whole: + /// + /// World.SandboxUseTraderArea != TraderAreaStates.Default || !world.IsWithinTraderArea(pos) + /// + /// The sandbox half is not padding. Trader protection is a server setting, and a server + /// that turned it off should not find this mod enforcing it anyway: where vanilla + /// protects, so does the bracelet; where it does not, neither does this. + /// - INDESTRUCTIBLE MATERIAL. The world's floor is the opposite case - nothing special about + /// the place, everything special about the block. Bedrock's material carries + /// CanDestroy=false (Data/Config/materials.xml, Mbedrock), and the engine reads exactly + /// `blockValue.Block.blockMaterial.CanDestroy` wherever it must not break something. Asked + /// as a material question rather than by block name, so it covers whatever else in this + /// game - or in another mod - is declared unbreakable. + /// + /// Both say so out loud, where vanilla stays silent. Vanilla can afford silence because a + /// pickaxe that does nothing is its own explanation - the block visibly refuses to break. An + /// indicator that simply never appears looks like this mod is broken instead, so these + /// refusals get a message like every other one in this file. + /// + /// CONTENTS CANNOT TRAVEL, AND THAT IS NOT A SHORTCUT. "In the state the original block was + /// in" holds for the block's identity and its integrity, but an ItemStack in this game has + /// nowhere to put another container's inventory - ToItemValue maps a block to an item and + /// stops there. Vanilla solves this by refusing: a workstation with anything in it cannot be + /// taken, and says so through ttWorkstationNotEmpty. The same refusal is used here, extended + /// to composite storage (chests) through ITileEntityLootable, which is how this version of the + /// game models a container's contents. + /// + public static class SpatialVaultPickup + { + /// The floor: what it costs to take a block you are standing against. Vanilla's + /// workbench is two; the Blue Portal Stone's channel in this mod is also ten, and this + /// reads as the same kind of deliberate act. + public const float BaseChannelSeconds = 10f; + + /// Added per full block of reach (user request 2026-09-14: "вплотную 10 сек, + /// если объект от персонажа в пяти блоках то 15 сек"). Distance is a cost, so pulling + /// something out of a wall across the room is a commitment rather than a trick. + public const float SecondsPerBlock = 1f; + + /// Vanilla's own refusal messages, already translated into every shipped + /// language. Reused rather than re-worded: a player who has taken a workbench has already + /// learned what these mean, and a second vocabulary for the same refusal would be worse + /// than no message. + public const string MsgRepairFirst = "ttRepairBeforePickup"; + public const string MsgBlockMissing = "ttBlockMissingPickup"; + public const string MsgInUse = "ttCantPickupInUse"; + public const string MsgNotEmpty = "ttWorkstationNotEmpty"; + + /// This mod's own, added with this feature - see Config/Localization.csv. + public const string MsgNoBlock = "braceletSpatialVaultNoBlock"; + public const string MsgNoItemForm = "braceletSpatialVaultNoItemForm"; + public const string MsgVaultFull = "braceletSpatialVaultFull"; + public const string MsgChanneling = "braceletSpatialVaultPickupChanneling"; + public const string MsgTraderArea = "braceletSpatialVaultTraderArea"; + public const string MsgIndestructible = "braceletSpatialVaultIndestructible"; + + /// The denial sound vanilla plays with these tooltips. + public const string DeniedSound = "ui_denied"; + + /// Unscaled time at which a cancel last opened the vault, or -1. Exists to stop + /// ONE press from opening the vault TWICE: the cancel reacts to the button going down, + /// while the bracelet's ordinary power attack reacts to it coming back up, and those are + /// the same press. Whether the release even reaches the item action through the modal + /// window is unknown - it is exactly the input suppression that forced the raw mouse read + /// below - so this guards the case rather than assuming either answer. + public static float CancelOpenedVaultAt = -1f; + + /// How long after a cancel a power-attack release is treated as the tail of that + /// same press. Long enough to cover a slow finger, far short of a deliberate second + /// click. + public const float CancelSwallowSeconds = 0.5f; + + /// True once, if the vault was just opened by cancelling a channel. Consuming it + /// rather than only reading it means a genuine second press right afterwards still + /// works. + public static bool ConsumeCancelOpen() + { + if (CancelOpenedVaultAt < 0f || Time.unscaledTime - CancelOpenedVaultAt > CancelSwallowSeconds) + { + return false; + } + CancelOpenedVaultAt = -1f; + return true; + } + + /// What the timer is working on, handed through TimerEventData.Data - the same + /// use vanilla makes of that field (it packs a BlockValue, a position and the player into + /// an object[] there). A small class instead of an array because this one is read back in + /// a method that has to be right about which field is which. + public class PickupJob + { + public EntityPlayerLocal Player; + public Vector3i Position; + public BlockValue Expected; + } + + /// Regular attack on the bracelet. Every refusal happens here, before the player + /// is asked to stand still for ten seconds. + public static void Begin(EntityPlayerLocal _player) + { + World world = GameManager.Instance != null ? GameManager.Instance.World : null; + if (world == null) + { + return; + } + + WorldRayHitInfo hitInfo = _player.HitInfo; + if (hitInfo == null || !hitInfo.bHitValid) + { + Deny(_player, MsgNoBlock); + return; + } + + Vector3i position = hitInfo.hit.blockPos; + BlockValue blockValue = world.GetBlock(position); + if (blockValue.isair || blockValue.Block == null) + { + Deny(_player, MsgNoBlock); + return; + } + + // A door or a bed is several cells and the crosshair lands on whichever one is + // nearest; removing that cell alone would leave the rest of the model standing. + if (blockValue.Block.isMultiBlock && blockValue.ischild) + { + position = blockValue.Block.multiBlockPos.GetParentPos(position, blockValue); + blockValue = world.GetBlock(position); + if (blockValue.isair || blockValue.Block == null) + { + Deny(_player, MsgNoBlock); + return; + } + } + + // Before anything else about the block is considered: whether it may be touched at + // all outranks what state it happens to be in. + if (!CanTakeHere(world, position, blockValue, _player)) + { + return; + } + + // Vanilla's first line, and the user's explicit requirement: a damaged block gets the + // message and no indicator whatsoever. + if (blockValue.damage > 0) + { + Deny(_player, MsgRepairFirst); + return; + } + + ItemValue itemValue = blockValue.ToItemValue(); + if (itemValue == null || itemValue.IsEmpty()) + { + Deny(_player, MsgNoItemForm); + return; + } + + if (!CanTakeTileEntity(world, position, _player)) + { + return; + } + + // Asked before the timer rather than after it, because ten seconds spent to be told + // the vault was full the whole time is the worst version of this feature. + Bag bag = GetVault(_player); + if (bag == null) + { + return; + } + if (!bag.CanTakeItem(new ItemStack(itemValue, 1))) + { + Deny(_player, MsgVaultFull); + return; + } + + TimerEventData timerData = new TimerEventData + { + Data = new PickupJob { Player = _player, Position = position, Expected = blockValue }, + // Vanilla's own two escapes: taking a hit stops the channel, and so does the + // activate key. Neither is built here - both are fields XUiC_Timer.Update reads. + CloseOnHit = true, + CancelWithActivateButton = true + }; + timerData.FullTimeFinishEvent += OnChannelComplete; + // Every way this ends that is NOT completion: damage, the activate key, the power + // attack. XUiC_Timer sets skipCloseEvent around the completion path specifically so + // the two are mutually exclusive, which is why the colour is restored in both places + // and not only here. + timerData.CloseEvent += delegate + { + ChannelVision.End(_player); + }; + + float channelSeconds = ChannelSecondsFor(hitInfo); + LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPlayer(_player); + XUiC_Timer.OpenTimer(playerUI.xui, channelSeconds, timerData, -1f, Localization.Get(MsgChanneling)); + // After the window is up, so a channel that somehow fails to open never leaves the + // world grey with nothing running. + ChannelVision.Begin(_player); + + Debug.Log("[NecromancerTome] SpatialVaultPickup: owner=" + _player.entityId + " started taking " + + blockValue.Block.GetBlockName() + " at " + position + " - " + + Mathf.Sqrt(hitInfo.hit.distanceSq).ToString("0.##") + " blocks away, " + + channelSeconds.ToString("0.#") + "s channel"); + } + + /// Ten seconds later. Everything is checked again from the live world rather than + /// trusted from the job, because the block that was there when the circle started filling + /// is not necessarily the block that is there now. + public static void OnChannelComplete(TimerEventData _timerData) + { + if (!(_timerData.Data is PickupJob job) || job.Player == null) + { + return; + } + // FIRST, before any of the checks below can take an early exit: the ten seconds are + // over however this turns out, so the colour comes back whether the block is taken or + // refused. + ChannelVision.End(job.Player); + + World world = GameManager.Instance != null ? GameManager.Instance.World : null; + if (world == null) + { + return; + } + + BlockValue blockValue = world.GetBlock(job.Position); + if (!CanTakeHere(world, job.Position, blockValue, job.Player)) + { + return; + } + if (blockValue.damage > 0) + { + Deny(job.Player, MsgRepairFirst); + return; + } + // Shot out, mined, or replaced while the circle was filling. + if (blockValue.isair || blockValue.Block == null || blockValue.type != job.Expected.type) + { + Deny(job.Player, MsgBlockMissing); + return; + } + if (!CanTakeTileEntity(world, job.Position, job.Player)) + { + return; + } + + ItemValue itemValue = blockValue.ToItemValue(); + if (itemValue == null || itemValue.IsEmpty()) + { + Deny(job.Player, MsgNoItemForm); + return; + } + + Bag bag = GetVault(job.Player); + if (bag == null) + { + return; + } + + // ORDER MATTERS: the item goes in first, and the block is only removed if it got + // there. The other way round is how a block gets deleted out of the world in exchange + // for nothing when the vault filled up during those ten seconds. + if (!bag.AddItem(new ItemStack(itemValue, 1))) + { + Deny(job.Player, MsgVaultFull); + return; + } + + world.SetBlockRPC(job.Position, BlockValue.Air); + // The vault lives in memory and is written out with the player's own save data; this + // is the same commit point closing the vault window uses, so a block taken and then + // left alone is not waiting on the next autosave to become real. + GameManager.Instance.SaveLocalPlayerData(); + + Debug.Log("[NecromancerTome] SpatialVaultPickup: owner=" + job.Player.entityId + " took " + + blockValue.Block.GetBlockName() + " at " + job.Position + " into the vault"); + } + + /// How long this particular pull takes. See the class comment for why the ray's + /// own length is the measurement and why it is floored rather than rounded. + public static float ChannelSecondsFor(WorldRayHitInfo _hitInfo) + { + float distance = Mathf.Sqrt(_hitInfo.hit.distanceSq); + int blocks = Mathf.Max(0, Mathf.FloorToInt(distance)); + return BaseChannelSeconds + blocks * SecondsPerBlock; + } + + /// False (with the reason already shown) when this block is one the game itself + /// would not let a player break - because of where it stands, or because of what it is + /// made of. Split out because, like every other guard here, it is asked twice: once to + /// open the timer and once to finish it. + public static bool CanTakeHere(World _world, Vector3i _position, BlockValue _blockValue, EntityPlayerLocal _player) + { + if (World.SandboxUseTraderArea == TraderAreaStates.Default && _world.IsWithinTraderArea(_position)) + { + Deny(_player, MsgTraderArea); + return false; + } + if (_blockValue.Block != null && _blockValue.Block.blockMaterial != null && + !_blockValue.Block.blockMaterial.CanDestroy) + { + Deny(_player, MsgIndestructible); + return false; + } + return true; + } + + /// False (with the reason already shown) when a tile entity at this position + /// stands in the way: someone has it open, or it has something inside it. Contents cannot + /// travel inside an ItemStack, so a container has to be emptied first - vanilla's own rule + /// for its workstations, applied here to chests as well. + public static bool CanTakeTileEntity(World _world, Vector3i _position, EntityPlayerLocal _player) + { + TileEntity tileEntity = _world.GetTileEntity(_position); + if (tileEntity == null) + { + return true; + } + if (tileEntity.IsUserAccessing()) + { + Deny(_player, MsgInUse); + return false; + } + if (tileEntity is TileEntityWorkstation workstation && !workstation.IsEmpty) + { + Deny(_player, MsgNotEmpty); + return false; + } + if (tileEntity is TileEntityCollector collector && !collector.IsEmpty()) + { + Deny(_player, MsgNotEmpty); + return false; + } + // Chests and everything else that holds loot: this version of the game models them as + // a composite tile entity with a storage FEATURE rather than as their own class, so + // the question has to be asked of the feature - the same TryGetSelfOrFeature call the + // engine's own storage code uses. + if (tileEntity.TryGetSelfOrFeature(out ITileEntityLootable lootable) && !lootable.IsEmpty()) + { + Deny(_player, MsgNotEmpty); + return false; + } + return true; + } + + /// The player's vault, or null with the reason already shown. Deliberately the + /// SAME bag the bracelet's power attack opens, reached through the same cache - a block + /// taken here has to be in the window that opens there, and the level gate has to answer + /// the same way in both places. + public static Bag GetVault(EntityPlayerLocal _player) + { + ProgressionValue progressionValue = _player.Progression?.GetProgressionValue( + Patch_ItemActionEat_ExecuteAction_SpatialVault.NecromancySkillName); + int level = progressionValue != null ? progressionValue.Level : 0; + int slotCount = Mathf.RoundToInt(level / 10f); + if (slotCount <= 0) + { + Deny(_player, "braceletSpatialVaultTooWeak"); + return null; + } + + if (!Patch_ItemActionEat_ExecuteAction_SpatialVault.PlayerVaults.TryGetValue(_player.entityId, out Bag bag)) + { + bag = SpatialVaultPersistence.LastLoadedVault ?? new Bag(slotCount); + Patch_ItemActionEat_ExecuteAction_SpatialVault.PlayerVaults[_player.entityId] = bag; + } + if (bag.SlotCount < slotCount) + { + ItemStack[] oldSlots = bag.GetSlots(); + ItemStack[] newSlots = ItemStack.CreateArray(slotCount); + System.Array.Copy(oldSlots, newSlots, oldSlots.Length); + bag.SetSlots(newSlots); + } + return bag; + } + + /// A refusal, in vanilla's shape: the tooltip plus the denial sound. One method + /// so that no refusal in this file can accidentally go out silent. + public static void Deny(EntityPlayerLocal _player, string _localizationKey) + { + GameManager.ShowTooltip(_player, Localization.Get(_localizationKey), string.Empty, DeniedSound); + } + } + + /// Lets the power attack cancel a block pickup in progress and open the vault + /// instead. A separate patch class on XUiC_Timer.Update, not on the item action, because this + /// has to be asked every frame WHILE the timer is open rather than once at click time - the + /// same shape Patch_XUiC_Timer_Update_PortalStoneCancel already uses for the Blue Portal + /// Stone's channel. + /// + /// BOTH INPUT CHECKS ARE DELIBERATE, AND THE RAW ONE IS THE ONE THAT WORKS. The portal stone + /// shipped with only the semantic PlayerActionsLocal.Secondary check and the user reported + /// that cancelling did not work at all: the modal timer window has input focus, and the press + /// never reached PlayerAction's polling layer. The fix there was a second, independent read of + /// Unity's raw Input.GetMouseButtonDown(1) - right mouse, confirmed as Secondary's real + /// default KBM binding by decompiling PlayerActionsLocal.CreateActions - which reads hardware + /// state directly and bypasses whatever swallows the other one. That lesson is reused here + /// rather than re-learned: the semantic check is kept because it costs nothing and would cover + /// a gamepad's Secondary if that one does get through, and the raw check is what is actually + /// expected to fire. A gamepad-only player still has no cancel - the same open gap the portal + /// stone has, and the same fix would close both. + /// + /// THE TIMER IS CLOSED BEFORE THE VAULT IS OPENED, not after: closing runs OnClose, which is + /// what hands control back to the player and drops the event data. Opening a window on top of + /// one that is still closing is how two windows end up fighting over the same input. + [HarmonyPatch(typeof(XUiC_Timer), "Update")] + public static class Patch_XUiC_Timer_Update_VaultPickupCancel + { + public static void Postfix(XUiC_Timer __instance) + { + if (__instance == null || __instance.eventData == null || + !(__instance.eventData.Data is SpatialVaultPickup.PickupJob job) || job.Player == null) + { + return; + } + + PlayerActionsLocal input = __instance.xui?.playerUI?.playerInput; + bool cancelPressed = (input != null && input.Secondary.WasPressed) || Input.GetMouseButtonDown(1); + if (!cancelPressed) + { + return; + } + + EntityPlayerLocal player = job.Player; + Debug.Log("[NecromancerTome] SpatialVaultPickup: pickup cancelled via power attack by owner=" + player.entityId); + __instance.xui.playerUI.windowManager.Close(__instance.windowGroup); + SpatialVaultPickup.CancelOpenedVaultAt = Time.unscaledTime; + Patch_ItemActionEat_ExecuteAction_SpatialVault.OpenVault(player); + } + } +} diff --git a/NecromancerHarmony.dll b/NecromancerHarmony.dll index b6cf2eb..16f693c 100644 Binary files a/NecromancerHarmony.dll and b/NecromancerHarmony.dll differ diff --git a/NecromancerHarmony.pdb b/NecromancerHarmony.pdb index ab27063..37d4945 100644 Binary files a/NecromancerHarmony.pdb and b/NecromancerHarmony.pdb differ