commit e8f064f5ec6b902eaed91e9bc5e5ada90ec9c103 Author: Alex Cube Date: Wed Sep 9 21:13:03 2026 +0300 Книга некроманта 1.0 — первая публичная версия Мод для 7 Days to Die 3.2: навык «Некромантия», растущий от счётчика убитых зомби, тёмное оружие с шестью собственными модами, призывная нежить, пирамида духов и сюжетный финал через Чёрный портал. Локализация на 13 языках. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..55a72b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Build intermediates - the shipped DLLs in the mod root are committed instead. +HarmonySrc/obj/ +HarmonySrc/bin/ +TEPersistenceSrc/obj/ +TEPersistenceSrc/bin/ +*.user +BACKLOG.md +FINAL_TEXT.md diff --git a/Config/Localization.csv b/Config/Localization.csv new file mode 100644 index 0000000..9f0df20 --- /dev/null +++ b/Config/Localization.csv @@ -0,0 +1,124 @@ +Key,File,Type,UsedInMainMenu,NoTranslate,KeepLoaded,english,Context / Alternate Text,german,spanish,french,italian,japanese,koreana,polish,brazilian,russian,turkish,schinese,tchinese +buffNecroZombieKillTrackerDisplay,buffs,Buff,,,,Zombie Kill Tracker,,Zähler getöteter Zombies,Contador de zombis eliminados,Compteur de zombies tués,Contatore di zombie uccisi,ゾンビ討伐カウンター,좀비 처치 기록기,Licznik zabitych zombie,Contador de zumbis mortos,Счётчик убитых зомби,Öldürülen Zombi Sayacı,僵尸击杀计数器,殭屍擊殺計數器 +buffNecroZombieKillTrackerDisplayName,buffs,Buff,,,,Total Zombie Kills,,Getötete Zombies insgesamt,Total de zombis eliminados,Total de zombies tués,Zombie uccisi in totale,ゾンビ討伐総数,총 좀비 처치 수,Łącznie zabitych zombie,Total de zumbis mortos,Всего убито зомби,Toplam Öldürülen Zombi,僵尸击杀总数,殭屍擊殺總數 +buffNecroZombieKillTrackerDisplayDesc,buffs,Buff,,,,Tracks the total number of zombies you have killed. Never resets.,,"Zählt, wie viele Zombies du insgesamt getötet hast. Wird nie zurückgesetzt.",Cuenta el número total de zombis que has eliminado. Nunca se reinicia.,Compte le nombre total de zombies que tu as tués. Ne se réinitialise jamais.,Conta il numero totale di zombie che hai ucciso. Non si azzera mai.,あなたが倒したゾンビの総数を数える。リセットされることはない。,당신이 처치한 좀비의 총수를 셉니다. 절대 초기화되지 않습니다.,Liczy łączną liczbę zabitych przez ciebie zombie. Nigdy się nie zeruje.,Conta o número total de zumbis que você matou. Nunca é reiniciado.,Считает общее количество убитых вами зомби. Никогда не сбрасывается.,Öldürdüğün toplam zombi sayısını takip eder. Asla sıfırlanmaz.,统计你击杀的僵尸总数。永不重置。,統計你擊殺的殭屍總數。永不重置。 +craftingNecroNecromancyName,progression,Attribute,,,,Necromancy,,Nekromantie,Nigromancia,Nécromancie,Negromanzia,降霊術,강령술,Nekromancja,Necromancia,Некромантия,Nekromansi,亡灵法术,亡靈法術 +craftingNecroNecromancyDesc,progression,Attribute,,,,Levels up as you kill zombies.,,"Steigt, während du Zombies tötest.",Sube de nivel a medida que matas zombis.,Progresse à mesure que tu tues des zombies.,Sale di livello man mano che uccidi zombie.,ゾンビを倒すほどレベルが上がる。,좀비를 처치할수록 레벨이 오릅니다.,Rośnie w miarę zabijania zombie.,Sobe de nível conforme você mata zumbis.,Растёт по мере убийства зомби.,Zombi öldürdükçe seviye kazanır.,随着击杀僵尸而提升。,隨著擊殺殭屍而提升。 +craftingNecroNecromancyLongDesc,progression,Attribute,,,,"Every zombie you kill adds a level to this skill, up to 5000. Its recipes unlock in 5 groups by total zombies killed: Adept from the start, Journeyman at 500, Apprentice at 2000, Necromancer at 3000, Master at 5000.",,"Jeder getötete Zombie bringt dieser Fertigkeit eine Stufe, bis zu 5000. Ihre Rezepte schalten sich in 5 Gruppen frei, nach der Gesamtzahl getöteter Zombies: Adept von Anfang an, Geselle bei 500, Lehrling bei 2000, Nekromant bei 3000, Meister bei 5000.","Cada zombi que matas añade un nivel a esta habilidad, hasta 5000. Sus recetas se desbloquean en 5 grupos según el total de zombis eliminados: Adepto desde el principio, Oficial a los 500, Aprendiz a los 2000, Nigromante a los 3000, Maestro a los 5000.","Chaque zombie tué ajoute un niveau à cette compétence, jusqu'à 5000. Ses recettes se débloquent en 5 groupes selon le total de zombies tués : Adepte dès le début, Compagnon à 500, Apprenti à 2000, Nécromancien à 3000, Maître à 5000.","Ogni zombie che uccidi aggiunge un livello a questa abilità, fino a 5000. Le sue ricette si sbloccano in 5 gruppi in base al totale di zombie uccisi: Adepto fin dall'inizio, Artigiano a 500, Apprendista a 2000, Negromante a 3000, Maestro a 5000.",ゾンビを1体倒すごとに、このスキルのレベルが1上がる(上限5000)。レシピは討伐総数に応じて5つのグループで解放される。アデプトは最初から、ジャーニーマンは500、見習いは2000、ネクロマンサーは3000、マスターは5000。,"좀비를 하나 처치할 때마다 이 기술의 레벨이 1씩 오릅니다(최대 5000). 제작법은 누적 처치 수에 따라 5개 그룹으로 해금됩니다. 숙련자는 처음부터, 직인은 500, 도제는 2000, 강령술사는 3000, 명인은 5000.","Każdy zabity zombie dodaje poziom tej umiejętności, aż do 5000. Jej receptury odblokowują się w 5 grupach według łącznej liczby zabitych zombie: Adept od początku, Czeladnik przy 500, Uczeń przy 2000, Nekromanta przy 3000, Mistrz przy 5000.","Cada zumbi que você mata adiciona um nível a esta habilidade, até 5000. Suas receitas são desbloqueadas em 5 grupos conforme o total de zumbis mortos: Adepto desde o início, Oficial aos 500, Aprendiz aos 2000, Necromante aos 3000, Mestre aos 5000.","Каждый убитый зомби добавляет уровень этому навыку, максимум 5000. Рецепты открываются 5 группами по общему числу убитых зомби: Адепт — с самого начала, Подмастерье — 500, Ученик — 2000, Некромант — 3000, Мастер — 5000.","Öldürdüğün her zombi bu yeteneğe bir seviye ekler, en fazla 5000'e kadar. Tarifleri, toplam öldürülen zombi sayısına göre 5 grup hâlinde açılır: Ehil en baştan, Kalfa 500'de, Çırak 2000'de, Nekromant 3000'de, Usta 5000'de.",每击杀一只僵尸,此技能便提升一级,上限 5000。其配方按击杀总数分 5 组解锁:修习者一开始即可用,熟手 500,学徒 2000,亡灵法师 3000,宗师 5000。,每擊殺一隻殭屍,此技能便提升一級,上限 5000。其配方按擊殺總數分 5 組解鎖:修習者一開始即可用,熟手 500,學徒 2000,亡靈法師 3000,宗師 5000。 +craftingNecroNecromancyTier1Name,progression,Attribute,,,,Adept,,Adept,Adepto,Adepte,Adepto,アデプト,숙련자,Adept,Adepto,Адепт,Ehil,修习者,修習者 +craftingNecroNecromancyTier2Name,progression,Attribute,,,,Journeyman,,Geselle,Oficial,Compagnon,Artigiano,ジャーニーマン,직인,Czeladnik,Oficial,Подмастерье,Kalfa,熟手,熟手 +craftingNecroNecromancyTier3Name,progression,Attribute,,,,Apprentice,,Lehrling,Aprendiz,Apprenti,Apprendista,見習い,도제,Uczeń,Aprendiz,Ученик,Çırak,学徒,學徒 +craftingNecroNecromancyTier4Name,progression,Attribute,,,,Necromancer,,Nekromant,Nigromante,Nécromancien,Negromante,ネクロマンサー,강령술사,Nekromanta,Necromante,Некромант,Nekromant,亡灵法师,亡靈法師 +craftingNecroNecromancyTier5Name,progression,Attribute,,,,Master,,Meister,Maestro,Maître,Maestro,マスター,명인,Mistrz,Mestre,Мастер,Usta,宗师,宗師 +thrownStoneSpirit,items,Item,,,,Spirit Stone,,Geisterstein,Piedra de espíritus,Pierre des esprits,Pietra degli spiriti,精霊の石,영혼의 돌,Kamień duchów,Pedra dos espíritos,Камень духов,Ruh Taşı,灵魂之石,靈魂之石 +thrownStoneSpiritDesc,items,Item,,,,"A stone glowing with necromantic energy. Thrown at a zombie, it turns it to your side, making it attack other zombies instead of you.",,"Ein Stein, der von nekromantischer Energie glüht. Auf einen Zombie geworfen, zieht er ihn auf deine Seite, sodass er andere Zombies angreift statt dich.","Una piedra que brilla con energía nigromántica. Lanzada a un zombi, lo pone de tu lado y hace que ataque a otros zombis en vez de a ti.","Une pierre qui rougeoie d'énergie nécromantique. Lancée sur un zombie, elle le fait passer de ton côté : il attaque alors les autres zombies au lieu de toi.","Una pietra che brilla di energia negromantica. Lanciata contro uno zombie, lo porta dalla tua parte e lo fa attaccare altri zombie invece di te.",降霊術のエネルギーで輝く石。ゾンビに投げつけると、そのゾンビはあなたの側につき、あなたではなく他のゾンビを襲うようになる。,"강령술의 기운으로 빛나는 돌. 좀비에게 던지면 그 좀비가 당신 편이 되어, 당신 대신 다른 좀비를 공격합니다.","Kamień jarzący się nekromancką energią. Rzucony w zombie, przeciąga go na twoją stronę, przez co atakuje inne zombie zamiast ciebie.","Uma pedra que brilha com energia necromântica. Arremessada em um zumbi, o coloca do seu lado, fazendo com que ataque outros zumbis em vez de você.","Камень, светящийся некромантской энергией. Брошенный в зомби, переманивает его на вашу сторону: тот начинает атаковать других зомби вместо вас.","Nekromansi enerjisiyle parlayan bir taş. Bir zombiye atıldığında onu senin tarafına geçirir; artık sana değil, diğer zombilere saldırır.",一块闪耀着亡灵能量的石头。掷向僵尸后,它会转而站在你这边,攻击其他僵尸而不是你。,一塊閃耀著亡靈能量的石頭。擲向殭屍後,牠會轉而站在你這邊,攻擊其他殭屍而不是你。 +thrownBookGrimoireDeviation,items,Item,,,,Scroll of Deviation,,Schriftrolle der Abweichung,Pergamino de desviación,Parchemin de déviation,Pergamena della deviazione,逸脱の巻物,일탈의 두루마리,Zwój dewiacji,Pergaminho do Desvio,Свиток девиации,Sapma Tomarı,悖逆卷轴,悖逆卷軸 +thrownBookGrimoireDeviationDesc,items,Item,,,,"A necromantic scroll glowing with the same energy as the Spirit Stone, but far more potent. Thrown, it turns every zombie caught in the blast to your side at once, making them attack each other instead of you. Consumed on use.",,"Eine nekromantische Schriftrolle, die von derselben Energie glüht wie der Geisterstein, nur weit stärker. Geworfen zieht sie jeden Zombie in der Druckwelle auf einen Schlag auf deine Seite, sodass sie einander angreifen statt dich. Wird beim Gebrauch verbraucht.","Un pergamino nigromántico que brilla con la misma energía que la Piedra de espíritus, pero mucho más potente. Al lanzarlo, pone de tu lado a todos los zombis alcanzados por la explosión a la vez, haciendo que se ataquen entre sí en vez de a ti. Se consume al usarlo.","Un parchemin nécromantique qui rougeoie de la même énergie que la Pierre des esprits, mais bien plus puissante. Lancé, il fait passer de ton côté tous les zombies pris dans le souffle d'un seul coup : ils s'attaquent alors entre eux au lieu de toi. Consommé à l'usage.","Una pergamena negromantica che brilla della stessa energia della Pietra degli spiriti, ma molto più potente. Lanciata, porta dalla tua parte tutti gli zombie colpiti dall'esplosione in una sola volta, facendoli attaccare tra loro invece che te. Si consuma con l'uso.",精霊の石と同じエネルギーで輝く降霊術の巻物。ただし、その力ははるかに強い。投げると、爆発に巻き込まれたゾンビをすべて一度に味方に変え、あなたではなく互いを襲わせる。使用すると失われる。,"영혼의 돌과 같은 기운으로 빛나지만 훨씬 강력한 강령술 두루마리. 던지면 폭발에 휘말린 모든 좀비가 한 번에 당신 편이 되어, 당신 대신 서로를 공격합니다. 사용하면 소모됩니다.","Nekromancki zwój jarzący się tą samą energią co Kamień duchów, tyle że o wiele potężniejszą. Rzucony, jednym razem przeciąga na twoją stronę każde zombie objęte wybuchem, przez co atakują siebie nawzajem zamiast ciebie. Zużywa się przy użyciu.","Um pergaminho necromântico que brilha com a mesma energia da Pedra dos espíritos, mas muito mais potente. Arremessado, coloca do seu lado todos os zumbis atingidos pela explosão de uma só vez, fazendo com que ataquem uns aos outros em vez de você. Consumido ao usar.","Некромантский свиток, светящийся той же энергией, что и Камень духов, но гораздо мощнее. При броске переманивает на вашу сторону сразу всех зомби в области взрыва: они начинают атаковать друг друга вместо вас. Расходуется при использовании.","Ruh Taşı ile aynı enerjiyle parlayan, ama ondan çok daha güçlü bir nekromansi tomarı. Atıldığında patlamaya yakalanan her zombiyi bir anda senin tarafına geçirir; artık sana değil, birbirlerine saldırırlar. Kullanıldığında tükenir.",一卷亡灵法术卷轴,闪耀着与灵魂之石相同的能量,却强大得多。掷出后,爆炸范围内的每一只僵尸都会立刻倒向你这边,转而互相攻击而不是攻击你。使用后消耗。,一卷亡靈法術卷軸,閃耀著與靈魂之石相同的能量,卻強大得多。擲出後,爆炸範圍內的每一隻殭屍都會立刻倒向你這邊,轉而互相攻擊而不是攻擊你。使用後消耗。 +bookSummonZombieDog,items,Item,,,,Summon Zombie Dog,,Zombiehund beschwören,Invocar Perro Zombi,Invoquer un Chien Zombie,Evoca Cane Zombie,ゾンビ犬召喚,좀비 개 소환,Przyzwanie: Pies Zombie,Invocar Cão Zumbi,Призыв зомбособаки,Zombi Köpek Çağırma,召唤僵尸犬,召喚殭屍犬 +bookSummonZombieDogDesc,items,Item,,,,A necromantic tome that raises a zombie dog bound to your will. It fights alongside you against other zombies until it dies. You can only have one bound at a time. Power attack to recall it back into the book.,,"Ein nekromantisches Buch, das einen Zombiehund erweckt, der deinem Willen unterworfen ist. Er kämpft an deiner Seite gegen andere Zombies, bis er stirbt. Du kannst immer nur einen gebunden haben. Mit dem Powerangriff rufst du ihn ins Buch zurück.",Un tomo nigromántico que alza un Perro Zombi sometido a tu voluntad. Lucha a tu lado contra otros zombis hasta morir. Solo puedes tener uno vinculado a la vez. Usa el ataque potente para devolverlo al libro.,Un ouvrage nécromantique qui relève un Chien Zombie soumis à ta volonté. Il combat à tes côtés contre les autres zombies jusqu'à sa mort. Tu ne peux en lier qu'un seul à la fois. L'attaque puissante le rappelle dans le livre.,Un tomo negromantico che rialza un Cane Zombie soggetto alla tua volontà. Combatte al tuo fianco contro gli altri zombie finché non muore. Puoi averne vincolato solo uno alla volta. L'attacco potente lo richiama nel libro.,あなたの意志に縛られたゾンビ犬を起こす降霊術の書。死ぬまで、あなたの側で他のゾンビと戦う。同時に縛っておけるのは一体だけ。強攻撃で書物へ呼び戻せる。,당신의 의지에 묶인 좀비 개을(를) 일으키는 강령술 서적. 죽을 때까지 당신 곁에서 다른 좀비와 싸운다. 한 번에 하나만 묶어 둘 수 있다. 강공격으로 책 속에 불러들인다.,"Nekromancki tom, który wskrzesza Psa Zombie związanego twoją wolą. Walczy u twojego boku z innymi zombie, aż zginie. Naraz możesz mieć związanego tylko jednego. Atak specjalny odwołuje go z powrotem do księgi.",Um tomo necromântico que ergue um Cão Zumbi preso à sua vontade. Luta ao seu lado contra outros zumbis até morrer. Você só pode ter um vinculado por vez. O ataque forte o chama de volta para o livro.,"Некромантский фолиант, поднимающий зомбособаку, подчинённую вашей воле. Она сражается на вашей стороне против других зомби, пока не погибнет. Одновременно можно держать только одну. Силовая атака отзывает её обратно в книгу.",İradene bağlı bir Zombi Köpek dirilten bir nekromansi kitabı. Ölene kadar yanında diğer zombilere karşı savaşır. Aynı anda yalnızca bir tanesini bağlı tutabilirsin. Güçlü saldırı onu kitaba geri çağırır.,一本亡灵法术典籍,唤起一只受你意志束缚的僵尸犬。它会在你身边与其他僵尸战斗,直到死亡。同时只能束缚一只。使用强力攻击可将其召回书中。,一本亡靈法術典籍,喚起一隻受你意志束縛的殭屍犬。牠會在你身邊與其他殭屍戰鬥,直到死亡。同時只能束縛一隻。使用強力攻擊可將其召回書中。 +necroZombieDogAlreadyActive,items,Item,,,,You already have a Zombie Dog - use the power attack to recall it,,Du hast bereits einen Zombiehund – rufe ihn mit dem Powerangriff zurück,Ya tienes un Perro Zombi: usa el ataque potente para devolverlo,Tu as déjà un Chien Zombie – utilise l'attaque puissante pour le rappeler,Hai già un Cane Zombie: usa l'attacco potente per richiamarlo,すでにゾンビ犬がいます – 強攻撃で呼び戻してください,이미 좀비 개이(가) 있습니다 - 강공격으로 불러들이세요,Masz już Psa Zombie – odwołaj go atakiem specjalnym,Você já tem um Cão Zumbi - use o ataque forte para chamá-lo de volta,У вас уже есть зомбособака - отзовите её силовой атакой,Zaten bir Zombi Köpek var - geri çağırmak için güçlü saldırıyı kullan,你已经有一只僵尸犬了——用强力攻击将其召回,你已經有一隻殭屍犬了——用強力攻擊將其召回 +necroInsectSwarmAlreadyActive,items,Item,,,,You already have Beetles active - wait for them to be destroyed,,"Du hast bereits Käfer im Einsatz – warte, bis sie vernichtet sind",Ya tienes escarabajos activos: espera a que sean destruidos,Tu as déjà des scarabées actifs – attends qu'ils soient détruits,Hai già degli scarabei attivi: aspetta che vengano distrutti,すでに甲虫が展開中です – 全滅するまで待ってください,이미 딱정벌레가 활동 중입니다 - 전멸할 때까지 기다리세요,"Masz już aktywne żuki – poczekaj, aż zostaną zniszczone",Você já tem besouros ativos - espere até que sejam destruídos,"У вас уже есть активные жуки - дождитесь, пока они будут уничтожены",Zaten etkin böceklerin var - yok edilmelerini bekle,你已经放出了甲虫——等它们被消灭再说,你已經放出了甲蟲——等牠們被消滅再說 +necroZombieDogRecalled,items,Item,,,,Zombie Dog recalled,,Zombiehund zurückgerufen,Perro Zombi devuelto,Chien Zombie rappelé,Cane Zombie richiamato,ゾンビ犬を呼び戻しました,좀비 개을(를) 불러들였습니다,Odwołano: Pies Zombie,Cão Zumbi chamado de volta,Зомбособака отозвана,Zombi Köpek geri çağrıldı,已召回僵尸犬,已召回殭屍犬 +necroZombieDogNothingToRecall,items,Item,,,,No Zombie Dog to recall,,Kein Zombiehund zum Zurückrufen vorhanden,No hay ningún Perro Zombi que devolver,Aucun Chien Zombie à rappeler,Nessun Cane Zombie da richiamare,呼び戻せるゾンビ犬がいません,불러들일 좀비 개이(가) 없습니다,Brak Psa Zombie do odwołania,Não há nenhum Cão Zumbi para chamar de volta,Нет зомбособаки для отзыва,Geri çağrılacak bir Zombi Köpek yok,没有可召回的僵尸犬,沒有可召回的殭屍犬 +bookSummonInsectSwarm,items,Item,,,,Beetles of the Lord,,Käfer des Herrn,Escarabajos del Señor,Scarabées du Seigneur,Scarabei del Signore,主の甲虫,군주의 딱정벌레,Żuki Władcy,Besouros do Senhor,Жуки Властелина,Efendi'nin Böcekleri,主宰之甲虫,主宰之甲蟲 +bookSummonInsectSwarmDesc,items,Item,,,,"A necromantic scroll that unleashes a swarm of beetles bound to your will, one time only - once cast, they cannot be recalled. They seek out every zombie in a wide radius and sting them; a stung zombie turns to your side, just like the Spirit Stone or Scroll of Deviation. You can only have one swarm active at a time. Consumed on use.",,"Eine nekromantische Schriftrolle, die einen Schwarm deinem Willen unterworfener Käfer entfesselt – ein einziges Mal: einmal beschworen, lassen sie sich nicht mehr zurückrufen. Sie suchen jeden Zombie im weiten Umkreis und stechen ihn; ein gestochener Zombie wechselt auf deine Seite, genau wie beim Geisterstein oder der Schriftrolle der Abweichung. Du kannst immer nur einen Schwarm gleichzeitig haben. Wird beim Gebrauch verbraucht.","Un pergamino nigromántico que desata un enjambre de escarabajos sometidos a tu voluntad, una sola vez: una vez lanzados, no se pueden hacer volver. Buscan a todos los zombis en un amplio radio y los pican; un zombi picado se pone de tu lado, igual que con la Piedra de espíritus o el Pergamino de desviación. Solo puedes tener un enjambre activo a la vez. Se consume al usarlo.","Un parchemin nécromantique qui déchaîne un essaim de scarabées soumis à ta volonté, une seule fois : une fois lancés, on ne peut plus les rappeler. Ils cherchent tous les zombies dans un large rayon et les piquent ; un zombie piqué passe de ton côté, exactement comme avec la Pierre des esprits ou le Parchemin de déviation. Tu ne peux avoir qu'un seul essaim actif à la fois. Consommé à l'usage.","Una pergamena negromantica che scatena uno sciame di scarabei soggetti alla tua volontà, una volta sola: una volta lanciati non possono più essere richiamati. Cercano ogni zombie in un ampio raggio e lo pungono; uno zombie punto passa dalla tua parte, proprio come con la Pietra degli spiriti o la Pergamena della deviazione. Puoi avere un solo sciame attivo alla volta. Si consuma con l'uso.",あなたの意志に縛られた甲虫の群れを解き放つ降霊術の巻物。ただし一度きりで、放ったあとは呼び戻せない。広い範囲のゾンビを探し出して刺し、刺されたゾンビは精霊の石や逸脱の巻物と同じようにあなたの側につく。同時に展開できる群れは一つだけ。使用すると失われる。,"당신의 의지에 묶인 딱정벌레 떼를 단 한 번 풀어놓는 강령술 두루마리 - 한 번 풀면 다시 불러들일 수 없습니다. 넓은 범위의 좀비를 모두 찾아내 쏘며, 쏘인 좀비는 영혼의 돌이나 일탈의 두루마리처럼 당신 편이 됩니다. 한 번에 한 무리만 유지할 수 있습니다. 사용하면 소모됩니다.","Nekromancki zwój, który wypuszcza rój żuków związanych twoją wolą – tylko raz: po rzuceniu nie da się ich już odwołać. Wyszukują każde zombie w szerokim promieniu i żądlą je; użądlone zombie przechodzi na twoją stronę, tak samo jak przy Kamieniu duchów czy Zwoju dewiacji. Naraz możesz mieć tylko jeden aktywny rój. Zużywa się przy użyciu.","Um pergaminho necromântico que libera um enxame de besouros presos à sua vontade, uma única vez: depois de lançados, não podem ser chamados de volta. Eles procuram cada zumbi num amplo raio e o picam; um zumbi picado passa para o seu lado, igual à Pedra dos espíritos ou ao Pergaminho do Desvio. Você só pode ter um enxame ativo por vez. Consumido ao usar.","Некромантский свиток, высвобождающий рой жуков, подчинённых вашей воле, только один раз - после призыва отозвать их нельзя. Они ищут всех зомби в широком радиусе и жалят их; ужаленный зомби переходит на вашу сторону, точно так же, как от Камня духов или Свитка девиации. Одновременно может быть активен только один рой. Расходуется при использовании.","İradene bağlı bir böcek sürüsünü tek seferliğine salan bir nekromansi tomarı - bir kez salındıklarında geri çağrılamazlar. Geniş bir yarıçaptaki her zombiyi bulup sokarlar; sokulan zombi, tıpkı Ruh Taşı ya da Sapma Tomarı'nda olduğu gibi senin tarafına geçer. Aynı anda yalnızca bir sürü etkin olabilir. Kullanıldığında tükenir.",一卷亡灵法术卷轴,释放出一群受你意志束缚的甲虫,仅此一次——一旦放出便无法召回。它们会搜寻大范围内的每一只僵尸并加以叮蜇;被蜇中的僵尸会像受灵魂之石或悖逆卷轴影响那样倒向你这边。同时只能维持一群。使用后消耗。,一卷亡靈法術卷軸,釋放出一群受你意志束縛的甲蟲,僅此一次——一旦放出便無法召回。牠們會搜尋大範圍內的每一隻殭屍並加以叮螫;被螫中的殭屍會像受靈魂之石或悖逆卷軸影響那樣倒向你這邊。同時只能維持一群。使用後消耗。 +tinCanEmpty,items,Item,,,,Empty Tin Can,,Leere Blechdose,Lata vacía,Boîte de conserve vide,Lattina vuota,空き缶,빈 깡통,Pusta puszka,Lata vazia,Пустая консервная банка,Boş Teneke Kutu,空罐头,空罐頭 +tinCanEmptyDesc,items,Item,,,,An empty tin can. Can be filled with water or used in crafting.,,Eine leere Blechdose. Kann mit Wasser gefüllt oder beim Herstellen verwendet werden.,Una lata vacía. Se puede llenar de agua o usar en fabricación.,Une boîte de conserve vide. Peut être remplie d'eau ou utilisée en fabrication.,Una lattina vuota. Può essere riempita d'acqua o usata per fabbricare.,空の缶。水を入れるか、クラフトに使える。,빈 깡통. 물을 채우거나 제작에 쓸 수 있습니다.,Pusta puszka. Można ją napełnić wodą lub wykorzystać przy wytwarzaniu.,Uma lata vazia. Pode ser enchida com água ou usada na fabricação.,Пустая консервная банка. Можно наполнить водой или использовать в крафте.,Boş bir teneke kutu. Suyla doldurulabilir ya da üretimde kullanılabilir.,一个空罐头。可以装水,也可用于制作。,一個空罐頭。可以裝水,也可用於製作。 +tinCanRiverWater,items,Item,,,,Tin Can of Murky Water,,Blechdose mit trübem Wasser,Lata de agua turbia,Boîte de conserve d'eau trouble,Lattina d'acqua torbida,濁った水の缶,탁한 물이 담긴 깡통,Puszka mętnej wody,Lata de água turva,Консервная банка с мутной водой,Bulanık Su Dolu Teneke Kutu,装浑水的罐头,裝濁水的罐頭 +tinCanRiverWaterDesc,items,Item,,,,Murky water collected in a tin can. Drinking may cause dysentery. Holds less water than glass jars.,,"Trübes Wasser, in einer Blechdose gesammelt. Trinken kann Ruhr verursachen. Fasst weniger Wasser als ein Einmachglas.",Agua turbia recogida en una lata. Beberla puede causar disentería. Contiene menos agua que un tarro de cristal.,De l'eau trouble recueillie dans une boîte de conserve. En boire peut provoquer la dysenterie. Contient moins d'eau qu'un bocal en verre.,Acqua torbida raccolta in una lattina. Berla può causare dissenteria. Contiene meno acqua di un barattolo di vetro.,缶に汲んだ濁った水。飲むと赤痢にかかることがある。ガラス瓶より容量が少ない。,깡통에 담은 탁한 물. 마시면 이질에 걸릴 수 있습니다. 유리병보다 적게 담깁니다.,Mętna woda zebrana do puszki. Picie może wywołać czerwonkę. Mieści mniej wody niż szklany słoik.,Água turva coletada numa lata. Beber pode causar disenteria. Contém menos água que um pote de vidro.,"Мутная вода в консервной банке. Употребление может вызвать дизентерию. Вмещает меньше воды, чем стеклянные банки.",Teneke kutuya doldurulmuş bulanık su. İçmek dizanteriye yol açabilir. Cam kavanozdan daha az su alır.,用罐头装的浑水。饮用可能引发痢疾。容量比玻璃罐少。,用罐頭裝的濁水。飲用可能引發痢疾。容量比玻璃罐少。 +tinCanBoiledWater,items,Item,,,,Tin Can of Boiled Water,,Blechdose mit abgekochtem Wasser,Lata de agua hervida,Boîte de conserve d'eau bouillie,Lattina d'acqua bollita,煮沸した水の缶,끓인 물이 담긴 깡통,Puszka przegotowanej wody,Lata de água fervida,Консервная банка с кипячёной водой,Kaynatılmış Su Dolu Teneke Kutu,装开水的罐头,裝開水的罐頭 +tinCanBoiledWaterDesc,items,Item,,,,Purified boiled water in a tin can. Safe to drink. Holds less water than glass jars.,,"Gereinigtes, abgekochtes Wasser in einer Blechdose. Sicher zu trinken. Fasst weniger Wasser als ein Einmachglas.",Agua hervida y purificada en una lata. Segura para beber. Contiene menos agua que un tarro de cristal.,De l'eau bouillie et purifiée dans une boîte de conserve. Potable sans risque. Contient moins d'eau qu'un bocal en verre.,Acqua bollita e purificata in una lattina. Si può bere senza rischi. Contiene meno acqua di un barattolo di vetro.,缶に入った煮沸済みの浄化された水。安全に飲める。ガラス瓶より容量が少ない。,깡통에 담긴 끓여서 정화한 물. 안전하게 마실 수 있습니다. 유리병보다 적게 담깁니다.,"Oczyszczona, przegotowana woda w puszce. Można ją bezpiecznie pić. Mieści mniej wody niż szklany słoik.",Água fervida e purificada numa lata. Segura para beber. Contém menos água que um pote de vidro.,"Очищенная кипячёная вода в консервной банке. Безопасна для питья. Вмещает меньше воды, чем стеклянные банки.","Teneke kutuda kaynatılmış, arıtılmış su. İçmek güvenlidir. Cam kavanozdan daha az su alır.",罐头里的煮沸净水。可安全饮用。容量比玻璃罐少。,罐頭裡的煮沸淨水。可安全飲用。容量比玻璃罐少。 +necroZombieDog,entityclasses,Entity,,,,Zombie Dog,,Zombiehund,Perro Zombi,Chien Zombie,Cane Zombie,ゾンビ犬,좀비 개,Pies Zombie,Cão Zumbi,Зомбособака,Zombi Köpek,僵尸犬,殭屍犬 +necroInsectSwarm,entityclasses,Entity,,,,Beetles of the Lord,,Käfer des Herrn,Escarabajos del Señor,Scarabées du Seigneur,Scarabei del Signore,主の甲虫,군주의 딱정벌레,Żuki Władcy,Besouros do Senhor,Жуки Властелина,Efendi'nin Böcekleri,主宰之甲虫,主宰之甲蟲 +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, guaranteeing a special bag of loot when it dies. Its power attack deals no extra damage and returns nothing to you - instead it always knocks its target off its feet, and takes 5 health from you as the price.",,"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, was nach seinem Tod einen besonderen Beutebeutel garantiert. 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 5 Lebenspunkte.","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, lo que garantiza una bolsa de botín especial cuando muera. Su ataque potente no hace daño adicional ni te devuelve nada: en cambio, siempre derriba a su objetivo y te cobra 5 puntos de salud.","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, ce qui garantit un sac de butin spécial à sa mort. 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 5 points de vie en paiement.","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, garantendo un sacco di bottino speciale alla sua morte. L'attacco potente non infligge danno extra e non ti restituisce nulla: in compenso atterra sempre il bersaglio e ti costa 5 punti salute.",あなたの降霊術の力に結びついた、黒ずんだ骨のナイフ。その威力は降霊術スキルとともに伸びる。未熟な手では役に立たず、熟練したネクロマンサーの手では致命的だ。通常の一撃は与えたダメージの半分をあなたに回復させ、傷を負ったゾンビは「犠牲者」として刻まれ、死んだときに特別な戦利品袋を必ず落とす。強攻撃は追加ダメージを与えず、あなたに何も返さない。その代わり、必ず相手を打ち倒し、代償としてあなたの体力を5奪う。,"당신의 강령술 힘에 묶인 검게 그을린 뼈 단검. 위력은 강령술 기술에 따라 커집니다. 미숙한 손에서는 쓸모없고, 숙련된 강령술사의 손에서는 치명적입니다. 평범한 일격은 입힌 피해의 절반만큼 당신을 치유하고, 상처 입은 좀비는 '희생자'로 표시되어 죽을 때 특별한 전리품 자루를 반드시 남깁니다. 강공격은 추가 피해를 주지 않고 당신에게 아무것도 돌려주지 않습니다. 대신 언제나 대상을 넘어뜨리며, 그 대가로 당신의 체력 5를 가져갑니다.","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, co gwarantuje specjalny worek łupu po jego śmierci. 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 5 punktów zdrowia.","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, garantindo um saco de espólio especial quando morrer. O ataque forte não causa dano extra e não devolve nada a você: em vez disso, sempre derruba o alvo e cobra 5 pontos da sua vida.","Почерневший костяной нож, подчинённый вашей некромантской силе. Его урон растёт вместе с навыком Некромантии - бесполезен в неопытных руках, смертоносен в руках опытного некроманта. Обычный удар лечит вас на половину нанесённого урона, а любой раненый им зомби помечен как Жертва, что гарантирует особый мешок с добычей после его смерти. Силовая атака не бьёт сильнее и ничего вам не возвращает - вместо этого она всегда сбивает цель с ног, забирая у вас 5 единиц здоровья как плату.","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; öldüğünde özel bir ganimet çantası bırakacağı garanti olur. 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 5 can alır.",一把与你的亡灵之力相连的漆黑骨刃。其伤害随亡灵法术技能增长——在生手手中一文不值,在老练的亡灵法师手中则致命。普通挥击可为你回复所造成伤害的一半,被它所伤的僵尸都会被标记为「祭品」,死亡时必定掉落特殊的战利品袋。强力攻击不会造成额外伤害,也不会回报给你任何东西——但它必定将目标掀翻在地,并以你 5 点生命为代价。,一把與你的亡靈之力相連的漆黑骨刃。其傷害隨亡靈法術技能增長——在生手手中一文不值,在老練的亡靈法師手中則致命。普通揮擊可為你回復所造成傷害的一半,被它所傷的殭屍都會被標記為「祭品」,死亡時必定掉落特殊的戰利品袋。強力攻擊不會造成額外傷害,也不會回報給你任何東西——但它必定將目標掀翻在地,並以你 5 點生命為代價。 +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.,从被亡灵法师之刃标记为「祭品」的僵尸身上剥下的皮。亡灵召唤典籍的材料。,從被亡靈法師之刃標記為「祭品」的殭屍身上剝下的皮。亡靈召喚典籍的材料。 +resourceZombieAsh,items,Item,,,,Zombie Ash,,Zombieasche,Ceniza de zombi,Cendre de zombie,Cenere di zombie,ゾンビの灰,좀비의 재,Popiół zombie,Cinza de zumbi,Прах зомби,Zombi Külü,僵尸灰烬,殭屍灰燼 +resourceZombieAshDesc,items,Item,,,,Ash left over from a zombie marked as a Victim by the Necromancer's Knife. An ingredient in most necromantic recipes.,,"Asche, die von einem Zombie übrig blieb, den das Messer des Nekromanten als Opfer markiert hat. Zutat der meisten nekromantischen Rezepte.",Ceniza que queda de un zombi marcado como Víctima por el Cuchillo del nigromante. Ingrediente de la mayoría de las recetas nigrománticas.,Cendre laissée par un zombie marqué comme Victime par le Couteau du nécromancien. Ingrédient de la plupart des recettes nécromantiques.,Cenere rimasta da uno zombie marchiato come Vittima dal Coltello del negromante. Ingrediente della maggior parte delle ricette negromantiche.,ネクロマンサーのナイフに「犠牲者」として刻まれたゾンビの残した灰。ほとんどの降霊術レシピの材料。,강령술사의 단검이 '희생자'로 표시한 좀비가 남긴 재. 대부분의 강령술 제작법에 들어가는 재료입니다.,Popiół pozostały po zombie oznaczonym jako Ofiara przez Nóż nekromanty. Składnik większości nekromanckich receptur.,Cinza deixada por um zumbi marcado como Vítima pela Faca do necromante. Ingrediente da maioria das receitas necromânticas.,"Прах, оставшийся от зомби, помеченного Ножом некроманта как Жертва. Ингредиент большинства некромантских рецептов.",Nekromantın Bıçağı tarafından Kurban olarak işaretlenmiş bir zombiden geriye kalan kül. Nekromansi tariflerinin çoğunda kullanılır.,被亡灵法师之刃标记为「祭品」的僵尸所留下的灰烬。多数亡灵配方的材料。,被亡靈法師之刃標記為「祭品」的殭屍所留下的灰燼。多數亡靈配方的材料。 +resourceNecromancerBlood,items,Item,,,,Necromancer's Blood,,Blut des Nekromanten,Sangre del nigromante,Sang du nécromancien,Sangue del negromante,ネクロマンサーの血,강령술사의 피,Krew nekromanty,Sangue do necromante,Кровь некроманта,Nekromantın Kanı,亡灵法师之血,亡靈法師之血 +resourceNecromancerBloodDesc,items,Item,,,,"A jar filled with the necromancer's own blood, drawn during a ritual requiring a knife. An ingredient for the darkest necromantic rites.",,"Ein Glas, gefüllt mit dem eigenen Blut des Nekromanten, in einem Ritual abgezapft, für das ein Messer nötig ist. Zutat für die finstersten nekromantischen Riten.","Un tarro lleno de la propia sangre del nigromante, extraída en un ritual que requiere un cuchillo. Ingrediente para los ritos nigrománticos más oscuros.","Un bocal rempli du sang même du nécromancien, tiré lors d'un rituel qui exige un couteau. Ingrédient des rites nécromantiques les plus sombres.","Un barattolo pieno del sangue stesso del negromante, prelevato in un rituale che richiede un coltello. Ingrediente dei riti negromantici più oscuri.",儀式によって抜かれた、ネクロマンサー自身の血で満たされた瓶。儀式にはナイフが要る。最も暗い降霊術の儀式の材料。,"칼이 필요한 의식으로 뽑아낸, 강령술사 자신의 피가 담긴 병. 가장 어두운 강령술 의식의 재료입니다.","Słoik wypełniony własną krwią nekromanty, utoczoną w rytuale wymagającym noża. Składnik najmroczniejszych nekromanckich obrzędów.","Um pote cheio do próprio sangue do necromante, extraído num ritual que exige uma faca. Ingrediente dos ritos necromânticos mais sombrios.","Банка, наполненная кровью самого некроманта, взятой во время ритуала, для которого нужен нож. Ингредиент для самых тёмных некромантских ритуалов.","Bir bıçak gerektiren bir ritüelde alınmış, nekromantın kendi kanıyla dolu bir kavanoz. En karanlık nekromansi ayinlerinin malzemesi.",一罐盛满亡灵法师自身鲜血的容器,取自一场需要刀刃的仪式。最黑暗的亡灵仪式所用的材料。,一罐盛滿亡靈法師自身鮮血的容器,取自一場需要刀刃的儀式。最黑暗的亡靈儀式所用的材料。 +resourceNecromancerBloodNeedsKnife,items,Item,,,,You need a knife to perform this ritual,,Für dieses Ritual brauchst du ein Messer,Necesitas un cuchillo para realizar este ritual,Il te faut un couteau pour accomplir ce rituel,Ti serve un coltello per compiere questo rituale,この儀式にはナイフが必要です,이 의식을 치르려면 칼이 필요합니다,Do tego rytuału potrzebujesz noża,Você precisa de uma faca para realizar este ritual,Для этого ритуала нужен нож,Bu ritüeli gerçekleştirmek için bir bıçağa ihtiyacın var,进行此仪式需要一把刀,進行此儀式需要一把刀 +bookSummonZombieGriffin,items,Item,,,,Summon Zombie Griffin,,Zombiegreif beschwören,Invocar Grifo Zombi,Invoquer un Griffon Zombie,Evoca Grifone Zombie,ゾンビグリフォン召喚,좀비 그리핀 소환,Przyzwanie: Gryf Zombie,Invocar Grifo Zumbi,Призыв зомбогрифа,Zombi Grifin Çağırma,召唤僵尸狮鹫,召喚殭屍獅鷲 +bookSummonZombieGriffinDesc,items,Item,,,,A necromantic tome that raises a zombie griffin bound to your will. It fights alongside you against other zombies until it dies. You can only have one bound at a time. Power attack to recall it back into the book.,,"Ein nekromantisches Buch, das einen Zombiegreifen erweckt, der deinem Willen unterworfen ist. Er kämpft an deiner Seite gegen andere Zombies, bis er stirbt. Du kannst immer nur einen gebunden haben. Mit dem Powerangriff rufst du ihn ins Buch zurück.",Un tomo nigromántico que alza un Grifo Zombi sometido a tu voluntad. Lucha a tu lado contra otros zombis hasta morir. Solo puedes tener uno vinculado a la vez. Usa el ataque potente para devolverlo al libro.,Un ouvrage nécromantique qui relève un Griffon Zombie soumis à ta volonté. Il combat à tes côtés contre les autres zombies jusqu'à sa mort. Tu ne peux en lier qu'un seul à la fois. L'attaque puissante le rappelle dans le livre.,Un tomo negromantico che rialza un Grifone Zombie soggetto alla tua volontà. Combatte al tuo fianco contro gli altri zombie finché non muore. Puoi averne vincolato solo uno alla volta. L'attacco potente lo richiama nel libro.,あなたの意志に縛られたゾンビグリフォンを起こす降霊術の書。死ぬまで、あなたの側で他のゾンビと戦う。同時に縛っておけるのは一体だけ。強攻撃で書物へ呼び戻せる。,당신의 의지에 묶인 좀비 그리핀을(를) 일으키는 강령술 서적. 죽을 때까지 당신 곁에서 다른 좀비와 싸운다. 한 번에 하나만 묶어 둘 수 있다. 강공격으로 책 속에 불러들인다.,"Nekromancki tom, który wskrzesza Gryfa Zombie związanego twoją wolą. Walczy u twojego boku z innymi zombie, aż zginie. Naraz możesz mieć związanego tylko jednego. Atak specjalny odwołuje go z powrotem do księgi.",Um tomo necromântico que ergue um Grifo Zumbi preso à sua vontade. Luta ao seu lado contra outros zumbis até morrer. Você só pode ter um vinculado por vez. O ataque forte o chama de volta para o livro.,"Некромантский фолиант, поднимающий зомбогрифа, подчинённого вашей воле. Он сражается на вашей стороне против других зомби, пока не погибнет. Одновременно можно держать только одного. Силовая атака отзывает его обратно в книгу.",İradene bağlı bir Zombi Grifin dirilten bir nekromansi kitabı. Ölene kadar yanında diğer zombilere karşı savaşır. Aynı anda yalnızca bir tanesini bağlı tutabilirsin. Güçlü saldırı onu kitaba geri çağırır.,一本亡灵法术典籍,唤起一只受你意志束缚的僵尸狮鹫。它会在你身边与其他僵尸战斗,直到死亡。同时只能束缚一只。使用强力攻击可将其召回书中。,一本亡靈法術典籍,喚起一隻受你意志束縛的殭屍獅鷲。牠會在你身邊與其他殭屍戰鬥,直到死亡。同時只能束縛一隻。使用強力攻擊可將其召回書中。 +necroZombieGriffinAlreadyActive,items,Item,,,,You already have a Zombie Griffin - use the power attack to recall it,,Du hast bereits einen Zombiegreifen – rufe ihn mit dem Powerangriff zurück,Ya tienes un Grifo Zombi: usa el ataque potente para devolverlo,Tu as déjà un Griffon Zombie – utilise l'attaque puissante pour le rappeler,Hai già un Grifone Zombie: usa l'attacco potente per richiamarlo,すでにゾンビグリフォンがいます – 強攻撃で呼び戻してください,이미 좀비 그리핀이(가) 있습니다 - 강공격으로 불러들이세요,Masz już Gryfa Zombie – odwołaj go atakiem specjalnym,Você já tem um Grifo Zumbi - use o ataque forte para chamá-lo de volta,У вас уже есть зомбогриф - отзовите его силовой атакой,Zaten bir Zombi Grifin var - geri çağırmak için güçlü saldırıyı kullan,你已经有一只僵尸狮鹫了——用强力攻击将其召回,你已經有一隻殭屍獅鷲了——用強力攻擊將其召回 +necroZombieGriffinRecalled,items,Item,,,,Zombie Griffin recalled,,Zombiegreif zurückgerufen,Grifo Zombi devuelto,Griffon Zombie rappelé,Grifone Zombie richiamato,ゾンビグリフォンを呼び戻しました,좀비 그리핀을(를) 불러들였습니다,Odwołano: Gryf Zombie,Grifo Zumbi chamado de volta,Зомбогриф отозван,Zombi Grifin geri çağrıldı,已召回僵尸狮鹫,已召回殭屍獅鷲 +necroZombieGriffinNothingToRecall,items,Item,,,,No Zombie Griffin to recall,,Kein Zombiegreif zum Zurückrufen vorhanden,No hay ningún Grifo Zombi que devolver,Aucun Griffon Zombie à rappeler,Nessun Grifone Zombie da richiamare,呼び戻せるゾンビグリフォンがいません,불러들일 좀비 그리핀이(가) 없습니다,Brak Gryfa Zombie do odwołania,Não há nenhum Grifo Zumbi para chamar de volta,Нет зомбогрифа для отзыва,Geri çağrılacak bir Zombi Grifin yok,没有可召回的僵尸狮鹫,沒有可召回的殭屍獅鷲 +bookSummonZombieBear,items,Item,,,,Summon Zombie Bear,,Zombiebär beschwören,Invocar Oso Zombi,Invoquer un Ours Zombie,Evoca Orso Zombie,ゾンビ熊召喚,좀비 곰 소환,Przyzwanie: Niedźwiedź Zombie,Invocar Urso Zumbi,Призыв зомбомедведя,Zombi Ayı Çağırma,召唤僵尸熊,召喚殭屍熊 +bookSummonZombieBearDesc,items,Item,,,,A necromantic tome that raises a zombie bear bound to your will. It fights alongside you against other zombies until it dies. You can only have one bound at a time. Power attack to recall it back into the book.,,"Ein nekromantisches Buch, das einen Zombiebären erweckt, der deinem Willen unterworfen ist. Er kämpft an deiner Seite gegen andere Zombies, bis er stirbt. Du kannst immer nur einen gebunden haben. Mit dem Powerangriff rufst du ihn ins Buch zurück.",Un tomo nigromántico que alza un Oso Zombi sometido a tu voluntad. Lucha a tu lado contra otros zombis hasta morir. Solo puedes tener uno vinculado a la vez. Usa el ataque potente para devolverlo al libro.,Un ouvrage nécromantique qui relève un Ours Zombie soumis à ta volonté. Il combat à tes côtés contre les autres zombies jusqu'à sa mort. Tu ne peux en lier qu'un seul à la fois. L'attaque puissante le rappelle dans le livre.,Un tomo negromantico che rialza un Orso Zombie soggetto alla tua volontà. Combatte al tuo fianco contro gli altri zombie finché non muore. Puoi averne vincolato solo uno alla volta. L'attacco potente lo richiama nel libro.,あなたの意志に縛られたゾンビ熊を起こす降霊術の書。死ぬまで、あなたの側で他のゾンビと戦う。同時に縛っておけるのは一体だけ。強攻撃で書物へ呼び戻せる。,당신의 의지에 묶인 좀비 곰을(를) 일으키는 강령술 서적. 죽을 때까지 당신 곁에서 다른 좀비와 싸운다. 한 번에 하나만 묶어 둘 수 있다. 강공격으로 책 속에 불러들인다.,"Nekromancki tom, który wskrzesza Niedźwiedzia Zombie związanego twoją wolą. Walczy u twojego boku z innymi zombie, aż zginie. Naraz możesz mieć związanego tylko jednego. Atak specjalny odwołuje go z powrotem do księgi.",Um tomo necromântico que ergue um Urso Zumbi preso à sua vontade. Luta ao seu lado contra outros zumbis até morrer. Você só pode ter um vinculado por vez. O ataque forte o chama de volta para o livro.,"Некромантский фолиант, поднимающий зомбомедведя, подчинённого вашей воле. Он сражается на вашей стороне против других зомби, пока не погибнет. Одновременно можно держать только одного. Силовая атака отзывает его обратно в книгу.",İradene bağlı bir Zombi Ayı dirilten bir nekromansi kitabı. Ölene kadar yanında diğer zombilere karşı savaşır. Aynı anda yalnızca bir tanesini bağlı tutabilirsin. Güçlü saldırı onu kitaba geri çağırır.,一本亡灵法术典籍,唤起一只受你意志束缚的僵尸熊。它会在你身边与其他僵尸战斗,直到死亡。同时只能束缚一只。使用强力攻击可将其召回书中。,一本亡靈法術典籍,喚起一隻受你意志束縛的殭屍熊。牠會在你身邊與其他殭屍戰鬥,直到死亡。同時只能束縛一隻。使用強力攻擊可將其召回書中。 +necroZombieBearAlreadyActive,items,Item,,,,You already have a Zombie Bear - use the power attack to recall it,,Du hast bereits einen Zombiebären – rufe ihn mit dem Powerangriff zurück,Ya tienes un Oso Zombi: usa el ataque potente para devolverlo,Tu as déjà un Ours Zombie – utilise l'attaque puissante pour le rappeler,Hai già un Orso Zombie: usa l'attacco potente per richiamarlo,すでにゾンビ熊がいます – 強攻撃で呼び戻してください,이미 좀비 곰이(가) 있습니다 - 강공격으로 불러들이세요,Masz już Niedźwiedzia Zombie – odwołaj go atakiem specjalnym,Você já tem um Urso Zumbi - use o ataque forte para chamá-lo de volta,У вас уже есть зомбомедведь - отзовите его силовой атакой,Zaten bir Zombi Ayı var - geri çağırmak için güçlü saldırıyı kullan,你已经有一只僵尸熊了——用强力攻击将其召回,你已經有一隻殭屍熊了——用強力攻擊將其召回 +necroZombieBearRecalled,items,Item,,,,Zombie Bear recalled,,Zombiebär zurückgerufen,Oso Zombi devuelto,Ours Zombie rappelé,Orso Zombie richiamato,ゾンビ熊を呼び戻しました,좀비 곰을(를) 불러들였습니다,Odwołano: Niedźwiedź Zombie,Urso Zumbi chamado de volta,Зомбомедведь отозван,Zombi Ayı geri çağrıldı,已召回僵尸熊,已召回殭屍熊 +necroZombieBearNothingToRecall,items,Item,,,,No Zombie Bear to recall,,Kein Zombiebär zum Zurückrufen vorhanden,No hay ningún Oso Zombi que devolver,Aucun Ours Zombie à rappeler,Nessun Orso Zombie da richiamare,呼び戻せるゾンビ熊がいません,불러들일 좀비 곰이(가) 없습니다,Brak Niedźwiedzia Zombie do odwołania,Não há nenhum Urso Zumbi para chamar de volta,Нет зомбомедведя для отзыва,Geri çağrılacak bir Zombi Ayı yok,没有可召回的僵尸熊,沒有可召回的殭屍熊 +bookSummonZombieWolf,items,Item,,,,Summon Zombie Wolf,,Zombiewolf beschwören,Invocar Lobo Zombi,Invoquer un Loup Zombie,Evoca Lupo Zombie,ゾンビ狼召喚,좀비 늑대 소환,Przyzwanie: Wilk Zombie,Invocar Lobo Zumbi,Призыв зомбоволка,Zombi Kurt Çağırma,召唤僵尸狼,召喚殭屍狼 +bookSummonZombieWolfDesc,items,Item,,,,A necromantic tome that raises a zombie wolf bound to your will. It fights alongside you against other zombies until it dies. You can only have one bound at a time. Power attack to recall it back into the book.,,"Ein nekromantisches Buch, das einen Zombiewolf erweckt, der deinem Willen unterworfen ist. Er kämpft an deiner Seite gegen andere Zombies, bis er stirbt. Du kannst immer nur einen gebunden haben. Mit dem Powerangriff rufst du ihn ins Buch zurück.",Un tomo nigromántico que alza un Lobo Zombi sometido a tu voluntad. Lucha a tu lado contra otros zombis hasta morir. Solo puedes tener uno vinculado a la vez. Usa el ataque potente para devolverlo al libro.,Un ouvrage nécromantique qui relève un Loup Zombie soumis à ta volonté. Il combat à tes côtés contre les autres zombies jusqu'à sa mort. Tu ne peux en lier qu'un seul à la fois. L'attaque puissante le rappelle dans le livre.,Un tomo negromantico che rialza un Lupo Zombie soggetto alla tua volontà. Combatte al tuo fianco contro gli altri zombie finché non muore. Puoi averne vincolato solo uno alla volta. L'attacco potente lo richiama nel libro.,あなたの意志に縛られたゾンビ狼を起こす降霊術の書。死ぬまで、あなたの側で他のゾンビと戦う。同時に縛っておけるのは一体だけ。強攻撃で書物へ呼び戻せる。,당신의 의지에 묶인 좀비 늑대을(를) 일으키는 강령술 서적. 죽을 때까지 당신 곁에서 다른 좀비와 싸운다. 한 번에 하나만 묶어 둘 수 있다. 강공격으로 책 속에 불러들인다.,"Nekromancki tom, który wskrzesza Wilka Zombie związanego twoją wolą. Walczy u twojego boku z innymi zombie, aż zginie. Naraz możesz mieć związanego tylko jednego. Atak specjalny odwołuje go z powrotem do księgi.",Um tomo necromântico que ergue um Lobo Zumbi preso à sua vontade. Luta ao seu lado contra outros zumbis até morrer. Você só pode ter um vinculado por vez. O ataque forte o chama de volta para o livro.,"Некромантский фолиант, поднимающий зомбоволка, подчинённого вашей воле. Он сражается на вашей стороне против других зомби, пока не погибнет. Одновременно можно держать только одного. Силовая атака отзывает его обратно в книгу.",İradene bağlı bir Zombi Kurt dirilten bir nekromansi kitabı. Ölene kadar yanında diğer zombilere karşı savaşır. Aynı anda yalnızca bir tanesini bağlı tutabilirsin. Güçlü saldırı onu kitaba geri çağırır.,一本亡灵法术典籍,唤起一只受你意志束缚的僵尸狼。它会在你身边与其他僵尸战斗,直到死亡。同时只能束缚一只。使用强力攻击可将其召回书中。,一本亡靈法術典籍,喚起一隻受你意志束縛的殭屍狼。牠會在你身邊與其他殭屍戰鬥,直到死亡。同時只能束縛一隻。使用強力攻擊可將其召回書中。 +necroZombieWolfAlreadyActive,items,Item,,,,You already have a Zombie Wolf - use the power attack to recall it,,Du hast bereits einen Zombiewolf – rufe ihn mit dem Powerangriff zurück,Ya tienes un Lobo Zombi: usa el ataque potente para devolverlo,Tu as déjà un Loup Zombie – utilise l'attaque puissante pour le rappeler,Hai già un Lupo Zombie: usa l'attacco potente per richiamarlo,すでにゾンビ狼がいます – 強攻撃で呼び戻してください,이미 좀비 늑대이(가) 있습니다 - 강공격으로 불러들이세요,Masz już Wilka Zombie – odwołaj go atakiem specjalnym,Você já tem um Lobo Zumbi - use o ataque forte para chamá-lo de volta,У вас уже есть зомбоволк - отзовите его силовой атакой,Zaten bir Zombi Kurt var - geri çağırmak için güçlü saldırıyı kullan,你已经有一只僵尸狼了——用强力攻击将其召回,你已經有一隻殭屍狼了——用強力攻擊將其召回 +necroZombieWolfRecalled,items,Item,,,,Zombie Wolf recalled,,Zombiewolf zurückgerufen,Lobo Zombi devuelto,Loup Zombie rappelé,Lupo Zombie richiamato,ゾンビ狼を呼び戻しました,좀비 늑대을(를) 불러들였습니다,Odwołano: Wilk Zombie,Lobo Zumbi chamado de volta,Зомбоволк отозван,Zombi Kurt geri çağrıldı,已召回僵尸狼,已召回殭屍狼 +necroZombieWolfNothingToRecall,items,Item,,,,No Zombie Wolf to recall,,Kein Zombiewolf zum Zurückrufen vorhanden,No hay ningún Lobo Zombi que devolver,Aucun Loup Zombie à rappeler,Nessun Lupo Zombie da richiamare,呼び戻せるゾンビ狼がいません,불러들일 좀비 늑대이(가) 없습니다,Brak Wilka Zombie do odwołania,Não há nenhum Lobo Zumbi para chamar de volta,Нет зомбоволка для отзыва,Geri çağrılacak bir Zombi Kurt yok,没有可召回的僵尸狼,沒有可召回的殭屍狼 +thrownStonePortalBlue,items,Item,,,,Blue Portal Stone,,Blauer Portalstein,Piedra de portal azul,Pierre de portail bleue,Pietra del portale blu,青の門石,푸른 관문석,Niebieski kamień portalowy,Pedra de portal azul,Синий портальный камень,Mavi Geçit Taşı,蓝色传送石,藍色傳送石 +thrownStonePortalBlueDesc,items,Item,,,,"A stone humming with necromantic energy. Use it and a ten-second channel begins, at the end of which you are carried back to your bedroll. It breaks if you take damage, and the power attack cancels it deliberately. Not consumed on use.",,"Ein Stein, der von nekromantischer Energie summt. Benutze ihn, und ein zehn Sekunden langes Wirken beginnt, an dessen Ende du zu deinem Schlafsack zurückgetragen wirst. Es bricht ab, wenn du Schaden nimmst, und der Powerangriff bricht es absichtlich ab. Wird beim Gebrauch nicht verbraucht.","Una piedra que zumba con energía nigromántica. Úsala y comenzará una canalización de diez segundos, al final de la cual serás llevado de vuelta a tu saco de dormir. Se rompe si recibes daño, y el ataque potente la cancela a propósito. No se consume al usarla.","Une pierre qui bourdonne d'énergie nécromantique. Utilise-la et une canalisation de dix secondes commence, au terme de laquelle tu es ramené à ton sac de couchage. Elle se rompt si tu subis des dégâts, et l'attaque puissante l'annule volontairement. N'est pas consommée à l'usage.","Una pietra che ronza di energia negromantica. Usala e comincia un incanalamento di dieci secondi, al termine del quale vieni riportato al tuo sacco a pelo. Si spezza se subisci danni, e l'attacco potente lo annulla di proposito. Non si consuma con l'uso.",降霊術のエネルギーで唸る石。使用すると10秒の詠唱が始まり、終わるとあなたは寝袋へ運ばれる。ダメージを受けると中断され、強攻撃を押せば自分から取りやめられる。使用しても失われない。,"강령술의 기운으로 울리는 돌. 사용하면 10초의 시전이 시작되고, 끝나면 침낭으로 옮겨집니다. 피해를 입으면 끊기며, 강공격을 누르면 스스로 취소할 수 있습니다. 사용해도 소모되지 않습니다.","Kamień buczący nekromancką energią. Użyj go, a rozpocznie się dziesięciosekundowe rzucanie, po którym przeniesiesz się do swojego śpiwora. Przerywa się, gdy oberwiesz, a atak specjalny anuluje je celowo. Nie zużywa się przy użyciu.","Uma pedra que zumbe com energia necromântica. Use-a e começa uma canalização de dez segundos, ao fim da qual você é levado de volta ao seu saco de dormir. Ela se rompe se você tomar dano, e o ataque forte a cancela de propósito. Não é consumida ao usar.","Камень, гудящий некромантской энергией. Примените его — начнётся десятисекундное применение, в конце которого вас перенесёт к спальнику. Срывается, если вы получите урон, а силовая атака отменяет его намеренно. Не расходуется при использовании.","Nekromansi enerjisiyle uğuldayan bir taş. Kullandığında on saniyelik bir yönlendirme başlar; sonunda uyku tulumuna taşınırsın. Hasar alırsan kesilir, güçlü saldırı ise onu bilerek iptal eder. Kullanıldığında tükenmez.",一块嗡鸣着亡灵能量的石头。使用后开始十秒引导,结束时你会被带回自己的睡袋。受到伤害会中断,按强力攻击则可主动取消。使用后不消耗。,一塊嗡鳴著亡靈能量的石頭。使用後開始十秒引導,結束時你會被帶回自己的睡袋。受到傷害會中斷,按強力攻擊則可主動取消。使用後不消耗。 +thrownStonePortalBlack,items,Item,,,,Black Portal Stone,,Schwarzer Portalstein,Piedra de portal negra,Pierre de portail noire,Pietra del portale nera,黒の門石,검은 관문석,Czarny kamień portalowy,Pedra de portal negra,Чёрный портальный камень,Kara Geçit Taşı,黑色传送石,黑色傳送石 +thrownStonePortalBlueNoBedroll,items,Item,,,,You have no bedroll set - nowhere to teleport to,,Du hast keinen Schlafsack gesetzt – es gibt kein Ziel zum Teleportieren,No tienes un saco de dormir colocado: no hay adónde teletransportarse,Tu n'as pas placé de sac de couchage – aucun endroit où te téléporter,Non hai posizionato un sacco a pelo: non c'è dove teletrasportarti,寝袋が設置されていません – 転移先がありません,설치된 침낭이 없습니다 - 순간이동할 곳이 없습니다,Nie masz rozłożonego śpiwora – nie ma dokąd się teleportować,Você não tem um saco de dormir colocado - não há para onde se teletransportar,У вас не установлен спальник - телепортироваться некуда,Yerleştirilmiş bir uyku tulumun yok - ışınlanacak bir yer bulunamadı,你尚未放置睡袋——无处可传送,你尚未放置睡袋——無處可傳送 +thrownStonePortalBlackNotBound,items,Item,,,,This stone's destination is not bound yet,,Das Ziel dieses Steins ist noch nicht gebunden,El destino de esta piedra aún no está vinculado,La destination de cette pierre n'est pas encore liée,La destinazione di questa pietra non è ancora vincolata,この石の行き先はまだ結ばれていません,이 돌의 목적지는 아직 연결되지 않았습니다,Cel tego kamienia nie został jeszcze powiązany,O destino desta pedra ainda não foi vinculado,Место назначения этого камня ещё не привязано,Bu taşın varış noktası henüz bağlanmadı,这块石头的目的地尚未绑定,這塊石頭的目的地尚未綁定 +thrownStonePortalBlackConfirmTitle,items,Item,,,,Black Portal,,Schwarzes Portal,Portal negro,Portail noir,Portale nero,黒の門,검은 관문,Czarny portal,Portal negro,Чёрный портал,Kara Geçit,黑色传送门,黑色傳送門 +thrownStonePortalBlackConfirmText,items,Item,,,,Are you sure you want to activate the black portal?,,Willst du das schwarze Portal wirklich aktivieren?,¿Seguro que quieres activar el portal negro?,Es-tu sûr de vouloir activer le portail noir ?,Sei sicuro di voler attivare il portale nero?,黒の門を起動してよろしいですか?,검은 관문을 정말 여시겠습니까?,Czy na pewno chcesz aktywować czarny portal?,Tem certeza de que deseja ativar o portal negro?,"Вы уверены, что хотите активировать чёрный портал?",Kara geçidi etkinleştirmek istediğine emin misin?,确定要启动黑色传送门吗?,確定要啟動黑色傳送門嗎? +thrownStonePortalBlueChanneling,items,Item,,,,Opening a portal to your bedroll...,,Portal zum Schlafsack wird geöffnet ...,Abriendo un portal a tu saco de dormir...,Ouverture d'un portail vers ton sac de couchage...,Apertura di un portale verso il tuo sacco a pelo...,寝袋への門を開いています……,침낭으로 향하는 관문을 여는 중...,Otwieranie portalu do śpiwora...,Abrindo um portal para o seu saco de dormir...,Открытие портала к спальнику...,Uyku tulumuna geçit açılıyor...,正在开启通往睡袋的传送门……,正在開啟通往睡袋的傳送門…… +thrownStonePortalBlackChanneling,items,Item,,,,Channeling...,,Wird gewirkt ...,Canalizando...,Canalisation...,Incanalamento...,詠唱中……,시전 중...,Kanalizowanie...,Canalizando...,Применение...,Yönlendiriliyor...,引导中……,引導中…… +bookBanshee,items,Item,,,,Banshee's Scroll,,Schriftrolle der Banshee,Pergamino de la banshee,Parchemin de la banshee,Pergamena della banshee,バンシーの巻物,밴시의 두루마리,Zwój banshee,Pergaminho da banshee,Свиток банши,Banshee Tomarı,报丧女妖卷轴,報喪女妖卷軸 +bookBansheeDesc,items,Item,,,,"A necromantic scroll that screams with a banshee's voice when opened, consuming itself and summoning a small hostile horde of zombies near you. Not a summoned ally - these zombies are as dangerous to you as any other. Consumed on use.",,"Eine nekromantische Schriftrolle, die beim Öffnen mit der Stimme einer Banshee schreit, sich dabei selbst verzehrt und in deiner Nähe eine kleine feindliche Zombiehorde herbeiruft. Keine verbündete Beschwörung – diese Zombies sind für dich so gefährlich wie alle anderen. Wird beim Gebrauch verbraucht.","Un pergamino nigromántico que grita con voz de banshee al abrirse, se consume a sí mismo e invoca una pequeña horda hostil de zombis cerca de ti. No son aliados invocados: estos zombis son tan peligrosos para ti como cualquier otro. Se consume al usarlo.","Un parchemin nécromantique qui hurle d'une voix de banshee quand on l'ouvre, se consume et invoque une petite horde de zombies hostiles près de toi. Ce ne sont pas des alliés invoqués : ces zombies sont aussi dangereux pour toi que n'importe quels autres. Consommé à l'usage.","Una pergamena negromantica che, una volta aperta, urla con la voce di una banshee, si consuma da sé ed evoca una piccola orda ostile di zombie vicino a te. Non sono alleati evocati: questi zombie sono pericolosi per te quanto tutti gli altri. Si consuma con l'uso.",開くとバンシーの声で叫び、自らを焼き尽くしながら、あなたの近くに小規模な敵対ゾンビの群れを呼び寄せる降霊術の巻物。召喚された味方ではない。これらのゾンビは他のゾンビと同じくあなたにとって危険だ。使用すると失われる。,"펼치면 밴시의 목소리로 비명을 지르며 스스로를 태워, 당신 근처에 적대적인 소규모 좀비 무리를 불러내는 강령술 두루마리. 소환된 아군이 아닙니다. 이 좀비들은 다른 좀비와 똑같이 당신에게 위험합니다. 사용하면 소모됩니다.","Nekromancki zwój, który po otwarciu krzyczy głosem banshee, sam się spala i przyzywa w twoim pobliżu niewielką wrogą hordę zombie. To nie są przyzwani sojusznicy – te zombie są dla ciebie tak samo groźne jak wszystkie inne. Zużywa się przy użyciu.","Um pergaminho necromântico que grita com a voz de uma banshee ao ser aberto, consumindo a si mesmo e invocando uma pequena horda hostil de zumbis perto de você. Não são aliados invocados: esses zumbis são tão perigosos para você quanto quaisquer outros. Consumido ao usar.","Некромантский свиток, вопящий голосом банши при открытии - расходуется при использовании и призывает небольшую враждебную орду зомби рядом с вами. Это не союзники - эти зомби так же опасны для вас, как и любые другие.","Açıldığında bir banshee sesiyle çığlık atan, kendini tüketen ve yakınında küçük bir düşman zombi sürüsü çağıran bir nekromansi tomarı. Bunlar çağrılmış müttefikler değildir; bu zombiler senin için diğerleri kadar tehlikelidir. Kullanıldığında tükenir.",一卷亡灵法术卷轴,展开时会以报丧女妖之声尖啸,随即自我焚尽,并在你附近召来一小群敌对僵尸。它们不是召唤来的盟友——这些僵尸对你和其他僵尸一样危险。使用后消耗。,一卷亡靈法術卷軸,展開時會以報喪女妖之聲尖嘯,隨即自我焚盡,並在你附近召來一小群敵對殭屍。牠們不是召喚來的盟友——這些殭屍對你和其他殭屍一樣危險。使用後消耗。 +thrownStonePortalBlackDesc,items,Item,,,,A stone humming with a far darker necromantic energy than its blue cousin. Use it and a ten-second channel begins; at the end you confirm you are ready to go through with it. What waits on the other side is for you to find out. The stone does not survive the crossing.,,"Ein Stein, der von einer weit dunkleren nekromantischen Energie summt als sein blauer Verwandter. Benutze ihn, und ein zehn Sekunden langes Wirken beginnt; am Ende bestätigst du, dass du es wirklich durchziehen willst. Was auf der anderen Seite wartet, wirst du selbst herausfinden. Der Stein übersteht den Übergang nicht.",Una piedra que zumba con una energía nigromántica mucho más oscura que la de su prima azul. Úsala y comenzará una canalización de diez segundos; al final confirmas que estás dispuesto a llevarlo hasta el final. Lo que aguarda al otro lado lo descubrirás tú mismo. La piedra no sobrevive al cruce.,"Une pierre qui bourdonne d'une énergie nécromantique bien plus sombre que celle de sa cousine bleue. Utilise-la et une canalisation de dix secondes commence ; au terme, tu confirmes que tu es prêt à aller jusqu'au bout. Ce qui attend de l'autre côté, tu le découvriras toi-même. La pierre ne survit pas à la traversée.",Una pietra che ronza di un'energia negromantica assai più oscura di quella della sua cugina blu. Usala e comincia un incanalamento di dieci secondi; al termine confermi di voler andare fino in fondo. Cosa attenda dall'altra parte lo scoprirai da solo. La pietra non sopravvive alla traversata.,青の門石よりはるかに暗い降霊術のエネルギーで唸る石。使用すると10秒の詠唱が始まり、終わったところで本当に踏み切るかを確認する。向こう側に何が待っているかは、自分の目で確かめることになる。この石は渡りきることができない。,"푸른 관문석보다 훨씬 어두운 강령술의 기운으로 울리는 돌. 사용하면 10초의 시전이 시작되고, 끝나면 정말로 끝까지 갈 것인지 확인하게 됩니다. 저편에 무엇이 기다리는지는 직접 확인하게 됩니다. 돌은 그 건넘을 견디지 못합니다.","Kamień buczący nekromancką energią o wiele mroczniejszą niż jego niebieski kuzyn. Użyj go, a rozpocznie się dziesięciosekundowe rzucanie; na koniec potwierdzasz, że naprawdę chcesz to zrobić. Co czeka po drugiej stronie, przekonasz się sam. Kamień nie przeżywa tego przejścia.","Uma pedra que zumbe com uma energia necromântica muito mais sombria que a de sua prima azul. Use-a e começa uma canalização de dez segundos; ao fim, você confirma que está disposto a ir até o fim. O que aguarda do outro lado você mesmo vai descobrir. A pedra não sobrevive à travessia.","Камень, гудящий гораздо более тёмной некромантской энергией, чем его синий собрат. Примените его — начнётся десятисекундное применение, в конце которого нужно подтвердить, что вы готовы идти до конца. Что ждёт на той стороне — увидите сами. Камень этого перехода не переживает.",Mavi akrabasından çok daha karanlık bir nekromansi enerjisiyle uğuldayan bir taş. Kullandığında on saniyelik bir yönlendirme başlar; sonunda sonuna kadar gitmeye hazır olduğunu onaylarsın. Öteki tarafta neyin beklediğini kendin göreceksin. Taş bu geçişten sağ çıkmaz.,一块嗡鸣着远比蓝色同类更幽暗的亡灵能量的石头。使用后开始十秒引导,结束时需确认你真要走到底。彼岸等着什么,要你自己去看。这块石头撑不过这一次穿越。,一塊嗡鳴著遠比藍色同類更幽暗的亡靈能量的石頭。使用後開始十秒引導,結束時需確認你真要走到底。彼岸等著什麼,要你自己去看。這塊石頭撐不過這一次穿越。 +necroZombieGriffin,entityclasses,Entity,,,,Zombie Griffin,,Zombiegreif,Grifo Zombi,Griffon Zombie,Grifone Zombie,ゾンビグリフォン,좀비 그리핀,Gryf Zombie,Grifo Zumbi,Зомбогриф,Zombi Grifin,僵尸狮鹫,殭屍獅鷲 +necroZombieBear,entityclasses,Entity,,,,Zombie Bear,,Zombiebär,Oso Zombi,Ours Zombie,Orso Zombie,ゾンビ熊,좀비 곰,Niedźwiedź Zombie,Urso Zumbi,Зомбомедведь,Zombi Ayı,僵尸熊,殭屍熊 +necroZombieWolf,entityclasses,Entity,,,,Zombie Wolf,,Zombiewolf,Lobo Zombi,Loup Zombie,Lupo Zombie,ゾンビ狼,좀비 늑대,Wilk Zombie,Lobo Zumbi,Зомбоволк,Zombi Kurt,僵尸狼,殭屍狼 +EntityLootContainerVictim,entityclasses,Entity Info,,,,Victim's Bag,,Beutel des Opfers,Bolsa de la víctima,Sac de la victime,Sacco della vittima,犠牲者の袋,희생자의 자루,Worek ofiary,Saco da vítima,Мешок жертвы,Kurbanın Çantası,祭品之袋,祭品之袋 +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.,一只亡灵法术手环。使用强力攻击可撕开一道私人储物裂隙——其容量随亡灵法术技能增长。普通攻击目前没有任何作用。,一只亡靈法術手環。使用強力攻擊可撕開一道私人儲物裂隙——其容量隨亡靈法術技能增長。普通攻擊目前沒有任何作用。 +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,视野中没有僵尸,視野中沒有殭屍 +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ç,启用效果,啟用效果 +blockcommand_TEFeaturePyramidWard:effect_off,blocks,Block,,,,Disable Effect,,Effekt deaktivieren,Desactivar efecto,Désactiver l'effet,Disattiva effetto,効果をオフ,효과 끄기,Wyłącz efekt,Desativar efeito,Отключить эффект,Etkiyi Kapat,停用效果,停用效果 +blockcommand_TEFeaturePyramidWard:zone_show,blocks,Block,,,,Show Zone,,Zone anzeigen,Mostrar zona,Afficher la zone,Mostra zona,範囲を表示,구역 표시,Pokaż strefę,Mostrar zona,Показать зону,Bölgeyi Göster,显示范围,顯示範圍 +blockcommand_TEFeaturePyramidWard:zone_hide,blocks,Block,,,,Hide Zone,,Zone ausblenden,Ocultar zona,Masquer la zone,Nascondi zona,範囲を非表示,구역 숨기기,Ukryj strefę,Ocultar zona,Скрыть зону,Bölgeyi Gizle,隐藏范围,隱藏範圍 +blockcommand_effect_on,blocks,Block,,,,Enable Effect,,Effekt aktivieren,Activar efecto,Activer l'effet,Attiva effetto,効果をオン,효과 켜기,Włącz efekt,Ativar efeito,Включить эффект,Etkiyi Aç,启用效果,啟用效果 +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,隐藏范围,隱藏範圍 +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,食腐者的盛宴,食腐者的盛宴 +necroModKnifeScavengersFeastDesc,item_modifiers,Item,,,,"A modification for the Necromancer's Knife alone. The blade takes its own share of every zombie it kills and passes it to you: 2 units of food per corpse. Best not to think about it too closely. 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. Die Klinge nimmt sich von jedem getöteten Zombie ihren Anteil und reicht ihn an dich weiter: 2 Einheiten Nahrung pro Leiche. Denk lieber nicht zu genau darüber nach. Gewöhnliche Messermodifikationen passen nicht in diese Waffe, und diese hier passt in keine andere.","Una modificación exclusiva para el Cuchillo del nigromante. La hoja toma su parte de cada zombi que mata y te la pasa: 2 unidades de comida por cadáver. Mejor no pensarlo demasiado. 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. La lame prélève sa part sur chaque zombie qu'elle tue et te la transmet : 2 unités de nourriture par cadavre. Mieux vaut ne pas y penser de trop près. 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. La lama si prende la sua parte da ogni zombie che uccide e te la passa: 2 unità di cibo per cadavere. Meglio non pensarci troppo. 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. Ostrze bierze swoją część z każdego zabitego zombie i przekazuje ją tobie: 2 jednostki jedzenia na zwłoki. Lepiej nie zastanawiać się nad tym zbyt dokładnie. 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. A lâmina tira sua parte de cada zumbi que mata e a repassa a você: 2 unidades de comida por cadáver. Melhor não pensar muito a respeito. 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ıçak öldürdüğü her zombiden kendi payını alır ve sana aktarır: ceset başına 2 birim yiyecek. Üzerinde fazla düşünmesen iyi olur. Sıradan bıçak modifikasyonları bu silaha takılmaz, bu da başka hiçbir silaha takılmaz.",亡灵法师之刃专用的改装件。此刃会从它杀死的每一只僵尸身上取走自己的那一份,再交给你:每具尸体 2 单位食物。最好别细想。寻常的匕首改装件装不进这把武器,而这一件也装不进其他任何武器。,亡靈法師之刃專用的改裝件。此刃會從牠殺死的每一隻殭屍身上取走自己的那一份,再交給你:每具屍體 2 單位食物。最好別細想。尋常的匕首改裝件裝不進這把武器,而這一件也裝不進其他任何武器。 +necroModKnifeDeadMansGrip,item_modifiers,Item,,,,Dead Man's Grip,,Griff des Toten,Agarre del muerto,Poigne du mort,Presa del morto,死者の握り,죽은 자의 손아귀,Uścisk umarlaka,Aperto do morto,Хватка мертвеца,Ölünün Kavrayışı,亡者之握,亡者之握 +necroModKnifeDeadMansGripDesc,item_modifiers,Item,,,,"A modification for the Necromancer's Knife alone. Bone worked into the grip by a hand that is no longer yours. Every zombie the blade wounds is slowed, as though something underneath were holding on to it. 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. Knochen, von einer Hand in den Griff eingearbeitet, die nicht mehr die deine ist. Jeder Zombie, den die Klinge verwundet, wird langsamer, als hielte ihn etwas von unten fest. Gewöhnliche Messermodifikationen passen nicht in diese Waffe, und diese hier passt in keine andere.","Una modificación exclusiva para el Cuchillo del nigromante. Hueso trabajado en la empuñadura por una mano que ya no es la tuya. Todo zombi al que la hoja hiera se ralentiza, como si algo lo sujetara desde abajo. 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. De l'os travaillé dans la poignée par une main qui n'est plus la tienne. Chaque zombie blessé par la lame est ralenti, comme si quelque chose le retenait par en dessous. 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. Osso lavorato nell'impugnatura da una mano che non è più la tua. Ogni zombie ferito dalla lama viene rallentato, come se qualcosa lo trattenesse da sotto. Le modifiche per coltelli comuni non entrano in quest'arma, e questa non entra in nessun'altra.",ネクロマンサーのナイフ専用の改造パーツ。もはやあなたのものではない手が柄に編み込んだ骨。この刃に傷つけられたゾンビは、下から何かに掴まれているかのように動きが鈍る。通常のナイフ用改造パーツはこの武器に取り付けられず、これも他のどんな武器にも取り付けられない。,"강령술사의 단검 전용 개조 부품입니다. 더 이상 당신의 것이 아닌 손이 손잡이에 엮어 넣은 뼈. 칼날에 상처 입은 좀비는 무언가가 아래에서 붙잡기라도 하듯 느려집니다. 일반 단검용 부품은 이 무기에 장착되지 않으며, 이 부품도 다른 무기에는 장착되지 않습니다.","Modyfikacja wyłącznie do Noża nekromanty. Kość wpleciona w rękojeść przez dłoń, która nie jest już twoja. Każde zombie zranione tym ostrzem zwalnia, jakby coś trzymało je od spodu. 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. Osso trabalhado no cabo por uma mão que já não é sua. Todo zumbi ferido pela lâmina fica mais lento, como se algo o segurasse por baixo. Modificações comuns de faca não servem nesta arma, e esta não serve em nenhuma outra.","Модификация только для Ножа некроманта. Кость, вплетённая в рукоять рукой, которая уже не ваша. Каждый раненный клинком зомби замедляется, будто что-то снизу держит его. Обычные моды для ножей в это оружие не встанут, а этот не встанет ни во что другое.","Yalnızca Nekromantın Bıçağı'na özel bir modifikasyon. Artık senin olmayan bir elin sapa işlediği kemik. Bıçağın yaraladığı her zombi, sanki altından bir şey onu tutuyormuş gibi yavaşlar. Sıradan bıçak modifikasyonları bu silaha takılmaz, bu da başka hiçbir silaha takılmaz.",亡灵法师之刃专用的改装件。由一只不再属于你的手编入柄中的骨。被此刃所伤的僵尸都会变慢,仿佛底下有什么东西攥着它。寻常的匕首改装件装不进这把武器,而这一件也装不进其他任何武器。,亡靈法師之刃專用的改裝件。由一隻不再屬於你的手編入柄中的骨。被此刃所傷的殭屍都會變慢,彷彿底下有什麼東西攥著牠。尋常的匕首改裝件裝不進這把武器,而這一件也裝不進其他任何武器。 +necroModKnifeGravesRepose,item_modifiers,Item,,,,Grave's Repose,,Ruhe des Grabes,Reposo de la tumba,Repos de la tombe,Quiete della tomba,墓の安らぎ,무덤의 안식,Grobowy spokój,Repouso da sepultura,Могильный покой,Mezarın Huzuru,墓中安眠,墓中安眠 +necroModKnifeGravesReposeDesc,item_modifiers,Item,,,,"A modification for the Necromancer's Knife alone. A grave is neither hot nor cold, and the blade shares that indifference with whoever carries it: strong protection from both freezing and heat. Works only while the knife is in your hands - sheathe it and the cold finds you again. 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. Ein Grab ist weder heiß noch kalt, und die Klinge teilt diese Gleichgültigkeit mit dem, der sie trägt: starker Schutz gegen Erfrieren wie gegen Hitze. Wirkt nur, solange du das Messer in Händen hältst – steck es weg, und die Kälte findet dich wieder. Gewöhnliche Messermodifikationen passen nicht in diese Waffe, und diese hier passt in keine andere.","Una modificación exclusiva para el Cuchillo del nigromante. Una tumba no es ni cálida ni fría, y la hoja comparte esa indiferencia con quien la lleva: fuerte protección tanto contra el frío como contra el calor. Solo funciona mientras tengas el cuchillo en las manos: guárdalo y el frío volverá a encontrarte. 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. Une tombe n'est ni chaude ni froide, et la lame partage cette indifférence avec celui qui la porte : forte protection contre le gel comme contre la chaleur. N'agit que tant que le couteau est en main – range-le, et le froid te retrouve. 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. Una tomba non è né calda né fredda, e la lama condivide quell'indifferenza con chi la porta: forte protezione sia dal gelo che dal caldo. Funziona solo finché tieni il coltello in mano: riponilo e il freddo ti ritrova. Le modifiche per coltelli comuni non entrano in quest'arma, e questa non entra in nessun'altra.",ネクロマンサーのナイフ専用の改造パーツ。墓は暑くも寒くもない。この刃は、その無関心を持ち主にも分け与える。凍えにも熱にも強い守りだ。ナイフを手にしているあいだしか働かない。しまえば、寒さはまたあなたを見つけ出す。通常のナイフ用改造パーツはこの武器に取り付けられず、これも他のどんな武器にも取り付けられない。,"강령술사의 단검 전용 개조 부품입니다. 무덤은 덥지도 춥지도 않으며, 칼날은 그 무심함을 지닌 자와 나눕니다. 추위와 더위 모두에 대한 강력한 보호입니다. 단검을 손에 들고 있는 동안에만 작동합니다. 집어넣으면 추위가 다시 당신을 찾아냅니다. 일반 단검용 부품은 이 무기에 장착되지 않으며, 이 부품도 다른 무기에는 장착되지 않습니다.","Modyfikacja wyłącznie do Noża nekromanty. Grób nie jest ani gorący, ani zimny, a ostrze dzieli tę obojętność z tym, kto je niesie: silna ochrona zarówno przed mrozem, jak i przed upałem. Działa tylko wtedy, gdy trzymasz nóż w dłoniach – schowaj go, a chłód znów cię znajdzie. 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. Uma sepultura não é quente nem fria, e a lâmina divide essa indiferença com quem a carrega: forte proteção tanto contra o frio quanto contra o calor. Funciona apenas enquanto a faca estiver em suas mãos - guarde-a e o frio volta a te encontrar. Modificações comuns de faca não servem nesta arma, e esta não serve em nenhuma outra.","Модификация только для Ножа некроманта. В могиле не бывает ни жары, ни холода, и клинок делится этим безразличием с тем, кто его несёт: серьёзная защита и от переохлаждения, и от перегрева. Работает, только пока нож у вас в руках - уберите его, и холод снова вас найдёт. Обычные моды для ножей в это оружие не встанут, а этот не встанет ни во что другое.","Yalnızca Nekromantın Bıçağı'na özel bir modifikasyon. Bir mezar ne sıcaktır ne soğuk; bıçak da bu kayıtsızlığı onu taşıyanla paylaşır: hem dondurucu soğuğa hem de sıcağa karşı güçlü koruma. Yalnızca bıçak elindeyken çalışır; kaldırdığın anda soğuk seni yeniden bulur. Sıradan bıçak modifikasyonları bu silaha takılmaz, bu da başka hiçbir silaha takılmaz.",亡灵法师之刃专用的改装件。坟墓不冷不热,而此刃将这份漠然分给持有它的人:对严寒与酷热皆有强力防护。只在刀握在手中时生效——收起它,寒意便会再度找上你。寻常的匕首改装件装不进这把武器,而这一件也装不进其他任何武器。,亡靈法師之刃專用的改裝件。墳墓不冷不熱,而此刃將這份漠然分給持有牠的人:對嚴寒與酷熱皆有強力防護。只在刀握在手中時生效——收起牠,寒意便會再度找上你。尋常的匕首改裝件裝不進這把武器,而這一件也裝不進其他任何武器。 +necroModKnifeDeadStorm,item_modifiers,Item,,,,Dead Storm,,Totensturm,Tormenta muerta,Tempête morte,Tempesta morta,死の嵐,죽음의 폭풍,Martwa burza,Tempestade morta,Мёртвая буря,Ölü Fırtına,死亡风暴,死亡風暴 +necroModKnifeDeadStormDesc,item_modifiers,Item,,,,"A modification for the Necromancer's Knife alone. It rebuilds the power attack into a storm: everything undead around your target is thrown down, opened up and left bleeding, and wrapped in crawling sparks. The knife takes 10 of your health for it instead of 5, and the swing costs twice the stamina. 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. Sie baut den Powerangriff zu einem Sturm um: alles Untote rings um dein Ziel wird zu Boden geworfen, aufgerissen, blutend zurückgelassen und in kriechende Funken gehüllt. Das Messer nimmt dir dafür 10 statt 5 Lebenspunkte, und der Hieb kostet doppelt so viel Ausdauer. Gewöhnliche Messermodifikationen passen nicht in diese Waffe, und diese hier passt in keine andere.","Una modificación exclusiva para el Cuchillo del nigromante. Reconstruye el ataque potente convirtiéndolo en una tormenta: todo lo no muerto alrededor de tu objetivo es derribado, abierto en canal y dejado sangrando, y envuelto en chispas reptantes. El cuchillo te cobra por ello 10 puntos de salud en vez de 5, y el golpe cuesta el doble de resistencia. 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. Elle transforme l'attaque puissante en tempête : tout ce qui est mort-vivant autour de ta cible est jeté à terre, ouvert et laissé en sang, puis enveloppé d'étincelles rampantes. Le couteau te prend pour cela 10 points de vie au lieu de 5, et le coup coûte deux fois plus d'endurance. 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. Trasforma l'attacco potente in una tempesta: tutto ciò che è non morto attorno al tuo bersaglio viene atterrato, squarciato e lasciato a sanguinare, e avvolto in scintille striscianti. Il coltello ti costa 10 punti salute invece di 5, e il colpo consuma il doppio della stamina. Le modifiche per coltelli comuni non entrano in quest'arma, e questa non entra in nessun'altra.",ネクロマンサーのナイフ専用の改造パーツ。強攻撃を嵐へと作り替える。標的の周囲の不死者はすべて薙ぎ倒され、切り裂かれて出血し、這う火花に包まれる。ナイフはその代償として、5ではなく10の体力を奪い、一振りに倍のスタミナを要求する。通常のナイフ用改造パーツはこの武器に取り付けられず、これも他のどんな武器にも取り付けられない。,"강령술사의 단검 전용 개조 부품입니다. 강공격을 폭풍으로 재구성합니다. 대상 주변의 모든 언데드가 쓰러지고, 찢겨 피를 흘리며, 기어다니는 불꽃에 휩싸입니다. 그 대가로 단검은 체력 5 대신 10을 가져가고, 휘두르는 데 두 배의 스태미나가 듭니다. 일반 단검용 부품은 이 무기에 장착되지 않으며, 이 부품도 다른 무기에는 장착되지 않습니다.","Modyfikacja wyłącznie do Noża nekromanty. Przebudowuje atak specjalny w burzę: wszystko, co nieumarłe wokół twojego celu, zostaje zwalone z nóg, rozcięte, pozostawione krwawiące i owinięte pełzającymi iskrami. Nóż bierze za to 10 punktów twojego zdrowia zamiast 5, a zamach kosztuje dwa razy więcej wytrzymałości. 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. Reconstrói o ataque forte em uma tempestade: tudo o que for morto-vivo ao redor do seu alvo é derrubado, rasgado e deixado sangrando, e envolto em faíscas rastejantes. A faca cobra por isso 10 pontos da sua vida em vez de 5, e o golpe custa o dobro de estamina. Modificações comuns de faca não servem nesta arma, e esta não serve em nenhuma outra.","Модификация только для Ножа некроманта. Она перестраивает силовую атаку в бурю: всё немёртвое вокруг вашей цели сбивается с ног, вскрывается кровотечением и заволакивается ползущими искрами. Нож берёт за это 10 единиц вашего здоровья вместо 5, а сам замах стоит вдвое больше выносливости. Обычные моды для ножей в это оружие не встанут, а этот не встанет ни во что другое.","Yalnızca Nekromantın Bıçağı'na özel bir modifikasyon. Güçlü saldırıyı bir fırtınaya dönüştürür: hedefinin çevresindeki her ölümsüz yere serilir, yarılır, kanamaya bırakılır ve sürünen kıvılcımlara sarılır. Bıçak bunun için senden 5 yerine 10 can alır ve savurma iki katı dayanıklılığa mal olur. Sıradan bıçak modifikasyonları bu silaha takılmaz, bu da başka hiçbir silaha takılmaz.",亡灵法师之刃专用的改装件。它将强力攻击重塑为一场风暴:目标周围一切不死之物都会被掀翻、割裂、留下流血,并被爬行的电火花裹住。为此,此刃从你身上取走 10 点生命而非 5 点,且这一挥消耗双倍耐力。寻常的匕首改装件装不进这把武器,而这一件也装不进其他任何武器。,亡靈法師之刃專用的改裝件。牠將強力攻擊重塑為一場風暴:目標周圍一切不死之物都會被掀翻、割裂、留下流血,並被爬行的電火花裹住。為此,此刃從你身上取走 10 點生命而非 5 點,且這一揮消耗雙倍耐力。尋常的匕首改裝件裝不進這把武器,而這一件也裝不進其他任何武器。 +necroModKnifeDarkSense,item_modifiers,Item,,,,Dark Sense,,Dunkler Sinn,Sentido oscuro,Sens obscur,Senso oscuro,闇の感覚,어둠의 감각,Mroczny zmysł,Sentido sombrio,Тёмное чутьё,Kara Sezgi,幽暗感知,幽暗感知 +necroModKnifeDarkSenseDesc,item_modifiers,Item,,,,"A modification for the Necromancer's Knife alone. The dead cannot hide from one who commands them: while the knife is in your hands, every zombie nearby is marked on your compass and map, the way a cleared building marks its sleepers. Sheathe the knife and the sense goes dark. 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. Vor dem, der ihnen gebietet, können sich die Toten nicht verbergen: solange du das Messer in Händen hältst, wird jeder Zombie in der Nähe auf deinem Kompass und deiner Karte markiert, so wie ein geräumtes Gebäude seine Schläfer markiert. Steck das Messer weg, und der Sinn erlischt. Gewöhnliche Messermodifikationen passen nicht in diese Waffe, und diese hier passt in keine andere.","Una modificación exclusiva para el Cuchillo del nigromante. Los muertos no pueden esconderse de quien los gobierna: mientras tengas el cuchillo en las manos, todos los zombis cercanos aparecen marcados en tu brújula y en tu mapa, igual que un edificio despejado marca a sus durmientes. Guarda el cuchillo y el sentido se apaga. 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. Les morts ne peuvent se cacher de celui qui les commande : tant que le couteau est en main, chaque zombie proche est marqué sur ta boussole et sur ta carte, comme un bâtiment nettoyé marque ses dormeurs. Range le couteau et le sens s'éteint. 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. I morti non possono nascondersi da chi li comanda: finché tieni il coltello in mano, ogni zombie vicino viene segnato sulla tua bussola e sulla tua mappa, proprio come un edificio ripulito segnala i suoi dormienti. Riponi il coltello e il senso si spegne. Le modifiche per coltelli comuni non entrano in quest'arma, e questa non entra in nessun'altra.",ネクロマンサーのナイフ専用の改造パーツ。死者は、己を統べる者から身を隠せない。ナイフを手にしているあいだ、近くのゾンビはすべて、掃討済みの建物が眠り手を示すのと同じように、コンパスとマップ上に印される。ナイフをしまえば、その感覚は消える。通常のナイフ用改造パーツはこの武器に取り付けられず、これも他のどんな武器にも取り付けられない。,"강령술사의 단검 전용 개조 부품입니다. 죽은 자들은 자신을 다스리는 이에게서 숨을 수 없습니다. 단검을 손에 들고 있는 동안, 근처의 모든 좀비가 소탕한 건물이 잠든 자들을 표시하듯 나침반과 지도에 표시됩니다. 단검을 집어넣으면 그 감각은 꺼집니다. 일반 단검용 부품은 이 무기에 장착되지 않으며, 이 부품도 다른 무기에는 장착되지 않습니다.","Modyfikacja wyłącznie do Noża nekromanty. Umarli nie ukryją się przed tym, kto nimi włada: dopóki trzymasz nóż w dłoniach, każde pobliskie zombie jest zaznaczone na twoim kompasie i mapie, tak samo jak oczyszczony budynek zaznacza swoich śpiących. Schowaj nóż, a zmysł gaśnie. 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. Os mortos não conseguem se esconder de quem os comanda: enquanto a faca estiver em suas mãos, todo zumbi por perto é marcado na sua bússola e no seu mapa, do mesmo jeito que um prédio limpo marca seus adormecidos. Guarde a faca e o sentido se apaga. Modificações comuns de faca não servem nesta arma, e esta não serve em nenhuma outra.","Модификация только для Ножа некроманта. Мёртвые не укроются от того, кто ими повелевает: пока нож у вас в руках, каждый зомби поблизости отмечен на вашем компасе и карте - так же, как спящие в здании во время зачистки. Уберите нож, и чутьё угаснет. Обычные моды для ножей в это оружие не встанут, а этот не встанет ни во что другое.","Yalnızca Nekromantın Bıçağı'na özel bir modifikasyon. Ölüler, kendilerine hükmedenden saklanamaz: bıçak elindeyken yakındaki her zombi, temizlenmiş bir binanın uyuyanlarını gösterdiği gibi pusulanda ve haritanda işaretlenir. Bıçağı kaldırdığında bu sezgi söner. Sıradan bıçak modifikasyonları bu silaha takılmaz, bu da başka hiçbir silaha takılmaz.",亡灵法师之刃专用的改装件。死者无法躲开号令他们的人:只要刀握在手中,附近的每一只僵尸都会被标注在你的罗盘与地图上,正如清剿过的建筑会标出其中的沉睡者。收起刀,这份感知便会熄灭。寻常的匕首改装件装不进这把武器,而这一件也装不进其他任何武器。,亡靈法師之刃專用的改裝件。死者無法躲開號令他們的人:只要刀握在手中,附近的每一隻殭屍都會被標註在你的羅盤與地圖上,正如清剿過的建築會標出其中的沉睡者。收起刀,這份感知便會熄滅。尋常的匕首改裝件裝不進這把武器,而這一件也裝不進其他任何武器。 +buffNecroDarkSenseName,buffs,Buff,,,,Dark Sense,,Dunkler Sinn,Sentido oscuro,Sens obscur,Senso oscuro,闇の感覚,어둠의 감각,Mroczny zmysł,Sentido sombrio,Тёмное чутьё,Kara Sezgi,幽暗感知,幽暗感知 +buffNecroDarkSenseDesc,buffs,Buff,,,,Every zombie near you is marked on your compass and map.,,Jeder Zombie in deiner Nähe wird auf deinem Kompass und deiner Karte markiert.,Todos los zombis cercanos aparecen marcados en tu brújula y en tu mapa.,Chaque zombie proche est marqué sur ta boussole et sur ta carte.,Ogni zombie vicino a te è segnato sulla tua bussola e sulla tua mappa.,近くのゾンビはすべて、コンパスとマップ上に印される。,근처의 모든 좀비가 나침반과 지도에 표시됩니다.,Każde zombie w twoim pobliżu jest zaznaczone na kompasie i mapie.,Todo zumbi perto de você é marcado na sua bússola e no seu mapa.,Каждый зомби рядом с вами отмечен на компасе и карте.,Yakınındaki her zombi pusulanda ve haritanda işaretlenir.,你附近的每一只僵尸都会标注在罗盘与地图上。,你附近的每一隻殭屍都會標註在羅盤與地圖上。 +necroFinalPage1Title,XUi,Menu,,,,The Return,,Die Rückkehr,El regreso,Le retour,Il ritorno,帰還,귀환,Powrót,O retorno,Возвращение,Dönüş,归来,歸來 +necroFinalPage1Text,XUi,Menu,,,,"The black stone carries you home. Your study has been broken open: shelves gutted, glass smashed, a lifetime of your notes trampled into the floor. Someone was hunting for caches and hiding places.",,"Der schwarze Stein trägt dich nach Hause. Dein Arbeitszimmer ist aufgebrochen: die Regale ausgeweidet, die Phiolen zerschlagen, die Aufzeichnungen eines ganzen Lebens in den Boden getreten. Hier wurde nach Verstecken und Depots gesucht.","La piedra negra te lleva a casa. Tu estudio ha sido forzado: los estantes destripados, los frascos hechos añicos, las notas de toda una vida pisoteadas contra el suelo. Aquí buscaban escondrijos y alijos.","La pierre noire te ramène chez toi. Ton cabinet a été forcé : les étagères éventrées, les fioles brisées, les notes de toute une vie piétinées sur le sol. On y cherchait des caches et des réserves.","La pietra nera ti riporta a casa. Il tuo studio è stato forzato: gli scaffali sventrati, le ampolle in frantumi, gli appunti di una vita calpestati sul pavimento. Qui cercavano nascondigli e depositi.",黒い石があなたを家へ連れ戻す。書斎はこじ開けられていた。棚は荒らされ、瓶は砕かれ、生涯をかけて集めた記録は床に踏みにじられている。ここで探されていたのは、隠し場所と隠し財産だ。,"검은 돌이 당신을 집으로 데려간다. 서재는 부서져 열려 있다. 선반은 헤집어졌고, 유리병은 깨졌으며, 평생 모은 기록은 바닥에 짓밟혀 있다. 여기서 찾던 것은 은닉처와 비장품이었다.","Czarny kamień przenosi cię do domu. Twój gabinet został wyważony: półki wypatroszone, fiolki potłuczone, notatki zbierane przez całe życie wdeptane w podłogę. Szukano tu skrytek i schowków.","A pedra negra te leva para casa. Seu gabinete foi arrombado: as prateleiras estripadas, os frascos quebrados, as anotações de uma vida inteira pisoteadas no chão. Procuravam aqui por esconderijos e reservas.","Чёрный камень выносит вас домой. Кабинет вскрыт: полки выпотрошены, склянки перебиты, записи, которые вы собирали всю жизнь, втоптаны в пол. Здесь искали тайники и схроны.","Kara taş seni evine taşıyor. Çalışma odan kırılıp açılmış: raflar boşaltılmış, şişeler parçalanmış, bir ömür boyu topladığın notlar yere çiğnenmiş. Burada zulalar ve saklı yerler aranmış.",黑石将你带回家中。你的书房已被撬开:书架被翻个底朝天,瓶罐尽碎,你毕生收集的手稿被踩进地板。他们在这里找的是密室与藏货。,黑石將你帶回家中。你的書房已被撬開:書架被翻個底朝天,瓶罐盡碎,你畢生收集的手稿被踩進地板。他們在這裡找的是密室與藏貨。 +necroFinalPage2Title,XUi,Menu,,,,Ruins of the City,,Ruinen der Stadt,Ruinas de la ciudad,Les ruines de la ville,Le rovine della città,都市の廃墟,도시의 폐허,Ruiny miasta,Ruínas da cidade,Руины города,Şehrin harabeleri,城市的废墟,城市的廢墟 +necroFinalPage2Text,XUi,Menu,,,,"Beyond the broken glass lie the ruins of a city that was once beautiful. The streets are drifted with bones, the houses are hollow, the sky is the colour of cold coal - and nothing is alive in it all the way to the horizon.",,"Hinter dem zerbrochenen Glas liegen die Ruinen einer einst wunderschönen Stadt. Die Straßen sind von Knochen bedeckt, die Häuser leer, der Himmel hat die Farbe erkalteter Kohle – und bis zum Horizont nichts Lebendiges.","Tras el cristal roto se extienden las ruinas de una ciudad que un día fue hermosa. Las calles están cubiertas de huesos, las casas vacías, el cielo del color del carbón apagado, y no queda nada vivo hasta el horizonte.","Derrière la vitre brisée s'étendent les ruines d'une ville jadis magnifique. Les rues sont jonchées d'ossements, les maisons vides, le ciel a la couleur du charbon éteint — et rien de vivant jusqu'à l'horizon.","Oltre il vetro rotto si stendono le rovine di una città un tempo bellissima. Le strade sono ricoperte di ossa, le case vuote, il cielo ha il colore del carbone spento, e non c'è nulla di vivo fino all'orizzonte.",割れた窓の向こうに、かつて美しかった都市の廃墟が広がる。通りは骨で埋まり、家々は空っぽで、空は冷めた石炭の色。地平線まで、生きているものは何ひとつない。,"깨진 유리 너머로 한때 아름다웠던 도시의 폐허가 펼쳐진다. 거리는 뼈로 뒤덮였고, 집들은 텅 비었으며, 하늘은 식은 석탄의 빛깔이다. 지평선까지 살아 있는 것은 아무것도 없다.","Za rozbitą szybą leżą ruiny niegdyś pięknego miasta. Ulice zasypane kośćmi, domy puste, niebo w kolorze wystygłego węgla — i ani jednej żywej istoty aż po horyzont.","Atrás do vidro quebrado estendem-se as ruínas de uma cidade outrora bela. As ruas estão cobertas de ossos, as casas vazias, o céu tem a cor do carvão apagado — e nada vivo até o horizonte.","За разбитым стеклом лежат руины некогда прекрасного города. Улицы завалены костями, дома пусты, небо цвета остывшего угля — и ни одного живого до самого горизонта.","Kırık camın ardında, bir zamanlar güzel olan bir şehrin harabeleri uzanıyor. Sokaklar kemiklerle dolu, evler bomboş, gökyüzü sönmüş kömür renginde — ve ufka kadar tek bir canlı yok.",破碎的玻璃之外,是一座昔日美丽城市的废墟。街道被白骨掩埋,房屋空无一人,天空是冷却煤炭的颜色——直到地平线,没有一个活物。,破碎的玻璃之外,是一座昔日美麗城市的廢墟。街道被白骨掩埋,房屋空無一人,天空是冷卻煤炭的顏色——直到地平線,沒有一個活物。 +necroFinalPage3Title,XUi,Menu,,,,The Writing,,Die Inschriften,Las inscripciones,Les inscriptions,Le scritte,壁の文字,벽의 글씨,Napisy,As inscrições,Надписи,Yazılar,墙上的字,牆上的字 +necroFinalPage3Text,XUi,Menu,,,,"The wall is written over in every language the dead still remembered. Your faithful wights did not know what you would come back as, or whether you would come back at all. ""We waited for you, and then we got hungry."" They waited as long as they were able...",,"Die Wand ist beschrieben, in allen Sprachen, an die sich die Untoten noch erinnerten. Deine treuen Wiedergänger wussten nicht, als was du zurückkehren würdest – und ob überhaupt. „Wir haben auf dich gewartet, und dann wurden wir hungrig.“ Sie haben gewartet, solange sie konnten...","La pared está escrita en todos los idiomas que los muertos aún recordaban. Tus fieles espectros no sabían cómo volverías, ni si volverías siquiera. «Te esperábamos, y luego tuvimos hambre». Esperaron todo lo que pudieron...","Le mur est couvert d'écritures, dans toutes les langues dont les morts se souvenaient encore. Tes fidèles spectres ignoraient sous quelle forme tu reviendrais, et même si tu reviendrais. « Nous t'avons attendu, puis nous avons eu faim. » Ils ont attendu aussi longtemps qu'ils l'ont pu...","Il muro è coperto di scritte, in tutte le lingue che i non morti ricordavano ancora. I tuoi fedeli spettri non sapevano come saresti tornato, né se saresti tornato. «Ti abbiamo aspettato, e poi ci è venuta fame». Hanno aspettato finché hanno potuto...",壁は、不死者たちが覚えていたかぎりのあらゆる言語で書き埋められている。忠実な亡霊たちは、あなたがどんな姿で戻るのか、そもそも戻るのかを知らなかった。「私たちはあなたを待っていた、そして、その後飢えました」。彼らは、耐えられるかぎり待ち続けたのだ……,"벽은 언데드가 기억하던 모든 언어로 뒤덮여 있다. 충직한 망령들은 당신이 어떤 모습으로 돌아올지, 아니 돌아오기는 할지 알지 못했다. “우리는 너를 기다렸다. 그리고 배가 고파졌다.” 그들은 견딜 수 있는 만큼 기다렸다...","Ściana jest zapisana we wszystkich językach, jakie nieumarli jeszcze pamiętali. Twoje wierne upiory nie wiedziały, jaki wrócisz ani czy wrócisz w ogóle. „Czekaliśmy na ciebie, a potem zgłodnieliśmy”. Czekały tak długo, jak zdołały...","A parede está escrita em todas as línguas de que os mortos-vivos ainda se lembravam. Seus fiéis espectros não sabiam como você voltaria, nem se voltaria. “Esperamos por você, e depois ficamos com fome.” Esperaram o quanto puderam...","Стена исписана на всех языках, какие помнила нежить. Верные умертвия не знали, каким вы вернётесь и вернётесь ли вообще. «Мы ждали тебя, а потом проголодались». Они ждали столько, сколько смогли...","Duvar, ölülerin hatırladığı bütün dillerde yazılarla kaplı. Sadık hortlakların nasıl döneceğini, hatta dönüp dönmeyeceğini bilmiyordu. “Seni bekledik, sonra acıktık.” Dayanabildikleri kadar beklediler...",整面墙都写满了字,用亡灵还记得的每一种语言。你忠实的尸鬼不知道你会以什么模样归来,甚至不知道你是否还会归来。“我们等过你,后来我们饿了。”它们等了它们所能等的全部时间……,整面牆都寫滿了字,用亡靈還記得的每一種語言。你忠實的屍鬼不知道你會以什麼模樣歸來,甚至不知道你是否還會歸來。「我們等過你,後來我們餓了。」牠們等了牠們所能等的全部時間…… +necroFinalPage4Title,XUi,Menu,,,,The Undead,,Die Untoten,No-muertos,Les morts-vivants,I non morti,不死者,언데드,Nieumarli,Mortos-vivos,Нежить,Ölümsüzler,亡灵,亡靈 +necroFinalPage4Text,XUi,Menu,,,,"A lich steps through the breach. In its torn rags you recognise an archmage's robe. The very one who sentenced you to death. ""Let the dead make you dead,"" he wrote in the note you were left to die with.",,"Durch die Bresche tritt ein Lich. In seinen zerfetzten Lumpen erkennst du die Robe eines Erzmagiers. Genau jenes, der dich zum Tode verurteilt hat. „Sollen die Toten dich tot machen“, schrieb er in der Notiz, mit der man dich zum Sterben zurückließ.","Por la brecha entra un liche. En sus harapos reconoces la túnica de un archimago. El mismo que te condenó a muerte. «Que los muertos te hagan morir», escribió en la nota con la que te dejaron morir.","Une liche franchit la brèche. Dans ses haillons, tu reconnais la robe d'un archimage. Celui-là même qui t'a condamné à mort. « Que les morts te rendent mort », écrivait-il dans le mot avec lequel on t'a laissé mourir.","Dalla breccia entra un lich. Nei suoi stracci riconosci la veste di un arcimago. Proprio quello che ti ha condannato a morte. «Che i morti ti rendano morto», scriveva nel biglietto con cui ti hanno lasciato a morire.",破れた壁からリッチが入ってくる。その裂けたぼろ布に、あなたは大魔導師のローブを見て取る。あなたを死刑に処した、まさにその男のものだ。「死者にお前を死者たらしめよ」——あなたを死ぬに任せて置き去りにした、あの書き置きに彼はそう記していた。,무너진 벽으로 리치가 들어선다. 찢어진 누더기 속에서 당신은 대마법사의 로브를 알아본다. 당신에게 사형을 선고한 바로 그자의 것이다. “죽은 자들이 너를 죽게 하리라.” 당신을 죽으라고 내던질 때 함께 있던 쪽지에 그가 쓴 말이다.,"Przez wyłom wchodzi licz. W jego podartych łachmanach rozpoznajesz szatę arcymaga. Tego samego, który skazał cię na śmierć. „Niech umarli uczynią cię umarłym” — pisał w liście, z którym zostawiono cię na śmierć.","Pela brecha entra um lich. Em seus trapos rasgados você reconhece o manto de um arquimago. O mesmo que te condenou à morte. “Que os mortos te façam morto”, escreveu ele no bilhete com o qual te deixaram morrer.","В пролом входит лич. Вы узнаёте в его рваных обносках балахон архимага. Того самого, что приговорил вас к смерти. «Пусть мёртвые сделают тебя мёртвым», — писал он в записке, с которой вас бросили умирать.","Gedikten bir lich giriyor. Yırtık paçavralarında bir baş büyücünün cübbesini tanıyorsun. Seni ölüme mahkûm eden ta kendisinin. “Bırak ölüler seni öldürsün”, diye yazmıştı, seni ölüme terk ettikleri o notta.",一具巫妖从墙洞走进来。在它褴褛的破布中,你认出了大法师的长袍。正是判你死刑的那一位。“让死者置你于死地。”——他在那张纸条上如此写道,而你就是带着它被丢下等死的。,一具巫妖從牆洞走進來。在牠襤褸的破布中,你認出了大法師的長袍。正是判你死刑的那一位。「讓死者置你於死地。」——他在那張紙條上如此寫道,而你就是帶著它被丟下等死的。 +necroFinalPage5Title,XUi,Menu,,,,The Service,,Der Dienst,El servicio,Le service,Il servizio,臣従,복종,Służba,O serviço,Служение,Hizmet,效忠,效忠 +necroFinalPage5Text,XUi,Menu,,,,"Having sent you off to die, the archmage came back here for the inheritance and opened the cellars. But those were not locked against thieves. Your dead killed the intruders, and then raised them as servants of yours. The lich lowers itself to one knee. It is waiting for an order.",,"Nachdem er dich in den Tod geschickt hatte, kehrte der Erzmagier hierher zurück, um das Erbe zu holen, und brach die Keller auf. Doch die waren nicht gegen Diebe verschlossen. Deine Untoten töteten die Eindringlinge – und erhoben sie dann als deine Diener. Der Lich sinkt auf ein Knie. Er wartet auf einen Befehl.","Tras enviarte a morir, el archimago volvió aquí por la herencia y forzó los sótanos. Pero aquellos no estaban cerrados contra ladrones. Tus muertos mataron a los invasores y luego los alzaron como siervos tuyos. El liche hinca una rodilla. Espera una orden.","Après t'avoir envoyé à la mort, l'archimage est revenu ici pour l'héritage et a forcé les caves. Mais celles-ci n'étaient pas fermées contre les voleurs. Tes morts ont tué les intrus, puis les ont relevés comme tes serviteurs. La liche met un genou à terre. Elle attend un ordre.","Dopo averti mandato a morire, l'arcimago è tornato qui per l'eredità e ha forzato le cantine. Ma quelle non erano chiuse contro i ladri. I tuoi morti hanno ucciso gli invasori, e poi li hanno rialzati come tuoi servitori. Il lich piega un ginocchio. Attende un ordine.",あなたを死地へ送り出したあと、大魔導師は遺産を求めてここへ戻り、地下室をこじ開けた。だがあの扉は、盗人を防ぐためのものではなかった。あなたの不死者たちは侵入者を殺し、そしてあなたの僕として起こしたのだ。リッチが片膝をつく。命令を待っている。,"당신을 죽으라고 보낸 뒤, 대마법사는 유산을 노리고 이곳으로 돌아와 지하실을 뜯어냈다. 그러나 그 문은 도둑을 막으려 잠긴 것이 아니었다. 당신의 언데드가 침입자들을 죽였고, 그다음 당신의 종으로 일으켰다. 리치가 한쪽 무릎을 꿇는다. 명령을 기다리고 있다.","Wysławszy cię na śmierć, arcymag wrócił tu po spadek i wyważył piwnice. Ale te nie były zamknięte przed złodziejami. Twoi umarli zabili najeźdźców, a potem podnieśli ich już jako twoje sługi. Licz opada na jedno kolano. Czeka na rozkaz.","Depois de mandar você para a morte, o arquimago voltou aqui pela herança e arrombou os porões. Mas aqueles não estavam trancados contra ladrões. Seus mortos mataram os invasores e depois os ergueram como servos seus. O lich baixa um joelho. Ele espera uma ordem.","Отправив вас умирать, архимаг вернулся сюда за наследством и вскрыл подвалы. Но те были заперты вовсе не от воров. Ваша нежить убила захватчиков, а потом подняла их уже вашими слугами. Лич опускается на колено. Он ждёт приказа.","Seni ölüme yolladıktan sonra baş büyücü mirası almak için buraya döndü ve mahzenleri kırıp açtı. Ama oralar hırsızlara karşı kilitli değildi. Ölülerin işgalcileri öldürdü, sonra da onları senin hizmetkârların olarak ayağa kaldırdı. Lich tek dizinin üstüne çöküyor. Bir emir bekliyor.",把你送去送死之后,大法师回到这里取走遗产,撬开了地窖。可那些门锁着,防的从来不是贼。你的亡灵杀光了闯入者,随后又把他们抬起来,成了你的仆从。巫妖单膝跪地。它在等一道命令。,把你送去送死之後,大法師回到這裡取走遺產,撬開了地窖。可那些門鎖著,防的從來不是賊。你的亡靈殺光了闖入者,隨後又把他們抬起來,成了你的僕從。巫妖單膝跪地。牠在等一道命令。 +necroFinalPage6Title,XUi,Menu,,,,The Choice,,Die Wahl,La elección,Le choix,La scelta,選択,선택,Wybór,A escolha,Выбор,Seçim,抉择,抉擇 +necroFinalPage6Text,XUi,Menu,,,,"Will you stay here, to lead the army of the dead and bring this world back - or take a pause and return to the place that was meant to be your execution?",,"Bleibst du hier, um die Armee der Untoten zu führen und diese Welt wiederzuerwecken – oder legst du eine Pause ein und kehrst an den Ort zurück, der deine Hinrichtungsstätte werden sollte?","¿Te quedarás aquí para encabezar el ejército de los muertos y hacer renacer este mundo, o harás una pausa y volverás al lugar que debía ser el de tu ejecución?",Resteras-tu ici pour prendre la tête de l'armée des morts et faire renaître ce monde — ou feras-tu une pause et retourneras-tu à l'endroit qui devait être celui de ton exécution ?,"Resterai qui a guidare l'esercito dei non morti e a far rinascere questo mondo, oppure ti prenderai una pausa e tornerai nel luogo che doveva essere quello della tua esecuzione?",ここに留まって不死者の軍勢を率い、この世界を蘇らせるか。それとも一度身を引き、あなたの処刑の地となるはずだった場所へ戻るか。,"이곳에 남아 언데드 군단을 이끌고 이 세계를 되살리겠는가, 아니면 잠시 물러나 당신의 처형지가 되었어야 할 그곳으로 돌아가겠는가?","Zostaniesz tutaj, by stanąć na czele armii nieumarłych i wskrzesić ten świat, czy zrobisz przerwę i wrócisz w miejsce, które miało stać się miejscem twojej egzekucji?","Você vai ficar aqui para liderar o exército dos mortos-vivos e fazer este mundo renascer, ou vai dar uma pausa e voltar ao lugar que deveria ter sido o da sua execução?","Останетесь ли вы здесь, чтобы возглавить армию нежити и возродить этот мир, или возьмёте паузу и вернётесь в то место, которое должно было стать местом вашей казни?","Burada kalıp ölüler ordusunun başına geçerek bu dünyayı yeniden mi canlandıracaksın, yoksa bir ara verip idam yerin olması gereken o yere mi döneceksin?",你要留在这里,率领亡灵大军让这个世界重生;还是暂且抽身,回到那个本该成为你刑场的地方?,你要留在這裡,率領亡靈大軍讓這個世界重生;還是暫且抽身,回到那個本該成為你刑場的地方? +necroFinalBtnBack,XUi,Menu,,,,Back,,Zurück,Atrás,Retour,Indietro,戻る,뒤로,Wstecz,Voltar,Назад,Geri,返回,返回 +necroFinalBtnNext,XUi,Menu,,,,Next,,Weiter,Siguiente,Suivant,Avanti,次へ,다음,Dalej,Avançar,Дальше,İleri,继续,繼續 +necroFinalBtnStay,XUi,Menu,,,,Stay,,Bleiben,Quedarse,Rester,Restare,留まる,남는다,Zostać,Ficar,Остаться,Kal,留下,留下 +necroFinalBtnReturn,XUi,Menu,,,,Return,,Zurückkehren,Volver,Revenir,Tornare,帰還する,돌아간다,Wrócić,Retornar,Вернуться,Dön,回去,回去 +necroFinalStayTitle,XUi,Menu,,,,The Reign,,Die Herrschaft,El reinado,Le règne,Il regno,統治,치세,Panowanie,O reinado,Правление,Hükümdarlık,治世,治世 +necroFinalStayText,XUi,Menu,,,,"You took the army of the dead under your command, sought out the few who had survived, and rebuilt the houses and the workshops. All the heavy and dangerous work here is done by the dead now. The living got used to it surprisingly quickly. You rule this land.",,"Du hast die Armee der Toten unter deinen Befehl gestellt, die wenigen Überlebenden aufgesucht und die Häuser und Werkstätten wieder aufgebaut. Alle schwere und gefährliche Arbeit verrichten hier jetzt die Toten. Die Lebenden haben sich erstaunlich schnell daran gewöhnt. Du regierst dieses Land.","Pusiste bajo tu mando al ejército de los muertos, buscaste a los pocos que habían sobrevivido y reconstruiste las casas y los talleres. Todo el trabajo pesado y peligroso lo hacen aquí ahora los muertos. Los vivos se acostumbraron sorprendentemente rápido. Tú gobiernas esta tierra.","Tu as pris le commandement de l'armée des morts, retrouvé les rares survivants et rebâti les maisons et les ateliers. Tout le travail lourd et dangereux est désormais accompli ici par les morts. Les vivants s'y sont habitués étonnamment vite. Tu règnes sur ce pays.","Hai preso il comando dell'esercito dei morti, cercato i pochi sopravvissuti e ricostruito le case e le officine. Tutto il lavoro pesante e pericoloso qui lo fanno ormai i morti. I vivi ci si sono abituati sorprendentemente in fretta. Tu governi questa terra.",あなたは不死者の軍勢を掌握し、わずかに生き残った者たちを探し出し、家々と工房を建て直した。重く危険な仕事は、いまやすべて死者が担っている。生者は驚くほど早くそれに慣れた。あなたはこの国を統べている。,"당신은 언데드 군단을 손에 넣고, 살아남은 몇 안 되는 이들을 찾아냈으며, 집과 공방을 다시 세웠다. 무겁고 위험한 일은 이제 모두 죽은 자들이 한다. 산 자들은 놀라울 만큼 빨리 익숙해졌다. 당신이 이 땅을 다스린다.","Wziąłeś armię umarłych pod swoje dowództwo, odszukałeś nielicznych ocalałych i odbudowałeś domy oraz warsztaty. Całą ciężką i niebezpieczną pracę wykonują tu teraz umarli. Żywi przywykli do tego zaskakująco szybko. Ty rządzisz tym krajem.","Você pôs o exército dos mortos sob seu comando, procurou os poucos que sobreviveram e reconstruiu as casas e as oficinas. Todo o trabalho pesado e perigoso aqui é feito agora pelos mortos. Os vivos se acostumaram surpreendentemente rápido. Você governa esta terra.","Вы взяли под контроль армию нежити, разыскали немногих уцелевших и заново отстроили дома и производства. Всю тяжёлую и опасную работу здесь теперь делают мёртвые. Живые привыкли к этому на удивление быстро. Вы правите этой страной.","Ölüler ordusunu komutan altına aldın, sağ kalan bir avuç insanı bulup çıkardın, evleri ve atölyeleri yeniden inşa ettin. Buradaki bütün ağır ve tehlikeli işi artık ölüler yapıyor. Yaşayanlar buna şaşırtıcı bir hızla alıştı. Bu ülkeyi sen yönetiyorsun.",你将亡灵大军收归麾下,寻回了为数不多的幸存者,重建了房屋与工坊。如今这里一切沉重而危险的活计都由死者来做。活人适应得快得出奇。你统治着这片土地。,你將亡靈大軍收歸麾下,尋回了為數不多的倖存者,重建了房屋與工坊。如今這裡一切沉重而危險的活計都由死者來做。活人適應得快得出奇。你統治著這片土地。 +necroFinalReturnTitle,XUi,Menu,,,,The Quiet Harbour,,Der stille Hafen,El puerto tranquilo,Le havre tranquille,Il porto tranquillo,静かな港,고요한 항구,Cicha przystań,O porto tranquilo,Тихая гавань,Sessiz Liman,静港,靜港 +necroFinalReturnText,XUi,Menu,,,,"You ordered the lich to lead the dead away into the forests, so that people could come out of their shelters and start surviving on their own, the way you once survived. If they manage, good. If they do not... then such is their fate. And you went back: to the world where you had already settled in and found your quiet harbour.",,"Du hast dem Lich befohlen, die Toten fort in die Wälder zu führen – damit die Menschen aus ihren Verstecken treten und selbst zu überleben beginnen können, so wie du einst überlebt hast. Schaffen sie es, gut. Schaffen sie es nicht ... dann ist das eben ihr Schicksal. Und du bist zurückgegangen: in die Welt, in der du dich bereits eingerichtet und deinen stillen Hafen gefunden hast.","Ordenaste al liche que se llevara a los muertos a los bosques, para que la gente pudiera salir de sus refugios y empezar a sobrevivir por su cuenta, igual que sobreviviste tú en su día. Si lo consiguen, bien. Si no... será su destino. Y tú regresaste: al mundo donde ya te habías asentado y habías encontrado tu puerto tranquilo.","Tu as ordonné à la liche d'emmener les morts au loin dans les forêts – pour que les gens puissent sortir de leurs abris et commencer à survivre seuls, comme tu as survécu autrefois. S'ils y parviennent, tant mieux. Sinon... c'est leur destin. Et toi, tu es reparti : vers le monde où tu t'étais déjà installé et où tu avais trouvé ton havre tranquille.","Hai ordinato al lich di condurre i morti via nelle foreste, perché la gente potesse uscire dai rifugi e iniziare a sopravvivere da sé, come un tempo sei sopravvissuto tu. Se ce la fanno, bene. Se non ce la fanno... allora è il loro destino. E tu sei tornato indietro: nel mondo dove ti eri già sistemato e avevi trovato il tuo porto tranquillo.",あなたはリッチに命じ、不死者をすべて森へ連れ去らせた。人々が避難所から出て、かつてのあなたと同じように、自らの力で生き延び始められるように。やり遂げるならそれでいい。できなければ……それが彼らの定めというだけだ。そしてあなたは戻っていった。すでに身を落ち着け、静かな港を見つけたあの世界へ。,"당신은 리치에게 명하여 언데드를 모두 숲으로 데려가게 했다. 사람들이 피난처에서 나와, 한때 당신이 그랬듯 스스로 살아남기 시작할 수 있도록. 해낸다면 좋은 일이다. 해내지 못한다면... 그것이 그들의 운명일 뿐이다. 그리고 당신은 돌아갔다. 이미 자리를 잡고 자신만의 고요한 항구를 찾아낸 그 세계로.","Rozkazałeś liczowi wyprowadzić umarłych w lasy – żeby ludzie mogli wyjść ze schronów i zacząć przetrwać o własnych siłach, tak jak kiedyś przetrwałeś ty. Poradzą sobie – dobrze. Nie poradzą... cóż, taki ich los. A ty wróciłeś: do świata, w którym zdążyłeś się urządzić i znaleźć swoją cichą przystań.","Você ordenou ao lich que levasse os mortos embora para as florestas - para que as pessoas pudessem sair dos abrigos e começar a sobreviver por conta própria, como você mesmo sobreviveu um dia. Se conseguirem, ótimo. Se não... então é o destino delas. E você voltou: para o mundo onde já havia se estabelecido e encontrado o seu porto tranquilo.","Вы приказали личу увести нежить в леса — чтобы люди вышли из убежищ и начали выживать сами, как когда-то выживали вы. Справятся — хорошо. Не справятся... значит, такова их судьба. А вы вернулись обратно: в мир, где успели обустроиться и найти свою тихую гавань.","Liche, ölüleri alıp ormanlara götürmesini emrettin - insanlar sığınaklarından çıkıp, bir zamanlar senin hayatta kaldığın gibi kendi başlarına hayatta kalmaya başlayabilsinler diye. Başarırlarsa ne âlâ. Başaramazlarsa... demek ki kaderleri buymuş. Sen ise geri döndün: çoktan yerleşip kendi sessiz limanını bulduğun o dünyaya.",你命令巫妖将亡灵尽数带往森林深处——好让人们走出避难所,像你当年那样,靠自己活下去。若他们撑得住,那很好;若撑不住……那便是他们的命。而你回去了:回到那个你早已安顿下来、寻得自己静港的世界。,你命令巫妖將亡靈盡數帶往森林深處——好讓人們走出避難所,像你當年那樣,靠自己活下去。若他們撐得住,那很好;若撐不住……那便是他們的命。而你回去了:回到那個你早已安頓下來、尋得自己靜港的世界。 +necroFinalBtnTheEnd,XUi,Menu,,,,The End,,Ende,Fin,Fin,Fine,おわり,끝,Koniec,Fim,Конец,Son,全剧终,全劇終 diff --git a/Config/XUi_InGame/windows.xml b/Config/XUi_InGame/windows.xml new file mode 100644 index 0000000..e7ad249 --- /dev/null +++ b/Config/XUi_InGame/windows.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Config/XUi_InGame/xui.xml b/Config/XUi_InGame/xui.xml new file mode 100644 index 0000000..83ac261 --- /dev/null +++ b/Config/XUi_InGame/xui.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Config/blocks.xml b/Config/blocks.xml new file mode 100644 index 0000000..7412727 --- /dev/null +++ b/Config/blocks.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Config/buffs.xml b/Config/buffs.xml new file mode 100644 index 0000000..2473799 --- /dev/null +++ b/Config/buffs.xml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Config/entityclasses.xml b/Config/entityclasses.xml new file mode 100644 index 0000000..9a17acd --- /dev/null +++ b/Config/entityclasses.xml @@ -0,0 +1,284 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Config/item_modifiers.xml b/Config/item_modifiers.xml new file mode 100644 index 0000000..f957993 --- /dev/null +++ b/Config/item_modifiers.xml @@ -0,0 +1,317 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Config/items.xml b/Config/items.xml new file mode 100644 index 0000000..a74ae95 --- /dev/null +++ b/Config/items.xml @@ -0,0 +1,1346 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Config/loot.xml b/Config/loot.xml new file mode 100644 index 0000000..6f6a1ce --- /dev/null +++ b/Config/loot.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + diff --git a/Config/progression.xml b/Config/progression.xml new file mode 100644 index 0000000..8faf610 --- /dev/null +++ b/Config/progression.xml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Config/recipes.xml b/Config/recipes.xml new file mode 100644 index 0000000..5397d55 --- /dev/null +++ b/Config/recipes.xml @@ -0,0 +1,379 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HarmonySrc/BansheePatch.cs b/HarmonySrc/BansheePatch.cs new file mode 100644 index 0000000..7f69e39 --- /dev/null +++ b/HarmonySrc/BansheePatch.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using HarmonyLib; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// "Книга банши" (Banshee's Book) - BACKLOG.md item 7 (dictated 2026-08-28, implemented + /// 2026-08-29 "без вопросов" per user request). On use, the book is consumed, plays a + /// screamer's own scream sound, and spawns a small HOSTILE horde near the player - hostile to + /// the player, unlike the Dog/Swarm/three new zombie pets, which are all summoned allies. + /// + /// DELIBERATE SIMPLIFICATION, flagged rather than guessed past - read before touching this + /// file again: the backlog's own research pointed at AIScoutHordeSpawner (decompiled here, + /// see the class itself in Assembly-CSharp) as "the" mechanism a real zombieScreamer's scream + /// uses. Decompiling it in full shows it is NOT a simple "spawn N zombies now" call - it's a + /// whole per-tick simulation object (constructed with an EntitySpawner, driven by + /// AIDirector.CanSpawn()/EntitySpawner.CurrentWave/SpawnManually, tracking a scout zombie + /// that has to physically wander off, spot a player, and only THEN calls its own + /// spawnHordeNear near that scout) - built for the existing "distant scout triggers a + /// blood-moon-style horde" system, not for "an item makes a horde appear right now". Wiring a + /// real AIScoutHordeSpawner up from a one-shot item click would mean also owning a per-tick + /// driver for it (another ModEvents.UnityUpdate loop, same shape as PetFollowPatch.cs) and an + /// EntitySpawner instance to hand it, for a payoff (a scout that has to run off and get + /// spotted first) the user's own description doesn't ask for - they asked for the scream and + /// the horde appearing "poblizosti" (nearby), not a scout-fetch quest. + /// + /// So instead: this directly builds a small set of ordinary hostile zombies near the player + /// using the exact same EntityFactory.CreateEntity -> SetSpawnerSource -> SpawnEntityInWorld + /// sequence ItemActionSpawnEntity.Spawn itself uses (decompiled to confirm, not guessed) - + /// the same primitive this mod's own pet summons are already built on + /// (HarmonySrc/SummonPatch.cs), just spawning several real vanilla zombie classes instead of + /// one tamed pet, and never touching their AI/flags at all (they're meant to be hostile, + /// which is what an untouched vanilla zombie already is by default - the OPPOSITE of the + /// pet-taming work the rest of this mod does). + /// + /// Zombie pool (zombieArlene/zombieBoe/zombieYo/zombieJoe) is a hardcoded guess at "generic + /// early/mid walker" flavor, picked because all four are confirmed real vanilla entity_class + /// names (checked directly against Data/Config/entityclasses.xml) with no special + /// gimmick (no explosion, no ranged attack) - NOT gamestage-scaled or otherwise tied to the + /// real difficulty-scaling horde-selection system real hordes use. Spawn positions come from + /// World.GetMobRandomSpawnPosWithWater (decompiled from AIScoutHordeSpawner's own use of it) - + /// the same "find a valid ground spot near here" helper the real scout-horde system itself + /// calls, just pointed at the player directly instead of a scout zombie's position. + /// + [HarmonyPatch(typeof(ItemActionEat))] + [HarmonyPatch("consume")] + public static class Patch_ItemActionEat_Consume_Banshee + { + public const string ItemName = "bookBanshee"; + + /// Real, confirmed vanilla entity_class names - not invented. Deliberately no + /// exploders/spitters/screamers-of-their-own in the pool (would either be anticlimactic - + /// a screamer summoning more screamers - or risk chain-reaction explosions on the caster). + public static readonly string[] ZombiePool = { "zombieArlene", "zombieBoe", "zombieYo", "zombieJoe" }; + + /// "Небольшая орда" - not specified by the user as an exact number, guessed + /// modest (comparable to a small blood-moon wave, not a screen-filling swarm). + public const int HordeSize = 5; + + public static readonly System.Random Rand = new System.Random(); + + public static void Postfix(ItemActionData _actionData) + { + string itemName = _actionData?.invData?.itemValue?.ItemClass?.Name; + if (itemName != ItemName) + { + return; + } + EntityAlive holdingEntity = _actionData.invData.holdingEntity; + World world = holdingEntity?.world; + if (holdingEntity == null || world == null) + { + return; + } + Debug.Log("[NecromancerTome] BansheePatch: casting near owner=" + holdingEntity.entityId); + + // Reuses the real screamer's own alert sound (zombiefemalescoutalert, see + // zombieScreamer in Data/Config/entityclasses.xml's SoundAlert) rather than inventing + // a new one - PlayOneShot(string) confirmed via ItemActionEat's own + // ExecuteInstantAction, same call shape used there. + holdingEntity.PlayOneShot("zombiefemalescoutalert"); + + Vector3 casterPos = holdingEntity.GetPosition(); + for (int i = 0; i < HordeSize; i++) + { + string zombieClassName = ZombiePool[Rand.Next(ZombiePool.Length)]; + int classId = EntityClass.GetId(zombieClassName); + if (classId == -1) + { + Debug.LogWarning("[NecromancerTome] BansheePatch: entity class '" + zombieClassName + "' not found"); + continue; + } + // 15-30m out, never closer than 15m to the caster - same shape of call + // AIScoutHordeSpawner itself makes to place a zombie near a point without + // dropping it inside terrain/right on top of a player. + if (!world.GetMobRandomSpawnPosWithWater(casterPos, 15, 30, 15, true, out Vector3 spawnPos)) + { + Debug.LogWarning("[NecromancerTome] BansheePatch: no valid spawn position found for " + zombieClassName); + continue; + } + Entity entity = EntityFactory.CreateEntity(classId, spawnPos, new Vector3(0f, holdingEntity.rotation.y, 0f)); + entity.SetSpawnerSource(EnumSpawnerSource.StaticSpawner); + world.SpawnEntityInWorld(entity); + Debug.Log("[NecromancerTome] BansheePatch: spawned " + zombieClassName + " (" + entity.entityId + ") at " + spawnPos); + } + } + } +} diff --git a/HarmonySrc/BlockPickupPatch.cs b/HarmonySrc/BlockPickupPatch.cs new file mode 100644 index 0000000..28cdb6b --- /dev/null +++ b/HarmonySrc/BlockPickupPatch.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using HarmonyLib; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// BACKLOG.md item 9 (dictated 2026-08-29, implemented same day "без вопросов" per user + /// request, clarified same day to drop the land-claim requirement entirely). Lets specific + /// decorative/prop blocks be picked back up as an item via hold-E + a hand icon + a progress + /// timer, the same VISUAL mechanic vanilla workstations (workbench/forge/etc.) already use + /// when placed inside your own land claim - but WITHOUT the land-claim requirement, works + /// anywhere on the map, per the user's explicit clarification. + /// + /// RESEARCH FIRST, not guessed (decompiled Assembly-CSharp's Block/BlockWorkstation/ + /// BlockCompositeTileEntity/BlockTrunkTip classes directly): + /// + /// - The actual "hold E, see hand icon + timer, get the item back" mechanic is a GENERIC + /// pair of methods already on the base `Block` class itself, not something + /// BlockWorkstation invented: `Block.takeItemWithTimer(...)` (instance) calls the static + /// `Block.TakeItemWithTimer(pos, blockValue, player, delaySeconds, canTakeCallback)`, which + /// opens the real timer UI (XUiC_Timer.OpenTimer) and, on completion, converts the block to + /// an item, adds it to inventory (or drops it if full), and clears the block - all engine- + /// native, nothing reimplemented here. BlockWorkstation's own "take" activation command is + /// just ONE caller of this generic method, gated behind + /// `_world.IsMyLandProtectedBlock(...) && tileEntityWorkstation.IsPlayerPlaced` (that IS + /// real land-claim gating in vanilla, confirmed - the backlog's original worry about + /// needing Harmony either way was right) - this patch calls the SAME generic + /// TakeItemWithTimer directly, deliberately WITHOUT that land-claim check, per the user's + /// own clarification. + /// - Which "take" appears on a block at all is decided by `Block.GetBlockActivationCommands`/ + /// `HasBlockActivationCommands`/`OnBlockActivated(string,...)` - all three are `virtual` on + /// the base `Block` class, so a plain undecorated block (no Class= override in XML) runs the + /// base implementation and can be patched there directly. But several candidate blocks use a + /// DIFFERENT C# class that overrides all three (confirmed by decompiling it) - + /// `BlockCompositeTileEntity` (used by the water cooler/cardboard box below) - so those need + /// their own separate patches on that type; a patch on the base `Block` type alone would + /// never run for them (Harmony patches the actual method that executes via virtual dispatch, + /// not every subclass "logically implementing the same slot"). + /// + /// TARGET BLOCKS - best-guess mapping from the user's four Russian category names to real + /// Data/Config/blocks.xml block names (checked directly, matched by name PREFIX since most + /// categories have many color/variant blocks) - tell me if any of these aren't what was + /// meant, this is an interpretation, not a spec: + /// - "Кровати" (beds) -> bedMadeNoFrame*/bedMessyNoFrame* ONLY. Deliberately excludes + /// bed02*/bunkBedMade*/bunkBedMessy* even though they look like beds too - decompiling + /// showed those all use Class="SleepingBag" (they're actually functional sleeping-bag/ + /// respawn-anchor blocks, not pure decoration - same family as the player's own bedroll, + /// which the user explicitly said NOT to touch). "NoFrame" variants have no Class= + /// override at all (plain decorative furniture), a clean match for "decorative bed". + /// - "Колья" (stakes) -> NOT IMPLEMENTED. The real spike-trap blocks + /// (trapSpikesWood*/trapSpikesIron*) use Class="TrunkTip" (BlockTrunkTip : BlockDamage), + /// which does NOT override GetBlockActivationCommands/OnBlockActivated at all - it isn't + /// built on the activation-command system this "take" mechanic depends on (harvest-node- + /// style blocks are typically hit-to-harvest instead). Making these pickable would need a + /// genuinely different mechanism, not a variant of this one - left out rather than forced + /// in broken. Say if a different "колья" block was meant. + /// - "Кулеры с водой" (water coolers) -> cntWaterCooler* (Class="CompositeTileEntity"). + /// - "Коробки" (boxes) -> cntCardboardBox (Class="CompositeTileEntity") - the one + /// unambiguous plain-cardboard-box block; there are dozens of OTHER "*box*" blocks in + /// vanilla (mailboxes, breaker boxes, truck cargo) not included here since they don't + /// read as "coробки, расставленные на карте" the way a cardboard box does. + /// + /// TakeDelay (8s) is a guess, not specified by the user - shorter than the workstation + /// default (15s) since these are simpler props, not a full crafting station. + /// NOT VERIFIED IN-GAME - same caution as everything else added 2026-08-29. + /// + public static class BlockPickupPatch + { + public const float TakeDelay = 8f; + + public static readonly string[] TargetPrefixes = new string[] + { + "bedMadeNoFrame", + "bedMessyNoFrame", + "cntWaterCooler", + "cntCardboardBox", + }; + + public static bool IsTargetBlock(BlockValue _blockValue) + { + Block block = _blockValue.Block; + if (block == null) + { + return false; + } + string name = block.GetBlockName(); + if (string.IsNullOrEmpty(name)) + { + return false; + } + foreach (string prefix in TargetPrefixes) + { + if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + + public static void AppendTakeCommand(BlockValue _blockValue, ref BlockActivationCommand[] __result) + { + if (!IsTargetBlock(_blockValue)) + { + return; + } + List commands = new List(__result ?? Array.Empty()); + commands.Add(new BlockActivationCommand("take", "hand", true)); + __result = commands.ToArray(); + } + + public static bool HandleTakeActivation(string _commandName, Vector3i _blockPos, BlockValue _blockValue, EntityPlayerLocal _player, ref bool __result) + { + if (_commandName != "take" || !IsTargetBlock(_blockValue)) + { + return true; + } + Debug.Log("[NecromancerTome] BlockPickupPatch: take activated on " + _blockValue.Block.GetBlockName() + " at " + _blockPos); + // Deliberately calls the static TakeItemWithTimer directly (no canTakeCallback - + // null means "always takeable", same default as the base Block.takeItemWithTimer + // virtual's own unconditional `return true`) rather than going through + // BlockWorkstation's land-claim-gated instance wrapper. + Block.TakeItemWithTimer(_blockPos, _blockValue, _player, TakeDelay); + __result = true; + return false; + } + + // --- Plain Block-class targets (bedMadeNoFrame*/bedMessyNoFrame*) --- + + [HarmonyPatch(typeof(Block), "HasBlockActivationCommands")] + public static class Patch_Block_HasBlockActivationCommands + { + public static void Postfix(BlockValue _blockValue, ref bool __result) + { + if (IsTargetBlock(_blockValue)) + { + __result = true; + } + } + } + + [HarmonyPatch(typeof(Block), "GetBlockActivationCommands")] + public static class Patch_Block_GetBlockActivationCommands + { + public static void Postfix(BlockValue _blockValue, ref BlockActivationCommand[] __result) + { + AppendTakeCommand(_blockValue, ref __result); + } + } + + [HarmonyPatch(typeof(Block), "OnBlockActivated", new Type[] { typeof(string), typeof(WorldBase), typeof(Vector3i), typeof(BlockValue), typeof(EntityPlayerLocal) })] + public static class Patch_Block_OnBlockActivated + { + public static bool Prefix(string _commandName, Vector3i _blockPos, BlockValue _blockValue, EntityPlayerLocal _player, ref bool __result) + { + return HandleTakeActivation(_commandName, _blockPos, _blockValue, _player, ref __result); + } + } + + // --- BlockCompositeTileEntity targets (cntWaterCooler*/cntCardboardBox) - a DIFFERENT + // C# class that overrides the same three methods, so needs its own separate patches; + // see the class-level comment above for why patching Block alone wouldn't reach these. --- + + [HarmonyPatch(typeof(BlockCompositeTileEntity), "HasBlockActivationCommands")] + public static class Patch_Composite_HasBlockActivationCommands + { + public static void Postfix(BlockValue _blockValue, ref bool __result) + { + if (IsTargetBlock(_blockValue)) + { + __result = true; + } + } + } + + [HarmonyPatch(typeof(BlockCompositeTileEntity), "GetBlockActivationCommands")] + public static class Patch_Composite_GetBlockActivationCommands + { + public static void Postfix(BlockValue _blockValue, ref BlockActivationCommand[] __result) + { + AppendTakeCommand(_blockValue, ref __result); + } + } + + [HarmonyPatch(typeof(BlockCompositeTileEntity), "OnBlockActivated", new Type[] { typeof(string), typeof(WorldBase), typeof(Vector3i), typeof(BlockValue), typeof(EntityPlayerLocal) })] + public static class Patch_Composite_OnBlockActivated + { + public static bool Prefix(string _commandName, Vector3i _blockPos, BlockValue _blockValue, EntityPlayerLocal _player, ref bool __result) + { + return HandleTakeActivation(_commandName, _blockPos, _blockValue, _player, ref __result); + } + } + } +} diff --git a/HarmonySrc/CharmPatch.cs b/HarmonySrc/CharmPatch.cs new file mode 100644 index 0000000..507b69e --- /dev/null +++ b/HarmonySrc/CharmPatch.cs @@ -0,0 +1,134 @@ +using System.Collections.Generic; +using HarmonyLib; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// The core "Deviator" effect: whichever zombie the buff named in CharmBuffName lands on + /// (via the item's own XML - see Config/items.xml/buffs.xml) stops targeting the player + /// and starts targeting other zombies instead. + /// + /// Why this needs Harmony at all: a zombie's valid-target class list (who it's even allowed + /// to look for/attack) is parsed once from its entity_class XML into per-instance AI task + /// objects at spawn time (EAIApproachAndAttackTarget / EAISetNearestEntityAsTarget). There is + /// no XML-level action to change it afterwards - the buff system alone cannot flip a live + /// zombie's allegiance. This patch is the smallest hook that can: it reacts to our specific + /// buff being added and then rewrites those same per-instance task objects directly, using + /// the exact same fields the game's own EAIManager.SetTargetOnlyPlayers() helper uses for the + /// mirror-image trick (restricting a task to players only). We do the same thing in reverse: + /// restrict to zombies only. + /// + [HarmonyPatch(typeof(EntityBuffs), "AddBuff", new System.Type[] { typeof(string), typeof(Vector3i), typeof(int), typeof(bool), typeof(bool), typeof(float) })] + public static class Patch_EntityBuffs_AddBuff_DeviatorCharm + { + public const string CharmBuffName = "buffNecroDeviatorCharm"; + + /// Diagnostic-only, added 2026-08-28 while chasing "the Knife's Victim debuff + /// never sticks" - this patch already logs every AddBuff call for our other marker buff, + /// so logging buffNecroVictim's calls here too (no CharmZombie() side effects for it, + /// just visibility) answers the actual open question directly: does + /// necroWpnBladeNecroKnife's onSelfAttackedOther trigger ever even call AddBuff at all, + /// or does it call it but something downstream (target class mismatch, requirement gate, + /// stacking) rejects it. No log line at all next time means the XML trigger itself isn't + /// firing (likely because EntityDamage computes to 0 and a 0-damage swing doesn't count + /// as a landed hit); a log line with a non-Added result narrows it further. + public const string VictimBuffName = "buffNecroVictim"; + + public static void Postfix(EntityBuffs __instance, string _name, EntityBuffs.BuffStatus __result) + { + if (_name == CharmBuffName || _name == VictimBuffName) + { + // entityId added 2026-08-28 while chasing "the zombie that died didn't have the + // buff even though AddBuff Added fired repeatedly" - logging just the TYPE name + // couldn't tell whether it landed on the same zombie that later died or a + // different one nearby (the knife swings a 90-degree arc, plausible it's hitting + // more than one zombie in a group). This settles it directly against + // VictimPatch's own "dropItemOnDeath Prefix entered for " line. + string parentId = __instance.parent != null ? __instance.parent.entityId.ToString() : "?"; + Debug.Log("[NecromancerTome] AddBuff(" + _name + ") result=" + __result + " parent=" + (__instance.parent != null ? __instance.parent.GetType().Name : "null") + " id=" + parentId); + } + if (__result != EntityBuffs.BuffStatus.Added) + { + return; + } + if (_name != CharmBuffName) + { + return; + } + // HUMANOID-ONLY IS A DELIBERATE DESIGN DECISION, NOT A GAP TO CLOSE (user, 2026-09-07: + // "Камень духов не годится, он для зомбо-гуманоидов, и пусть так и остаётся"). + // Do not "fix" this by widening the type check. + // + // The user had noticed the behaviour in play first ("камень душ на зомбособаках не + // работает, это я проверил однажды") and was right. The reason is this line: zombie + // ANIMALS sit on a completely different branch of the C# hierarchy, so the pattern + // match below is false for every one of them - + // EntityZombie : EntityHuman <- the only charmable one + // EntityZombieDog : EntityEnemyAnimal : EntityEnemy + // EntityEnemyAnimal : EntityEnemy (bear, boar) + // EntityVulture : EntityFlying (not even EntityEnemy) + // - while the XML side still lets the buff land on them, because they DO carry the + // "zombie" tag ("entity,animal,zombie,zombieAnimal,hostile,..."), which is what + // EntityTagCompare gates on. So the buff is added, this check rejects it, and the log + // line below fires. That log line is expected on zombie animals and is not an error. + // + // Note the same decision applies to the other half of this patch: CharmZombie() below + // restricts targetClasses to typeof(EntityZombie), so a charmed humanoid also ignores + // zombie animals as targets. Consistent with "the Spirit Stone is a humanoid tool". + // Countering zombie animals/birds is meant to be a separate mechanic - open topic, + // see BACKLOG.md 2026-09-07. + if (!(__instance.parent is EntityZombie zombie) || zombie.aiManager == null) + { + Debug.Log("[NecromancerTome] charm buff added but parent is not a charmable EntityZombie (aiManager null or wrong type)"); + return; + } + CharmZombie(zombie); + } + + public static void CharmZombie(EntityZombie zombie) + { + // AITask: whichever task actually chases/melees the current attack target. + // Restrict it to EntityZombie only, same shape as SetTargetOnlyPlayers() but reversed. + List approachTasks = zombie.aiManager.GetTasks(); + Debug.Log("[NecromancerTome] CharmZombie: approachTasks=" + (approachTasks != null ? approachTasks.Count.ToString() : "null")); + if (approachTasks != null) + { + foreach (EAIApproachAndAttackTarget task in approachTasks) + { + task.targetClasses.Clear(); + task.targetClasses.Add(new EAIApproachAndAttackTarget.TargetClass + { + type = typeof(EntityZombie), + chaseTimeMax = 0f + }); + } + } + + // AITarget: whichever task picks the nearest valid entity to go attack in the first + // place. Same restriction here, or the zombie would never even pick a target to hand + // off to the approach task above. + List targetTasks = zombie.aiManager.GetTargetTasks(); + Debug.Log("[NecromancerTome] CharmZombie: targetTasks=" + (targetTasks != null ? targetTasks.Count.ToString() : "null")); + if (targetTasks != null) + { + foreach (EAISetNearestEntityAsTarget task in targetTasks) + { + task.targetClasses.Clear(); + task.targetClasses.Add(new EAISetNearestEntityAsTarget.TargetClass + { + type = typeof(EntityZombie), + hearDistMax = 50f, + seeDistMax = 0f + }); + task.playerTargetClassIndex = -1; + } + } + + // Drop whatever it was mid-attack on (almost certainly the player who just threw the + // book at it) so the switch takes effect immediately instead of after its current target dies/despawns. + zombie.SetAttackTarget(null, 0); + Debug.Log("[NecromancerTome] CharmZombie: done, attack target cleared for " + zombie.EntityName); + } + } +} diff --git a/HarmonySrc/DamagePatch.cs b/HarmonySrc/DamagePatch.cs new file mode 100644 index 0000000..b4d05e4 --- /dev/null +++ b/HarmonySrc/DamagePatch.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using HarmonyLib; + +namespace NecromancerTome +{ + /// + /// Lets a charmed ("Devi­ator"-hit) zombie actually hurt other zombies. + /// + /// EntityAlive.DamageEntity has a hardcoded block, independent of anything AI/targeting + /// related: `if (!isHeatDamage && (entityFlags & attacker.entityFlags & EntityFlags.Zombie) != None) + /// return -1;` - i.e. any two entities that are BOTH flagged EntityFlags.Zombie (which is + /// every zombie, always) simply cannot damage each other, full stop, no matter what + /// CharmPatch.cs did to the attacker's AI targeting. Without this, a charmed zombie would + /// walk up to another zombie and "attack" it forever with every hit landing as a no-op. + /// + /// Fix: for the duration of a single DamageEntity call where the attacker currently has + /// buffNecroDeviatorCharm and the target is also a zombie, temporarily clear + /// EntityFlags.Zombie on the ATTACKER only (never the target, never persisted) so the AND + /// check in the original method comes up empty and damage proceeds normally. Restored + /// immediately afterward, so nothing else about that zombie (kill-counting via its Tags + /// property, quest tracking, anything else keyed off EntityFlags) is ever affected outside + /// this one call. A Stack (not a single field) survives re-entrant DamageEntity calls + /// correctly (e.g. explosions/knockback triggering further damage inside the same callstack). + /// + [HarmonyPatch(typeof(EntityAlive), "DamageEntity")] + public static class Patch_EntityAlive_DamageEntity_CharmedZombieVsZombie + { + public static readonly Stack ToggledAttackers = new Stack(); + + public static void Prefix(EntityAlive __instance, DamageSource _damageSource) + { + EntityZombie toggled = null; + if (__instance is EntityZombie && __instance.world != null) + { + EntityAlive attacker = __instance.world.GetEntity(_damageSource.getEntityId()) as EntityAlive; + if (attacker is EntityZombie attackerZombie + && attackerZombie.Buffs != null + && attackerZombie.Buffs.HasBuff(Patch_EntityBuffs_AddBuff_DeviatorCharm.CharmBuffName) + && (attackerZombie.entityFlags & EntityFlags.Zombie) != EntityFlags.None) + { + attackerZombie.entityFlags &= ~EntityFlags.Zombie; + toggled = attackerZombie; + } + } + ToggledAttackers.Push(toggled); + } + + public static void Postfix() + { + EntityZombie toggled = ToggledAttackers.Pop(); + if (toggled != null) + { + toggled.entityFlags |= EntityFlags.Zombie; + } + } + } +} diff --git a/HarmonySrc/FinalSlides.cs b/HarmonySrc/FinalSlides.cs new file mode 100644 index 0000000..9906df0 --- /dev/null +++ b/HarmonySrc/FinalSlides.cs @@ -0,0 +1,360 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// Финальная сцена Чёрного портального камня: шесть полноэкранных слайдов с текстом и + /// кнопками Назад/Дальше, а на последнем - выбор концовки. Заменяет собой прежнюю связку + /// "подтверждение -> сразу видео" (см. PortalStonePatch.ActivateBlackPortal, которая теперь + /// зовёт Begin вместо PlayVideo). + /// + /// ЗАЧЕМ ЭТО ВООБЩЕ (BACKLOG.md, "концовка серией диалоговых окон вместо видео", запись + /// 2026-09-07): видео не локализуется - под английскую версию пришлось бы держать второй + /// файл. Текст слайдов идёт через Localization.Get + Localization.csv, то есть переводится + /// строкой, как всё остальное в моде. + /// + /// НИ ОДНОГО НОВОГО UI-ПРИМИТИВА ЗДЕСЬ НЕ ИЗОБРЕТЕНО - вся сцена собрана из двух уже + /// работающих в этом моде кусков: + /// - Картинка: своё окно на слайд (Config/XUi_InGame/windows.xml, шесть штук + /// necroFinalSlide1..6) плюс свой атлас UIAtlases/NecroFinal. Механизм атласа тот же, + /// что у ItemIconAtlas/ItemIconAtlasGreyscale. Окна БЕЗ контроллера и без привязок - + /// спрайт в каждом прописан жёстко, поэтому листание это просто Close одного окна и + /// Open другого, см. ShowSlideWindow. Открываются НЕмодально: в + /// GUIWindowManager.openInternal модальное открытие зовёт CloseAllOpenModalWindows(), и + /// модальный messageBox снёс бы модальный слайд. + /// - Текст и кнопки: ванильный XUiC_MessageBoxWindowGroup.ShowCustom - ровно тот же вызов, + /// которым PortalStonePatch уже показывает подтверждение "да/нет". Шаблон + /// <messagebox> (Data/Config/XUi_Common/templates.xml:142) объявляет таблицу кнопок с + /// repeat_count="3", то есть три слота - их хватает и на Назад/Дальше, и на выбор из + /// двух вариантов с Назад в придачу. + /// + /// ПОЧЕМУ ПЕРЕЛИСТЫВАНИЕ НЕ МОРГАЕТ: showMessage (декомпилировано) делает + /// "if (windowGroup.isShowing) OnOpen(); else windowManager.Open(...)" - то есть повторный + /// ShowCustom на уже открытой коробке просто обновляет её содержимое на месте, не закрывая + /// и не открывая окно заново. + /// + /// ESC. _buttonOnExternalClose говорит, какую кнопку "нажать", если окно закрыли снаружи + /// (Esc). Ставим 2 ("Дальше") на слайдах 1-5 и 0 ("Назад") на шестом. Первое - чтобы Esc не + /// оставлял игрока в замершей паузе с картинкой на весь экран и без единого элемента + /// управления; второе - чтобы случайным Esc нельзя было ВЫБРАТЬ концовку. При -1 окно + /// закрылось бы, не нажав ничего, и сцена повисла бы намертво. + /// + /// ПАУЗА. Begin ставит GameManager.Instance.Pause(true) один раз на всю сцену и больше её не + /// трогает: снимать паузу незачем, потому что любой выход отсюда ведёт в главное меню, а + /// GameManager.Disconnect() зовёт Pause(false) внутри себя (см. комментарий в + /// PortalStonePatch.ActivateBlackPortal). Как и вся остальная UI-часть этого мода, сцена + /// рассчитана на локального игрока - Pause вообще работает только в одиночной игре, это + /// ограничение самой ванили, а не мода. + /// + public static class FinalSlides + { + public const int PageCount = 6; + + /// Имена окон и групп в Config/XUi_InGame/windows.xml + xui.xml: к префиксу + /// приписывается номер страницы, 1..PageCount. + public const string SlideWindowPrefix = "necroFinalSlide"; + + /// Окно-заливка под текст эпилога: картинки к этому моменту кончились, текст + /// идёт по чёрному. Объявлено в Config/XUi_InGame/windows.xml + xui.xml. + public const string BlackWindow = "necroFinalBlack"; + + /// Имя Чёрного портального камня - его отбирают у игрока на концовке + /// "Вернуться" (см. ConsumeBlackStone). Совпадает с + /// Patch_ItemActionEat_ExecuteAction_PortalStones.BlackStoneName, продублировано здесь + /// строкой, чтобы этот класс не зависел от патча в другом файле. + public const string BlackStoneName = "thrownStonePortalBlack"; + + /// Имя спрайта с картинкой внутри окна слайда. ОБЯЗАНО совпадать с + /// name="slideArt" в Config/XUi_InGame/windows.xml - по нему ищется Transform, который + /// потом масштабируется наездом (см. RevealPage). + public const string SlideArtId = "slideArt"; + + /// Пауза между появлением картинки и появлением текстовой коробки поверх неё, + /// в секундах. Продиктовано 2026-09-09: "можно ли выводить диалоговое окно с задержкой в + /// 5 секунд, чтобы пользователь успевал увидеть картинку". + /// + /// ВРЕМЯ СЧИТАЕТСЯ НЕМАСШТАБИРУЕМОЕ, И ЭТО НЕ ПРИДИРКА. Сцена стартует из Begin сразу + /// после GameManager.Instance.Pause(true), а тот выставляет Time.timeScale = 0. Обычный + /// WaitForSeconds и Time.deltaTime считают как раз по масштабированному времени, то есть + /// при timeScale = 0 не досчитали бы НИКОГДА: текст не появился бы вообще, и игрок + /// остался бы с картинкой и без единой кнопки. Поэтому везде ниже - unscaledDeltaTime. + /// (Ровно на этом уже обжигались в PyramidWardPatch, см. BACKLOG.md.) + /// + /// Поставить 0, чтобы вернуть прежнее поведение "текст сразу, без наезда". + public const float SlideRevealSeconds = 5f; + + /// Насколько картинка увеличивается за эти секунды: 0.10 = медленный наезд на + /// 10%. Продиктовано 2026-09-09. Ноль - наезда нет, картинка просто стоит. + /// + /// Делается через localScale спрайта, а не через пересчёт якорей. Спрайт растянут по + /// #cam на все четыре стороны, то есть его РАЗМЕР каждый кадр пересчитывает сама NGUI по + /// якорям - трогать размер бессмысленно, его тут же перезапишут. А localScale к этому + /// отношения не имеет: XUiView выставляет его один раз при создании (Vector3.one) и + /// больше не трогает, так что наше значение держится. Масштабирование идёт от центра + /// виджета, поэтому кадр наезжает симметрично и ничего не перекашивает. + /// + /// Пропорции при этом не плывут: картинка уже приведена к 16:9 чёрными полями по бокам + /// (см. UIAtlases/NecroFinal и комментарий в windows.xml), а наезд на 10% срезает по 4.5% + /// с каждой стороны - поля становятся уже, но не исчезают и не растягиваются. + public const float SlideZoomAmount = 0.10f; + + /// Номер сейчас открытого слайда, 0 = ни одного. Статика, а не поле игрока: + /// сцена по построению одна на весь клиент и заканчивается выходом в главное меню. + private static int openSlide; + + /// Слайды, которые игрок уже видел. Задержка и наезд нужны только при ПЕРВОМ + /// показе: на кнопку "Назад" картинка уже знакома, и повторное пятисекундное ожидание + /// читалось бы как зависание, а не как пауза на разглядывание. + private static readonly HashSet revealedPages = new HashSet(); + + public static void Begin(EntityPlayerLocal player) + { + Debug.Log("[NecromancerTome] FinalSlides: starting finale for owner=" + player.entityId); + GameManager.Instance.Pause(true); + openSlide = 0; + revealedPages.Clear(); + ShowPage(player, 1); + } + + private static void ShowPage(EntityPlayerLocal player, int page) + { + LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPlayer(player); + ShowSlideWindow(playerUI, page); + + // Первый показ слайда - сначала голая картинка с медленным наездом, текстовая коробка + // приезжает через SlideRevealSeconds. Возврат на уже виденный слайд - сразу с текстом + // и без повторного наезда. + if (SlideRevealSeconds > 0f && revealedPages.Add(page)) + { + GameManager.Instance.StartCoroutine(RevealPage(player, page)); + return; + } + ShowText(player, page); + } + + /// Пять секунд наезда, потом текст. Крутится покадрово, а не через + /// WaitForSecondsRealtime, потому что наезд всё равно надо обновлять каждый кадр - так + /// одна корутина делает обе вещи. + /// + /// GameManager - обычный MonoBehaviour, и корутины на нём тикают из Update, то есть при + /// timeScale = 0 продолжают идти; замирает только само ожидание, если считать его + /// масштабированным временем (см. SlideRevealSeconds). Тот же способ запуска корутины + /// использует и ваниль - MinEventActionModifyStats.executeDelayed для Delay= у + /// triggered_effect. + private static IEnumerator RevealPage(EntityPlayerLocal player, int page) + { + Transform art = FindSlideArt(player, page); + if (art != null) + { + art.localScale = Vector3.one; + } + + float elapsed = 0f; + while (elapsed < SlideRevealSeconds) + { + // Сцену успели закрыть (например, игрок вышел через Esc-меню, пока коробки на + // экране не было) - бросаем и наезд, и показ текста. + if (openSlide != page) + { + yield break; + } + elapsed += Time.unscaledDeltaTime; + if (art != null) + { + float k = 1f + SlideZoomAmount * Mathf.Clamp01(elapsed / SlideRevealSeconds); + art.localScale = new Vector3(k, k, 1f); + } + yield return null; + } + + if (openSlide == page) + { + ShowText(player, page); + } + } + + /// Ищет Transform картинки внутри окна слайда: группа по имени -> потомок по + /// id -> его view. GetChildById рекурсивный (декомпилировано), так что вложенность + /// значения не имеет. Возвращает null, если что-то из цепочки не нашлось - тогда наезда + /// просто не будет, а текст всё равно покажется: анимация не должна уметь сломать + /// сцену. + private static Transform FindSlideArt(EntityPlayerLocal player, int page) + { + XUiController group = LocalPlayerUI.GetUIForPlayer(player).xui.FindWindowGroupByName(SlideWindowPrefix + page); + XUiController art = (group != null) ? group.GetChildById(SlideArtId) : null; + if (art == null || art.ViewComponent == null) + { + Debug.LogWarning("[NecromancerTome] FinalSlides: no \"" + SlideArtId + "\" view on slide " + page + ", zoom skipped"); + return null; + } + return art.ViewComponent.UiTransform; + } + + private static void ShowText(EntityPlayerLocal player, int page) + { + LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPlayer(player); + bool lastPage = page >= PageCount; + XUiC_MessageBoxWindowGroup.ShowCustom( + playerUI.xui, + Localization.Get("necroFinalPage" + page + "Title"), + Localization.Get("necroFinalPage" + page + "Text"), + "", + delegate(XUiC_MessageBoxWindowGroup mb) + { + // Слот 0 - "Назад", кроме самого первого слайда, где назад некуда. + // Хоткея нарочно нет (null): Esc уже разобран через + // _buttonOnExternalClose ниже, и вешать его сюда же значило бы обработать + // одно нажатие дважды. + if (page > 1) + { + mb.Buttons[0].Set("necroFinalBtnBack", null, delegate { ShowPage(player, page - 1); }); + } + + if (!lastPage) + { + // DefaultConfirm вешает на кнопку хоткей Submit (Enter) - листать можно + // и с клавиатуры, не целясь мышью. + mb.Buttons[2].DefaultConfirm("necroFinalBtnNext", delegate { ShowPage(player, page + 1); }); + } + else + { + mb.Buttons[1].Set("necroFinalBtnStay", null, delegate { ShowEpilogue(player, _stay: true); }); + mb.Buttons[2].Set("necroFinalBtnReturn", null, delegate { ShowEpilogue(player, _stay: false); }); + } + }, + _openMainMenuOnClose: false, + _modal: true, + _buttonOnOutsideClick: -1, + // См. блок "ESC" в комментарии к классу. + _buttonOnExternalClose: lastPage ? 0 : 2); + } + + /// Закрывает предыдущее окно-слайд и открывает нужное. Открытие немодальное - + /// иначе следующий же модальный ShowCustom закрыл бы картинку (GUIWindowManager + /// .openInternal -> CloseAllOpenModalWindows). + private static void ShowSlideWindow(LocalPlayerUI playerUI, int page) + { + if (openSlide == page) + { + return; + } + CloseSlideWindow(playerUI); + playerUI.windowManager.Open(SlideWindowPrefix + page, false); + openSlide = page; + } + + private static void CloseSlideWindow(LocalPlayerUI playerUI) + { + if (openSlide > 0) + { + playerUI.windowManager.Close(SlideWindowPrefix + openSlide); + openSlide = 0; + } + } + + /// Седьмой экран: чёрный фон и текст выбранной концовки, единственная кнопка - + /// "Конец". + /// + /// ВИДЕО ОТСЮДА УБРАНО 2026-09-09 по прямому указанию ("временно, убираем вообще видосы + /// из финала"). Файлы Video/FinalStay.webm и FinalReturn.webm остались лежать на месте, а + /// сам вызов XUiC_VideoPlayer.PlayVideo целиком сохранён в + /// PortalStonePatch.PlayBlackPortalVideoLegacy - вернуть видео можно, не восстанавливая + /// код по кускам. + /// + /// Задержки и наезда здесь нет намеренно: смотреть на чёрный экран пять секунд незачем, + /// текст показывается сразу. + private static void ShowEpilogue(EntityPlayerLocal player, bool _stay) + { + string choice = _stay ? "stay" : "return"; + Debug.Log("[NecromancerTome] FinalSlides: ending chosen (" + choice + ") by owner=" + player.entityId); + + LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPlayer(player); + CloseSlideWindow(playerUI); + playerUI.windowManager.Open(BlackWindow, false); + + string prefix = _stay ? "necroFinalStay" : "necroFinalReturn"; + XUiC_MessageBoxWindowGroup.ShowCustom( + playerUI.xui, + Localization.Get(prefix + "Title"), + Localization.Get(prefix + "Text"), + "", + delegate(XUiC_MessageBoxWindowGroup mb) + { + mb.Buttons[0].DefaultConfirm("necroFinalBtnTheEnd", delegate { FinishEnding(player, _stay); }); + }, + _openMainMenuOnClose: false, + _modal: true, + _buttonOnOutsideClick: -1, + // Esc здесь равносилен "Конец": выбор уже сделан, отменять нечего, а оставить + // игрока на чёрном экране в замершей паузе нельзя. + _buttonOnExternalClose: 0); + } + + /// Две концовки расходятся именно здесь, и только здесь. + private static void FinishEnding(EntityPlayerLocal player, bool _stay) + { + LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPlayer(player); + playerUI.windowManager.Close(BlackWindow); + + if (_stay) + { + // "Остаться" - игра действительно кончилась. Disconnect() это ровно то, что зовёт + // кнопка "Выйти в главное меню" из игрового Esc-меню: она закрывает окна, сама + // снимает паузу (Pause(false) внутри), сохраняет и гасит локальный сервер. + Debug.Log("[NecromancerTome] FinalSlides: staying - exiting to main menu"); + GameManager.Instance.Disconnect(); + return; + } + + // "Вернуться" - игрок продолжает играть в том же мире. Паузу здесь снимаем сами: + // Disconnect(), который делал это за нас, не вызывается. + Debug.Log("[NecromancerTome] FinalSlides: returning to the game"); + ConsumeBlackStone(player); + GameManager.Instance.Pause(false); + } + + /// Забирает у игрока один Чёрный портальный камень. Продиктовано 2026-09-09: + /// "портальный камень исчезает из инвентаря. Разумеется, его потом можно скрафтить + /// заново" - рецепт не трогаем, только предмет. + /// + /// Ищем и в поясе (inventory), и в рюкзаке (bag): камень применяется из руки, то есть + /// лежит в поясе, но игрок мог за время сцены... вообще-то не мог - игра на паузе, а + /// сцена модальная. Проверяем оба всё равно, это дешевле, чем полагаться на догадку о + /// том, где предмет обязан оказаться. + /// + /// DecItem у Bag и Inventory имеет одинаковую сигнатуру и возвращает, сколько СНЯТЬ НЕ + /// УДАЛОСЬ (декомпилировано) - поэтому остаток от первого вызова передаётся во второй. + /// Если камня не нашлось нигде, пишем варнинг и молча продолжаем: концовка не должна + /// падать из-за инвентаря. + private static void ConsumeBlackStone(EntityPlayerLocal player) + { + ItemValue stone = ItemClass.GetItem(BlackStoneName); + if (stone == null || stone.IsEmpty()) + { + Debug.LogWarning("[NecromancerTome] FinalSlides: item \"" + BlackStoneName + "\" not found, nothing consumed"); + return; + } + + int left = 1; + if (player.inventory != null) + { + left = player.inventory.DecItem(stone, left); + } + if (left > 0 && player.bag != null) + { + left = player.bag.DecItem(stone, left); + } + + if (left > 0) + { + Debug.LogWarning("[NecromancerTome] FinalSlides: no " + BlackStoneName + " found on owner=" + player.entityId + " to consume"); + } + else + { + Debug.Log("[NecromancerTome] FinalSlides: consumed one " + BlackStoneName + " from owner=" + player.entityId); + } + } + } +} diff --git a/HarmonySrc/ModEntry.cs b/HarmonySrc/ModEntry.cs new file mode 100644 index 0000000..fd86381 --- /dev/null +++ b/HarmonySrc/ModEntry.cs @@ -0,0 +1,45 @@ +using System.Reflection; +using HarmonyLib; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// Mod entry point. The game finds this by scanning every assembly dropped in a + /// Mods/<ModFolder>/ directory for a type implementing IModApi. + /// + public class ModEntry : IModApi + { + public void InitMod(Mod _modInstance) + { + var harmony = new Harmony("necromancertome.harmony"); + harmony.PatchAll(Assembly.GetExecutingAssembly()); + PetFollowPatch.Init(); + // PyramidWardPatch.cs's TEFeaturePyramidWard needs no Init() call - it's discovered + // automatically by the engine's own TileEntityCompositeData reflection scan (see that + // file's class doc comment), not registered here like PetFollowPatch's UnityUpdate hook. + + // Diagnostic-only, added 2026-08-28 while chasing "VictimPatch never logs anything at + // all for dropItemOnDeath, even though decompiling EntityAlive.OnEntityDeath() + // confirms it's called unconditionally right after the 'killed by' line seen in the + // log". This checks, at load time, whether Harmony actually attached our Prefix to + // that method at all - rules "patch never applied" in or out without waiting on + // another in-game death. + VerifyPrefixAttached(typeof(EntityAlive), "dropItemOnDeath"); + VerifyPrefixAttached(typeof(Entity), "DropBagServer"); + } + + public static void VerifyPrefixAttached(System.Type type, string methodName) + { + MethodBase method = AccessTools.Method(type, methodName); + if (method == null) + { + Debug.LogWarning("[NecromancerTome] ModEntry: could not resolve " + type.Name + "." + methodName + " via AccessTools - method not found"); + return; + } + Patches info = Harmony.GetPatchInfo(method); + int prefixCount = info != null && info.Prefixes != null ? info.Prefixes.Count : 0; + Debug.Log("[NecromancerTome] ModEntry: " + type.Name + "." + methodName + " resolved, has " + prefixCount + " prefix patch(es) attached after PatchAll"); + } + } +} diff --git a/HarmonySrc/NecromancerBloodPatch.cs b/HarmonySrc/NecromancerBloodPatch.cs new file mode 100644 index 0000000..6c3c415 --- /dev/null +++ b/HarmonySrc/NecromancerBloodPatch.cs @@ -0,0 +1,137 @@ +using HarmonyLib; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// "Кровь некроманта" (Necromancer's Blood) - dictated 2026-08-30. See items.xml + /// (resourceNecromancerBlood) for the item, recipes.xml for the base recipe (an empty jar, + /// like any other resource conversion). Two rules the user asked for have NO vanilla XML + /// equivalent at all, so both are enforced here instead: + /// 1. "нужна... наличие любого ножа" - a knife must be present (in the toolbelt or + /// backpack) to craft this, but is NOT consumed. recipes.xml has no "required but not + /// consumed" ingredient flag (confirmed - only `craft_tool="itemName"` exists for + /// something adjacent, but it takes exactly ONE item name, not "any item of a category", + /// and its actual runtime enforcement point wasn't confirmed by decompilation either - + /// not risking an untested mechanism for this). + /// 2. "При крафте нужно отнимать у персонажа 90% имеющегося ХП" - crafting this recipe + /// costs 90% of the player's CURRENT health. Recipes have no cost hook beyond their + /// ingredient list at all. + /// + /// "любого ножа" (ANY knife) is detected via ItemClass.DisplayType == "meleeKnife" - decompiled + /// Data/Config/items.xml directly: every real vanilla knife (meleeWpnBladeT0BoneKnife, + /// meleeWpnBladeT1HuntingKnife, even meleeWpnBladeT3Machete) shares this exact DisplayType, + /// which is how the game itself categorizes "the knife family" in its own UI - a single, + /// reliable check instead of hand-maintaining a list of item names. necroWpnBladeNecroKnife + /// (Extends meleeWpnBladeT0BoneKnife, never overrides DisplayType) is covered by the same + /// check automatically. + /// + /// PATCH POINTS - both on XUiC_RecipeStack, decompiled directly (not guessed): + /// - SetRecipe(...) Prefix: the earliest confirmed point a "craft this recipe" click reaches + /// (XUiC_CraftingQueue.AddRecipeToCraftAtIndex calls straight into this). Blocking here + /// (return false) stops the craft from ever starting - recipe/recipeCount never get set, + /// isCrafting never becomes true. + /// - outputStack() Prefix+Postfix (via __state): outputStack() is where the output item is + /// actually granted, once per queued unit - HP is only deducted when __result is true, + /// i.e. the item genuinely was produced this call, not on a failed/blocked attempt. + /// + /// CAVEAT - not glossed over: decompiling XUiC_RecipeStack/XUiC_CraftingQueue/XUiM_Recipes did + /// NOT turn up the exact line that removes ingredients from the player's inventory (it happens + /// somewhere upstream of SetRecipe, in whatever UI code handles the actual "Craft" button click + /// - not found within reasonable search). XUiC_RecipeStack.HandleOnPress (the CANCEL button) + /// refunds ingredients, which proves they're already gone by the time SetRecipe runs - so if + /// SetRecipe's Prefix blocks a no-knife attempt, the jar may already be spent with nothing + /// granted back. Blocking at the earliest CONFIRMED point was judged better than not blocking + /// at all; a lost jar on a rare misclick is a minor rough edge, not a correctness bug. Revisit + /// if this turns out to happen often in practice. + /// + public static class NecromancerBloodPatch + { + public const string BloodItemName = "resourceNecromancerBlood"; + public const float HealthCostFraction = 0.9f; + + public static bool HasAnyKnife(EntityPlayerLocal player) + { + return ContainsKnife(player.inventory?.GetSlots()) || ContainsKnife(player.bag?.GetSlots()); + } + + private static bool ContainsKnife(ItemStack[] slots) + { + if (slots == null) + { + return false; + } + foreach (ItemStack stack in slots) + { + if (stack == null || stack.IsEmpty()) + { + continue; + } + if (stack.itemValue?.ItemClass?.DisplayType == "meleeKnife") + { + return true; + } + } + return false; + } + } + + [HarmonyPatch(typeof(XUiC_RecipeStack), "SetRecipe")] + public static class Patch_XUiC_RecipeStack_SetRecipe_NecromancerBlood + { + public static bool Prefix(XUiC_RecipeStack __instance, Recipe _recipe, bool recipeModification) + { + // recipeModification covers the "clear this slot" calls (ClearQueue/RefreshQueue/ + // cancel) - never block those, only an actual attempt to start crafting our recipe. + if (recipeModification || _recipe == null || _recipe.GetName() != NecromancerBloodPatch.BloodItemName) + { + return true; + } + EntityPlayerLocal player = __instance.xui?.playerUI?.entityPlayer; + if (player == null) + { + return true; + } + if (!NecromancerBloodPatch.HasAnyKnife(player)) + { + GameManager.ShowTooltip(player, "resourceNecromancerBloodNeedsKnife"); + Debug.Log("[NecromancerTome] NecromancerBloodPatch: blocked craft (no knife present) for owner=" + player.entityId); + return false; + } + return true; + } + } + + [HarmonyPatch(typeof(XUiC_RecipeStack), "outputStack")] + public static class Patch_XUiC_RecipeStack_outputStack_NecromancerBlood + { + public static void Prefix(XUiC_RecipeStack __instance, out EntityPlayerLocal __state) + { + __state = null; + if (__instance.recipe != null && __instance.recipe.GetName() == NecromancerBloodPatch.BloodItemName) + { + __state = __instance.xui?.playerUI?.entityPlayer; + } + } + + public static void Postfix(bool __result, EntityPlayerLocal __state) + { + if (!__result || __state == null) + { + return; + } + // Deliberately not clamped to leave the player at least 1 HP - the user asked for a + // straight 90% cost, and a blood ritual that can genuinely kill you if you're already + // badly hurt fits the theme. AddHealth is the same safe, non-combat HP-modification + // API vanilla itself uses (decompiled EntityAlive.AddHealth) - not DamageEntity/ + // DamageResponse, since this isn't damage from a source, it's a direct self-cost. + int amount = Mathf.RoundToInt(__state.Health * NecromancerBloodPatch.HealthCostFraction); + if (amount <= 0) + { + return; + } + __state.AddHealth(-amount); + Debug.Log("[NecromancerTome] NecromancerBloodPatch: crafted blood, deducted " + amount + " HP from owner=" + __state.entityId); + } + } +} diff --git a/HarmonySrc/NecromancerHarmony.csproj b/HarmonySrc/NecromancerHarmony.csproj new file mode 100644 index 0000000..9284fdf --- /dev/null +++ b/HarmonySrc/NecromancerHarmony.csproj @@ -0,0 +1,71 @@ + + + + netstandard2.1 + NecromancerHarmony + NecromancerTome + latest + disable + + false + false + bin\ + + + + + + ..\..\0_TFP_Harmony\0Harmony.dll + false + + + ..\..\..\7DaysToDie_Data\Managed\Assembly-CSharp.dll + false + + + ..\..\..\7DaysToDie_Data\Managed\UnityEngine.CoreModule.dll + false + + + ..\..\..\7DaysToDie_Data\Managed\UnityEngine.ParticleSystemModule.dll + false + + + + ..\..\..\7DaysToDie_Data\Managed\UnityEngine.PhysicsModule.dll + false + + + + ..\..\..\7DaysToDie_Data\Managed\UnityEngine.AnimationModule.dll + false + + + + ..\..\..\7DaysToDie_Data\Managed\InControl.dll + false + + + + ..\..\..\7DaysToDie_Data\Managed\UnityEngine.InputLegacyModule.dll + false + + + + ..\TEPersistenceSrc\bin\NecromancerTEPersistence.dll + false + + + + diff --git a/HarmonySrc/NoteFlashbackPatch.cs b/HarmonySrc/NoteFlashbackPatch.cs new file mode 100644 index 0000000..9b82cb3 --- /dev/null +++ b/HarmonySrc/NoteFlashbackPatch.cs @@ -0,0 +1,99 @@ +using System; +using HarmonyLib; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// Duke's note ("Записка от Дюка", item noteDuke01) - user request 2026-08-30: "в момент + /// открытия записки, ставить игру на паузу и проигрывать флэшбек" (at the moment the note is + /// opened, pause the game and play a flashback). Reuses the exact pause+video pipeline + /// already built and tested for the Black Portal Stone (see PortalStonePatch.cs's + /// ActivateBlackPortal - GameManager.Instance.Pause/XUiC_VideoPlayer.PlayVideo, both APIs + /// decompiled there already, same reasoning applies unchanged here). + /// + /// FINDING THE RIGHT PATCH POINT: noteDuke01 has no custom C# class of its own - it's a + /// plain Class="Eat" item (items.xml) whose entire "reading" experience is a vanilla trick: + /// PromptTitle="noteDuke01"/PromptDescription="noteDuke01Desc" make the ENGINE ITSELF show + /// the note as a XUiC_MessageBoxWindowGroup.ShowOkCancel(...) confirm box - decompiled + /// ItemActionEat directly and confirmed it has no UI-showing code of its own at all + /// (NeedPrompt/PromptTitle/PromptDescription/bPromptChecked are all read, never acted on, + /// inside that class); the actual ShowOkCancel call lives in the CALLERS instead - two + /// separate, decompiled call sites: + /// 1. ItemClass.ExecuteAction(int, ItemInventoryData, bool, PlayerActionsLocal) - the + /// holding-the-item-and-clicking path. + /// 2. XUiC_ItemStack's inventory "Use" (double-click / context-menu) path. + /// Both funnel through the exact same static XUiC_MessageBoxWindowGroup.ShowOkCancel call - + /// patching THAT one method, instead of either call site separately, covers both input paths + /// with a single patch. + /// + /// IDENTIFYING OUR NOTE: ShowOkCancel receives only already-localized strings, not an item + /// reference - decompilation confirms this overload has no ItemValue/ItemClass parameter at + /// all. Matched by comparing the incoming title against Localization.Get("noteDuke01") (the + /// exact PromptTitle key from items.xml) - unique to this one item in the whole game, not a + /// generic vanilla dialog string, so this is a safe match, not a guess. + /// + /// FLOW: on match, suppress the real dialog for now (Prefix returns false), pause the game, + /// and play the flashback; only once the video finishes (or is skipped/errors - PlayVideo's + /// own onFinished callback fires in every case, confirmed by decompiling + /// XUiC_VideoPlayer.OnClose/FinishAndClose, so this can never soft-lock the pause) does it + /// unpause and open the REAL note-text dialog (a re-entrant call to ShowOkCancel itself, via + /// a bypass flag so the Prefix doesn't intercept its own follow-up call) - "open note -> + /// flashback -> read text -> confirm", rather than overlapping the video with the text box. + /// + /// VIDEO FILE: Video/DukeNoteFlashback.mp4 - the user's real flashback clip (delivered + /// 2026-08-30 as exch/flashbback.mp4), kept as .mp4 rather than renamed to .webm like the + /// Black Portal placeholder: Unity's VideoPlayer component (confirmed by decompiling + /// XUiV_Video - it wraps a plain UnityEngine.Video.VideoPlayer) natively decodes MP4/H.264 on + /// Windows via Media Foundation, and re-labeling an actual MP4 container as .webm would just + /// make it fail to decode (VP8/VP9 container expected, not H.264) - not decompiled/proven + /// that MP4 plays correctly in THIS build, but there is no reason implied by the decompiled + /// code to expect otherwise, and even a decode failure only degrades to a skipped video (see + /// FLOW above), never a stuck pause. **Not confirmed in game.** + /// + [HarmonyPatch(typeof(XUiC_MessageBoxWindowGroup), "ShowOkCancel")] + public static class Patch_XUiC_MessageBoxWindowGroup_ShowOkCancel_NoteFlashback + { + public const string NoteFlashbackVideoPath = "@modfolder(NecromancerTome):Video/DukeNoteFlashback.mp4"; + + /// Guards the re-entrant call this patch makes to the very method it patches + /// (to actually show the note text once the flashback is done) - without this, that + /// second call would just trigger the Prefix again and loop back into another flashback + /// instead of showing the dialog. Not [ThreadStatic]: XUi/UI code in this game only ever + /// runs on the main thread (every other UI-touching patch in this mod makes the same + /// assumption, e.g. PortalStonePatch.cs's local-player-only UI calls), so a plain static + /// bool is enough here. + public static bool bypass; + + public static bool Prefix(XUi _xuiInstance, string _title, string _text, string _icon, Action _onOk, Action _onCancel, bool _openMainMenuOnClose, bool _modal, bool _cancelOnOutsideClick) + { + if (bypass) + { + return true; + } + if (_xuiInstance == null || _title != Localization.Get("noteDuke01")) + { + return true; + } + + Debug.Log("[NecromancerTome] NoteFlashbackPatch: Duke's note opened, pausing + playing flashback"); + GameManager.Instance.Pause(true); + VideoData videoData = new VideoData { url = NoteFlashbackVideoPath }; + XUiC_VideoPlayer.PlayVideo(_xuiInstance, videoData, true, delegate(bool skipped) + { + Debug.Log("[NecromancerTome] NoteFlashbackPatch: flashback finished (skipped=" + skipped + "), unpausing and showing note text"); + GameManager.Instance.Pause(false); + bypass = true; + try + { + XUiC_MessageBoxWindowGroup.ShowOkCancel(_xuiInstance, _title, _text, _icon, _onOk, _onCancel, _openMainMenuOnClose, _modal, _cancelOnOutsideClick); + } + finally + { + bypass = false; + } + }); + return false; + } + } +} diff --git a/HarmonySrc/ParticlePatch.cs b/HarmonySrc/ParticlePatch.cs new file mode 100644 index 0000000..c043739 --- /dev/null +++ b/HarmonySrc/ParticlePatch.cs @@ -0,0 +1,171 @@ +using HarmonyLib; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// Shrinks and recolors the "RadiatedParticlesOnMesh" glow that both buffNecroDeviatorCharm + /// and buffNecroVictim attach to a zombie (see buffs.xml, action="AttachParticleEffectToEntity" + /// - both buffs reuse the same particle prefab rather than needing two different ones). + /// + /// Why this needs Harmony: AttachParticleEffectToEntity's XML attributes are limited to + /// particle/parent_transform/local_offset/local_rotation/oneshot/shape_mesh/sound - there is + /// no scale or color/alpha attribute (confirmed by decompiling + /// MinEventActionAttachParticleEffectToEntity.ParseXmlAttribute - that's the exhaustive list). + /// The prefab always instantiates at its own authored size/color; nothing in XML can change + /// that. So instead we let the vanilla action run as normal (Postfix, not Prefix - the + /// particle GameObject has to already exist), then find the same child object it just created + /// and adjust it directly - same lookup the engine itself uses internally: a child transform + /// named "Ptl_" + the particle prefab's name, parented under the entity's mesh transform. + /// + /// SizeFactor dropped 0.5 -> 0.2 2026-08-28 (user: "выглядят как шар вне зомби" - even the + /// original half-size shrink still read as a floating ball rather than a mesh-hugging glow). + /// + /// Color (2026-08-28): Deviator green and Victim purple are both explicit now (Deviator used + /// to just be whatever RadiatedParticlesOnMesh's own baked-in color happens to be - reads + /// "green/energy" on its own, never actually set). Explicit per user request: "если оба бафа, + /// то пусть свечения смешиваются" - a zombie carrying both gets Color.Lerp(charm, victim, .5), + /// not one color just overriding the other. + /// + /// Gated to only affect entities carrying at least one of OUR buffs (checked per-buff, not + /// just "is this the right particle name") - not vanilla naturally-irradiated zombies that + /// happen to reuse the same particle prefab elsewhere. + /// + /// GENERALIZED 2026-08-29 for buffNecroPortalChannel (BACKLOG.md item 6, user request: + /// dense green-blue particles while a portal stone channels, thick enough to partially + /// obscure the player) - was hard-gated to `EntityZombie` specifically (`_params.Self is + /// EntityZombie`) since the two original buffs are both zombie-facing; this new one targets + /// the PLAYER, so the check is now against the common `EntityAlive` base (where + /// `.Buffs`/`.emodel` actually live) instead. Also needed its own size/alpha/DENSITY numbers + /// separate from the zombie glow's - the first version reused the same SizeFactor/AlphaFactor + /// constants for all three buffs, which the user confirmed reads as "редкие-редкие" (way too + /// sparse) for a "should partly cover you" effect - see GlowConfig below, one per buff now + /// instead of two shared constants. + /// + [HarmonyPatch(typeof(MinEventActionAttachParticleEffectToEntity), "Execute")] + public static class Patch_AttachParticleEffectToEntity_ShrinkCharmGlow + { + public const string ParticleName = "RadiatedParticlesOnMesh"; + + public class GlowConfig + { + public Color Tint; + public float SizeFactor; + public float AlphaFactor; + /// Multiplies both the emission rate (particles/second) AND maxParticles by + /// this factor together - raising rate alone caps out silently once the system hits + /// its authored maxParticles ceiling, so both have to move together to actually get a + /// visibly denser cloud instead of the same particle count arriving faster. + public float DensityFactor; + } + + /// Unchanged from the original 2026-08-28 tuning - the zombie-facing glow was + /// never asked to get denser/bigger, only the new portal-channel one was. + public static readonly GlowConfig CharmGlow = new GlowConfig { Tint = new Color(0.2f, 1f, 0.3f), SizeFactor = 0.2f, AlphaFactor = 0.5f, DensityFactor = 1f }; + /// Purple, per user request 2026-08-28 ("подсвети бафнутого зомби... фиолетовым"). + public static readonly GlowConfig VictimGlow = new GlowConfig { Tint = new Color(0.55f, 0.05f, 0.85f), SizeFactor = 0.2f, AlphaFactor = 0.5f, DensityFactor = 1f }; + /// RE-TUNED 2026-08-29 (user: "частицы есть, но они редкие-редкие. А надо чтобы + /// прямо густо располагались... чтобы частично перекрывали внешний вид" + colour changed + /// from the first version's near-black to green-blue/teal, "зелёноголубые"). SizeFactor + /// bumped from a shrink (0.2, matching the mesh-hugging zombie glow) to just under full + /// size (0.9) - a swirl meant to partly obscure the player needs to actually be + /// body-sized, not a tight skin-hugging glow. AlphaFactor raised to near-opaque (0.9) for + /// the same "obscures the view" reason - the zombie glow's own 0.5 was deliberately subtle, + /// this one shouldn't be. DensityFactor=5 - the actual fix for "редкие-редкие", multiplies + /// both emission rate and maxParticles together (see GlowConfig's own doc on why both). + public static readonly GlowConfig PortalChannelGlow = new GlowConfig { Tint = new Color(0.1f, 0.85f, 0.8f), SizeFactor = 0.9f, AlphaFactor = 0.9f, DensityFactor = 5f }; + + public static void Postfix(MinEventActionAttachParticleEffectToEntity __instance, MinEventParams _params) + { + if (_params.Self == null || __instance.goToInstantiate == null) + { + return; + } + if (__instance.goToInstantiate.name != ParticleName) + { + return; + } + if (!(_params.Self is EntityAlive entity) || entity.Buffs == null) + { + return; + } + bool isPortalChannel = entity.Buffs.HasBuff(Patch_ItemActionEat_ExecuteAction_PortalStones.ChannelBuffName); + bool isVictim = entity.Buffs.HasBuff(Patch_Entity_DropBagServer_VictimBag.VictimBuffName); + bool isCharm = entity.Buffs.HasBuff(Patch_EntityBuffs_AddBuff_DeviatorCharm.CharmBuffName); + if (!isPortalChannel && !isVictim && !isCharm) + { + return; + } + // Portal channel is player-only and never coexists with the zombie-facing buffs + // below in practice, so it's kept as a simple separate branch rather than folded + // into the same Lerp blend those two use with each other. + GlowConfig config; + if (isPortalChannel) + { + config = PortalChannelGlow; + } + else if (isVictim && isCharm) + { + config = new GlowConfig { Tint = Color.Lerp(CharmGlow.Tint, VictimGlow.Tint, 0.5f), SizeFactor = CharmGlow.SizeFactor, AlphaFactor = CharmGlow.AlphaFactor, DensityFactor = 1f }; + } + else + { + config = isVictim ? VictimGlow : CharmGlow; + } + + Transform meshTransform = entity.emodel != null ? entity.emodel.meshTransform : null; + if (meshTransform == null) + { + return; + } + Transform particleTransform = meshTransform.Find("Ptl_" + ParticleName); + if (particleTransform == null) + { + return; + } + + // Belt-and-suspenders for size: not every particle system's Scaling Mode respects + // transform scale, but startSizeMultiplier always does regardless of that setting. + particleTransform.localScale = Vector3.one * config.SizeFactor; + ParticleSystem[] systems = particleTransform.GetComponentsInChildren(true); + foreach (ParticleSystem ps in systems) + { + ParticleSystem.MainModule main = ps.main; + main.startSizeMultiplier *= config.SizeFactor; + main.maxParticles = Mathf.Max(1, Mathf.RoundToInt(main.maxParticles * config.DensityFactor)); + + if (config.DensityFactor != 1f) + { + ParticleSystem.EmissionModule emission = ps.emission; + emission.rateOverTimeMultiplier *= config.DensityFactor; + emission.rateOverDistanceMultiplier *= config.DensityFactor; + } + + ParticleSystem.MinMaxGradient startColor = main.startColor; + switch (startColor.mode) + { + case ParticleSystemGradientMode.Color: + { + Color c = config.Tint; + c.a = startColor.color.a * config.AlphaFactor; + startColor.color = c; + break; + } + case ParticleSystemGradientMode.TwoColors: + { + Color min = config.Tint; + Color max = config.Tint; + min.a = startColor.colorMin.a * config.AlphaFactor; + max.a = startColor.colorMax.a * config.AlphaFactor; + startColor.colorMin = min; + startColor.colorMax = max; + break; + } + // Gradient/TwoGradients modes bake color+alpha into the gradient asset itself - + // no generic way to override that from code, so those are left as-is. + } + main.startColor = startColor; + } + } + } +} diff --git a/HarmonySrc/PetFollowPatch.cs b/HarmonySrc/PetFollowPatch.cs new file mode 100644 index 0000000..43fe779 --- /dev/null +++ b/HarmonySrc/PetFollowPatch.cs @@ -0,0 +1,171 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// Keeps a summoned pet (see SummonPatch.cs) from wandering off and getting permanently + /// lost/unsummonable - user report 2026-08-28: the Zombie Dog didn't follow like a drone + /// would, wandered off (heard, not seen), and a second summon attempt did nothing while the + /// first was presumably still out there somewhere. + /// + /// Two separate problems, one fix: + /// + /// 1. "Doesn't follow": there is no generic "follow a specific entity" AI task anywhere in + /// this game - confirmed by listing every EAI*-named type in Assembly-CSharp (EAIWander, + /// EAITerritorial, EAIApproachSpot, EAIApproachAndAttackTarget, etc. - nothing + /// follow-shaped). EntityDrone's follow behavior is hardcoded C# specific to that one + /// class, not something an XML entity_class can opt into. Writing a real custom AITask + /// (actual pathfinding, priority-tuned against the pet's existing Wander/Territorial/ + /// ApproachSpot tasks) is real engineering the backlog didn't ask for - so this copies + /// vanilla's own fallback for the identical problem instead: DroneManager.Update() + /// teleports a drone back near its owner once it's more than 32m away (sqrMagnitude > + /// 1024) and not doing something else (OrderState != Stay) - confirmed by decompiling + /// it. Same threshold, same idea, generalized to any pet class instead of drones only, + /// and skipped while the pet has a live attack target (don't yank it out of a fight). + /// + /// 2. "Doesn't reappear on retry": SummonPatch.cs's one-pet-per-species limit checks + /// EntityAlive.ownedEntities, but nothing was ever removing a pet from that list once it + /// died or its chunk unloaded - unlike the drone, where DroneManager's own death/unload + /// callbacks do that cleanup. A dead or vanished pet left the slot "occupied" forever. + /// The same periodic check below detects a tracked pet that's gone (world.GetEntity + /// returns null - dead, or its chunk unloaded and it despawned like any untracked + /// entity, per the known persistence gap documented in SummonPatch.cs) and clears + /// ownership so the player can summon a fresh one. + /// + /// 3. (added 2026-08-28) Insect Swarm only: makes it move on to the next zombie once its + /// current target is already Deviator-charmed, instead of camping the same converted + /// zombie forever - see the inline comment below, right where it happens. + /// + /// Hooked via ModEvents.UnityUpdate - the same supported per-frame mod event GameManager's own + /// gmUpdate() fires DroneManager.Update() from (confirmed by decompiling GameManager) - not a + /// Harmony patch, since this is a genuine public extension point, no reason to patch around + /// it. Throttled to run the real check once a second (CheckInterval): a distance/liveness + /// check on a handful of pets is cheap, but no reason to do it 60x/sec either. + /// + public static class PetFollowPatch + { + public const float LeashDistance = 32f; // matches DroneManager's own 32m leash + public const float LeashDistanceSq = LeashDistance * LeashDistance; + public const float CheckInterval = 1f; + + public class TrackedPet + { + public int OwnerEntityId; + public int PetEntityId; + } + + public static readonly List TrackedPets = new List(); + + public static float timer; + + public static void Init() + { + ModEvents.UnityUpdate.RegisterHandler(OnUnityUpdate); + } + + /// Called from SummonPatch.cs right after a pet is created and owned. + public static void Register(EntityAlive owner, Entity pet) + { + TrackedPets.Add(new TrackedPet { OwnerEntityId = owner.entityId, PetEntityId = pet.entityId }); + } + + /// Called from SummonPatch.cs's manual recall path so a recalled pet stops being + /// tracked immediately, instead of lingering until the next tick notices it's gone. + public static void Unregister(int petEntityId) + { + for (int i = TrackedPets.Count - 1; i >= 0; i--) + { + if (TrackedPets[i].PetEntityId == petEntityId) + { + TrackedPets.RemoveAt(i); + } + } + } + + public static void OnUnityUpdate(ref ModEvents.SUnityUpdateData _data) + { + timer += Time.deltaTime; + if (timer < CheckInterval) + { + return; + } + timer = 0f; + if (TrackedPets.Count == 0) + { + return; + } + + World world = GameManager.Instance != null ? GameManager.Instance.World : null; + if (world == null) + { + return; + } + + for (int i = TrackedPets.Count - 1; i >= 0; i--) + { + TrackedPet tracked = TrackedPets[i]; + EntityAlive pet = world.GetEntity(tracked.PetEntityId) as EntityAlive; + if (pet == null || pet.IsDead()) + { + EntityAlive ownerForCleanup = world.GetEntity(tracked.OwnerEntityId) as EntityAlive; + if (ownerForCleanup != null) + { + ownerForCleanup.RemoveOwnedEntity(tracked.PetEntityId); + Debug.Log("[NecromancerTome] PetFollowPatch: pet " + tracked.PetEntityId + " gone, cleared ownership for " + ownerForCleanup.entityId); + } + TrackedPets.RemoveAt(i); + continue; + } + + EntityAlive owner = world.GetEntity(tracked.OwnerEntityId) as EntityAlive; + if (owner == null) + { + // Owner not currently loaded (e.g. disconnected) - leave the pet tracked, + // nothing useful to do until they're back. + continue; + } + + // User request 2026-08-28: "рой будет заражать зомби девиацией и лететь к + // следующему незаражённому?" - not on its own, so this makes it one. EntityVulture + // (the Swarm's real base class - see SwarmTargetPatch.cs) has no notion of + // "this target is already converted, go find another" - once it has a live target + // it just keeps attacking it until that target dies, charmed or not (repeated + // AddBuff on an already-charmed zombie is a harmless no-op, so it wasn't wrong, + // just stuck). Clearing the attack target here when it's already charmed makes + // EntityVulture's own FindTarget()/SetAttackTarget cycle kick back in on its next + // pass (SwarmTargetPatch.cs redirects that straight to the nearest zombie again, + // same as any other retarget) - not instant (that cycle runs on its own ~2s timer, + // not driven by us), but converts-then-moves-on within a couple seconds. + if (pet.entityClass == Patch_EntityAlive_SetAttackTarget_SwarmRetarget.SwarmOnlyClassId()) + { + EntityAlive currentTarget = pet.GetAttackTarget(); + if (currentTarget != null && currentTarget.Buffs != null && currentTarget.Buffs.HasBuff(Patch_EntityBuffs_AddBuff_DeviatorCharm.CharmBuffName)) + { + pet.SetAttackTarget(null, 0); + Debug.Log("[NecromancerTome] PetFollowPatch: swarm " + pet.entityId + " dropped already-charmed target " + currentTarget.entityId); + } + } + + if (pet.GetAttackTarget() != null) + { + // Mid-fight - let it finish rather than teleporting it away. + continue; + } + + float distSq = (pet.position - owner.position).sqrMagnitude; + if (distSq > LeashDistanceSq) + { + // 2m in front of the owner, not exactly on top of them - landing right on the + // owner's own position stacks the pet's collider into the player's and shoves + // them (this is exactly what happened during the "<=0 vs -1" bug above: a pile + // of a dozen undespawned dogs all teleporting onto the same point as the player + // every second launched them into the air). + Vector3 dest = owner.position + owner.qrotation * new Vector3(0f, 0f, 2f); + pet.SetPosition(dest, true); + Debug.Log("[NecromancerTome] PetFollowPatch: teleported pet " + pet.entityId + " back to owner " + owner.entityId); + } + } + } + } +} diff --git a/HarmonySrc/PortalStonePatch.cs b/HarmonySrc/PortalStonePatch.cs new file mode 100644 index 0000000..d261793 --- /dev/null +++ b/HarmonySrc/PortalStonePatch.cs @@ -0,0 +1,340 @@ +using HarmonyLib; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// Portal stones - BACKLOG.md item 6. See items.xml (thrownStonePortalBlue/ + /// thrownStonePortalBlack) for the item definitions. + /// + /// REWRITTEN 2026-08-29 after the first version's core assumption turned out wrong, confirmed + /// by the user testing it ("срабатывает мгновенно" - fires instantly, no 10s indicator). The + /// original version used Class="Eat"/Delay="10" on the assumption that Delay was a HELD-hold + /// duration (like the workbench's TakeDelay). Re-decompiling ItemActionEat more carefully + /// shows that's wrong: ExecuteAction only runs once per click, ON RELEASE + /// (`if (!_bReleased || ...) return;`), and for UseAnimation items the actual "eating in + /// progress" duration comes from `AnimationDelayData.AnimationDelay[HoldType].RayCast` - a + /// fixed-per-HoldType table that isn't exposed anywhere in Data/Config's XML at all (checked + /// directly - no matches for "RayCast"/"AnimationDelay" in any vanilla XML), so `Delay` on a + /// Class="Eat" item is really just a re-click COOLDOWN (how soon it can fire again), not a + /// channel length. That's why it looked instant - the real channel was whatever HoldType 40's + /// (the rock's) built-in eating-animation length happens to be, a couple seconds at most, not + /// our intended 10. + /// + /// Fix: stop trying to make Class="Eat" do a long channel at all. Instead, Prefix + /// ItemActionEat.ExecuteAction itself (the exact click-release entry point, still using + /// Class="Eat" in XML purely as "a clickable item action", nothing about its own timing is + /// used any more) and, for our two stones, skip the original method entirely and open the + /// game's own generic countdown-timer UI directly - XUiC_Timer.OpenTimer(xui, seconds, + /// TimerEventData, ...), the exact same low-level primitive Block.TakeItemWithTimer itself + /// calls for the workbench pickup timer (decompiled both to confirm - TakeItemWithTimer is + /// just a block-flavored wrapper around this same generic UI system, nothing block-specific + /// about the timer itself). This gives a REAL visible progress bar/percent-fill UI (confirmed + /// via XUiC_Timer's own "percent"/"timeleft" bindings) for the full 10 seconds, and + /// TimerEventData.CloseOnHit=true makes it cancel automatically if the player takes damage + /// mid-channel (a real engine feature, not something built by hand) - matching "прерывается + /// при получении урона" without any extra code. The actual teleport only runs from + /// FullTimeFinishEvent, i.e. only if the timer runs all the way to completion. + /// + /// Local-player-only, like every other UI-touching thing in this mod (SummonPatch.cs's + /// tooltips, etc.) - the underlying XUi/LocalPlayerUI system this timer needs is a + /// client-side-only concept, not something that makes sense for a remote player in this mod's + /// existing (single-player-focused) design. + /// + /// POWER-ATTACK CANCEL added 2026-08-29 (user request: "прервать кнопкой силовой атаки" - a + /// zombie could jump the player mid-channel and they want an explicit escape, not just + /// CloseOnHit's "already got hit" reaction). Confirmed a cancelled channel never teleports + /// either way - closing the timer window early (Escape/CloseOnHit/this) fires + /// TimerEventData.CloseEvent, not FullTimeFinishEvent, and TeleportToBedroll only ever runs + /// from the latter (see OnChannelComplete below) - so "cancel = no teleport" was already true + /// by construction, just needed a new way to trigger a cancel. + /// vanilla's own TimerEventData.CancelWithActivateButton (already set true above) only checks + /// PlayerActionsPermanent.Activate (decompiled XUiC_Timer.Update to confirm) - a small + /// always-live action set TFP built specifically to stay readable during modal UI, which does + /// NOT include Secondary/power-attack at all (checked its full field list). Rather than + /// hijack Activate (the same key that STARTS the channel) or Cancel (Escape, not the button + /// asked for), Patch_XUiC_Timer_Update_PortalStoneCancel below Postfixes XUiC_Timer.Update + /// itself and polls PlayerActionsLocal.Secondary.WasPressed directly (the same underlying + /// action already used elsewhere in this mod as "power attack", e.g. summon books' Action1) + /// - NOT decompiled-confirmed whether this action still registers while the timer's modal + /// window has input focus (SetControllable(false) fires on open, decompiled from XUiC_Timer, + /// but that's a character/gameplay-layer flag, separate from the InControl input-polling + /// layer PlayerAction reads from - the two are believed independent, not proven end-to-end). + /// Test in-game; if power attack doesn't register while the bar is up, that gap is the first + /// thing to dig into (possibly needs reading raw InControl device state instead of the + /// semantic PlayerAction). + /// + [HarmonyPatch(typeof(ItemActionEat), "ExecuteAction")] + public static class Patch_ItemActionEat_ExecuteAction_PortalStones + { + public const string BlueStoneName = "thrownStonePortalBlue"; + public const string BlackStoneName = "thrownStonePortalBlack"; + public const float ChannelSeconds = 10f; + + /// Sentinel stashed in TimerEventData.Data (an unused generic object field on + /// vanilla's own class) purely so Patch_XUiC_Timer_Update_PortalStoneCancel below can + /// tell "this is one of our portal-stone timers" apart from any other TimerEventData the + /// engine or another mod might have open (e.g. a workstation pickup timer, BACKLOG.md + /// item 9) - a reference-equality check on a private static object, nothing exposed or + /// read by vanilla code. + public static readonly object ChannelMarker = new object(); + + /// See buffs.xml - a marker/particle-carrier buff, added/removed directly by + /// this file rather than by any buff-trigger vocabulary. + public const string ChannelBuffName = "buffNecroPortalChannel"; + + public static bool Prefix(ItemActionData _actionData, bool _bReleased) + { + if (!_bReleased) + { + return true; + } + string itemName = _actionData?.invData?.itemValue?.ItemClass?.Name; + if (itemName != BlueStoneName && itemName != BlackStoneName) + { + return true; + } + if (!(_actionData.invData.holdingEntity is EntityPlayerLocal player)) + { + // Not the local player (e.g. an AI or remote entity somehow holding this) - let + // vanilla Eat behavior run rather than silently doing nothing, same fallback + // shape used elsewhere in this mod for the local-player-only simplification. + return true; + } + + Debug.Log("[NecromancerTome] PortalStonePatch: channel started for " + itemName + ", owner=" + player.entityId); + // Played directly here since ItemActionEat's own Sound_start handling is skipped + // entirely along with the rest of its ExecuteAction (see class comment) - a plain + // XML Sound_start property on this item would never fire otherwise. + player.PlayOneShot("swoosh"); + // Black/smoke particle swirl for the duration of the channel (user request + // 2026-08-29) - see buffs.xml's buffNecroPortalChannel + ParticlePatch.cs (generalized + // to handle a player-targeted buff, not just the two zombie-facing ones it already + // had). A plain marker buff, added/removed directly here rather than through any + // 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); + + TimerEventData timerData = new TimerEventData + { + CloseOnHit = true, + CancelWithActivateButton = true, + Data = ChannelMarker, + }; + timerData.FullTimeFinishEvent += delegate + { + OnChannelComplete(player, itemName); + }; + // CloseEvent fires when the timer window closes WITHOUT completing (cancelled by + // damage/power-attack/Cancel) - confirmed by decompiling XUiC_Timer.OnClose/ + // timeReachedNull: timeReachedNull sets skipCloseEvent=true around the completion + // path specifically so CloseEvent does NOT also fire on a successful finish, only on + // every other way the window can close. FullTimeFinishEvent and CloseEvent are + // therefore mutually exclusive per channel - exactly "however it ends" from the + // class-level comment. + timerData.CloseEvent += delegate + { + Debug.Log("[NecromancerTome] PortalStonePatch: channel cancelled for " + itemName + ", owner=" + player.entityId); + player.Buffs.RemoveBuff(ChannelBuffName); + }; + + string labelKey = (itemName == BlueStoneName) ? "thrownStonePortalBlueChanneling" : "thrownStonePortalBlackChanneling"; + LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPlayer(player); + XUiC_Timer.OpenTimer(playerUI.xui, ChannelSeconds, timerData, -1f, Localization.Get(labelKey)); + + // Skip ItemActionEat's own logic entirely for these two items - the click has been + // fully handled by opening our own timer instead. + return false; + } + + public static void OnChannelComplete(EntityPlayerLocal player, string itemName) + { + Debug.Log("[NecromancerTome] PortalStonePatch: channel completed for " + itemName + ", owner=" + player.entityId); + player.Buffs.RemoveBuff(ChannelBuffName); + if (itemName == BlackStoneName) + { + ShowBlackPortalConfirmation(player); + return; + } + TeleportToBedroll(player); + } + + /// Black portal confirmation + fullscreen video, user request 2026-08-30 + /// ("диалоговое окно... вы уверены... Если Да, то игра останавливается и проигрывается + /// видео"). Real APIs, both decompiled directly: + /// - XUiC_MessageBoxWindowGroup.ShowCustom(xui, title, text, icon, setupCallback, ...) - + /// the same generic Yes/No popup vanilla itself uses (its own delete-item/disconnect + /// confirmations, etc). ShowOkCancel/ShowConfirmCancel exist too but hardcode their + /// button caption keys ("xuiOk"/"xuiCancel"/"btnConfirm") - ShowCustom's + /// _setupCallback is the only variant that lets the two buttons be captioned + /// "xuiYes"/"xuiNo" directly (both are real, already-localized vanilla keys, confirmed + /// against Data/Config/Localization.csv), matching the user's literal "да/нет" + /// wording. Buttons[0]/[2] (not [1]) is the same slot pairing ShowOkCancel/ + /// ShowConfirmCancel themselves use internally - Buttons[1] is left unused, same as + /// vanilla's own 2-button dialogs. + /// - GameManager.Instance.Pause(bool) - decompiled GameManager.updatePauseState: sets + /// Time.timeScale=0 for real, but ONLY takes effect in singleplayer (an SP-only check + /// baked into vanilla itself, not a limitation added by this mod) - a deliberate, + /// documented no-op in multiplayer rather than something silently broken. + /// - XUiC_VideoPlayer.PlayVideo(xui, VideoData, skippable, onFinished) - opens the same + /// fullscreen "VideoPlayer" window vanilla's own TFP intro/menu-background videos use. + /// Decompiled XUiV_Video confirms video playback isn't gated by Time.timeScale, so it + /// keeps playing correctly while paused. skippable=true (Cancel key) so a broken/ + /// missing video file can't soft-lock the player - XUiV_Video.OnVideoErrorReceived + /// already auto-closes on a bad file on its own, this is just a second, player-facing + /// way out. + /// + /// VIDEO FILE: per direct user instruction 2026-08-30 ("Пока файл видео замени + /// заглушкой. Потом поставим нормальный"), Video/BlackPortal.webm is currently a COPY OF + /// VANILLA'S OWN TFP_Intro.webm (from 7DaysToDie_Data/StreamingAssets/Video/), not real + /// mod content - purely so the full dialog -> pause -> video -> unpause pipeline is + /// genuinely testable end-to-end right now. Swap that one file for the real video later; + /// nothing else needs to change (reuse the same filename, or update BlackPortalVideoPath + /// below if the real file gets a different name). + /// "@modfolder(NecromancerTome):..." is the exact mod-relative path syntax + /// XUiV_Video.startVideo resolves via ModManager.TryPatchModPathString (decompiled to + /// confirm - looks for "@modfolder(<mod name>):" and substitutes the mod's real + /// install path; "NecromancerTome" here is this mod's own ModInfo.xml Name, not its + /// DisplayName). + public const string BlackPortalVideoPath = "@modfolder(NecromancerTome):Video/BlackPortal.webm"; + + public static void ShowBlackPortalConfirmation(EntityPlayerLocal player) + { + LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPlayer(player); + XUiC_MessageBoxWindowGroup.ShowCustom( + playerUI.xui, + Localization.Get("thrownStonePortalBlackConfirmTitle"), + Localization.Get("thrownStonePortalBlackConfirmText"), + "", + delegate(XUiC_MessageBoxWindowGroup mb) + { + mb.Buttons[0].DefaultConfirm("xuiYes", delegate { ActivateBlackPortal(player); }); + mb.Buttons[2].DefaultCancel("xuiNo", null); + }, + _openMainMenuOnClose: false, + _modal: true, + _buttonOnOutsideClick: -1, + // Esc/outside-close counts as "No" - same convention ShowOkCancel/ + // ShowConfirmCancel themselves use for their own Cancel slot (index 2). + _buttonOnExternalClose: 2); + } + + /// ЗАМЕНЕНО 2026-09-09: раньше отсюда сразу стартовало полноэкранное видео + /// (BlackPortalVideoPath), теперь запускается финальная сцена из шести слайдов с текстом + /// - FinalSlides.Begin. Причина в BACKLOG.md ("концовка серией диалоговых окон вместо + /// видео"): видео не локализуется, а текст слайдов идёт обычной строкой через + /// Localization.csv. Пауза и выход в главное меню никуда не делись - и то и другое + /// теперь живёт внутри FinalSlides, а видео осталось финальным аккордом ПОСЛЕ выбора + /// концовки на последнем слайде. + /// + /// Всё, что описано в комментарии к BlackPortalVideoPath выше, по-прежнему верно и + /// применяется - просто к двум новым файлам (FinalSlides.StayVideoPath / + /// ReturnVideoPath) вместо одного. Сама константа BlackPortalVideoPath больше не + /// используется и оставлена только как документация к разбору "@modfolder(...)" и + /// XUiC_VideoPlayer.PlayVideo, на который FinalSlides ссылается. + public static void ActivateBlackPortal(EntityPlayerLocal player) + { + Debug.Log("[NecromancerTome] PortalStonePatch: black portal confirmed by owner=" + player.entityId + ", handing over to FinalSlides"); + FinalSlides.Begin(player); + } + + /// Прежняя концовка "сразу видео, потом главное меню". Больше ниоткуда не + /// вызывается (см. ActivateBlackPortal выше) - оставлена целиком, потому что весь разбор + /// Pause/PlayVideo/Disconnect в её комментариях остаётся актуальным и на неё ссылается + /// FinalSlides. Удалять при следующей уборке, если так и не понадобится. + public static void PlayBlackPortalVideoLegacy(EntityPlayerLocal player) + { + Debug.Log("[NecromancerTome] PortalStonePatch: black portal confirmed by owner=" + player.entityId + ", pausing + playing video"); + GameManager.Instance.Pause(true); + LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPlayer(player); + VideoData videoData = new VideoData { url = BlackPortalVideoPath }; + XUiC_VideoPlayer.PlayVideo(playerUI.xui, videoData, true, delegate(bool skipped) + { + // EXIT TO MAIN MENU after the video, user request 2026-08-30 ("После видео нужно + // выходить из игры в главное меню") - fires whether the video played to the end + // or was skipped (Cancel key / a bad file), same as any other "the video is over" + // outcome. GameManager.Instance.Disconnect() is not a guess - it's the EXACT same + // call the real in-game ESC menu's own "Exit to Main Menu" button uses + // (decompiled XUiC_InGameMenuWindow.exitGame/BtnExit_OnPressed to confirm: it's a + // thin wrapper straight to this method). Handles everything a clean exit needs by + // itself - closes modal windows, un-pauses (calls Pause(false) internally, so no + // separate unpause call needed here), saves/shuts down the local server, and + // returns to XUiC_MainMenu - not reinventing any of that by hand. Replaces the + // earlier "thrownStonePortalBlackNotBound" tooltip placeholder entirely: with a + // real exit-to-menu ending, staying in-game and showing a tooltip no longer makes + // sense (BACKLOG.md item 6's "destination not decided" placeholder is now this + // exit itself, not a tooltip). + Debug.Log("[NecromancerTome] PortalStonePatch: black portal video finished (skipped=" + skipped + "), exiting to main menu"); + GameManager.Instance.Disconnect(); + }); + } + + /// BedrollPos comes from EntityPlayer.PersistentPlayerData (decompiled - reads + /// GameManager.Instance.persistentPlayers.GetPlayerDataFromEntityID(entityId)), the same + /// field the game's own respawn-at-bedroll flow reads (PersistentPlayerData.BedrollPos / + /// HasBedrollPos, confirmed by decompiling that class directly). +0.5 on x/z centers the + /// block, +1 on y lifts the destination clear of the bedroll block itself - a reasonable + /// guess at a safe landing offset, not a decompiled/confirmed "correct" one (the + /// respawn-specific code that actually places a resurrected player likely does more + /// ground-safety checking than this; worth revisiting if the stone ever drops the player + /// inside a block). Teleport itself uses NetPackageTeleportPlayer, the exact same package + /// ConsoleCmdTeleportsAbs.ExecuteTeleport (the real "teleportplayer" console command) + /// uses - decompiled to confirm, not invented. + public static void TeleportToBedroll(EntityPlayerLocal player) + { + PersistentPlayerData data = player.PersistentPlayerData; + if (data == null || !data.HasBedrollPos) + { + GameManager.ShowTooltip(player, "thrownStonePortalBlueNoBedroll"); + Debug.LogWarning("[NecromancerTome] PortalStonePatch: owner=" + player.entityId + " has no bedroll set, can't teleport"); + return; + } + Vector3i bedrollPos = data.BedrollPos; + Vector3 destination = new Vector3(bedrollPos.x + 0.5f, bedrollPos.y + 1f, bedrollPos.z + 0.5f); + NetPackageTeleportPlayer package = NetPackageManager.GetPackage().Setup(destination, null); + package.ProcessPackage(GameManager.Instance.World, GameManager.Instance); + player.PlayOneShot("spawnInStinger"); + Debug.Log("[NecromancerTome] PortalStonePatch: owner=" + player.entityId + " teleported to bedroll " + bedrollPos); + } + } + + /// Lets the power-attack ("Secondary") input cancel an in-progress portal-stone + /// channel - see the long comment on Patch_ItemActionEat_ExecuteAction_PortalStones above for + /// the full reasoning. Separate patch class/target method (XUiC_Timer.Update, not + /// ItemActionEat.ExecuteAction) since this has to run every frame WHILE the timer is open, not + /// once at click time. + /// + /// FIXED 2026-08-29 (user report: cancel didn't work at all) - the semantic + /// PlayerActionsLocal.Secondary check alone (first version) apparently never registered while + /// the timer's modal window has input focus, confirming the exact risk flagged when this was + /// first written. Root cause not fully pinned down by decompilation (XUiC_Timer.OnOpen sets + /// SetControllable(false) on the player, and nothing found ties that flag directly to + /// PlayerAction's own InControl polling layer - the two are presumed independent but the + /// actual suppression point wasn't located). Rather than keep guessing which exact system + /// swallows it, added a SECOND, independent check straight to Unity's raw + /// Input.GetMouseButtonDown(1) (right mouse button - confirmed as Secondary's real default + /// KBM binding by decompiling PlayerActionsLocal.CreateActions) - raw Input polling reads + /// hardware state directly, bypassing InControl/PlayerAction and whatever gates it, so this + /// should fire regardless of modal-window suppression. Either check firing cancels the + /// channel; keeping the semantic one too costs nothing and covers gamepad Secondary + /// (LeftTrigger) if that one turns out to work. KBM-only fallback - if a gamepad player still + /// can't cancel, that's the next gap to close (would need the equivalent raw axis read). + [HarmonyPatch(typeof(XUiC_Timer), "Update")] + public static class Patch_XUiC_Timer_Update_PortalStoneCancel + { + public static void Postfix(XUiC_Timer __instance) + { + if (__instance == null || __instance.eventData == null || __instance.eventData.Data != Patch_ItemActionEat_ExecuteAction_PortalStones.ChannelMarker) + { + return; + } + PlayerActionsLocal input = __instance.xui?.playerUI?.playerInput; + bool cancelPressed = (input != null && input.Secondary.WasPressed) || Input.GetMouseButtonDown(1); + if (cancelPressed) + { + Debug.Log("[NecromancerTome] PortalStonePatch: channel cancelled via power attack"); + __instance.xui.playerUI.windowManager.Close(__instance.windowGroup); + } + } + } +} diff --git a/HarmonySrc/PyramidWardPatch.cs b/HarmonySrc/PyramidWardPatch.cs new file mode 100644 index 0000000..ad04be9 --- /dev/null +++ b/HarmonySrc/PyramidWardPatch.cs @@ -0,0 +1,680 @@ +using System; +using System.Collections.Generic; +using HarmonyLib; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// "Пирамида Ереси" (Pyramid of Heresy) - user request 2026-08-31, REWRITTEN 2026-09-01. + /// + /// FIRST VERSION (see BACKLOG.md's original entry) used Harmony patches on plain Block's + /// OnBlockAdded/OnBlockRemoved/GetBlockActivationCommands/OnBlockActivated, with all state in a + /// static Dictionary keyed by block position. Two real problems came out of actually testing it: + /// 1. USER REPORT: "Навожу прицел, но подсказка про E не появляется" - no E-prompt at all, + /// pressing E did nothing. Root cause, decompiled: there's a SEPARATE gate method, + /// `Block.HasBlockActivationCommands(WorldBase, BlockValue, Vector3i, EntityAlive)`, with + /// its OWN independent logic (not calling GetBlockActivationCommands at all) that the + /// game's HUD/input layer checks FIRST to decide whether to even show the prompt. It was + /// never patched, and for a plain decorative block it always returns false (no + /// CanPickup, no CustomCmds) - so the prompt correctly never appeared, and E correctly did + /// nothing, regardless of how correct the other three patches were. + /// 2. USER REQUEST: "Сделай TileEntity" - wants EffectOn/ZoneShown to actually survive a + /// save/reload, which the static-Dictionary version explicitly could not do (documented as + /// a known caveat at the time). + /// + /// Rather than patch a fourth Block method, this is a full rewrite onto the real, sanctioned + /// extension point for exactly this situation - the same one vanilla's own Land Claim block + /// uses: a CompositeTileEntity feature. Confirmed by decompiling the actual chain, not guessed: + /// - `TEFeatureAbs` (the real base class - `TEFeatureLandClaim : TEFeatureAbs`, decompiled to + /// confirm) already declares virtual OnAdded/OnRemove/UpdateTick/Read/Write/ + /// InitBlockActivationCommands/AllowBlockActivationCommand/OnBlockActivated - literally + /// every hook this feature needs, with NO separate "HasBlockActivationCommands" gap: that + /// whole problem belongs to plain Block's activation path, not this one. + /// - `BlockCompositeTileEntity` (the Class="CompositeTileEntity" block class - decompiled + /// directly) correctly overrides HasBlockActivationCommands/GetBlockActivationCommands/ + /// OnBlockActivated itself and wires them through `TileEntityComposite`/each feature - this + /// is the ALREADY-WORKING pipeline every vanilla composite block (Land Claim included) has + /// used for years; not new engineering, just finally the right entry point. + /// - Feature discovery is NOT a hardcoded switch (unlike raw `TileEntityType`/ + /// `TileEntity.InstantiateFromRead`, which genuinely IS a closed hardcoded enum switch with + /// no mod slot - checked this first and ruled it out for exactly that reason). + /// `TileEntityCompositeData.Init()` (decompiled) calls + /// `ReflectionHelpers.FindTypesImplementingBase(typeof(ITileEntityFeature), ...)` and keys + /// the result by `_type.Name` (the short type name, NAMESPACE-INDEPENDENT - confirmed by + /// reading the exact line) - so `TEFeaturePyramidWard` below is found automatically by the + /// engine's own startup scan of every loaded assembly (including this mod's DLL) purely by + /// matching that literal class name against blocks.xml's own + /// `` - the same mechanism vanilla's own + /// TEFeatureLandClaim/TEFeatureStorage/TEFeatureAreaRepair (see keystoneBlock) already rely + /// on. Only real requirement (also confirmed by decompile, `TileEntityCompositeData.Init` + /// warns and skips otherwise): a public, non-abstract class with a parameterless + /// constructor - both true here without writing one explicitly. + /// - Activation command TEXT is a real constraint worth noting: `BlockCompositeTileEntity` + /// caches its `BlockActivationCommand[]` PER BLOCK TYPE (a field on the Block instance + /// itself, shared by every placed pyramid), rebuilt once from InitBlockActivationCommands + /// and never again - only `.enabled` gets refreshed per-activation (via + /// AllowBlockActivationCommand). So button TEXT can't dynamically say "Enable"/"Disable" + /// per-instance; the real vanilla pattern (confirmed in TEFeatureLandClaim's own + /// show_bounds/hide_bounds pair) is to register BOTH command variants up front and only + /// ENABLE whichever one currently applies - copied exactly here for effect_on/effect_off + /// and zone_show/zone_hide. + /// - Command display text is resolved via `Localization.Get("blockcommand_" + fullCommandName)` + /// (confirmed by finding vanilla's own `blockcommand_show_bounds`/ + /// `blockcommand_TEFeatureLandClaim:show_bounds` keys in Data/Config/Localization.csv) - + /// NOT pre-resolved text passed directly to BlockActivationCommand's constructor (the + /// earlier version's mistake). See this mod's own Localization.csv for the + /// `blockcommand_TEFeaturePyramidWard:*` keys this relies on. + /// + /// NOT a Harmony patch, despite the filename/this mod's usual convention and despite still + /// living in HarmonySrc/ for continuity with the rest of this mod's file layout - nothing here + /// patches anything. `Patch_EntityBuffs_AddBuff_DeviatorCharm.CharmBuffName` (CharmPatch.cs) is + /// still reused as-is for the actual charm effect; that patch is untouched by this rewrite. + /// + public class TEFeaturePyramidWard : TEFeatureAbs + { + /// How far out (in blocks/meters) the ward reaches. Not specified by the user - + /// picked to roughly cover a small base perimeter, same ballpark as vanilla's own land + /// claim radius. Easy to retune, just one constant. + public const float EffectRadius = 15f; + + /// CHANGED 2026-09-02 (user report: "Никаких частиц на включённом состоянии не + /// летает" - literally nothing spawned, at all, neither the main glow nor the zone ring). + /// Root cause: "RadiatedParticlesOnMesh" is loaded/played through a completely DIFFERENT + /// mechanism than the one this file actually calls. Decompiled `ParticleEffect.LoadResources()` + /// (the loader behind `GameManager.SpawnBlockParticleEffect`/`new ParticleEffect(string,...)`, + /// which this file uses): it bulk-loads addressables from the "particleeffects" group whose + /// FIRST FOLDER SEGMENT starts with "p_", then keys each loaded prefab into a dictionary by + /// its own filename via `ToId(name)`. "RadiatedParticlesOnMesh" is referenced elsewhere in + /// this mod (buffs.xml's `AttachParticleEffectToEntity`) via the literal path + /// "ParticleEffects/RadiatedParticlesOnMesh" - no "p_"-prefixed folder anywhere in that path, + /// meaning it almost certainly never gets bulk-loaded into that same lookup dictionary at all + /// (that XML action resolves its own particle reference through an entirely separate, + /// direct-path mechanism, not this bulk-addressables-by-folder-prefix one) - so + /// `GetDynamicTransform`/`ToId` lookups for it here would always silently fail (logged as + /// "Unknown particle effect", nothing spawned) - exactly matching what got reported. Switched + /// to "campfire" instead - confirmed loadable through THIS exact code path already (it's + /// vanilla's own `` on the real campfire + /// block, going through the same GameManager block-particle registry) - and it happens to + /// double as the user's other request ("фиолетовое пламя, будто блок горит холодным + /// пламенем") almost for free: a real fire effect, tinted purple by ApplyGlowTint below + /// instead of its natural orange. + public const string GlowParticleName = "campfire"; + + /// CHANGED 2026-09-02 (user, after seeing the fire-ring in-game: "Границу лучше + /// показывать не огнём, а какими-нибудь частицами" - reversed their earlier "оставим так" + /// once they'd actually seen it). Only real vanilla `ParticleName` values confirmed to exist + /// at all (grepped every one in Data/Config/blocks.xml - the same property this whole + /// mechanism is built on): ember_pile/hotembers/campfire/forgeWorkstation/chemistryStation/ + /// flame_hazard - every single one of them is fire/ember/industrial-themed, there is no + /// confirmed "generic sparkle/magic" particle name to fall back on. Picked + /// "chemistryStation" specifically because it's the one NOT visually built around an open + /// flame (a chemistry set's bubbling/vapor effect) - best available guess from a short list, + /// not a confirmed-good look; say if it still reads wrong once seen; it tints purple the + /// same way as everything else here regardless of its native color. + public const string ZoneRingParticleName = "chemistryStation"; + + /// Purple, per the user's explicit request ("окрашивается фиолетовым" for the + /// zone, "светится фиолетовыми частицами"/"холодным пламенем" for the effect glow) - same + /// color used for both. + public static readonly Color WardTint = new Color(0.6f, 0.15f, 0.95f); + + /// Dropped from 32 - a full-size effect at every ring point would be both visually + /// overwhelming and comparatively expensive; 16 small markers still reads clearly as a + /// circle at EffectRadius=15. + public const int ZoneRingPointCount = 16; + + /// ADDED 2026-09-02, direct user request ("Можешь накладывать кроме девиации ещё + /// и дебаф горения?"). This is the real vanilla "a zombie is on fire" buff (Data/Config/ + /// buffs.xml - `damage_type="heat"`, cascades into `buffBurningElement`'s own 10s countdown/ + /// damage-over-time/AddBuff(buffIsOnFire) chain, the same one torches/molotovs/fire traps + /// trigger), not a new buff invented for this mod. Re-applied every scan tick (not + /// gated behind "already has it" like the charm below) since `buffBurningElement` itself + /// resets its own countdown on every re-trigger (`stack_type="replace"`) - the intent is + /// "keeps burning the whole time it's in the zone", not "burns once". + /// + /// Needs a REAL instigator entity id, unlike the charm buff: `EntityBuffs.AddBuff` (decompiled) + /// checks `buff.DamageType != None && ... && !FriendlyFireCheck(instigator)` and fails the + /// whole call outright if that trips - a buff with a real damage_type (this one has "heat"; + /// buffNecroDeviatorCharm has none, which is why it never needed this) requires a + /// non-null/valid instigator that FriendlyFireCheck accepts, or the call can fail. Passed the + /// in-zone player's own entityId (already resolved above for the player-presence gate) - + /// matches the fictional framing anyway (the necromancer is the one wielding this ward). + public const string BurnBuffName = "buffBurningZombie"; + + /// Persisted (see Read/Write below) - real per-instance state now, one pyramid's + /// toggle no longer affects any other's, and both survive a save/reload. + public bool EffectOn = true; + + public bool ZoneShown; + + /// Glow/ring particle keys queued via SpawnBlockParticleEffect but not tinted yet - + /// GameManager.updateBlockParticles() only processes its spawn queue once per frame + /// (decompiled to confirm), so tinting has to be deferred at least one tick rather than done + /// inline right after spawning. Instance-level now (was a shared static list in the old + /// version) - each pyramid only tracks its own pending keys. + public readonly List pendingTint = new List(); + + // ------------------------------------------------------------------ + // Lifecycle. + // ------------------------------------------------------------------ + + public override void CopyFromInternal(TileEntityComposite _other) + { + if (_other.TryGetSelfOrFeature(out TEFeaturePyramidWard other)) + { + EffectOn = other.EffectOn; + ZoneShown = other.ZoneShown; + } + } + + public override void OnAdded(Vector3i _blockPos, BlockValue _blockValue) + { + base.OnAdded(_blockPos, _blockValue); + if (EffectOn) + { + SpawnGlow(); + } + if (ZoneShown) + { + SpawnZoneRing(); + } + } + + public override void OnRemove(World _world) + { + base.OnRemove(_world); + if (ZoneShown) + { + RemoveZoneRing(); + } + RemoveGlow(); + } + + // ------------------------------------------------------------------ + // Persistence - real save/load now, per the user's direct request. + // ------------------------------------------------------------------ + + /// Routed through PyramidWardWriteHelper.Write (TEPersistenceSrc/, a separate + /// satellite project+DLL) rather than calling PooledBinaryWriter.Write directly - that call + /// does not compile from THIS project at all. Real, decompile/compiler-confirmed reason, not + /// a style choice: see NecromancerTEPersistence.csproj's own comment for the full story + /// (short version: Assembly-CSharp.dll's Write overload set includes a + /// ReadOnlySpan<byte> variant that only resolves against Unity/Mono's own mscorlib, + /// which conflicts with this project's UnityEngine-type usage everywhere else if referenced + /// directly here - isolating the one call that needs it into its own tiny project was the + /// only combination found that keeps both working). No version byte (kept simple per the + /// user's "как проще" - this is a brand-new feature, nothing to migrate from yet; + /// PooledBinaryReader.ReadBoolean() below has no such compile restriction, confirmed + /// separately, so Read() needs no equivalent workaround. + public override void Write(PooledBinaryWriter _bw, TileEntity.StreamModeWrite _eStreamMode) + { + base.Write(_bw, _eStreamMode); + PyramidWardWriteHelper.Write(_bw, EffectOn, ZoneShown); + } + + public override void Read(PooledBinaryReader _br, TileEntity.StreamModeRead _eStreamMode) + { + base.Read(_br, _eStreamMode); + EffectOn = _br.ReadBoolean(); + ZoneShown = _br.ReadBoolean(); + } + + // ------------------------------------------------------------------ + // E-menu (activation commands) - see class doc comment for why both states of each + // toggle are registered up front rather than swapping text dynamically. + // ------------------------------------------------------------------ + + /// Icons FIXED 2026-09-02 (user report: "на 'Отключить эффект' нету иконки"). + /// "ui_game_symbol_zombie"/"hand" (the first version's guesses) were never real + /// BlockActivationCommand icon names - that field takes a small closed set of simple + /// glyph-font names, NOT full UI sprite-atlas names (confirmed by harvesting every real + /// `new BlockActivationCommand(...)` call across every other TEFeature class decompiled so + /// far: "frames"/"x" (TEFeatureLandClaim), "door" (TEFeatureDoor), "lock"/"unlock"/"keypad" + /// (TEFeatureLockable), "search" (TEFeatureStorage), "wrench" (the trigger command on + /// TileEntityComposite itself)). Reused two of those real, confirmed names instead of + /// guessing again: "unlock"/"lock" for effect on/off (a locked/unlocked padlock reads fine + /// as "active"/"inactive"), and "frames" - literally the SAME icon vanilla's own Land Claim + /// uses for its own show_bounds/hide_bounds pair - for our own zone_show/zone_hide, since + /// it's the exact same kind of toggle. + public override void InitBlockActivationCommands(Action _addCallback) + { + base.InitBlockActivationCommands(_addCallback); + _addCallback(new BlockActivationCommand("effect_on", "unlock", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData); + _addCallback(new BlockActivationCommand("effect_off", "lock", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData); + _addCallback(new BlockActivationCommand("zone_show", "frames", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData); + _addCallback(new BlockActivationCommand("zone_hide", "frames", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData); + } + + /// ADDED 2026-09-02 (user report: "при наведении нет никакой надписи-подсказки про + /// E"). TEFeatureAbs.GetActivationText was never overridden at all, so it fell through to + /// the base's default `return null` - no ReadOnlySpan in this method's signature (confirmed + /// by decompile), so unlike AllowBlockActivationCommand/OnBlockActivated below, this one + /// overrides cleanly with no workaround needed. Mirrors TEFeatureLandClaim's own + /// GetActivationText shape (`_activateHotkeyMarkup` + the block's own localized name) - + /// same real, decompiled API, not guessed. + 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(); + } + + // AllowBlockActivationCommand/OnBlockActivated deliberately NOT overridden here - see the + // long comment block below (right above the two Harmony patches that replace them) for why + // this specific pair of TEFeatureAbs virtuals cannot be overridden from this mod's project + // at all, and how the same behavior is achieved instead. + + // ------------------------------------------------------------------ + // Visuals - same GameManager block-particle registry as the first version, just called on + // `ToWorldPos()` (this feature's own position) instead of a dictionary-passed key. + // ------------------------------------------------------------------ + + public void SpawnGlow() + { + if (GameManager.Instance == null) + { + return; + } + Vector3i pos = ToWorldPos(); + if (GameManager.Instance.HasBlockParticleEffect(pos)) + { + return; + } + // FIXED 2026-09-02 (user report: "горение пирамидки почему-то смещено на куб вверх и + // вбок"): World.blockToTransformPos(Vector3i) ALREADY returns (x+0.5, y, z+0.5) + // (decompiled to confirm - horizontally centered, y left raw/un-centered) - adding + // another +0.5 on x/z on top of that (the original bug) double-centered it, landing a + // full extra block over on both horizontal axes. Only the vertical lift (how far above + // the block the flame sits) is actually ours to add. Height LOWERED again same day + // ("сделай ниже не 2/3 куба" - after the sideways offset was gone, still sat too high): + // 1.2 (0.2 above the full block top) -> 0.6, under 2/3 (0.667) of a block as asked. + // Height lowered twice same day: 1.2 -> 0.6 ("сделай ниже не 2/3 куба"), then -> 0.4 + // (direct follow-up: "Снизь высоту пламени до +0.4"). + Vector3 worldPos = World.blockToTransformPos(pos) + new Vector3(0f, 0.4f, 0f); + // WardTint (not Color.white) as the ParticleEffect's own _color: ParticleEffect. + // SpawnParticleEffect applies this directly to any non-ParticleSystem Renderer on the + // prefab (decompiled to confirm) - covers a sub-emitter/glow sprite ApplyGlowTint's own + // ParticleSystem-only loop wouldn't reach, belt-and-suspenders alongside it. + GameManager.Instance.SpawnBlockParticleEffect(pos, new ParticleEffect(GlowParticleName, worldPos, Quaternion.identity, 0f, WardTint)); + pendingTint.Add(pos); + } + + public void RemoveGlow() + { + if (GameManager.Instance == null) + { + return; + } + Vector3i pos = ToWorldPos(); + if (GameManager.Instance.HasBlockParticleEffect(pos)) + { + GameManager.Instance.RemoveBlockParticleEffect(pos); + } + pendingTint.Remove(pos); + } + + /// Ring of glow points marking EffectRadius, keyed by y = -1000-i (real block + /// y-coordinates never go negative that far, so these keys can never collide with an + /// actual placed block's own glow key). + public void SpawnZoneRing() + { + if (GameManager.Instance == null) + { + return; + } + Vector3i basePos = ToWorldPos(); + // Same double-centering bug as SpawnGlow's own fix above - blockToTransformPos already + // centers x/z, only the vertical offset (0.5, mid-block height) is ours to add. + Vector3 center = World.blockToTransformPos(basePos) + new Vector3(0f, 0.5f, 0f); + for (int i = 0; i < ZoneRingPointCount; i++) + { + float angle = i * (360f / ZoneRingPointCount) * Mathf.Deg2Rad; + Vector3 point = center + new Vector3(Mathf.Cos(angle) * EffectRadius, 0f, Mathf.Sin(angle) * EffectRadius); + Vector3i key = new Vector3i(basePos.x, -1000 - i, basePos.z); + if (GameManager.Instance.HasBlockParticleEffect(key)) + { + continue; + } + // ZoneRingParticleName (not GlowParticleName) - see that constant's own comment: + // user asked for the boundary to read as "some particles", not literal fire. + GameManager.Instance.SpawnBlockParticleEffect(key, new ParticleEffect(ZoneRingParticleName, point, Quaternion.identity, 0f, WardTint)); + pendingTint.Add(key); + } + } + + public void RemoveZoneRing() + { + if (GameManager.Instance == null) + { + return; + } + Vector3i basePos = ToWorldPos(); + for (int i = 0; i < ZoneRingPointCount; i++) + { + Vector3i key = new Vector3i(basePos.x, -1000 - i, basePos.z); + if (GameManager.Instance.HasBlockParticleEffect(key)) + { + GameManager.Instance.RemoveBlockParticleEffect(key); + } + pendingTint.Remove(key); + } + } + + /// Mirrors ParticlePatch.cs's own tint technique (same ParticleSystem.MainModule + /// fields, same reasoning: the particle prefab always instantiates at its authored color, + /// nothing in XML/the block-particle API can override that) - kept as its own copy here + /// rather than refactoring ParticlePatch.cs itself, so this feature can't regress the + /// already-working zombie glow if something about this call site needs different handling + /// once tested in-game. + /// + /// EXTENDED 2026-09-02 for "campfire" (see GlowParticleName's own comment for why the + /// particle changed) with Gradient/TwoGradients handling - ParticlePatch.cs's original + /// only ever needed Color/TwoColors (RadiatedParticlesOnMesh's own authored mode) and + /// explicitly left Gradient/TwoGradients alone as "no generic way to override". That's not + /// actually true - a Gradient's color keys ARE reassignable at runtime via + /// `Gradient.SetKeys` - so it's handled here now, since a real fire effect plausibly + /// animates through multiple colors (yellow-orange-red-smoke) via an actual Gradient rather + /// than one flat color, and the request is specifically "холодным пламенем" (COLD flame) - + /// if this branch never actually runs because campfire turns out to use plain Color/ + /// TwoColors after all, no harm, the two branches above still cover it. + public static void ApplyGlowTint(Transform particleTransform, float sizeFactor, float alphaFactor) + { + if (particleTransform == null) + { + return; + } + particleTransform.localScale = Vector3.one * sizeFactor; + ParticleSystem[] systems = particleTransform.GetComponentsInChildren(true); + foreach (ParticleSystem ps in systems) + { + ParticleSystem.MainModule main = ps.main; + main.startSizeMultiplier *= sizeFactor; + ParticleSystem.MinMaxGradient startColor = main.startColor; + switch (startColor.mode) + { + case ParticleSystemGradientMode.Color: + { + Color c = WardTint; + c.a = startColor.color.a * alphaFactor; + startColor.color = c; + main.startColor = startColor; + break; + } + case ParticleSystemGradientMode.TwoColors: + { + Color min = WardTint; + Color max = WardTint; + min.a = startColor.colorMin.a * alphaFactor; + max.a = startColor.colorMax.a * alphaFactor; + startColor.colorMin = min; + startColor.colorMax = max; + main.startColor = startColor; + break; + } + case ParticleSystemGradientMode.Gradient: + startColor.gradient = TintGradient(startColor.gradient, alphaFactor); + main.startColor = startColor; + break; + case ParticleSystemGradientMode.TwoGradients: + startColor.gradientMin = TintGradient(startColor.gradientMin, alphaFactor); + startColor.gradientMax = TintGradient(startColor.gradientMax, alphaFactor); + main.startColor = startColor; + break; + } + + // Belt-and-suspenders for any sub-emitter/light-flicker Renderer that isn't a + // ParticleSystem itself - SpawnGlow/SpawnZoneRing already pass WardTint as the + // ParticleEffect's own _color (which ParticleEffect.SpawnParticleEffect applies to + // exactly this kind of non-ParticleSystem Renderer automatically), this loop only + // covers the ParticleSystem-driven part. A real-time Light component (if "campfire" + // has one for dynamic scene lighting) is NOT touched by either mechanism - if the + // flame reads purple but still casts an orange glow on nearby surfaces, that's why, + // and would need its own separate fix once actually seen in-game. + } + } + + /// Rebuilds a Gradient with every color key replaced by WardTint, keeping the + /// original alpha keys (and their timing) intact so the fade-in/fade-out shape of the + /// effect is preserved - only the color changes, not the timing/opacity curve. + public static Gradient TintGradient(Gradient original, float alphaFactor) + { + Gradient g = new Gradient(); + GradientAlphaKey[] alphaKeys = original != null ? original.alphaKeys : new GradientAlphaKey[] { new GradientAlphaKey(1f, 0f) }; + for (int i = 0; i < alphaKeys.Length; i++) + { + alphaKeys[i].alpha *= alphaFactor; + } + GradientColorKey[] colorKeys = new GradientColorKey[] { new GradientColorKey(WardTint, 0f), new GradientColorKey(WardTint, 1f) }; + g.SetKeys(colorKeys, alphaKeys); + return g; + } + + // ------------------------------------------------------------------ + // Per-tick: deferred tint + charm/burn scan. Replaces the first version's global + // ModEvents.UnityUpdate handler + static Dictionary loop entirely - each pyramid now ticks + // itself via this real per-feature hook, called directly by the engine (no throttle of our + // own - see the comment on `center` below for why a self-imposed one is actively wrong here). + // ------------------------------------------------------------------ + + public override void UpdateTick(World _world) + { + base.UpdateTick(_world); + if (GameManager.Instance == null) + { + return; + } + + for (int i = pendingTint.Count - 1; i >= 0; i--) + { + Vector3i key = pendingTint[i]; + if (!GameManager.Instance.HasBlockParticleEffect(key)) + { + continue; + } + Transform t = GameManager.Instance.GetBlockParticleEffect(key); + bool isRingPoint = key.y <= -1000; + // 1.0 = natural "campfire" size for the main glow; ring markers shrunk hard (0.35) + // so the 16 of them read as small flame-markers instead of a circle of bonfires. + // Near-opaque alpha (0.9) so the purple tint reads clearly. + ApplyGlowTint(t, isRingPoint ? 0.35f : 1.0f, 0.9f); + pendingTint.RemoveAt(i); + } + + if (!EffectOn) + { + return; + } + + Vector3i posI = ToWorldPos(); + // World.blockToTransformPos already returns X/Z centered on the block (confirmed by + // decompile) - only the vertical lift is ours to add. (A past version double-added the + // X/Z centering here, offsetting the whole detection circle by about a block - fixed.) + Vector3 center = World.blockToTransformPos(posI) + new Vector3(0f, 0.5f, 0f); + // Horizontal-only (X/Z) distance, ignoring Y: the zone-ring visual is a flat disc at one + // height, so the real detection area is a matching vertical column, not a shrinking + // sphere - also just more useful for a base with any stairs/floors. Bounds query + // widened vertically (256 = full world height) since the real filter below doesn't + // restrict Y at all. + Bounds bounds = new Bounds(center, new Vector3(EffectRadius * 2f, 256f, EffectRadius * 2f)); + + // Skip the (more expensive) zombie query/loop entirely unless a player is actually in + // range - also doubles as the burning debuff's required instigator id (a buff with a + // real damage_type, unlike the charm, needs one or EntityBuffs.AddBuff fails its + // FriendlyFireCheck outright - see BurnBuffName's own comment). + List playersNearby = new List(); + _world.GetEntitiesInBounds(typeof(EntityPlayer), bounds, playersNearby); + int playerInstigatorId = -1; + foreach (Entity p in playersNearby) + { + float pdx = p.position.x - center.x; + float pdz = p.position.z - center.z; + if (pdx * pdx + pdz * pdz <= EffectRadius * EffectRadius) + { + playerInstigatorId = p.entityId; + break; + } + } + if (playerInstigatorId == -1) + { + return; + } + + List nearby = new List(); + _world.GetEntitiesInBounds(typeof(EntityZombie), bounds, nearby); + foreach (Entity e in nearby) + { + if (!(e is EntityZombie zombie) || zombie.IsDead() || zombie.Buffs == null) + { + continue; + } + float dx = zombie.position.x - center.x; + float dz = zombie.position.z - center.z; + if (dx * dx + dz * dz > EffectRadius * EffectRadius) + { + continue; + } + if (!zombie.Buffs.HasBuff(Patch_EntityBuffs_AddBuff_DeviatorCharm.CharmBuffName)) + { + zombie.Buffs.AddBuff(Patch_EntityBuffs_AddBuff_DeviatorCharm.CharmBuffName); + Debug.Log("[NecromancerTome] TEFeaturePyramidWard: charmed zombie " + zombie.entityId + " near pyramid " + posI); + } + // Burning re-applied every tick a zombie is in the zone (not gated behind "already + // has it" like the charm above) - buffBurningElement resets its own countdown on + // every re-trigger, so this keeps it topped up rather than a one-shot. + zombie.Buffs.AddBuff(BurnBuffName, playerInstigatorId); + } + } + } + + /// + /// Real, verified-by-compiler blocker found while writing TEFeaturePyramidWard above: + /// TEFeatureAbs.AllowBlockActivationCommand and TEFeatureAbs.OnBlockActivated cannot be + /// overridden from this mod's project AT ALL, on any target framework tried (netstandard2.1 + /// AND net8.0 both fail identically, confirmed with an isolated throwaway repro project - this + /// is not a langversion/TargetFramework setting to tune away). Root cause, found by dumping raw + /// IL (`ilspycmd -il`): Assembly-CSharp.dll declares these two methods' shared parameter type as + /// `valuetype [mscorlib]System.ReadOnlySpan\`1<char>` - i.e. Unity's own Mono/IL2CPP + /// runtime backports Span/ReadOnlySpan INTO mscorlib itself, unlike a normal modern .NET SDK + /// project (this mod's own csproj included), where ReadOnlySpan<T> instead lives in + /// System.Private.CoreLib/System.Memory. Same type NAME, but the CLR treats a type's identity + /// as (name + DECLARING ASSEMBLY) - these are two different types to the compiler, so an + /// override that looks byte-for-byte identical in source (confirmed via a live reflection probe + /// against the real DLL, not just the decompiled source) still fails to bind as a valid + /// override. The only real fix on our side would be adding an explicit reference to the game's + /// own Managed/mscorlib.dll so our ReadOnlySpan<char> resolves from the same assembly - + /// not attempted, since forcing a second mscorlib into this project risks colliding with every + /// other basic type (string, object, List<T>...) the SDK's own implicit framework + /// reference already provides, for every file in this mod, not just this one method pair. Not + /// worth that blast radius for two methods this patch below covers just as well anyway. + /// + /// WORKAROUND: everything both blocked methods needed to do is instead done one layer up, on + /// the STRING/array-based (no ReadOnlySpan anywhere) methods that wrap them: + /// - `TileEntityComposite.UpdateBlockActivationCommands(BlockActivationCommand[], ...)` - + /// confirmed by decompile to run AFTER every feature's (unoverridden, always-true-by- + /// default) AllowBlockActivationCommand, so a Postfix here can simply overwrite `.enabled` + /// for our 4 known commands with the real per-instance answer - same end result. + /// - `BlockCompositeTileEntity.OnBlockActivated(string _commandName, ...)` - the SAME method + /// the pre-rewrite plain-Block version patched, just on the composite block class instead; + /// `_commandName` here is still the FULL "TEFeaturePyramidWard:effect_on" form (splitting + /// into module+bare command only happens one level deeper, inside TileEntityComposite's own + /// OnBlockActivated) - checked with plain string.EndsWith, no ReadOnlySpan needed. + /// Both patches guard on TryGetSelfOrFeature<TEFeaturePyramidWard> first and bail + /// immediately for every other composite block in the game (doors, Land Claim, etc.) - same + /// "patch the shared method, filter by identity" idiom as everywhere else in this mod. + /// + [HarmonyPatch(typeof(TileEntityComposite), "UpdateBlockActivationCommands")] + public static class Patch_TileEntityComposite_UpdateBlockActivationCommands_PyramidWard + { + public static void Postfix(TileEntityComposite __instance, BlockActivationCommand[] _commands) + { + if (__instance == null || _commands == null) + { + return; + } + if (!__instance.TryGetSelfOrFeature(out TEFeaturePyramidWard feature)) + { + return; + } + for (int i = 0; i < _commands.Length; i++) + { + string text = _commands[i].text; + if (string.IsNullOrEmpty(text)) + { + continue; + } + if (text.EndsWith("effect_on")) + { + _commands[i].enabled = !feature.EffectOn; + } + else if (text.EndsWith("effect_off")) + { + _commands[i].enabled = feature.EffectOn; + } + else if (text.EndsWith("zone_show")) + { + _commands[i].enabled = !feature.ZoneShown; + } + else if (text.EndsWith("zone_hide")) + { + _commands[i].enabled = feature.ZoneShown; + } + } + } + } + + [HarmonyPatch(typeof(BlockCompositeTileEntity), "OnBlockActivated", new Type[] { typeof(string), typeof(WorldBase), typeof(Vector3i), typeof(BlockValue), typeof(EntityPlayerLocal) })] + public static class Patch_BlockCompositeTileEntity_OnBlockActivated_PyramidWard + { + public static bool Prefix(string _commandName, Vector3i _blockPos, ref bool __result) + { + if (string.IsNullOrEmpty(_commandName)) + { + return true; + } + bool isEffectCommand = _commandName.EndsWith("effect_on") || _commandName.EndsWith("effect_off"); + bool isZoneCommand = _commandName.EndsWith("zone_show") || _commandName.EndsWith("zone_hide"); + if (!isEffectCommand && !isZoneCommand) + { + return true; + } + + World world = GameManager.Instance != null ? GameManager.Instance.World : null; + TileEntity te = world != null ? world.GetTileEntity(_blockPos) : null; + if (!(te is TileEntityComposite composite) || !composite.TryGetSelfOrFeature(out TEFeaturePyramidWard feature)) + { + __result = false; + return false; + } + + if (isEffectCommand) + { + feature.EffectOn = !feature.EffectOn; + if (feature.EffectOn) + { + feature.SpawnGlow(); + } + else + { + feature.RemoveGlow(); + } + Debug.Log("[NecromancerTome] TEFeaturePyramidWard: effect " + (feature.EffectOn ? "ON" : "OFF") + " at " + _blockPos); + } + else + { + feature.ZoneShown = !feature.ZoneShown; + if (feature.ZoneShown) + { + feature.SpawnZoneRing(); + } + else + { + feature.RemoveZoneRing(); + } + Debug.Log("[NecromancerTome] TEFeaturePyramidWard: zone display " + (feature.ZoneShown ? "ON" : "OFF") + " at " + _blockPos); + } + feature.SetModified(); + __result = true; + return false; + } + } +} diff --git a/HarmonySrc/SpatialVaultPatch.cs b/HarmonySrc/SpatialVaultPatch.cs new file mode 100644 index 0000000..dc65a6d --- /dev/null +++ b/HarmonySrc/SpatialVaultPatch.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using HarmonyLib; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// "Пространственный браслет" (Spatial Bracelet) - dictated 2026-08-30, implemented same + /// day. See items.xml (braceletSpatialVault) for the item - both Action0 and Action1 use + /// Class="Eat" purely as a click-catcher (same trick as every other Harmony-driven item this + /// mod already has), distinguished here by ItemActionData.indexInEntityOfAction (0/1), the + /// same field SummonPatch.cs already uses to tell a summon book's summon-click from its + /// recall-click. + /// + /// POWER ATTACK (index 1) - personal storage, size scaling with Necromancy skill level: + /// - XUiC_BagStorageWindowGroup.Open(xui, entity, bag, lootContainer, title, ...) is the + /// REAL API EntityDrone.openStorageWindow() itself calls to show the drone's own cargo + /// window (decompiled EntityDrone directly to find this, not guessed) - reused directly + /// rather than reinventing a storage UI. LootContainer.GetLootContainer("roboticDrone") + /// is the same display/behavior template the drone's own window uses too - "как у дрона" + /// taken literally, not just as a vague size comparison. + /// - Slot count = Mathf.RoundToInt(necromancyLevel / 10f), per the user's own exact formula + /// ("1*скилл_некроманта/10 округлённый до целого") - read live from + /// player.Progression.GetProgressionValue("craftingNecroNecromancy").Level (decompiled + /// EntityAlive/Progression/ProgressionValue directly to confirm this exact call shape, + /// not guessed) - the SAME skill the Knife's own damage already scales with (capped at + /// level 5000, one level per zombie kill - see buffs.xml/progression.xml), so this grows + /// at the same pace as every other kill-count-tied payoff in this mod. Below level 10 + /// this rounds to 0 - deliberately left as-is, not special-cased away, matching the + /// Knife's own "0 at 0 kills is a feature, not a bug" precedent - a tooltip explains it + /// instead of silently opening a useless empty window. + /// - PERSISTENCE - the one thing NOT fully solved here, flagged rather than silently + /// assumed: the Bag backing each player's vault lives in a plain in-memory + /// Dictionary<int, Bag> in this file (PlayerVaults below), keyed by entityId. This + /// is reliable for as long as the game process keeps running (survives death/respawn/ + /// relogging within one play session, confirmed by how a static field behaves) but has + /// NOT been wired into any save/load system - closing the game entirely and reloading the + /// save later will NOT bring the vault's contents back (no persistence file, no hook into + /// PersistentPlayerData or a world-save event). Building real cross-session persistence + /// (a custom save file + ModEvents.GameSave/Load hooks, or piggybacking on an owned + /// world entity the way the summoned pets do - unconfirmed whether THOSE actually survive + /// a full restart either) is real, separate follow-up work, not attempted here. Treat + /// this like a session-scoped stash until that's built and confirmed - don't rely on it + /// across game restarts yet. + /// + /// REGULAR ATTACK (index 0) - knock back + slow whatever zombie the crosshair is aimed at: + /// - Same raycast mechanism HarmonySrc/ThiefLoopPatch.cs already established for + /// braceletThiefLoop (GetLookRay + Physics.Raycast + RootTransformRefEntity. + /// FindEntityUpwards) - reused verbatim, just resolving to EntityZombie instead of + /// EntityLootContainer. + /// - Slow: zombie.Buffs.AddBuff("buffInjurySlow") - the exact same vanilla debuff already + /// reused elsewhere in this mod (the Dog's own bite, necroMeleeHandZombieDog). + /// - Knockback: DELIBERATELY a straight Entity.SetPosition "shove" (same API + /// PetFollowPatch.cs already uses to reposition pets), NOT a physics/ragdoll impulse. + /// Found real candidates for "proper" knockback while researching this + /// (EntityAlive.DoRagdoll(in DamageResponse), DamageResponse.ImpulseScale/HitDirection), + /// but fully reverse-engineering how a real DamageResponse gets built and fed into that + /// during normal combat - all its other fields (Source, Strength, Stun, ArmorSlot, etc.) + /// - would have taken real additional decompilation with no guarantee of getting all the + /// coordinate/enum conventions right on the first try. A direct position shove is cruder + /// (no animation, the zombie just appears further away) but uses an API this exact file's + /// own family already relies on successfully - chosen for certainty over polish. Revisit + /// with DoRagdoll if the teleport-shove feels too crude in testing. + /// + [HarmonyPatch(typeof(ItemActionEat), "ExecuteAction")] + public static class Patch_ItemActionEat_ExecuteAction_SpatialVault + { + public const string ItemName = "braceletSpatialVault"; + public const string NecromancySkillName = "craftingNecroNecromancy"; + public const float MaxRange = 50f; + public const float ShoveDistance = 6f; + + /// See the class-level comment above for exactly what this does and doesn't + /// guarantee - session-scoped only, not yet saved/loaded across game restarts. + public static readonly Dictionary PlayerVaults = new Dictionary(); + + public static bool Prefix(ItemActionData _actionData, bool _bReleased) + { + if (!_bReleased) + { + return true; + } + string itemName = _actionData?.invData?.itemValue?.ItemClass?.Name; + if (itemName != ItemName) + { + return true; + } + if (!(_actionData.invData.holdingEntity is EntityPlayerLocal player)) + { + return true; + } + + if (_actionData.indexInEntityOfAction == 1) + { + OpenVault(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; + } + + public static void OpenVault(EntityPlayerLocal player) + { + ProgressionValue progressionValue = player.Progression?.GetProgressionValue(NecromancySkillName); + int level = progressionValue != null ? progressionValue.Level : 0; + int slotCount = Mathf.RoundToInt(level / 10f); + if (slotCount <= 0) + { + GameManager.ShowTooltip(player, "braceletSpatialVaultTooWeak"); + return; + } + + if (!PlayerVaults.TryGetValue(player.entityId, out Bag bag)) + { + bag = new Bag(slotCount); + PlayerVaults[player.entityId] = bag; + } + else if (bag.SlotCount < slotCount) + { + // Grow, never shrink - the skill level only ever goes up, so this only ever + // copies existing stacks into a bigger array, same shape + // EntityLootContainer.SetContent itself uses when it needs to resize a bag. + ItemStack[] oldSlots = bag.GetSlots(); + ItemStack[] newSlots = ItemStack.CreateArray(slotCount); + Array.Copy(oldSlots, newSlots, oldSlots.Length); + bag.SetSlots(newSlots); + } + + Debug.Log("[NecromancerTome] SpatialVaultPatch: owner=" + player.entityId + " opened vault, " + slotCount + " slots (Necromancy level " + level + ")"); + LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPlayer(player); + XUiC_BagStorageWindowGroup.Open(playerUI.xui, player, bag, LootContainer.GetLootContainer("roboticDrone"), Localization.Get("braceletSpatialVaultWindowTitle")); + } + + public static void ShoveZombieAtCrosshair(EntityPlayerLocal player) + { + Ray ray = player.GetLookRay(); + if (!Physics.Raycast(ray, out RaycastHit hit, MaxRange)) + { + GameManager.ShowTooltip(player, "braceletSpatialVaultNoTarget"); + return; + } + Transform entityTransform = RootTransformRefEntity.FindEntityUpwards(hit.collider.transform); + Entity entity = entityTransform != null ? entityTransform.GetComponent() : null; + if (!(entity is EntityZombie zombie) || zombie.IsDead()) + { + GameManager.ShowTooltip(player, "braceletSpatialVaultNoTarget"); + return; + } + + zombie.Buffs?.AddBuff("buffInjurySlow"); + + Vector3 shoveDir = zombie.position - player.position; + shoveDir.y = 0f; + shoveDir = shoveDir.sqrMagnitude > 0.01f ? shoveDir.normalized : player.transform.forward; + Vector3 destination = zombie.position + shoveDir * ShoveDistance + Vector3.up * 1f; + zombie.SetPosition(destination, true); + + player.PlayOneShot("swoosh"); + Debug.Log("[NecromancerTome] SpatialVaultPatch: owner=" + player.entityId + " shoved zombie " + zombie.entityId); + } + } +} diff --git a/HarmonySrc/SummonPatch.cs b/HarmonySrc/SummonPatch.cs new file mode 100644 index 0000000..20b9f43 --- /dev/null +++ b/HarmonySrc/SummonPatch.cs @@ -0,0 +1,391 @@ +using System.Collections.Generic; +using HarmonyLib; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// Necromancer pet summon/ownership/recall - BACKLOG.md item 3 (Zombie Dog) plus the Insect + /// Swarm added alongside its bugfix (2026-08-28). Done the same way vanilla's own drone works + /// (per user request): one active pet per player PER SPECIES, and the pet is recorded as + /// owned via the same EntityAlive.ownedEntities API the drone itself uses + /// (ItemActionSpawnTurret.ExecuteAction calls holdingEntity.AddOwnedEntity(entityDrone) - + /// confirmed by decompiling it). + /// + /// Each summon book's item block now carries the SpawnEntity action on BOTH Action0 and + /// Action1 (user's own suggestion 2026-08-28, in response to "how do I put the dog back in + /// the book?"), pointed at the same pet entity in both slots, but the two slots mean + /// something different: Action0 (primary click) SUMMONS, blocked with a tooltip if one is + /// already out; Action1 (secondary/"power attack" click) RECALLS the owned one if there is + /// one, or no-ops with a tooltip if there isn't. This split is entirely our own Prefix logic + /// below, keyed off ItemActionData.indexInEntityOfAction (0/1, public field, confirmed by + /// decompiling ItemActionData) - ItemActionSpawnEntity itself has no concept of "recall", + /// it's purely a spawner. + /// + /// LimitedPets keys off entity_class name -> the three localization keys each species needs + /// (already-active, recalled, nothing-to-recall). Each species gets its own independent + /// 1-active limit (a Dog and a Swarm can be out at once; two Dogs can't). + /// + /// Two patch points for the SUMMON path, because entity creation and item consumption/ + /// ownership can't both live in one method without either a transpiler (to grab a local + /// variable) or a fragile "guess the newest entity of this class" lookup: + /// + /// 1. Prefix on ItemActionSpawnEntity.Spawn - runs BEFORE anything is created. Handles both + /// the summon-side block/allow AND the entire recall path (recall never lets the + /// original method run at all - there's nothing for vanilla Spawn to do on a recall). + /// 2. Postfix on EntityFactory.CreateEntity(int,Vector3,Vector3) - the exact overload + /// ItemActionSpawnEntity.Spawn() calls, and it returns the created Entity directly + /// (unlike Spawn() itself, which is void), so this is the only point that has both "an + /// entity was just created" and "here it is" without needing IL tricks. Filtered to our + /// pets' entityClassIds so it's a no-op for every other CreateEntity call in the game + /// (turrets, drones, zombie spawns, everything else uses this exact same overload). + /// Because the Prefix above already guarantees at most one pet of that species per + /// player, this postfix doesn't need to re-check the limit - it only ever fires for + /// allowed summons. + /// + /// This overload of CreateEntity has no "who spawned this" parameter, so the postfix finds + /// the owner as the nearest player to the spawn position - always correct here since + /// ItemActionSpawnEntity.Spawn() always spawns at (roughly) the caster's own head position. + /// Fine for this mod (no multiplayer/persistence layer exists anywhere else in it either); + /// not a general-purpose "find the real spawner" solution. + /// + /// Known gap vs. the real drone: no drones.dat-style save file, no despawn-on-owner-death. + /// A pet is a plain EntityAlive with no special unload/reload handling, so if its chunk + /// unloads while the player is away it despawns like any other wandering entity - the real + /// drone avoids that via DroneManager's own persistence system, which is a lot of machinery + /// (network sync, its own save file) this mod doesn't have a reason to take on for a couple + /// of pet types. Revisit only if that turns out to matter. + /// + /// See PetFollowPatch.cs (added 2026-08-28, after the Dog wandered off in-game and a second + /// one wouldn't summon) for the leash-back-to-owner behavior and the ownership cleanup that + /// runs when a tracked pet dies or its chunk unloads - both plug directly into this file's + /// LimitedPets/AddOwnedEntity machinery. The manual recall path added here calls + /// PetFollowPatch.Unregister so a recalled pet stops being tracked immediately instead of + /// waiting for that cleanup to notice it's gone. + /// + /// DEBUG LOGGING: added 2026-08-28 after the Zombie Dog silently failed to spawn in-game + /// (root cause was unrelated to this patch - see items.xml's AnimWait comment - but there was + /// no logging anywhere in this file to even rule that out quickly, unlike CharmPatch.cs's + /// verbose Debug.Log on every step). Keeping these permanently, same as CharmPatch.cs does. + /// + [HarmonyPatch(typeof(ItemActionSpawnEntity), "Spawn", new System.Type[] { typeof(ItemActionData) })] + public static class Patch_ItemActionSpawnEntity_Spawn_PetLimit + { + public class PetInfo + { + public string AlreadyActiveKey; + public string RecalledKey; + public string NothingToRecallKey; + public string SummonItemName; + /// User request 2026-08-28: "пусть книга вообще не тратится на призыв + /// собаки" - the Dog's book is a permanent bonded item, never consumed on summon (and + /// RecallPet never has anything to give back, since nothing was ever taken). The + /// Swarm keeps consuming its book per cast - it's the one-time/no-recall species, that + /// consumption is the actual "cost" of casting it. This single flag also happens to + /// fix the earlier "dog died, no way to get the book back" complaint: with nothing + /// ever taken, there's nothing to lose when the dog dies off-screen. + public bool ConsumesBook; + } + + public static readonly Dictionary LimitedPets = new Dictionary + { + { + "necroZombieDog", + new PetInfo + { + AlreadyActiveKey = "necroZombieDogAlreadyActive", + RecalledKey = "necroZombieDogRecalled", + NothingToRecallKey = "necroZombieDogNothingToRecall", + SummonItemName = "bookSummonZombieDog", + ConsumesBook = false, + } + }, + { + // User request 2026-08-28: "пусть вызов насекомых будет одноразовым" - no + // Action1 on bookSummonInsectSwarm any more (see items.xml), so + // indexInEntityOfAction can never be RecallActionIndex for this species and + // RecalledKey/NothingToRecallKey below are simply never read. Left null rather + // than pointed at deleted localization keys. + "necroInsectSwarm", + new PetInfo + { + AlreadyActiveKey = "necroInsectSwarmAlreadyActive", + RecalledKey = null, + NothingToRecallKey = null, + SummonItemName = "bookSummonInsectSwarm", + ConsumesBook = true, + } + }, + // Three more pets, BACKLOG.md item 4a. REPLACED 2026-08-29 - the original + // Stripper/Cop/Soldier (extending real EntityZombie-classed vanilla zombies) came + // back hostile to the player in testing despite copying the Dog's own recipe; see + // entityclasses.xml for the full story. New concept: Зомбогриф/Зомбомедведь/ + // Зомбоволк, extending animal-family bases (same category as the Dog itself, which + // DID work) - same shape otherwise (recallable, book never consumed). + { + "necroZombieGriffin", + new PetInfo + { + AlreadyActiveKey = "necroZombieGriffinAlreadyActive", + RecalledKey = "necroZombieGriffinRecalled", + NothingToRecallKey = "necroZombieGriffinNothingToRecall", + SummonItemName = "bookSummonZombieGriffin", + ConsumesBook = false, + } + }, + { + "necroZombieBear", + new PetInfo + { + AlreadyActiveKey = "necroZombieBearAlreadyActive", + RecalledKey = "necroZombieBearRecalled", + NothingToRecallKey = "necroZombieBearNothingToRecall", + SummonItemName = "bookSummonZombieBear", + ConsumesBook = false, + } + }, + { + "necroZombieWolf", + new PetInfo + { + AlreadyActiveKey = "necroZombieWolfAlreadyActive", + RecalledKey = "necroZombieWolfRecalled", + NothingToRecallKey = "necroZombieWolfNothingToRecall", + SummonItemName = "bookSummonZombieWolf", + ConsumesBook = false, + } + }, + }; + + /// Action1 ("power attack" slot) is always recall-only - see items.xml, both + /// summon books now declare Action1 with the same Class="SpawnEntity"/Entity as Action0. + public const int RecallActionIndex = 1; + + public static bool Prefix(ItemActionSpawnEntity __instance, ItemActionData _actionData) + { + if (!LimitedPets.TryGetValue(__instance.entityToSpawn, out PetInfo tooltips)) + { + return true; + } + EntityAlive holdingEntity = _actionData?.invData?.holdingEntity; + if (holdingEntity == null) + { + return true; + } + // BUG FIXED 2026-08-28 (unlimited summons, recall never ran, dogs pile-launching the + // player): this used to check "<= 0" for "not found", on the wrong assumption that + // valid ids are small positive numbers. Confirmed by decompiling EntityClass.GetId: + // it returns -1 (a clean sentinel) when not found, and otherwise the real class id - + // which is hash-based and can absolutely be negative (necroZombieDog's is, e.g., + // -779816341, confirmed by this method's own Debug.Log below during the actual bug). + // With "<= 0", that real, valid, negative id was misread as "not found" on every + // single call, so this returned true unconditionally - which skipped BOTH the + // summon-limit check AND the entire recall branch below (recall's check for it is + // also past this point), so every Action0 OR Action1 click just summoned yet another + // pet, forever, book and all. + int petClassId = EntityClass.GetId(__instance.entityToSpawn); + if (petClassId == -1) + { + Debug.LogWarning("[NecromancerTome] SummonPatch: entity class '" + __instance.entityToSpawn + "' not found"); + return true; + } + List owned = holdingEntity.GetOwnedEntities(petClassId); + Debug.Log("[NecromancerTome] SummonPatch: Spawn prefix for " + __instance.entityToSpawn + ", action index=" + _actionData.indexInEntityOfAction + ", owned count=" + owned.Count); + + if (_actionData.indexInEntityOfAction == RecallActionIndex) + { + if (owned.Count > 0) + { + RecallPet(holdingEntity, owned[0].Id, tooltips.RecalledKey, tooltips.ConsumesBook ? tooltips.SummonItemName : null); + } + else if (holdingEntity.world != null) + { + GameManager.ShowTooltip(GameManager.Instance.World.GetPrimaryPlayer(), tooltips.NothingToRecallKey); + } + return false; + } + + if (owned.Count > 0) + { + if (holdingEntity.world != null) + { + GameManager.ShowTooltip(GameManager.Instance.World.GetPrimaryPlayer(), tooltips.AlreadyActiveKey); + } + return false; + } + return true; + } + + /// summonItemName is null when this species doesn't consume its book on summon + /// (see PetInfo.ConsumesBook) - nothing was taken, so nothing is given back. + public static void RecallPet(EntityAlive owner, int petEntityId, string recalledTooltipKey, string summonItemName) + { + World world = owner.world; + if (world != null) + { + world.RemoveEntity(petEntityId, EnumRemoveEntityReason.Killed); + } + owner.RemoveOwnedEntity(petEntityId); + PetFollowPatch.Unregister(petEntityId); + if (summonItemName != null) + { + GiveBackSummonItem(owner, summonItemName); + } + if (world != null) + { + GameManager.ShowTooltip(GameManager.Instance.World.GetPrimaryPlayer(), recalledTooltipKey); + } + Debug.Log("[NecromancerTome] SummonPatch: owner=" + owner.entityId + " recalled pet " + petEntityId); + } + + /// Hands one copy of the summon book back to whoever just recalled their pet - + /// the dog/swarm "goes back into the book" literally, not just for free. Tries the + /// toolbelt first (Inventory.AddItem - confirmed by decompiling it, only searches the + /// toolbelt's own slots), then the backpack (EntityPlayer.bag, same AddItem shape) if that + /// didn't fit. If both are full the book is just lost - not worth building actual overflow + /// handling (a "drop it on the ground" fallback) for something this minor. + public static void GiveBackSummonItem(EntityAlive owner, string itemName) + { + ItemValue itemValue = ItemClass.GetItem(itemName); + if (itemValue == null || itemValue.type <= 0) + { + Debug.LogWarning("[NecromancerTome] SummonPatch: could not resolve item '" + itemName + "' to give back on recall"); + return; + } + ItemStack stack = new ItemStack(itemValue, 1); + bool added = owner.inventory != null && owner.inventory.AddItem(stack, out int slot); + if (!added && owner is EntityPlayer player && player.bag != null) + { + // Bag (InventoryBase) only exposes the single-arg AddItem overload, unlike + // Inventory's (ItemStack, out int) - confirmed by decompiling both. + added = player.bag.AddItem(stack); + } + if (!added) + { + Debug.LogWarning("[NecromancerTome] SummonPatch: " + itemName + " didn't fit back into " + owner.entityId + "'s inventory on recall (full?)"); + } + } + } + + [HarmonyPatch(typeof(EntityFactory), "CreateEntity", new System.Type[] { typeof(int), typeof(Vector3), typeof(Vector3) })] + public static class Patch_EntityFactory_CreateEntity_PetOwnership + { + public static void Postfix(int _et, Entity __result) + { + if (__result == null) + { + return; + } + Patch_ItemActionSpawnEntity_Spawn_PetLimit.PetInfo petInfo = null; + foreach (KeyValuePair entry in Patch_ItemActionSpawnEntity_Spawn_PetLimit.LimitedPets) + { + if (EntityClass.GetId(entry.Key) == _et) + { + petInfo = entry.Value; + break; + } + } + if (petInfo == null) + { + return; + } + Debug.Log("[NecromancerTome] SummonPatch: CreateEntity postfix, entity=" + __result.entityId + " et=" + _et); + + World world = GameManager.Instance != null ? GameManager.Instance.World : null; + if (world == null || world.Players == null || world.Players.list == null) + { + return; + } + + EntityPlayer owner = null; + float bestDistSq = float.MaxValue; + foreach (EntityPlayer player in world.Players.list) + { + if (player == null) + { + continue; + } + float distSq = (player.position - __result.position).sqrMagnitude; + if (distSq < bestDistSq) + { + bestDistSq = distSq; + owner = player; + } + } + if (owner == null) + { + Debug.LogWarning("[NecromancerTome] SummonPatch: no player found to own " + __result.entityId); + return; + } + + owner.AddOwnedEntity(__result); + if (petInfo.ConsumesBook && owner.inventory != null) + { + owner.inventory.DecHoldingItem(1); + } + PetFollowPatch.Register(owner, __result); + IgnoreCollisionWithOwner(owner, __result); + ApplyGhostlyTransparency(__result); + Debug.Log("[NecromancerTome] SummonPatch: owner=" + owner.entityId + " now owns pet " + __result.entityId); + } + + /// User request 2026-08-28 ("нематериальными") - makes the pet pass through its + /// own owner specifically, via Physics.IgnoreCollision on every collider pair between the + /// two, rather than stripping the pet's PhysicsBody entirely the way vanilla's own + /// animalInsectSwarm does for its "no physics body at all" look (confirmed by checking + /// entityclasses.xml). That works for a flying swarm; a ground-walking dog with no + /// collider at all would fall through the terrain. This keeps it solid against ground and + /// zombies - just not its owner - which also directly closes the last piece of the + /// spawn-launch bug documented in items.xml/PetFollowPatch.cs (nothing left to shove the + /// player if the two colliders can't touch in the first place). + public static void IgnoreCollisionWithOwner(EntityPlayer owner, Entity pet) + { + Collider[] ownerColliders = owner.GetComponentsInChildren(); + Collider[] petColliders = pet.GetComponentsInChildren(); + foreach (Collider oc in ownerColliders) + { + if (oc == null) + { + continue; + } + foreach (Collider pc in petColliders) + { + if (pc == null) + { + continue; + } + Physics.IgnoreCollision(oc, pc, true); + } + } + } + + /// User request 2026-08-28 ("слегка прозрачными") - best effort only. Directly + /// sets renderer.material.color's alpha, the same technique ItemActionSpawnTurret uses for + /// its own placement-preview tint (confirmed by decompiling it), but that only visibly + /// shows up if the model's actual shader supports alpha blending - most opaque mob + /// shaders in this game don't, and there's no reliable XML/reflection-only way to swap a + /// live renderer's shader to a transparent variant without risking breaking how it's lit. + /// + /// BUG FIXED 2026-08-28: the Insect Swarm's renderers use a particle shader + /// ("Game Particles/surfaceShader_masked_particleEnhanced") that has no "_Color" property + /// at all - setting .color on it doesn't throw, but Unity logs "doesn't have a color + /// property '_Color'" on every single access, once per renderer per spawn (confirmed in + /// output_log - this is what the user saw as "an error about colors"). HasProperty check + /// added so this silently skips any renderer whose shader doesn't support it instead of + /// spamming the log - the visual effect was never going to work on those anyway. + public static void ApplyGhostlyTransparency(Entity pet) + { + Renderer[] renderers = pet.GetComponentsInChildren(); + foreach (Renderer renderer in renderers) + { + if (renderer == null || renderer.material == null || !renderer.material.HasProperty("_Color")) + { + continue; + } + Color color = renderer.material.color; + color.a = 0.55f; + renderer.material.color = color; + } + } + } +} diff --git a/HarmonySrc/SwarmTargetPatch.cs b/HarmonySrc/SwarmTargetPatch.cs new file mode 100644 index 0000000..0c2b3d2 --- /dev/null +++ b/HarmonySrc/SwarmTargetPatch.cs @@ -0,0 +1,195 @@ +using System.Collections.Generic; +using HarmonyLib; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// BUG FIXED 2026-08-28 (Insect Swarm attacked the player instead of zombies, even after the + /// entityclasses.xml AITask-2/AITarget-4 fix): wasted effort, because none of that XML + /// mattered. Confirmed by decompiling the actual class chain - + /// necroInsectSwarm -> animalInsectSwarm -> Class="EntitySwarm" -> EntitySwarm : EntityVulture. + /// EntityVulture does NOT use the generic AITask/AITarget system for target selection at all - + /// it has its own hardcoded C# targeting (updateTasks()'s State.Wander branch calls + /// FindTarget(), which calls World.GetClosestPlayerSeen/GetClosestPlayer - literally typed to + /// return EntityPlayer, there is no "closest zombie" variant to point it at). Every ground + /// creature in this mod (the Dog, all vanilla animals) goes through the declarative AITask + /// system just fine; flying "swarm" creatures (insect swarm, bee swarm, vultures) are a + /// completely separate hardcoded-C# codepath. No XML property changes that. + /// + /// Fix: Prefix on EntityAlive.SetAttackTarget - the one non-EntityPlayer-typed choke point + /// every one of EntityVulture's several call sites funnels through (FindTarget() results, + /// revenge-target retaliation, sleeper wake-up - all of them end in a SetAttackTarget call, + /// confirmed by decompiling EntityVulture). Whenever the entity is one of OUR + /// EntityVulture-based pets AND the target it's about to be given is an EntityPlayer, swap in + /// the nearest EntityZombie instead (or null if none are nearby - just idles, better than + /// attacking the player). Only filters on our own entity classes, so vanilla's own + /// animalInsectSwarm/animalBeeSwarm/real vultures are entirely unaffected and keep hunting + /// players normally. + /// + /// GENERALIZED 2026-08-29, THEN UN-GENERALIZED SAME DAY: briefly also covered + /// necroZombieGriffin (extending animalZombieVulture, the same Class="EntityVulture" root as + /// the Swarm) via this same redirect. Confirmed live in-game that this didn't actually fix + /// the Griffin - it just flew around doing EntityVulture's own default Wander behavior, + /// never engaging zombies at all ("летает где-то в небе, и зомби его вообще не интересуют"). + /// Rather than keep debugging the redirect blind (each guess needs a full test cycle the + /// user has to run), the Griffin was converted to extend necroZombieDog directly instead (see + /// entityclasses.xml) - same proven-reliable ground-AI trick as the Bear/Wolf, no longer + /// EntityVulture-based at all, so it no longer needs this patch. Kept the Dictionary-based + /// shape below (rather than reverting to a single cached id) in case a genuinely flying pet + /// gets added again later - SpeciesByName just has one entry for now. + /// + [HarmonyPatch(typeof(EntityAlive), "SetAttackTarget", new System.Type[] { typeof(EntityAlive), typeof(int) })] + public static class Patch_EntityAlive_SetAttackTarget_SwarmRetarget + { + public class VultureBasedPetInfo + { + public bool SkipAlreadyCharmedZombies; + } + + /// HARDENED 2026-08-29 while chasing the user's "Griffin still attacks me" + /// report - the Griffin's own AI is not XML-driven at all (see class comment), so this + /// Harmony redirect not firing was the prime remaining suspect. Could not fully confirm + /// or rule this out by decompilation alone, but the ORIGINAL lazy-cache pattern here had + /// two real, independent failure modes worth closing regardless of which (if either) was + /// the actual cause: (1) EntityClass.GetId("necroInsectSwarm") and + /// EntityClass.GetId("necroZombieGriffin") were both looked up inside ONE dictionary + /// object-initializer - if EITHER happened to still return -1 (not yet registered) at + /// the exact moment some entirely unrelated zombie's very first SetAttackTarget call + /// triggered this lazy build (plausible - that can happen extremely early, before every + /// mod entity_class is guaranteed loaded), the -1 got cached FOREVER via the + /// cachedClassIds==null guard, silently never re-resolving even once the real class WAS + /// registered a moment later - and if BOTH happened to be -1 at once, the dictionary + /// initializer would throw (duplicate key), which could break unrelated zombie AI too. + /// Rewritten to resolve each species independently and only cache a REAL (non -1) id - + /// an unresolved species is retried on every subsequent call instead of being poisoned + /// permanently, and two entries can never collide on a shared -1 key. + public static readonly Dictionary SpeciesByName = new Dictionary + { + { "necroInsectSwarm", new VultureBasedPetInfo { SkipAlreadyCharmedZombies = true } }, + }; + + public static readonly Dictionary cachedClassIds = new Dictionary(); + + public static Dictionary ClassIds() + { + // Fast path once every species has resolved (the overwhelmingly common case, since + // this runs on EntityAlive.SetAttackTarget - a hot path called for every zombie in + // the game, not just ours) - skips the resolution loop below entirely instead of + // re-scanning it on every single call. + if (cachedClassIds.Count >= SpeciesByName.Count) + { + return cachedClassIds; + } + foreach (KeyValuePair species in SpeciesByName) + { + bool alreadyCached = false; + foreach (KeyValuePair cached in cachedClassIds) + { + if (cached.Value == species.Value) + { + alreadyCached = true; + break; + } + } + if (alreadyCached) + { + continue; + } + int id = EntityClass.GetId(species.Key); + if (id == -1) + { + continue; + } + cachedClassIds[id] = species.Value; + } + return cachedClassIds; + } + + /// Kept separate from ClassIds() above (which is about retargeting, shared by + /// both pets) - this one is Swarm-ONLY, used by PetFollowPatch.cs's "drop an + /// already-charmed target so it moves on" behavior, which only makes sense for a species + /// that actually charms zombies (the Griffin doesn't). Same retry-until-resolved shape as + /// ClassIds() above, for the same reason - never cache a -1. + public static int cachedSwarmOnlyClassId = -1; + + public static int SwarmOnlyClassId() + { + if (cachedSwarmOnlyClassId == -1) + { + cachedSwarmOnlyClassId = EntityClass.GetId("necroInsectSwarm"); + } + return cachedSwarmOnlyClassId; + } + + public static void Prefix(EntityAlive __instance, ref EntityAlive _attackTarget) + { + if (__instance == null || !(_attackTarget is EntityPlayer)) + { + return; + } + if (!ClassIds().TryGetValue(__instance.entityClass, out VultureBasedPetInfo petInfo)) + { + return; + } + EntityAlive nearestZombie = FindNearestZombie(__instance, petInfo.SkipAlreadyCharmedZombies); + Debug.Log("[NecromancerTome] SwarmTargetPatch: redirected " + __instance.entityId + " from player " + _attackTarget.entityId + " to " + (nearestZombie != null ? nearestZombie.entityId.ToString() : "nothing nearby")); + _attackTarget = nearestZombie; + } + + /// Same World.GetEntitiesInBounds(Type, Bounds, List<Entity>) API + /// EntityVulture itself uses for its own player search (confirmed by decompiling it) - + /// just pointed at EntityZombie instead of EntityPlayer. 80m box, matching FindTarget's + /// own cTargetDistanceMax constant, for "ищут всех зомби в радиусе". + /// + /// BUG FIXED 2026-08-28 ("покусав одного, летят куда-то далеко, вместо соседнего + /// незаражённого"): this didn't skip already-charmed zombies, so when + /// PetFollowPatch.cs's "drop an already-charmed target" cleared the swarm's target, the + /// very next FindTarget()->SetAttackTarget cycle would often just re-pick the SAME + /// zombie it had just charmed (still the physically nearest one right after biting it) - + /// PetFollowPatch would clear it again next tick, and in between, EntityVulture (a + /// flying creature) fell into its own Wander state, which for a flier means big aerial + /// loops away from its current spot, not calm circling. A second, genuinely uncharmed + /// zombie standing right next to the first one would lose out to this loop instead of + /// being picked immediately. Now skips any zombie that already carries + /// buffNecroDeviatorCharm - the real "next AND uncharmed" search the user asked for. Only + /// falls through to wide wandering when there truly isn't one nearby, same as before. + /// + /// added 2026-08-29 alongside the Griffin + /// generalization above - true for the Swarm (its own charm-on-bite behavior, unchanged), + /// false for the Griffin (a plain fighter with no reason to avoid already-charmed + /// zombies). + public static EntityAlive FindNearestZombie(EntityAlive swarm, bool skipAlreadyCharmed) + { + World world = swarm.world; + if (world == null) + { + return null; + } + List nearby = new List(); + Bounds bounds = new Bounds(swarm.position, new Vector3(80f, 80f, 80f)); + world.GetEntitiesInBounds(typeof(EntityZombie), bounds, nearby); + + EntityAlive nearest = null; + float bestDistSq = float.MaxValue; + foreach (Entity entity in nearby) + { + if (!(entity is EntityAlive zombie) || zombie.IsDead()) + { + continue; + } + if (skipAlreadyCharmed && zombie.Buffs != null && zombie.Buffs.HasBuff(Patch_EntityBuffs_AddBuff_DeviatorCharm.CharmBuffName)) + { + continue; + } + float distSq = (zombie.position - swarm.position).sqrMagnitude; + if (distSq < bestDistSq) + { + bestDistSq = distSq; + nearest = zombie; + } + } + return nearest; + } + } +} diff --git a/HarmonySrc/VictimPatch.cs b/HarmonySrc/VictimPatch.cs new file mode 100644 index 0000000..1e0b3d1 --- /dev/null +++ b/HarmonySrc/VictimPatch.cs @@ -0,0 +1,100 @@ +using HarmonyLib; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// Necromancer's Knife (BACKLOG.md item 5, user request 2026-08-28): a zombie hit by the + /// knife carries buffNecroVictim (buffs.xml) until it dies. On death, it's guaranteed to drop + /// a green "Жертва" loot bag (EntityLootContainerVictim in entityclasses.xml) instead of + /// whatever the normal random loot roll would have given it. + /// + /// Two patch points, found the hard way (2026-08-28, "лута всё ещё нет" after confirming via + /// the CharmPatch.cs-style AddBuff log that the debuff itself WAS being applied fine): + /// + /// 1. Patch_EntityAlive_dropItemOnDeath_VictimBag - the actual gate. Confirmed by + /// decompiling EntityAlive.dropItemOnDeath(): it only calls DropBagServer() at all if + /// `lootDropProb > rand.RandomFloat` passes first - vanilla zombies have LootDropProb + /// around .04 (4%), so DropBagServer() simply never runs for ~96% of deaths. The first + /// version of this file only patched DropBagServer() itself, which was correct once + /// inside it but never got a chance to run for most kills - confirmed by the "AddBuff + /// Added" log firing repeatedly while the "zombie died" log from the DropBagServer patch + /// never fired even once for a real, logged kill. This Prefix on dropItemOnDeath() calls + /// DropBagServer() directly for a Victim-tagged zombie, bypassing the probability roll + /// entirely, then skips the rest of the original method (the only other thing it does - + /// dropping a non-AI entity's own inventory - never applies to a zombie anyway, hasAI is + /// always true for those). + /// 2. Patch_Entity_DropBagServer_VictimBag - decides WHICH bag. Confirmed by decompiling + /// Entity.DropBagServer(): it reads entityClass.lootDrops/LootDropPick(rand) - a STATIC + /// per-species value from the zombie's own entity_class (XML's LootDropEntityClass + /// property), not anything a live buff or CVar can influence declaratively (unlike the + /// knife's damage, which only needed a CVar the passive_effect system already reads + /// live) - this needed an actual Harmony patch, not an XML trick. + /// + /// Both are Prefixes returning false: they fully replace what they intercept rather than + /// running alongside it. + /// + [HarmonyPatch(typeof(EntityAlive), "dropItemOnDeath")] + public static class Patch_EntityAlive_dropItemOnDeath_VictimBag + { + public static bool Prefix(EntityAlive __instance) + { + // Diagnostic-only, added 2026-08-28: unconditional, before any branching, to answer + // definitively whether Harmony is even entering this method at all - "лута всё ещё + // нет" after the first fix, but with zero sign of even the unconditional part of this + // Prefix ever running (not even a false-branch silently returning - literally no log + // line at all), which is otherwise unexplained since decompiling + // EntityAlive.OnEntityDeath() confirms it calls dropItemOnDeath() directly, + // unconditionally, right after the exact "Entity X killed by Y" line seen in the log. + Debug.Log("[NecromancerTome] VictimPatch: dropItemOnDeath Prefix entered for " + __instance.entityId + " (" + __instance.GetType().Name + "), hasVictimBuff=" + (__instance.Buffs != null && __instance.Buffs.HasBuff(Patch_Entity_DropBagServer_VictimBag.VictimBuffName))); + if (__instance.Buffs == null || !__instance.Buffs.HasBuff(Patch_Entity_DropBagServer_VictimBag.VictimBuffName)) + { + return true; + } + Debug.Log("[NecromancerTome] VictimPatch: " + __instance.entityId + " died carrying buffNecroVictim - forcing guaranteed bag, bypassing LootDropProb roll"); + __instance.DropBagServer(); + return false; + } + } + + [HarmonyPatch(typeof(Entity), "DropBagServer")] + public static class Patch_Entity_DropBagServer_VictimBag + { + public const string VictimBuffName = "buffNecroVictim"; + public const string VictimContainerClassName = "EntityLootContainerVictim"; + + public static bool Prefix(Entity __instance) + { + if (!(__instance is EntityAlive alive) || alive.Buffs == null || !alive.Buffs.HasBuff(VictimBuffName)) + { + return true; + } + if (!SingletonMonoBehaviour.Instance.IsServer || __instance is EntityLootContainer) + { + // Same guard the original method opens with - not our place to override these cases. + return true; + } + + int containerClassId = EntityClass.GetId(VictimContainerClassName); + if (containerClassId == -1) + { + Debug.LogWarning("[NecromancerTome] VictimPatch: entity class '" + VictimContainerClassName + "' not found, falling back to normal loot"); + return true; + } + + Vector3 pos = __instance.GetPosition(); + pos.y += 0.9f; + Entity spawned = EntityFactory.CreateEntity(containerClassId, pos, Vector3.zero); + if (spawned is EntityLootContainer lootContainer) + { + GameManager.Instance.World.SpawnEntityInWorld(lootContainer); + Debug.Log("[NecromancerTome] VictimPatch: " + __instance.entityId + " (victim) dropped guaranteed loot bag " + lootContainer.entityId); + } + else + { + Debug.LogWarning("[NecromancerTome] VictimPatch: created entity for '" + VictimContainerClassName + "' wasn't an EntityLootContainer"); + } + return false; + } + } +} diff --git a/ModInfo.xml b/ModInfo.xml new file mode 100644 index 0000000..5805c68 --- /dev/null +++ b/ModInfo.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/NecromancerHarmony.dll b/NecromancerHarmony.dll new file mode 100644 index 0000000..34220bf Binary files /dev/null and b/NecromancerHarmony.dll differ diff --git a/NecromancerHarmony.pdb b/NecromancerHarmony.pdb new file mode 100644 index 0000000..61c6f6e Binary files /dev/null and b/NecromancerHarmony.pdb differ diff --git a/NecromancerTEPersistence.dll b/NecromancerTEPersistence.dll new file mode 100644 index 0000000..8d64336 Binary files /dev/null and b/NecromancerTEPersistence.dll differ diff --git a/NecromancerTEPersistence.pdb b/NecromancerTEPersistence.pdb new file mode 100644 index 0000000..8a0b8dd Binary files /dev/null and b/NecromancerTEPersistence.pdb differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..eef4bea --- /dev/null +++ b/README.md @@ -0,0 +1,126 @@ +# Книга некроманта / Necromancer's Tome (NecromancerTome) + +**Версия 1.0** — для 7 Days to Die 3.2. Автор: Alex Cube. + +- Страница мода: https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/ +- Репозиторий: https://git.08h.ru/alex/necromants-tome-7d2d-3-2 +- YouTube-канал автора: https://www.youtube.com/@alexcube + +Мод для 7 Days to Die о пути от обычного выжившего до некроманта — с собственной веткой +прогрессии, тёмным оружием, призываемыми существами и сюжетной концовкой. + +## Завязка + +Всё начинается с записки, читая которую персонаж видит неясный флэшбек. + +Некромантия в этом моде — ответ на проклятие, а не побочная ветка крафта. Вместо того чтобы +однажды пасть перед ордой и присоединиться к ней, игрок учится подчинять мёртвых себе: заражать +зомби безумием, стравливать их друг с другом, поднимать против них собственных тварей. Не +выживание вопреки смерти, а власть. + +## Прогрессия + +Отдельный скилл **"Некромантия"** растёт не от опыта, а от счётчика упокоенных зомби — свой +счётчик, своя механика. Пять тиров, каждый открывает часть арсенала: + +| Тир | Порог | Что открывается | +|---|---|---| +| Адепт | сразу | Камень духов, Нож некроманта, Синий портальный камень, Пирамида духов | +| Адепт (доп.) | 20 зомби | Пространственный браслет | +| Адепт (доп.) | 30 / 60 / 100 / 300 зомби | Моды ножа: Слёзы мертвеца, Пир падальщика, Могильный покой, Тёмное чутьё | +| Подмастерье | 500 зомби | Свиток девиации | +| Подмастерье (доп.) | 1400 / 1700 зомби | Моды ножа: Хватка мертвеца, Мёртвая буря | +| Ученик | 2000 зомби | Призыв зомбособаки, Жуки Властелина, Призыв зомбогрифа | +| Некромант | 3000 зомби | Призыв зомбомедведя, Призыв зомбоволка, Свиток банши | +| Мастер | 5000 зомби | Чёрный портальный камень | + +## Арсенал + +- **Камень духов** — брошенный камень, светящийся некромантской энергией. Задевает одного зомби: + тот переходит на вашу сторону и начинает атаковать других зомби вместо вас. +- **Свиток девиации** — тот же эффект, но мощнее: при броске переманивает на вашу сторону сразу + всех зомби в области взрыва, а не одного. Расходуется при использовании. +- **Нож некроманта** — почерневший костяной клинок. Урон растёт вместе с навыком Некромантии: + почти бесполезен в неопытных руках, смертоносен у прокачанного игрока. Лечит владельца на + половину нанесённого урона и метит раненого зомби как Жертву — тот при смерти гарантированно + оставляет особый мешок с ингредиентами. +- **Шесть модов только для Ножа некроманта** — обычные моды для ножей в это оружие не встают, а + эти не встают ни во что другое: **Слёзы мертвеца** (2 воды с каждого убитого клинком зомби), + **Пир падальщика** (2 еды за труп), **Могильный покой** (защита от перегрева и переохлаждения, + пока нож в руках), **Хватка мертвеца** (раненый клинком зомби замедляется), **Мёртвая буря** + (силовая атака бьёт по площади с кровотечением, за 10 здоровья вместо 5 и двойную выносливость), + **Тёмное чутьё** (все зомби поблизости отмечены на компасе и карте, пока нож в руках). +- **Кровь некроманта** — ритуальный ресурс: чтобы получить банку, нужны пустая банка, любой нож + в руках и 90% текущего здоровья за одну порцию. Ингредиент для самых тёмных рецептов — Чёрного + портала и самого Ножа. +- **Кожа жертвы** и **Прах зомби** — остаются от зомби, помеченного Ножом некроманта как Жертва. + Ингредиенты для книг призыва и большинства некромантских рецептов соответственно. +- **Синий портальный камень** — держите кнопку использования 10 секунд, чтобы телепортироваться к + своему спальному мешку. Прерывается любым уроном или силовой атакой раньше времени. Не + расходуется. +- **Пространственный браслет** — силовая атака открывает личный разлом-хранилище, чей размер + растёт вместе с уровнем Некромантии. Обычная атака пока ничего не делает. +- **Консервные банки** (пустая / с речной водой / с кипячёной) — расходный цикл вместо + одноразовых банок: наполняются водой, кипятятся прямо на костре без кастрюли, выпиваются, банка + возвращается пустой. Речная вода из банки может вызвать дизентерию, как обычная мутная вода; + кипячёная безопасна. Вмещают меньше воды, чем стеклянные банки. + +## Питомцы + +Призывные книги поднимают союзных существ, которые дерутся с зомби, а не с игроком. Питомец не +«следует» за игроком в строгом смысле — просто бродит сам по себе, а если отойдёт дальше +32 блоков и в этот момент не занят боем, его телепортирует обратно к владельцу: + +- **Зомбособака**, **Зомбомедведь**, **Зомбоволк**, **Зомбогриф** — постоянные спутники. Можно + держать по одному экземпляру каждого вида одновременно; отзываются обратно в книгу силовой + атакой. +- **Жуки Властелина** — разовый свиток, выпускающий рой. Жуки сами разлетаются по широкому + радиусу и жалят зомби; ужаленный переходит на вашу сторону, как от Камня духов. Рой нельзя + отозвать обратно, активен может быть только один. Расходуется при использовании. +- **Свиток банши** — одноразовый: при открытии вопит голосом банши и поднимает рядом с игроком + небольшую враждебную орду. Это не союзники — эти зомби так же опасны для вас, как любые другие. + +## Оборона базы + +- **Пирамида духов** — деплоябл-блок, не удержимый предмет. Пока вы стоите в её радиусе, она сама + заряжает девиацией любого незаряженного зомби поблизости и поджигает его холодным фиолетовым + пламенем, обращая против других зомби вместо вас или вашей базы. В меню блока можно + включать/выключать эффект и показывать границу радиуса действия. + +## Чёрный портал — финал истории + +**Чёрный портальный камень открывается на вершине прогрессии (5000 зомби) и является концовкой +мода.** Активация запускает диалог подтверждения, останавливает игру и разворачивает +полноэкранную финальную сцену — она досказывает историю, начатую запиской в первый день. +Заканчивается сцена выбором из двух вариантов: один закрывает историю и выходит в главное меню, +другой возвращает игрока в мир и позволяет играть дальше. + +Сюжетные тексты — в `Config/Localization.csv`, ключи `necroFinal*`. Здесь они не пересказываются +намеренно: README читают до прохождения. + +## По мелочи + +- Стартовая записка при открытии тоже ставит игру на паузу и проигрывает короткий флэшбек. +- Некоторые декоративные блоки (кровати, кулеры, картонные коробки) можно разобрать удержанием, + как верстак. + +## Локализация + +**13 языков полностью:** русский, английский, немецкий, испанский, французский, итальянский, +японский, корейский, польский, португальский (Бразилия), турецкий, китайский упрощённый и +традиционный. Все 123 ключа `Config/Localization.csv` заполнены, пустых ячеек нет. + +## Установка + +Распакуйте папку `NecromancerTome` в `<папка игры>/Mods/` (или в `%APPDATA%/7DaysToDie/Mods/`) и +запустите игру. Мод содержит Harmony-библиотеки, поэтому **EAC должен быть выключен**. + +Рассчитан на одиночную игру: ванильная пауза работает только в сингле, поэтому в мультиплеере +сюжетные сцены проиграются без остановки времени. + +## Статус + +Версия 1.0 — весь заявленный контент реализован и проходит тесты в игре. Из запланированного не +сделана только часть фирменных звуков. Текст описания для сайта (RU + EN) — в +`SITE_DESCRIPTION.md`. Полная техническая история разработки и текст финала лежат рядом с модом +в `BACKLOG.md` и `FINAL_TEXT.md` — в репозиторий они не входят (спойлеры и внутренняя кухня). diff --git a/SITE_DESCRIPTION.md b/SITE_DESCRIPTION.md new file mode 100644 index 0000000..5d6a5ff --- /dev/null +++ b/SITE_DESCRIPTION.md @@ -0,0 +1,236 @@ +# Книга некроманта / Necromancer's Tome — описание для сайта + +Версия 1.0. Мод для 7 Days to Die 3.2. Автор: Alex Cube. +Текст ниже готов к публикации: сначала русская версия, затем английская. + +- Страница мода: https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/ +- Репозиторий: https://git.08h.ru/alex/necromants-tome-7d2d-3-2 +- YouTube-канал: https://www.youtube.com/@alexcube + +--- + +# RU + +## Книга некроманта + +**Мод для 7 Days to Die 3.2: путь от обычного выжившего до некроманта — со своей веткой прогрессии, +тёмным оружием, призывной нежитью и сюжетной концовкой.** + +Всё начинается с записки. Читая её, персонаж видит неясный флэшбек — обрывок того, что с ним +случилось до пробуждения в этом мире. + +Некромантия здесь — ответ на проклятие, а не побочная ветка крафта. Вместо того чтобы однажды +пасть перед ордой и присоединиться к ней, вы учитесь подчинять мёртвых: заражать зомби безумием, +стравливать их друг с другом, поднимать против них собственных тварей. Не выживание вопреки +смерти, а власть над ней. + +### Прогрессия, которая растёт от убийств + +Отдельный навык **«Некромантия»** качается не за очки перков и не за книги, а по счётчику +упокоенных зомби. Пять тиров, 5000 убийств до вершины, каждый порог открывает часть арсенала: + +| Тир | Порог | Что открывается | +|---|---|---| +| Адепт | сразу | Камень духов, Нож некроманта, Синий портальный камень, Пирамида духов | +| Адепт | 20 / 30 / 60 / 100 / 300 | Пространственный браслет, затем моды ножа: Слёзы мертвеца, Пир падальщика, Могильный покой, Тёмное чутьё | +| Подмастерье | 500 / 1400 / 1700 | Свиток девиации, моды ножа Хватка мертвеца и Мёртвая буря | +| Ученик | 2000 | Призыв зомбособаки, Жуки Властелина, Призыв зомбогрифа | +| Некромант | 3000 | Призыв зомбомедведя, Призыв зомбоволка, Свиток банши | +| Мастер | 5000 | Чёрный портальный камень | + +### Арсенал + +- **Камень духов** — брошенный камень, светящийся некромантской энергией. Задетый зомби переходит + на вашу сторону и начинает рвать других зомби вместо вас. +- **Свиток девиации** — то же самое, но по площади: переманивает сразу всех зомби в радиусе + взрыва. Расходуется при использовании. +- **Нож некроманта** — почерневший костяной клинок. Урон растёт вместе с навыком Некромантии: + почти бесполезен в неопытных руках, смертоносен у прокачанного игрока. Лечит владельца на + половину нанесённого урона и метит раненого зомби как Жертву — тот гарантированно оставляет + особый мешок с ингредиентами. +- **Шесть модов только для этого ножа** — вода и еда прямо из трупов (Слёзы мертвеца, Пир + падальщика), защита от жары и холода (Могильный покой), замедление раненых (Хватка мертвеца), + силовая атака-буря по площади за двойную цену здоровья и выносливости (Мёртвая буря) и подсветка + всех зомби поблизости на карте и компасе (Тёмное чутьё). Обычные моды в этот нож не встают, а + эти не встают ни во что другое. +- **Кровь некроманта** — ритуальный ресурс: пустая банка, любой нож в руках и 90% текущего + здоровья за одну порцию. Ингредиент для самых тёмных рецептов. +- **Кожа жертвы** и **Прах зомби** — падают с зомби, помеченного ножом как Жертва. Основа книг + призыва и большинства некромантских рецептов. +- **Синий портальный камень** — держите кнопку использования 10 секунд, чтобы телепортироваться + к своему спальнику. Любой урон прерывает переход. Не расходуется. +- **Пространственный браслет** — силовая атака открывает личный разлом-хранилище, размер которого + растёт вместе с уровнем Некромантии. +- **Консервные банки** — расходный цикл вместо одноразовых: наполнить водой, вскипятить прямо на + костре без кастрюли, выпить, банка остаётся. Речная вода из банки может вызвать дизентерию, + кипячёная безопасна. Вмещают меньше стеклянных. + +### Питомцы + +Призывные книги поднимают союзников, которые дерутся с зомби, а не с вами. Питомец бродит сам по +себе, а если отойдёт дальше 32 блоков и не занят боем — телепортируется обратно к владельцу. + +- **Зомбособака, зомбомедведь, зомбоволк, зомбогриф** — постоянные спутники, по одному + экземпляру каждого вида одновременно. Отзываются обратно в книгу силовой атакой. +- **Жуки Властелина** — разовый свиток с роем. Жуки разлетаются по широкому радиусу и жалят + зомби; ужаленный переходит на вашу сторону, как от Камня духов. Рой не отзывается, активен + может быть только один. +- **Свиток банши** — одноразовый: вопит голосом банши и поднимает рядом небольшую орду. Это + **не** союзники — эти зомби так же опасны для вас, как любые другие. + +### Оборона базы + +**Пирамида духов** — устанавливаемый блок. Пока вы в её радиусе, она сама заряжает девиацией +любого незаряженного зомби поблизости и поджигает его холодным фиолетовым пламенем, обращая +против других зомби вместо вашей базы. В меню блока эффект включается и выключается, границу +радиуса можно показать. + +### Чёрный портал — финал истории + +**Чёрный портальный камень открывается на вершине прогрессии, на 5000 убийств, и является +концовкой мода.** Он уносит некроманта туда, откуда всё началось, — и досказывает историю, +начатую запиской в первый день. + +Активация просит подтверждения, останавливает игру и разворачивает полноэкранную финальную +сцену. В конце — выбор из двух вариантов; один из них закрывает историю и выходит в главное +меню, другой позволяет вернуться и играть дальше. Что там, по ту сторону портала, лучше +увидеть самому. + +### По мелочи + +- Стартовая записка при открытии тоже ставит игру на паузу и проигрывает короткий флэшбек. +- Часть декоративных блоков (кровати, кулеры, картонные коробки) разбирается удержанием, как + верстак. + +### Локализация + +**13 языков полностью:** русский, английский, немецкий, испанский, французский, итальянский, +японский, корейский, польский, португальский (Бразилия), турецкий, китайский упрощённый и +традиционный. Все 123 строки переведены, пустых ячеек нет. + +### Установка + +Требуется **7 Days to Die 3.2**. Распакуйте папку `NecromancerTome` в `<папка игры>/Mods/` +(или в `%APPDATA%/7DaysToDie/Mods/`) и запустите игру. Мод содержит Harmony-библиотеки, поэтому +**EAC должен быть выключен**. + +Рассчитан на одиночную игру. В мультиплеере ванильная пауза не срабатывает — сюжетные сцены +проиграются без остановки времени. + +### Ссылки + +- Исходники и загрузка: [https://git.08h.ru/alex/necromants-tome-7d2d-3-2](https://git.08h.ru/alex/necromants-tome-7d2d-3-2) +- YouTube-канал автора: [@alexcube](https://www.youtube.com/@alexcube) + +--- + +# EN + +## Necromancer's Tome + +**A 7 Days to Die 3.2 mod: the road from ordinary survivor to necromancer — its own progression tree, +cursed weapons, summonable undead and a story ending.** + +It starts with a note. Reading it, your character sees a blurred flashback — a fragment of +whatever happened to them before they woke up in this world. + +Necromancy here is an answer to a curse, not a side branch of crafting. Instead of falling before +the horde one day and joining it, you learn to command the dead: infect zombies with madness, turn +them on each other, raise your own creatures against them. Not survival in spite of death — power +over it. + +### Progression driven by kills + +A dedicated **Necromancy** skill levels not from perk points and not from books, but from your +zombie kill count. Five tiers, 5000 kills to the top, each threshold opening part of the arsenal: + +| Tier | Threshold | What unlocks | +|---|---|---| +| Adept | from the start | Spirit Stone, Necromancer's Knife, Blue Portal Stone, Pyramid of Spirits | +| Adept | 20 / 30 / 60 / 100 / 300 | Spatial Bracelet, then the knife mods: Tears of the Dead, Scavenger's Feast, Grave's Repose, Dark Sense | +| Journeyman | 500 / 1400 / 1700 | Scroll of Deviation, knife mods Dead Man's Grip and Dead Storm | +| Apprentice | 2000 | Summon Zombie Dog, Beetles of the Lord, Summon Zombie Griffin | +| Necromancer | 3000 | Summon Zombie Bear, Summon Zombie Wolf, Banshee's Scroll | +| Master | 5000 | Black Portal Stone | + +### Arsenal + +- **Spirit Stone** — a thrown stone lit with necromantic energy. The zombie it touches switches to + your side and starts tearing into other zombies instead of you. +- **Scroll of Deviation** — the same effect, but area-wide: it charms every zombie in the blast + radius at once. Consumed on use. +- **Necromancer's Knife** — a blackened bone blade. Its damage scales with the Necromancy skill: + nearly useless in unskilled hands, lethal for a levelled player. Heals the wielder for half the + damage dealt and marks the wounded zombie as a Victim — it is guaranteed to drop a special bag + of ingredients. +- **Six mods for this knife only** — water and food straight out of corpses (Tears of the Dead, + Scavenger's Feast), protection from heat and cold (Grave's Repose), slowed victims (Dead Man's + Grip), an area power-attack storm at double the health and stamina cost (Dead Storm), and every + nearby zombie marked on your map and compass (Dark Sense). Ordinary knife mods will not fit this + weapon, and these will not fit any other. +- **Necromancer's Blood** — a ritual resource: an empty jar, any knife in hand and 90% of your + current health per portion. An ingredient for the darkest recipes. +- **Victim's Skin** and **Zombie Ash** — dropped by a zombie marked as a Victim. The basis of the + summoning books and most necromantic recipes. +- **Blue Portal Stone** — hold the use button for 10 seconds to teleport to your bedroll. Any + damage interrupts the channel. Not consumed. +- **Spatial Bracelet** — a power attack opens a personal storage rift whose size grows with your + Necromancy level. +- **Tin cans** — a reusable cycle instead of single-use jars: fill with water, boil it right on a + campfire without a pot, drink, keep the can. River water from a can can cause dysentery, boiled + water is safe. They hold less than glass jars. + +### Pets + +Summoning books raise allies that fight zombies, not you. A pet wanders on its own, and if it +strays more than 32 blocks away while out of combat, it teleports back to its owner. + +- **Zombie dog, bear, wolf and griffin** — permanent companions, one of each kind at a time. + A power attack recalls them into the book. +- **Beetles of the Lord** — a one-shot scroll releasing a swarm. The beetles scatter over a wide + radius and sting zombies; a stung zombie switches to your side just like with the Spirit Stone. + The swarm cannot be recalled, and only one can be active. +- **Banshee's Scroll** — single use: it screams with a banshee's voice and raises a small horde + next to you. These are **not** allies — they are as dangerous to you as any other zombies. + +### Base defence + +**Pyramid of Spirits** — a deployable block. While you stand in its radius, it charms any +uncharmed zombie nearby on its own and sets it alight with cold purple flame, turning it against +the other zombies instead of your base. Its block menu toggles the effect and shows the radius. + +### The Black Portal — the story's ending + +**The Black Portal Stone unlocks at the top of the progression, at 5000 kills, and it is the +mod's ending.** It carries the necromancer back to where all of this began, and finishes the +story the note started on day one. + +Activating it asks for confirmation, stops the game and unfolds a full-screen finale. It closes +on a choice of two: one ends the story and returns you to the main menu, the other lets you come +back and keep playing. What waits on the far side of the portal is better seen than described. + +### Small things + +- The opening note also pauses the game and plays a short flashback. +- Some decorative blocks (beds, water coolers, cardboard boxes) can be disassembled by holding + the pick-up key, like a workbench. + +### Localization + +**13 languages, complete:** English, German, Spanish, French, Italian, Japanese, Korean, Polish, +Brazilian Portuguese, Russian, Turkish, Simplified and Traditional Chinese. All 123 strings are +translated, with no empty cells. + +### Installation + +Requires **7 Days to Die 3.2**. Unpack the `NecromancerTome` folder into `/Mods/` +(or `%APPDATA%/7DaysToDie/Mods/`) and launch the game. The mod ships Harmony libraries, so +**EAC must be turned off**. + +Built for single-player. In multiplayer the vanilla pause does not apply, so the story scenes +play without stopping time. + +### Links + +- Source and download: [https://git.08h.ru/alex/necromants-tome-7d2d-3-2](https://git.08h.ru/alex/necromants-tome-7d2d-3-2) +- The author's YouTube channel: [@alexcube](https://www.youtube.com/@alexcube) diff --git a/TEPersistenceSrc/NecromancerTEPersistence.csproj b/TEPersistenceSrc/NecromancerTEPersistence.csproj new file mode 100644 index 0000000..12a5080 --- /dev/null +++ b/TEPersistenceSrc/NecromancerTEPersistence.csproj @@ -0,0 +1,52 @@ + + + + + netstandard2.1 + NecromancerTEPersistence + NecromancerTome + latest + disable + false + false + bin\ + true + true + + + + + + ..\..\..\7DaysToDie_Data\Managed\mscorlib.dll + false + + + ..\..\..\7DaysToDie_Data\Managed\Assembly-CSharp.dll + false + + + + diff --git a/TEPersistenceSrc/PyramidWardWriteHelper.cs b/TEPersistenceSrc/PyramidWardWriteHelper.cs new file mode 100644 index 0000000..898d459 --- /dev/null +++ b/TEPersistenceSrc/PyramidWardWriteHelper.cs @@ -0,0 +1,26 @@ +namespace NecromancerTome +{ + /// + /// Exists ONLY because `PooledBinaryWriter.Write(bool)` (and every other Write overload) cannot + /// be called from the main NecromancerHarmony project at all - see + /// NecromancerTEPersistence.csproj's own comment for the full decompiled/tested reasoning + /// (Assembly-CSharp.dll's overload set for Write includes a ReadOnlySpan<byte> variant + /// that resolves against Unity/Mono's own mscorlib, which the main project's plain netstandard2.1 + /// setup can't see - the compiler needs to resolve the WHOLE overload set just to pick the bool + /// one, and fails hard). PooledBinaryReader.ReadBoolean() has no such problem (confirmed + /// separately - it's a plain no-overload method, not an ambiguous Write-style one) and is called + /// directly from TEFeaturePyramidWard.Read() in the main project as normal; only the WRITE side + /// needs to go through this satellite assembly. + /// + /// TEFeaturePyramidWard.Write() (HarmonySrc/PyramidWardPatch.cs, in the main project) calls this + /// instead of touching PooledBinaryWriter.Write itself. + /// + public static class PyramidWardWriteHelper + { + public static void Write(PooledBinaryWriter _bw, bool _effectOn, bool _zoneShown) + { + _bw.Write(_effectOn); + _bw.Write(_zoneShown); + } + } +} diff --git a/UIAtlases/ItemIconAtlas/BansheesScroll.png b/UIAtlases/ItemIconAtlas/BansheesScroll.png new file mode 100644 index 0000000..27b61dd Binary files /dev/null and b/UIAtlases/ItemIconAtlas/BansheesScroll.png differ diff --git a/UIAtlases/ItemIconAtlas/BeetlesOfTheLord.png b/UIAtlases/ItemIconAtlas/BeetlesOfTheLord.png new file mode 100644 index 0000000..83c0399 Binary files /dev/null and b/UIAtlases/ItemIconAtlas/BeetlesOfTheLord.png differ diff --git a/UIAtlases/ItemIconAtlas/BlackPortalStone.png b/UIAtlases/ItemIconAtlas/BlackPortalStone.png new file mode 100644 index 0000000..09fa472 Binary files /dev/null and b/UIAtlases/ItemIconAtlas/BlackPortalStone.png differ diff --git a/UIAtlases/ItemIconAtlas/BluePortalStone.png b/UIAtlases/ItemIconAtlas/BluePortalStone.png new file mode 100644 index 0000000..4e36145 Binary files /dev/null and b/UIAtlases/ItemIconAtlas/BluePortalStone.png differ diff --git a/UIAtlases/ItemIconAtlas/DarkSense.png b/UIAtlases/ItemIconAtlas/DarkSense.png new file mode 100644 index 0000000..3ea02ec Binary files /dev/null and b/UIAtlases/ItemIconAtlas/DarkSense.png differ diff --git a/UIAtlases/ItemIconAtlas/DeadMansGrip.png b/UIAtlases/ItemIconAtlas/DeadMansGrip.png new file mode 100644 index 0000000..00e7f42 Binary files /dev/null and b/UIAtlases/ItemIconAtlas/DeadMansGrip.png differ diff --git a/UIAtlases/ItemIconAtlas/DeadStorm.png b/UIAtlases/ItemIconAtlas/DeadStorm.png new file mode 100644 index 0000000..a7907c7 Binary files /dev/null and b/UIAtlases/ItemIconAtlas/DeadStorm.png differ diff --git a/UIAtlases/ItemIconAtlas/GravesRepose.png b/UIAtlases/ItemIconAtlas/GravesRepose.png new file mode 100644 index 0000000..006aa87 Binary files /dev/null and b/UIAtlases/ItemIconAtlas/GravesRepose.png differ diff --git a/UIAtlases/ItemIconAtlas/NecromancersKnife.png b/UIAtlases/ItemIconAtlas/NecromancersKnife.png new file mode 100644 index 0000000..f293847 Binary files /dev/null and b/UIAtlases/ItemIconAtlas/NecromancersKnife.png differ diff --git a/UIAtlases/ItemIconAtlas/NecromantsBlood.png b/UIAtlases/ItemIconAtlas/NecromantsBlood.png new file mode 100644 index 0000000..41db37e Binary files /dev/null and b/UIAtlases/ItemIconAtlas/NecromantsBlood.png differ diff --git a/UIAtlases/ItemIconAtlas/ProstranstvennoeHranilische.png b/UIAtlases/ItemIconAtlas/ProstranstvennoeHranilische.png new file mode 100644 index 0000000..17f6d79 Binary files /dev/null and b/UIAtlases/ItemIconAtlas/ProstranstvennoeHranilische.png differ diff --git a/UIAtlases/ItemIconAtlas/ScavengersFeast.png b/UIAtlases/ItemIconAtlas/ScavengersFeast.png new file mode 100644 index 0000000..8732200 Binary files /dev/null and b/UIAtlases/ItemIconAtlas/ScavengersFeast.png differ diff --git a/UIAtlases/ItemIconAtlas/ScrollOfDeviation.png b/UIAtlases/ItemIconAtlas/ScrollOfDeviation.png new file mode 100644 index 0000000..11b4104 Binary files /dev/null and b/UIAtlases/ItemIconAtlas/ScrollOfDeviation.png differ diff --git a/UIAtlases/ItemIconAtlas/SpiritStone.png b/UIAtlases/ItemIconAtlas/SpiritStone.png new file mode 100644 index 0000000..5da5fbf Binary files /dev/null and b/UIAtlases/ItemIconAtlas/SpiritStone.png differ diff --git a/UIAtlases/ItemIconAtlas/SummonZombieBear.png b/UIAtlases/ItemIconAtlas/SummonZombieBear.png new file mode 100644 index 0000000..57fb2c5 Binary files /dev/null and b/UIAtlases/ItemIconAtlas/SummonZombieBear.png differ diff --git a/UIAtlases/ItemIconAtlas/SummonZombieDog.png b/UIAtlases/ItemIconAtlas/SummonZombieDog.png new file mode 100644 index 0000000..1102ba8 Binary files /dev/null and b/UIAtlases/ItemIconAtlas/SummonZombieDog.png differ diff --git a/UIAtlases/ItemIconAtlas/SummonZombieGriffin.png b/UIAtlases/ItemIconAtlas/SummonZombieGriffin.png new file mode 100644 index 0000000..0a10e48 Binary files /dev/null and b/UIAtlases/ItemIconAtlas/SummonZombieGriffin.png differ diff --git a/UIAtlases/ItemIconAtlas/SummonZombieWolf.png b/UIAtlases/ItemIconAtlas/SummonZombieWolf.png new file mode 100644 index 0000000..a39deca Binary files /dev/null and b/UIAtlases/ItemIconAtlas/SummonZombieWolf.png differ diff --git a/UIAtlases/ItemIconAtlas/TearsOfTheDead.png b/UIAtlases/ItemIconAtlas/TearsOfTheDead.png new file mode 100644 index 0000000..1539a41 Binary files /dev/null and b/UIAtlases/ItemIconAtlas/TearsOfTheDead.png differ diff --git a/UIAtlases/ItemIconAtlas/VictimsSkin.png b/UIAtlases/ItemIconAtlas/VictimsSkin.png new file mode 100644 index 0000000..bd30c9f Binary files /dev/null and b/UIAtlases/ItemIconAtlas/VictimsSkin.png differ diff --git a/UIAtlases/ItemIconAtlas/ZombieAsh.png b/UIAtlases/ItemIconAtlas/ZombieAsh.png new file mode 100644 index 0000000..5390547 Binary files /dev/null and b/UIAtlases/ItemIconAtlas/ZombieAsh.png differ diff --git a/UIAtlases/ItemIconAtlas/pyramidOfSpirit.png b/UIAtlases/ItemIconAtlas/pyramidOfSpirit.png new file mode 100644 index 0000000..36ec294 Binary files /dev/null and b/UIAtlases/ItemIconAtlas/pyramidOfSpirit.png differ diff --git a/UIAtlases/ItemIconAtlas/schematicDogSummon.png b/UIAtlases/ItemIconAtlas/schematicDogSummon.png new file mode 100644 index 0000000..b51a6ca Binary files /dev/null and b/UIAtlases/ItemIconAtlas/schematicDogSummon.png differ diff --git a/UIAtlases/ItemIconAtlas/schematicInsectoSummon.png b/UIAtlases/ItemIconAtlas/schematicInsectoSummon.png new file mode 100644 index 0000000..ea2539f Binary files /dev/null and b/UIAtlases/ItemIconAtlas/schematicInsectoSummon.png differ diff --git a/UIAtlases/ItemIconAtlas/tinCanBoiledWater.png b/UIAtlases/ItemIconAtlas/tinCanBoiledWater.png new file mode 100644 index 0000000..5dcb9bc Binary files /dev/null and b/UIAtlases/ItemIconAtlas/tinCanBoiledWater.png differ diff --git a/UIAtlases/ItemIconAtlas/tinCanEmpty.png b/UIAtlases/ItemIconAtlas/tinCanEmpty.png new file mode 100644 index 0000000..ad55bcf Binary files /dev/null and b/UIAtlases/ItemIconAtlas/tinCanEmpty.png differ diff --git a/UIAtlases/ItemIconAtlas/tinCanRiverWater.png b/UIAtlases/ItemIconAtlas/tinCanRiverWater.png new file mode 100644 index 0000000..2edc77f Binary files /dev/null and b/UIAtlases/ItemIconAtlas/tinCanRiverWater.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/BansheesScroll.png b/UIAtlases/ItemIconAtlasGreyscale/BansheesScroll.png new file mode 100644 index 0000000..a9a9050 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/BansheesScroll.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/BeetlesOfTheLord.png b/UIAtlases/ItemIconAtlasGreyscale/BeetlesOfTheLord.png new file mode 100644 index 0000000..e385792 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/BeetlesOfTheLord.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/BlackPortalStone.png b/UIAtlases/ItemIconAtlasGreyscale/BlackPortalStone.png new file mode 100644 index 0000000..a4abb4b Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/BlackPortalStone.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/BluePortalStone.png b/UIAtlases/ItemIconAtlasGreyscale/BluePortalStone.png new file mode 100644 index 0000000..6eb4481 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/BluePortalStone.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/DarkSense.png b/UIAtlases/ItemIconAtlasGreyscale/DarkSense.png new file mode 100644 index 0000000..c489ac9 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/DarkSense.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/DeadMansGrip.png b/UIAtlases/ItemIconAtlasGreyscale/DeadMansGrip.png new file mode 100644 index 0000000..8f8e002 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/DeadMansGrip.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/DeadStorm.png b/UIAtlases/ItemIconAtlasGreyscale/DeadStorm.png new file mode 100644 index 0000000..1c07d2b Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/DeadStorm.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/GravesRepose.png b/UIAtlases/ItemIconAtlasGreyscale/GravesRepose.png new file mode 100644 index 0000000..fd9b81b Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/GravesRepose.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/NecromancersKnife.png b/UIAtlases/ItemIconAtlasGreyscale/NecromancersKnife.png new file mode 100644 index 0000000..c014974 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/NecromancersKnife.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/NecromantsBlood.png b/UIAtlases/ItemIconAtlasGreyscale/NecromantsBlood.png new file mode 100644 index 0000000..b7577a3 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/NecromantsBlood.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/ProstranstvennoeHranilische.png b/UIAtlases/ItemIconAtlasGreyscale/ProstranstvennoeHranilische.png new file mode 100644 index 0000000..c170721 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/ProstranstvennoeHranilische.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/ScavengersFeast.png b/UIAtlases/ItemIconAtlasGreyscale/ScavengersFeast.png new file mode 100644 index 0000000..a42ad38 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/ScavengersFeast.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/ScrollOfDeviation.png b/UIAtlases/ItemIconAtlasGreyscale/ScrollOfDeviation.png new file mode 100644 index 0000000..9b00920 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/ScrollOfDeviation.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/SpiritStone.png b/UIAtlases/ItemIconAtlasGreyscale/SpiritStone.png new file mode 100644 index 0000000..9a8a36f Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/SpiritStone.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/SummonZombieBear.png b/UIAtlases/ItemIconAtlasGreyscale/SummonZombieBear.png new file mode 100644 index 0000000..65ac1ff Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/SummonZombieBear.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/SummonZombieDog.png b/UIAtlases/ItemIconAtlasGreyscale/SummonZombieDog.png new file mode 100644 index 0000000..432ddce Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/SummonZombieDog.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/SummonZombieGriffin.png b/UIAtlases/ItemIconAtlasGreyscale/SummonZombieGriffin.png new file mode 100644 index 0000000..2724dcb Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/SummonZombieGriffin.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/SummonZombieWolf.png b/UIAtlases/ItemIconAtlasGreyscale/SummonZombieWolf.png new file mode 100644 index 0000000..95c9719 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/SummonZombieWolf.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/TearsOfTheDead.png b/UIAtlases/ItemIconAtlasGreyscale/TearsOfTheDead.png new file mode 100644 index 0000000..fa74a84 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/TearsOfTheDead.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/VictimsSkin.png b/UIAtlases/ItemIconAtlasGreyscale/VictimsSkin.png new file mode 100644 index 0000000..2e91070 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/VictimsSkin.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/ZombieAsh.png b/UIAtlases/ItemIconAtlasGreyscale/ZombieAsh.png new file mode 100644 index 0000000..f3c20e6 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/ZombieAsh.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/pyramidOfSpirit.png b/UIAtlases/ItemIconAtlasGreyscale/pyramidOfSpirit.png new file mode 100644 index 0000000..6eaf0c6 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/pyramidOfSpirit.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/schematicDogSummon.png b/UIAtlases/ItemIconAtlasGreyscale/schematicDogSummon.png new file mode 100644 index 0000000..185a92a Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/schematicDogSummon.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/schematicInsectoSummon.png b/UIAtlases/ItemIconAtlasGreyscale/schematicInsectoSummon.png new file mode 100644 index 0000000..8e69a85 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/schematicInsectoSummon.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/tinCanBoiledWater.png b/UIAtlases/ItemIconAtlasGreyscale/tinCanBoiledWater.png new file mode 100644 index 0000000..dfdb317 Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/tinCanBoiledWater.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/tinCanEmpty.png b/UIAtlases/ItemIconAtlasGreyscale/tinCanEmpty.png new file mode 100644 index 0000000..12a73df Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/tinCanEmpty.png differ diff --git a/UIAtlases/ItemIconAtlasGreyscale/tinCanRiverWater.png b/UIAtlases/ItemIconAtlasGreyscale/tinCanRiverWater.png new file mode 100644 index 0000000..b8081af Binary files /dev/null and b/UIAtlases/ItemIconAtlasGreyscale/tinCanRiverWater.png differ diff --git a/UIAtlases/NecroFinal/NecroFinal1.png b/UIAtlases/NecroFinal/NecroFinal1.png new file mode 100644 index 0000000..04bd1ca Binary files /dev/null and b/UIAtlases/NecroFinal/NecroFinal1.png differ diff --git a/UIAtlases/NecroFinal/NecroFinal2.png b/UIAtlases/NecroFinal/NecroFinal2.png new file mode 100644 index 0000000..81b4642 Binary files /dev/null and b/UIAtlases/NecroFinal/NecroFinal2.png differ diff --git a/UIAtlases/NecroFinal/NecroFinal3.png b/UIAtlases/NecroFinal/NecroFinal3.png new file mode 100644 index 0000000..cf4a591 Binary files /dev/null and b/UIAtlases/NecroFinal/NecroFinal3.png differ diff --git a/UIAtlases/NecroFinal/NecroFinal4.png b/UIAtlases/NecroFinal/NecroFinal4.png new file mode 100644 index 0000000..dbaffd9 Binary files /dev/null and b/UIAtlases/NecroFinal/NecroFinal4.png differ diff --git a/UIAtlases/NecroFinal/NecroFinal5.png b/UIAtlases/NecroFinal/NecroFinal5.png new file mode 100644 index 0000000..10a3f7c Binary files /dev/null and b/UIAtlases/NecroFinal/NecroFinal5.png differ diff --git a/UIAtlases/NecroFinal/NecroFinal6.png b/UIAtlases/NecroFinal/NecroFinal6.png new file mode 100644 index 0000000..0afb201 Binary files /dev/null and b/UIAtlases/NecroFinal/NecroFinal6.png differ diff --git a/Video/BlackPortal.webm b/Video/BlackPortal.webm new file mode 100644 index 0000000..2c83a52 Binary files /dev/null and b/Video/BlackPortal.webm differ diff --git a/Video/DukeNoteFlashback.mp4 b/Video/DukeNoteFlashback.mp4 new file mode 100644 index 0000000..d6a05f1 Binary files /dev/null and b/Video/DukeNoteFlashback.mp4 differ diff --git a/Video/FinalReturn.webm b/Video/FinalReturn.webm new file mode 100644 index 0000000..2c83a52 Binary files /dev/null and b/Video/FinalReturn.webm differ diff --git a/Video/FinalStay.webm b/Video/FinalStay.webm new file mode 100644 index 0000000..2c83a52 Binary files /dev/null and b/Video/FinalStay.webm differ