diff --git a/Config/Localization.csv b/Config/Localization.csv index 8c4b862..c2daf08 100644 --- a/Config/Localization.csv +++ b/Config/Localization.csv @@ -196,22 +196,31 @@ necroPossessHintBase,items,Item,,,,"LMB - strike, R - leave the body",,"LMT - Sc necroPossessHintRanged,items,Item,,,,RMB - ranged attack,,RMT - Fernangriff,RMB: ataque a distancia,Clic droit : attaque à distance,RMB - attacco a distanza,右クリック: 遠距離攻撃,우클릭 - 원거리 공격,PPM - atak dystansowy,RMB - ataque à distância,ПКМ - дальняя атака,Sağ tık - menzilli saldırı,右键 - 远程攻击,右鍵 - 遠程攻擊 necroPossessHintDetonate,items,Item,,,,E - detonate,,E - Sprengen,E: detonar,E : exploser,E - detonare,E: 起爆,E - 폭발,E - detonacja,E - detonar,E - подрыв,E - patlat,E - 引爆,E - 引爆 necroPossessHintScream,items,Item,,,,RMB - scream (calls the dead to you),,RMT - Schrei (ruft die Toten herbei),RMB: grito (llama a los muertos),Clic droit : cri (appelle les morts),RMB - urlo (richiama i morti),右クリック: 叫び(死者を呼ぶ),우클릭 - 비명 (죽은 자를 부름),PPM - krzyk (wzywa umarłych),RMB - grito (chama os mortos),ПКМ - крик (созвать мертвецов),Sağ tık - çığlık (ölüleri çağırır),右键 - 尖叫(召唤亡者),右鍵 - 尖叫(召喚亡者) -necroPossessHintScreamF,items,Item,,,,F - scream (calls the dead to you),,F - Schrei (ruft die Toten herbei),F: grito (llama a los muertos),F : cri (appelle les morts),F - urlo (richiama i morti),F: 叫び(死者を呼ぶ),F - 비명 (죽은 자를 부름),F - krzyk (wzywa umarłych),F - grito (chama os mortos),F - крик (созвать мертвецов),F - çığlık (ölüleri çağırır),F - 尖叫(召唤亡者),F - 尖叫(召喚亡者) +necroPossessHintScreamE,items,Item,,,,E - scream (calls the dead to you),,E - Schrei (ruft die Toten herbei),E: grito (llama a los muertos),E : cri (appelle les morts),E - urlo (richiama i morti),E: 叫び(死者を呼ぶ),E - 비명 (죽은 자를 부름),E - krzyk (wzywa umarłych),E - grito (chama os mortos),E - крик (созвать мертвецов),E - çığlık (ölüleri çağırır),E - 尖叫(召唤亡者),E - 尖叫(召喚亡者) necroPossessHintZoom,items,Item,,,,RMB (hold) - keen eye: zoom like a scope,,RMT (halten) - scharfes Auge: Zoom wie ein Zielfernrohr,"RMB (mantener): ojo avizor, zoom como una mira","Clic droit (maintenir) : œil perçant, zoom comme une lunette",RMB (tieni premuto) - occhio acuto: zoom come un mirino,右クリック(長押し): 鋭い目・スコープのようにズーム,우클릭(누르기) - 매의 눈: 조준경처럼 확대,PPM (przytrzymaj) - bystre oko: przybliżenie jak luneta,RMB (segurar) - olho aguçado: zoom como uma luneta,"ПКМ (удерживать) - зоркий глаз: приближение, как у прицела",Sağ tık (basılı tut) - keskin göz: dürbün gibi yakınlaştır,右键(按住)- 锐眼:像瞄准镜一样放大,右鍵(按住)- 銳眼:像瞄準鏡一樣放大 necroPossessHintLeap,items,Item,,,,Space - long leap,,Leertaste - weiter Sprung,Espacio: salto largo,Espace : grand bond,Spazio - balzo lungo,スペース: 大ジャンプ,스페이스 - 멀리뛰기,Spacja - daleki skok,Espaço - salto longo,Пробел - дальний прыжок,Boşluk - uzun sıçrayış,空格 - 远跳,空白鍵 - 遠跳 necroPetRecallHint,items,Item,,,,Recall pet - E,,Diener zurückrufen - E,Devolver al sirviente - E,Rappeler le serviteur - E,Richiama il servitore - E,僕を呼び戻す - E,종복 불러들이기 - E,Odwołaj sługę - E,Chamar o servo de volta - E,Отозвать питомца - E,Hizmetkârı geri çağır - E,召回仆从 - E,召回僕從 - E necroPetNothingToPossess,items,Item,,,,No servant summoned to possess,,"Kein beschworener Diener, in den du fahren kannst",No hay ningún sirviente invocado que poseer,Aucun serviteur invoqué à posséder,Nessun servitore evocato da possedere,憑依できる僕が呼ばれていません,빙의할 종복이 소환되어 있지 않습니다,Brak przywołanego sługi do opętania,Nenhum servo invocado para possuir,Нет призванного слуги для вселения,Ele geçirilecek çağrılmış hizmetkâr yok,没有可附身的已召唤仆从,沒有可附身的已召喚僕從 necroPossessHintFly,items,Item,,,,"Space - up, C - down, Shift - dive",,"Leertaste - hoch, C - runter, Umschalt - Sturzflug","Espacio: subir, C: bajar, Mayús: picado","Espace : monter, C : descendre, Maj : piqué","Spazio - su, C - giù, Maiusc - picchiata",スペース: 上昇、C: 下降、Shift: 急降下,"스페이스 - 상승, C - 하강, Shift - 급강하","Spacja - w górę, C - w dół, Shift - nurkowanie","Espaço - subir, C - descer, Shift - mergulho","Пробел - вверх, C - вниз, Shift - пике","Boşluk - yukarı, C - aşağı, Shift - dalış",空格 - 上升,C - 下降,Shift - 俯冲,空白鍵 - 上升,C - 下降,Shift - 俯衝 -necroPossessHintEat,items,Item,,,,"E - feed on carrion (sitting by a corpse, heals)",,"E - Aas fressen (neben einer Leiche sitzend, heilt)","E: comer carroña (posado junto a un cadáver, cura)","E : se nourrir de charogne (posé près d'un cadavre, soigne)","E - nutrirsi di carogne (posato accanto a un cadavere, cura)",E: 死肉を食べる(死体のそばに止まって・回復),"E - 썩은 고기 먹기 (시체 옆에 앉아서, 회복)","E - żerowanie na padlinie (siedząc przy zwłokach, leczy)","E - comer carniça (pousado junto a um cadáver, cura)","E - питаться падалью (сидя у трупа, лечит)","E - leş ye (bir cesedin yanına konup, iyileştirir)",E - 食腐(落在尸体旁,治疗),E - 食腐(停在屍體旁,治療) +necroPossessHintEatF,items,Item,,,,"F - feed on carrion (sitting by a corpse, heals)",,"F - Aas fressen (neben einer Leiche sitzend, heilt)","F: comer carroña (posado junto a un cadáver, cura)","F : se nourrir de charogne (posé près d'un cadavre, soigne)","F - nutrirsi di carogne (posato accanto a un cadavere, cura)",F: 死肉を食べる(死体のそばに止まって・回復),"F - 썩은 고기 먹기 (시체 옆에 앉아서, 회복)","F - żerowanie na padlinie (siedząc przy zwłokach, leczy)","F - comer carniça (pousado junto a um cadáver, cura)","F - питаться падалью (сидя у трупа, лечит)","F - leş ye (bir cesedin yanına konup, iyileştirir)",F - 食腐(落在尸体旁,治疗),F - 食腐(停在屍體旁,治療) +necroPossessHintEatGroundF,items,Item,,,,"F - feed on carrion (standing still by a corpse, heals)",,"F - Aas fressen (still neben einer Leiche stehend, heilt)","F: comer carroña (quieto junto a un cadáver, cura)","F : se nourrir de charogne (immobile près d'un cadavre, soigne)","F - nutrirsi di carogne (fermo accanto a un cadavere, cura)",F: 死肉を食べる(死体のそばで止まって・回復),"F - 썩은 고기 먹기 (시체 옆에 멈춰 서서, 회복)","F - żerowanie na padlinie (stojąc przy zwłokach, leczy)","F - comer carniça (parado junto a um cadáver, cura)","F - питаться падалью (стоя у трупа, лечит)","F - leş ye (bir cesedin yanında durup, iyileştirir)",F - 食腐(停在尸体旁,治疗),F - 食腐(停在屍體旁,治療) necroPossessHintLook,items,Item,,,,"E - look inside a container (view only, nothing can be taken)",,"E - in einen Behälter schauen (nur ansehen, nichts mitnehmen)","E: mirar dentro de un contenedor (solo ver, no se puede coger nada)","E : regarder dans un conteneur (voir seulement, rien ne peut être pris)","E - guardare in un contenitore (solo vedere, non si può prendere nulla)",E: 容器の中を見る(見るだけ・取れない),"E - 용기 안 살펴보기 (보기만, 가져갈 수 없음)","E - zajrzyj do pojemnika (tylko podgląd, nic nie można zabrać)","E - olhar dentro de um contêiner (só ver, nada pode ser pego)","E - заглянуть в контейнер (только посмотреть, взять нельзя)","E - bir kabın içine bak (sadece bakılır, hiçbir şey alınamaz)",E - 查看容器(只能看,不能拿),E - 查看容器(只能看,不能拿) necroPossessHintPhase,items,Item,,,,"RMB (hold) - pass through buildings (not through earth and rock)",,"RMT (halten) - durch Gebäude gehen (nicht durch Erde und Fels)","Clic derecho (mantener): atravesar construcciones (no tierra ni roca)","Clic droit (maintenu) : traverser les constructions (pas la terre ni la roche)","Tasto destro (tieni premuto) - attraversare le costruzioni (non terra e roccia)",右クリック(長押し):建物をすり抜ける(土や岩は不可),"우클릭(누르고 있기) - 건물 통과 (흙과 바위는 불가)","PPM (przytrzymaj) - przechodzenie przez budowle (nie przez ziemię i skały)","Botão direito (segurar) - atravessar construções (não terra e rocha)","ПКМ (держать) - сквозь постройки (не сквозь землю и камень)","Sağ tık (basılı tut) - yapıların içinden geç (toprak ve kayadan değil)",右键(按住)- 穿过建筑(不能穿过泥土和岩石),右鍵(按住)- 穿過建築(不能穿過泥土和岩石) necroPossessHintSmell,items,Item,,,,"keen nose: every creature nearby is marked on the compass and map",,"feine Nase: jedes Lebewesen in der Nähe ist auf Kompass und Karte markiert","olfato: toda criatura cercana aparece en la brújula y el mapa","flair : chaque créature proche est marquée sur la boussole et la carte","fiuto: ogni creatura vicina è segnata su bussola e mappa",嗅覚:近くの生き物すべてがコンパスと地図に表示される,"후각: 주변의 모든 생물이 나침반과 지도에 표시됨","węch: każde stworzenie w pobliżu jest zaznaczone na kompasie i mapie","faro: toda criatura por perto aparece na bússola e no mapa","нюх: все существа рядом отмечены на компасе и карте","koku alma: yakındaki her canlı pusulada ve haritada işaretlenir",嗅觉:附近的所有生物都会标在指南针和地图上,嗅覺:附近的所有生物都會標在指南針和地圖上 necroPossessHintRoar,items,Item,,,,"RMB - roar: zombies nearby come for the bear",,"RMT - Brüllen: Zombies in der Nähe gehen auf den Bären los","Clic derecho: rugido, los zombis cercanos van a por el oso","Clic droit : rugissement, les zombies proches foncent sur l'ours","Tasto destro - ruggito: gli zombi vicini puntano l'orso",右クリック:咆哮、近くのゾンビが熊に向かってくる,"우클릭 - 포효: 주변 좀비가 곰에게 몰려옴","PPM - ryk: pobliskie zombie idą na niedźwiedzia","Botão direito - rugido: zumbis por perto vão atrás do urso","ПКМ - рёв: зомби рядом идут на медведя","Sağ tık - kükreme: yakındaki zombiler ayıya yönelir",右键 - 咆哮:附近的僵尸冲向熊,右鍵 - 咆哮:附近的殭屍衝向熊 +necroPossessHintRear,items,Item,,,,E - rear up: enemies nearby are knocked down,,E - aufbäumen: Feinde in der Nähe werden umgeworfen,E: alzarse: los enemigos cercanos caen al suelo,E : se cabrer : les ennemis proches sont renversés,E - impennarsi: i nemici vicini vengono atterrati,E: 立ち上がる:近くの敵をなぎ倒す,E - 일어서기: 주변의 적이 넘어진다,E - stanąć na tylnych łapach: wrogowie w pobliżu padają,E - empinar: inimigos próximos caem,E - встать на дыбы: враги рядом падают с ног,E - şahlan: yakındaki düşmanlar yere düşer,E - 人立而起:附近的敌人被击倒,E - 人立而起:附近的敵人被擊倒 +necroBearRoarResult,items,Item,,,,"Roar: {0} undead come for the bear",,"Brüllen: {0} Untote gehen auf den Bären los","Rugido: {0} no muertos van a por el oso","Rugissement : {0} morts-vivants foncent sur l'ours","Ruggito: {0} non morti puntano all'orso",咆哮:{0}体のアンデッドが熊に向かう,"포효: 언데드 {0}마리가 곰에게 달려든다","Ryk: {0} nieumarłych rusza na niedźwiedzia","Rugido: {0} mortos-vivos vêm atrás do urso","Рёв: на медведя идут {0}","Kükreme: {0} ölümsüz ayıya yöneliyor",咆哮:{0}个亡灵冲向熊,咆哮:{0}個亡靈衝向熊 +necroBearRoarCooldown,items,Item,,,,"The bear can roar again in {0} s",,"Der Bär kann in {0} s wieder brüllen","El oso podrá rugir de nuevo en {0} s","L'ours pourra rugir à nouveau dans {0} s","L'orso potrà ruggire di nuovo tra {0} s",熊は{0}秒後に再び咆哮できる,"곰은 {0}초 후 다시 포효할 수 있다","Niedźwiedź znów zaryczy za {0} s","O urso poderá rugir de novo em {0} s","Рёв снова через {0} с","Ayı {0} sn sonra tekrar kükreyebilir",熊将在{0}秒后可再次咆哮,熊將在{0}秒後可再次咆哮 +necroBearRearResult,items,Item,,,,Rear up: {0} knocked down,,Aufbäumen: {0} umgeworfen,Alzarse: {0} derribados,Cabré : {0} renversés,Impennata: {0} atterrati,立ち上がり:{0}体をなぎ倒した,일어서기: {0}마리 넘어뜨림,Na tylnych łapach: powalono {0},Empinada: {0} derrubados,На дыбы: сбито с ног {0},Şahlanma: {0} yere düştü,人立:击倒{0}个,人立:擊倒{0}個 +necroBearRearCooldown,items,Item,,,,The bear can rear up again in {0} s,,Der Bär kann sich in {0} s wieder aufbäumen,El oso podrá alzarse de nuevo en {0} s,L'ours pourra se cabrer à nouveau dans {0} s,L'orso potrà impennarsi di nuovo tra {0} s,熊は{0}秒後に再び立ち上がれる,곰은 {0}초 후 다시 일어설 수 있다,Niedźwiedź znów stanie na łapach za {0} s,O urso poderá se empinar de novo em {0} s,На дыбы снова через {0} с,Ayı {0} sn sonra tekrar şahlanabilir,熊将在{0}秒后可再次人立,熊將在{0}秒後可再次人立 necroPossessHintSmash,items,Item,,,,"LMB on a block - heavy paw blow",,"LMT auf einen Block - schwerer Prankenhieb","Clic izquierdo en un bloque: zarpazo demoledor","Clic gauche sur un bloc : coup de patte dévastateur","Tasto sinistro su un blocco - zampata devastante",ブロックに左クリック:強烈な前足の一撃,"블록에 좌클릭 - 강력한 앞발 일격","LPM w blok - potężne uderzenie łapą","Botão esquerdo num bloco - patada devastadora","ЛКМ по блоку - тяжёлый удар лапой","Bloğa sol tık - ağır pençe darbesi",左键点方块 - 重掌拍击,左鍵點方塊 - 重掌拍擊 necroPossessHintSkin,items,Item,,,,"thick hide: takes half damage",,"dickes Fell: nimmt halben Schaden","piel gruesa: recibe la mitad de daño","cuir épais : subit moitié moins de dégâts","pelle spessa: subisce metà danni",厚い毛皮:受けるダメージ半減,"두꺼운 가죽: 받는 피해 절반","gruba skóra: otrzymuje o połowę mniej obrażeń","couro grosso: recebe metade do dano","толстая шкура: получает вдвое меньше урона","kalın post: yarı hasar alır",厚皮:受到的伤害减半,厚皮:受到的傷害減半 necroPossessHintPounce,items,Item,,,,"RMB - pounce on the target in your sights, it falls",,"RMT - Sprung auf das anvisierte Ziel, es stürzt","Clic derecho: abalanzarse sobre el objetivo, cae al suelo","Clic droit : bondir sur la cible visée, elle tombe","Tasto destro - balzo sul bersaglio nel mirino, cade",右クリック:狙った標的に飛びかかり、倒す,"우클릭 - 조준한 대상에게 덮쳐 쓰러뜨림","PPM - skok na cel w celowniku, przewraca go","Botão direito - salto sobre o alvo na mira, ele cai","ПКМ - бросок на цель в прицеле, она падает","Sağ tık - nişandaki hedefe atıl, hedef düşer",右键 - 扑向准星中的目标,将其扑倒,右鍵 - 撲向準星中的目標,將其撲倒 necroPossessHintBlood,items,Item,,,,"blood trail: wounded enemies are marked on the compass and map",,"Blutspur: verwundete Feinde sind auf Kompass und Karte markiert","rastro de sangre: los enemigos heridos aparecen en la brújula y el mapa","piste de sang : les ennemis blessés sont marqués sur la boussole et la carte","traccia di sangue: i nemici feriti sono segnati su bussola e mappa",血の跡:傷ついた敵がコンパスと地図に表示される,"피의 흔적: 부상당한 적이 나침반과 지도에 표시됨","krwawy trop: ranni wrogowie są zaznaczeni na kompasie i mapie","rastro de sangue: inimigos feridos aparecem na bússola e no mapa","след крови: раненые враги отмечены на компасе и карте","kan izi: yaralı düşmanlar pusulada ve haritada işaretlenir",血迹:受伤的敌人会标在指南针和地图上,血跡:受傷的敵人會標在指南針和地圖上 -necroPossessHintWolfBag,items,Item,,,,"E at a container - loot into the 3-slot bag, E next to your body - unload it",,"E am Behälter - in die Tasche mit 3 Plätzen plündern, E neben deinem Körper - ausladen","E en un contenedor: saquear a la bolsa de 3 huecos; E junto a tu cuerpo: descargarla","E sur un conteneur : piller dans le sac à 3 emplacements ; E près de ton corps : le vider","E su un contenitore - saccheggia nella borsa da 3 posti, E vicino al tuo corpo - svuotala","容器でE:3枠の袋に略奪、体のそばでE:袋を降ろす","용기에서 E - 3칸 가방에 약탈, 몸 옆에서 E - 가방 비우기","E przy pojemniku - łup do torby na 3 miejsca, E przy ciele - rozładuj","E num contêiner - saquear para a bolsa de 3 espaços, E junto ao corpo - descarregar","E у контейнера - лутать в сумку на 3 ячейки, E у тела - выгрузить","Kapta E - 3 yuvalı çantaya yağmala, bedeninin yanında E - boşalt","在容器处按E - 拾取到3格背包,在身体旁按E - 卸下","在容器處按E - 拾取到3格背包,在身體旁按E - 卸下" +necroPossessHintWolfBag,items,Item,,,,"E at a container or loot bag - loot into the 3-slot bag, E next to your body - unload it, E elsewhere - open the bag",,"E am Behälter oder Beutel - in die Tasche mit 3 Plätzen plündern, E neben deinem Körper - ausladen, E sonst - Tasche öffnen",E en un contenedor o bolsa: saquear a la bolsa de 3 huecos; E junto a tu cuerpo: descargarla; E en otro sitio: abrir la bolsa,E sur un conteneur ou un sac : piller dans le sac de 3 emplacements ; E près de ton corps : le vider ; E ailleurs : ouvrir le sac,"E su un contenitore o una sacca - saccheggia nella borsa da 3 posti, E vicino al tuo corpo - svuotala, E altrove - apri la borsa",E: コンテナや袋を3枠の袋に漁る、体のそばでE: 荷下ろし、それ以外でE: 袋を開く,"E - 상자나 자루를 3칸 가방에 털기, 몸 옆에서 E - 비우기, 그 외 E - 가방 열기","E przy pojemniku lub worku - łup do torby na 3 miejsca, E przy twoim ciele - rozładuj, E gdzie indziej - otwórz torbę","E num contêiner ou saco - saquear para a bolsa de 3 espaços, E junto ao seu corpo - descarregar, E em outro lugar - abrir a bolsa","E у контейнера или мешка - лутать в сумку на 3 ячейки, E у своего тела - выгрузить, E в другом месте - открыть сумку","E konteyner veya torbada - 3 yuvalı çantaya yağmala, E bedeninin yanında - boşalt, E başka yerde - çantayı aç",E 对着容器或袋子 - 搜刮到3格包里,在身体旁按E - 卸下,其他地方按E - 打开包,E 對著容器或袋子 - 搜刮到3格包裡,在身體旁按E - 卸下,其他地方按E - 打開包 +necroPossessSearchBag,items,Item,,,,{0} Search: {1},,{0} Durchsuchen: {1},{0} Registrar: {1},{0} Fouiller : {1},{0} Perquisisci: {1},{0} 調べる: {1},{0} 뒤지기: {1},{0} Przeszukaj: {1},{0} Vasculhar: {1},{0} Обыскать: {1},{0} Ara: {1},{0} 搜索:{1},{0} 搜索:{1} +necroWolfBagWindowTitle,items,Item,,,,Wolf bag,,Wolfstasche,Bolsa del lobo,Sac du loup,Borsa del lupo,狼の袋,늑대 가방,Torba wilka,Bolsa do lobo,Сумка волка,Kurt çantası,狼的包,狼的包 +necroWolfBagWaypoint,items,Item,,,,Wolf bag,,Wolfstasche,Bolsa del lobo,Sac du loup,Borsa del lupo,狼の袋,늑대 가방,Torba wilka,Bolsa do lobo,Сумка волка,Kurt çantası,狼的包,狼的包 necroPossessHintAfterFear,items,Item,,,,"after you leave, the zombie flees in chilling terror",,"nach dem Verlassen flieht der Zombie in eisigem Schrecken","al salir, el zombi huye presa de un terror helado","quand tu le quittes, le zombie fuit, glacé d'effroi","quando lo lasci, lo zombi fugge in preda al terrore","離れるとゾンビは凍てつく恐怖で逃げ出す","빠져나가면 좀비는 얼어붙는 공포에 질려 달아남","po wyjściu zombie ucieka w lodowatym przerażeniu","ao sair, o zumbi foge em terror gelado","после выхода зомби в леденящем ужасе убегает","çıktığında zombi dondurucu bir dehşetle kaçar","离开后,僵尸会在刺骨的恐惧中逃跑","離開後,殭屍會在刺骨的恐懼中逃跑" necroWolfBagEmpty,items,Item,,,,"empty",,"leer","vacía","vide","vuota","空","비어 있음","pusta","vazia","пусто","boş","空","空" necroWolfBagTook,items,Item,,,,"Wolf bag ({1}/{2}): {0}",,"Wolfstasche ({1}/{2}): {0}","Bolsa del lobo ({1}/{2}): {0}","Sac du loup ({1}/{2}) : {0}","Borsa del lupo ({1}/{2}): {0}","狼の袋({1}/{2}):{0}","늑대 가방 ({1}/{2}): {0}","Torba wilka ({1}/{2}): {0}","Bolsa do lobo ({1}/{2}): {0}","Сумка волка ({1}/{2}): {0}","Kurt çantası ({1}/{2}): {0}","狼的背包({1}/{2}):{0}","狼的背包({1}/{2}):{0}" @@ -220,6 +229,7 @@ necroWolfBagShow,items,Item,,,,"Wolf bag ({1}/{2}): {0}",,"Wolfstasche ({1}/{2}) necroWolfBagUnloaded,items,Item,,,,"The wolf bag is unloaded into your backpack",,"Die Wolfstasche ist in deinen Rucksack ausgeladen","La bolsa del lobo se ha descargado en tu mochila","Le sac du loup est vidé dans ton sac à dos","La borsa del lupo è stata svuotata nel tuo zaino","狼の袋をバックパックに降ろした","늑대 가방을 배낭에 비웠습니다","Torba wilka rozładowana do plecaka","A bolsa do lobo foi descarregada na sua mochila","Сумка волка выгружена в рюкзак","Kurt çantası sırt çantana boşaltıldı","狼的背包已卸到你的背包里","狼的背包已卸到你的背包裡" necroWolfBagUnloadedPart,items,Item,,,,"The wolf bag is unloaded; what did not fit is in a bag on the ground",,"Die Wolfstasche ist ausgeladen; was nicht passte, liegt in einem Beutel am Boden","La bolsa del lobo se ha descargado; lo que no cabía está en una bolsa en el suelo","Le sac du loup est vidé ; ce qui ne rentrait pas est dans un sac au sol","La borsa del lupo è svuotata; ciò che non entrava è in un sacco a terra","狼の袋を降ろした。入りきらない物は地面の袋の中","늑대 가방을 비웠습니다. 들어가지 않은 것은 바닥의 자루에 있습니다","Torba wilka rozładowana; co się nie zmieściło, leży w worku na ziemi","A bolsa do lobo foi descarregada; o que não coube está num saco no chão","Сумка волка выгружена; что не влезло - в мешке на земле","Kurt çantası boşaltıldı; sığmayanlar yerdeki torbada","狼的背包已卸下;放不下的在地上的袋子里","狼的背包已卸下;放不下的在地上的袋子裡" necroPossessNoCarrion,items,Item,,,,"No carrion nearby - land next to a corpse",,"Kein Aas in der Nähe - neben einer Leiche landen","No hay carroña cerca: posa junto a un cadáver","Pas de charogne ici - posez-vous près d'un cadavre","Nessuna carogna vicino - posati accanto a un cadavere",近くに死肉がない - 死体のそばに降りろ,"근처에 썩은 고기가 없다 - 시체 옆에 내려앉아라","Brak padliny w pobliżu - wyląduj przy zwłokach","Nenhuma carniça por perto - pouse junto a um cadáver","Падали рядом нет - сядьте возле трупа","Yakında leş yok - bir cesedin yanına kon",附近没有腐肉 - 落在尸体旁边,附近沒有腐肉 - 停在屍體旁邊 +necroPossessNoCarrionGround,items,Item,,,,"No carrion nearby - stop next to a corpse",,"Kein Aas in der Nähe - neben einer Leiche anhalten","No hay carroña cerca: detente junto a un cadáver","Pas de charogne ici - arrêtez-vous près d'un cadavre","Nessuna carogna vicino - fermati accanto a un cadavere",近くに死肉がない - 死体のそばで止まれ,"근처에 썩은 고기가 없다 - 시체 옆에 멈춰라","Brak padliny w pobliżu - zatrzymaj się przy zwłokach","Nenhuma carniça por perto - pare junto a um cadáver","Падали рядом нет - остановитесь возле трупа","Yakında leş yok - bir cesedin yanında dur",附近没有腐肉 - 停在尸体旁边,附近沒有腐肉 - 停在屍體旁邊 necroSnakeSpirit,entityclasses,Entity,,,,Snake Spirit,,Schlangengeist,Espíritu de serpiente,Esprit du serpent,Spirito del serpente,蛇の霊,뱀의 영혼,Duch węża,Espírito da serpente,Дух змеи,Yılan Ruhu,蛇灵,蛇靈 necroGeoOssuary,blocks,Block,,,,Geo-Ossuary,,Geo-Ossarium,Geo-osario,Géo-ossuaire,Geo-ossario,ジオ納骨堂,지오 납골당,Geo-ossuarium,Geo-ossário,Гео-оссуарий,Jeo-Kemiklik,地脉骨堂,地脈骨堂 necroGeoOssuaryDesc,blocks,Block,,,,"An ossuary of bones and skulls that draws stone and ore up from the depths and feeds on blood. diff --git a/HarmonySrc/BearControl.cs b/HarmonySrc/BearControl.cs index a24e792..fa295d7 100644 --- a/HarmonySrc/BearControl.cs +++ b/HarmonySrc/BearControl.cs @@ -10,7 +10,9 @@ namespace NecromancerTome /// увести толпу от тела или базы. Та же провокация, что каждую секунду делает одержимость /// (Possession.Taunt), только шире, без «видит ли» и на 20 с; /// - удар лапой по блоку: ЛКМ по блоку под прицелом бьёт его на SmashDamage (дерево и двери за 2-3 - /// удара, бетон дольше). + /// удара, бетон дольше); + /// - на дыбы на E (указание 09.10: «на E - действие»): враги ближе RearRange сбиты с ног на RearStun с + /// и отброшены от медведя - передышка в толпе. Откат RearCooldown. /// public static class BearControl { @@ -25,6 +27,13 @@ namespace NecromancerTome public const float SmashReach = 2.6f; public const float SkinFactor = 0.5f; static float _nextRoar; + public const float RearRange = 3.5f; + public const float RearStun = 2f; + public const float RearCooldown = 6f; + /// Толчок от медведя, м за тик (как motion существ; гаснет сам). + public const float RearPush = 0.5f; + public const string RearSound = "bearattack"; + static float _nextRear; public static bool Is(EntityAlive _e) { @@ -36,6 +45,8 @@ namespace NecromancerTome { if (Time.time < _nextRoar) { + // Рёв в толпе не отличить от удара (тест 09.10) - пишем, сколько ждать и сколько пошло на медведя. + Tip(string.Format(Localization.Get("necroBearRoarCooldown"), Mathf.CeilToInt(_nextRoar - Time.time))); return; } _nextRoar = Time.time + RoarCooldown; @@ -59,6 +70,53 @@ namespace NecromancerTome n++; } Debug.Log("[NecromancerTome] BearControl: рёв - на медведя идут " + n); + Tip(string.Format(Localization.Get("necroBearRoarResult"), n)); + } + + /// Сразу, мимо очереди подсказок: очередь игры (XUiC_PopupToolTip) отсеивает только одинаковый текст, + /// и «через 7 с», «через 6 с»... при частых ПКМ копились бы и всплывали с опозданием. + static void Tip(string _text) + { + if (Possession.Player != null) + { + GameManager.ShowTooltip(Possession.Player, _text, true); + } + } + + /// E: на дыбы - враги рядом падают (тот же приём, что у броска волка: BeginStun Prone) и + /// отлетают от медведя. + public static void RearUp(World _world, EntityAlive _bear) + { + if (Time.time < _nextRear) + { + Tip(string.Format(Localization.Get("necroBearRearCooldown"), Mathf.CeilToInt(_nextRear - Time.time))); + return; + } + _nextRear = Time.time + RearCooldown; + _bear.PlayOneShot(RearSound); + _bear.emodel?.avatarController?.StartAnimationAttack(); + int n = 0; + for (int i = 0; i < _world.EntityAlives.Count; i++) + { + EntityAlive e = _world.EntityAlives[i]; + if (!SnakeControl.IsFoe(e, _bear) || (e.position - _bear.position).sqrMagnitude > RearRange * RearRange) + { + continue; + } + Vector3 away = e.position - _bear.position; + away.y = 0f; + away = away.sqrMagnitude > 0.01f ? away.normalized : _bear.GetForwardVector(); + e.motion += away * RearPush + Vector3.up * 0.15f; + if (e.bodyDamage.CurrentStun == EnumEntityStunType.None && e.emodel?.avatarController != null) + { + e.emodel.avatarController.BeginStun(EnumEntityStunType.Prone, EnumBodyPartHit.Torso, Utils.EnumHitDirection.Back, false, 0.5f); + e.SetStun(EnumEntityStunType.Prone); + e.bodyDamage.StunDuration = RearStun; + } + n++; + } + Tip(string.Format(Localization.Get("necroBearRearResult"), n)); + Debug.Log("[NecromancerTome] BearControl: на дыбы - сбиты с ног " + n); } /// ЛКМ: блок под прицелом в досягаемости лапы (не существо) - урон SmashDamage. diff --git a/HarmonySrc/HandHolds.cs b/HarmonySrc/HandHolds.cs index bea312e..30012ff 100644 --- a/HarmonySrc/HandHolds.cs +++ b/HarmonySrc/HandHolds.cs @@ -317,6 +317,11 @@ namespace NecromancerTome public static void Prefix(EntityPlayerLocal __instance, out Color? __state) { __state = null; + // В одержимости прицел красит Possession.AimTint по цели одержимого, а не хват тела. + if (Possession.Active) + { + return; + } int hold = HandHolds.Current(__instance); EntityAlive target = hold == HandHolds.None ? null : HandHolds.Target(__instance); // «Пиф-паф» берёт только зомби-гуманоидов - на зверях-зомби прицел не краснеет. diff --git a/HarmonySrc/Possession.cs b/HarmonySrc/Possession.cs index 72584c2..a704895 100644 --- a/HarmonySrc/Possession.cs +++ b/HarmonySrc/Possession.cs @@ -122,14 +122,14 @@ namespace NecromancerTome /// Толчок вверх при взлёте с земли на пробел, м за тик. public const float FlyTakeOff = 0.3f; static bool _flySitting; - /// Питание падалью (указание 06.10): сидя у трупа, F - EatTime секунд клюёт его и лечится на + /// Питание падалью (указание 06.10; у медведя и собаки - 09.10): сидя или стоя у трупа, E - EatTime секунд клюёт его и лечится на /// EatHealFrac здоровья. У трупа запас мяса - его полное здоровье при жизни; поедание отнимает /// столько же, сколько лечит (и на полном здоровье тоже). Запас кончился - труп исчезает (указание /// 06.10), вместе с содержимым, если это красный труп-добыча. Сдвинулся, взлетел, ударил - еда /// прервана, вылеченное и съеденное остаются; к трупу можно вернуться. public const float EatRange = 2.5f; public const float EatTime = 3f; - public const float EatHealFrac = 0.25f; + public const float EatHealFrac = 0.125f; // было 0.25 - указание 09.10: «лечится слишком быстро, вдвое» static EntityAlive _eatCorpse; static float _eatLeft, _eatHeal, _eatBite; static int _eatCount; @@ -325,6 +325,7 @@ namespace NecromancerTome } Active = false; RatLoot.CloseIfViewing(); + WolfBag.CloseWindow(); RatPhase.Stop(GameManager.Instance?.World, Zombie, "выход из одержимости"); DogSense.Clear(); WolfControl.Clear(); @@ -588,20 +589,23 @@ namespace NecromancerTome _nextAttack = Time.time + FlyAttackDelay; _meleeLookUntil = Time.time + FlyAttackDelay; } - // F - клич-приманка (указание 06.10; кнопка - 08.10, была ПКМ): крик крикуньи, только голосом - // паникующей курицы - зомби в ScreamRange идут на него; с воздуха уводит толпу от базы или от тела. - // F у игрока - фонарик, но его читает только выключенный PlayerMoveController.Update. - bool scream = input != null && !gui && (input.PermanentActions.ToggleFlashlight.WasPressed || input.ToggleFlashlight.WasPressed); + // E - клич-приманка (указание 06.10; кнопка - 08.10 F, была ПКМ; 09.10 - E: «на E действие, у стервятника крик»): + // крик крикуньи, только голосом паникующей курицы - зомби в ScreamRange идут на него; с воздуха уводит + // толпу от базы или от тела. detonate - это и есть нажатие E. + bool scream = detonate; if (scream) { Scream(world, z); } // ПКМ (держать) - «зоркий глаз» (указание 08.10: «приближение как у прицела»), в PlaceCamera. _flyZoomHeld = rangedHeld; - // E - питаться падалью (указание 06.10, кнопка - 08.10: «пусть ест по кнопке E» - у игрока E это - // взаимодействие, лут; было F). detonate - это и есть нажатие E. - bool eat = detonate; - Eat(world, z, eat, attack || scream); + // F - питаться падалью (указание 06.10; кнопка 08.10 - E, 09.10 - F у всех одержимых питомцев). + // F у игрока - фонарик, но его читает только выключенный PlayerMoveController.Update. + bool eat = FKey(input, gui); + // Есть можно сидя или просто остановившись рядом (зависнув над падалью): без ввода и почти без скорости. + bool still = _flySitting || _flyMove.sqrMagnitude < 0.01f && !_flyUp && + new Vector2(z.motion.x, z.motion.z).sqrMagnitude < FlySitSpeed * FlySitSpeed; + Eat(world, z, eat, attack || scream || _flyMove.sqrMagnitude > 0.01f || _flyUp || !_flySitting, still); if (_meleeLookUntil > 0f && Time.time >= _meleeLookUntil) { _meleeLookUntil = 0f; @@ -643,16 +647,29 @@ namespace NecromancerTome { // ПКМ - бросок на цель, след крови раненых (решено 09.10). WolfControl.Tick(world, _player, z, ranged, ref _bodyYaw); - // E: у тела - выгрузить сумку в рюкзак, у контейнера - лутать в сумку (3 ячейки). - if (!(detonate && WolfBag.TryUnload(_player, z))) + // E: на контейнер или мешок с зомби - лутать в сумку (3 ячейки); у тела - выгрузить сумку в рюкзак; + // иначе - «багажник», окно сумки (указание 09.10). + bool looted = RatLoot.Tick(world, _player, z, detonate, true); + if (detonate && !looted && !RatLoot.Viewing && !WolfBag.TryUnload(_player, z)) { - RatLoot.Tick(world, _player, z, detonate, true); + WolfBag.OpenWindow(_player, z); } } if (ranged && IsPet && BearControl.Is(z)) { BearControl.Roar(world, z); // ПКМ - рёв-провокация (решено 09.10) } + if (detonate && IsPet && BearControl.Is(z)) + { + BearControl.RearUp(world, z); // E - на дыбы, враги рядом падают (указание 09.10) + } + if (EatsCarrion(z)) + { + // F - есть падаль, как стервятник (указание 09.10: «поедание падали на F одержимым питомцам, на E - + // действие»; и одержимым зомби - их ИИ в игре и так ест трупы), стоя на месте рядом с трупом. + Eat(world, z, FKey(input, gui), moving || jump || attack || ranged, !moving && z.onGround); + } + AimTint(GroundAimTarget(world, z) != null); if (IsPet && DogSense.Is(z)) { // Нюх (указание 09.10): все существа рядом с собакой - на компасе и карте. @@ -758,11 +775,20 @@ namespace NecromancerTome } } - static void Eat(World _world, EntityAlive _v, bool _pressed, bool _acted) + /// Едят падаль на F, кроме стервятника (указание 09.10): чужие одержимые зомби и питомцы медведь, + /// собака, волк. Крыса - дух, не ест. + static bool EatsCarrion(EntityAlive _z) => !IsPet || BearControl.Is(_z) || DogSense.Is(_z) || WolfControl.Is(_z); + + /// F на клавиатуре или геймпаде (у игрока - фонарик). + static bool FKey(PlayerActionsLocal _input, bool _gui) => + _input != null && !_gui && (_input.PermanentActions.ToggleFlashlight.WasPressed || _input.ToggleFlashlight.WasPressed); + + /// _moved - сдвинулся или что-то сделал (еда прерывается), _still - стоит/сидит, можно начать. + static void Eat(World _world, EntityAlive _v, bool _pressed, bool _moved, bool _still) { if (_eating) { - bool moved = _flyMove.sqrMagnitude > 0.01f || _flyUp || _acted || !_flySitting; + bool moved = _moved; bool gone = _eatCorpse != null ? _eatCorpse.IsMarkedForUnload() : _world.GetBlock(_eatBlock).type != _eatBlockType; if (moved || gone) { @@ -822,17 +848,14 @@ namespace NecromancerTome { return; } - // Есть можно сидя или просто остановившись рядом (зависнув над падалью): без ввода и почти без скорости. - bool still = _flySitting || _flyMove.sqrMagnitude < 0.01f && !_flyUp && - new Vector2(_v.motion.x, _v.motion.z).sqrMagnitude < FlySitSpeed * FlySitSpeed; - if (!still || !FindCarrion(_world, _v)) + if (!_still || !FindCarrion(_world, _v)) { - GameManager.ShowTooltip(_player, Localization.Get("necroPossessNoCarrion")); - Debug.Log("[NecromancerTome] Possession: падали нет" + (still ? "" : " - стервятник не остановился")); + GameManager.ShowTooltip(_player, Localization.Get(Flying ? "necroPossessNoCarrion" : "necroPossessNoCarrionGround")); + Debug.Log("[NecromancerTome] Possession: падали нет" + (_still ? "" : " - " + _v.EntityName + " не остановился")); return; } _eating = true; - if (!_flySitting) + if (Flying && !_flySitting) { // Сел за еду сразу, не дожидаясь тика полёта: иначе проверка «не сидит» тут же прервала бы еду. _flySitting = true; @@ -845,7 +868,7 @@ namespace NecromancerTome _eatCount = 0; _v.SetLookPosition(_eatCorpse != null ? _eatCorpse.getChestPosition() : _eatBlock.ToVector3() + new Vector3(0.5f, 0.3f, 0.5f)); _v.IsEating = true; // как у зомби: анимация поедания и синхронизация по сети - Debug.Log("[NecromancerTome] Possession: стервятник ест " + EatName() + ", здоровье " + _v.Health + "/" + _v.GetMaxHealth() + + Debug.Log("[NecromancerTome] Possession: " + _v.EntityName + " ест " + EatName() + ", здоровье " + _v.Health + "/" + _v.GetMaxHealth() + ", мяса у падали " + Mathf.CeilToInt(EatMeat(0f))); } @@ -1011,7 +1034,6 @@ namespace NecromancerTome colors[_tintIndex] = _tintSaved; _tintIndex = -1; } - Debug.Log("[NecromancerTome] Possession: прицел " + (_on ? "на цели - красный" : "без цели")); } static readonly Color AimOnTarget = new Color(1f, 0.15f, 0.1f, 1f); @@ -1219,6 +1241,47 @@ namespace NecromancerTome static EntityAlive _aimMissNear; static float _aimMissDist, _aimMissOff; + /// Враг под прицелом наземного одержимого (указание 09.10: прицел краснеет на противнике у + /// всех одержимых): тело врага - отрезок от ног до головы - не дальше GroundAimRadius от луча + /// камеры и не дальше GroundAimRange от одержимого, и не за стеной - не дальше первого попадания + /// луча (AimPoint). + static EntityAlive GroundAimTarget(World _world, EntityAlive _z) + { + Transform cam = _player?.cameraTransform; + if (cam == null) + { + return null; + } + Vector3 from = cam.position + Origin.position; + Vector3 dir = cam.forward; + float wall = Vector3.Dot(AimPoint() - from, dir) + 0.5f; + EntityAlive best = null; + float bestAlong = float.MaxValue; + for (int i = 0; i < _world.EntityAlives.Count; i++) + { + EntityAlive e = _world.EntityAlives[i]; + if (!SnakeControl.IsFoe(e, _z) || (e.position - _z.position).sqrMagnitude > GroundAimRange * GroundAimRange) + { + continue; + } + Vector3 feet = e.position, head = e.getHeadPosition(); + for (int k = 0; k <= 4; k++) + { + Vector3 p = Vector3.Lerp(feet, head, k / 4f); + float a = Vector3.Dot(p - from, dir); + if (a > 0f && a < wall && a < bestAlong && Vector3.Cross(p - from, dir).magnitude < GroundAimRadius) + { + best = e; + bestAlong = a; + } + } + } + return best; + } + + public const float GroundAimRange = 30f; // зрение зомби, как TauntRange + public const float GroundAimRadius = 0.6f; + /// Раз в полсекунды: есть ли живой враг ближе BodyDangerRange к телу (как у змеи). static void CheckBody(World _world, EntityAlive _z, float _dt) { @@ -1288,11 +1351,11 @@ namespace NecromancerTome } if (Flying) { - text += ", " + Localization.Get("necroPossessHintZoom") + ", " + Localization.Get("necroPossessHintScreamF"); + text += ", " + Localization.Get("necroPossessHintZoom") + ", " + Localization.Get("necroPossessHintScreamE"); } if (Flying) { - text += ", " + Localization.Get("necroPossessHintEat"); + text += ", " + Localization.Get("necroPossessHintEatF"); } if (IsPet && RatModel.Is(_z)) { @@ -1309,9 +1372,14 @@ namespace NecromancerTome } if (IsPet && BearControl.Is(_z)) { - text += ", " + Localization.Get("necroPossessHintRoar") + ", " + Localization.Get("necroPossessHintSmash") + ", " + + text += ", " + Localization.Get("necroPossessHintRoar") + ", " + Localization.Get("necroPossessHintRear") + ", " + + Localization.Get("necroPossessHintSmash") + ", " + Localization.Get("necroPossessHintSkin"); } + if (EatsCarrion(_z)) + { + text += ", " + Localization.Get("necroPossessHintEatGroundF"); + } if (_z.jumpMaxDistance >= 5f) { text += ", " + Localization.Get("necroPossessHintLeap"); @@ -1765,6 +1833,43 @@ namespace NecromancerTome } } + /// Прицел одержимого - всегда перекрестие (указание 09.10: «то перекрестие, то точка»). Вид прицела + /// игра берёт у предмета в руке тела, а у книги призыва и пустой руки он зависит от проверки камеры от + /// третьего лица (ItemAction.GetCrosshairType - EntityPlayerLocal.CharacterCameraAngleValid): камера дальше + /// 20° от того, куда повёрнуто тело, - прицела нет, и игра рисует точку. В одержимости камера - глаза + /// одержимого, тело стоит, так что вид менялся от поворота мыши. Рисуем сами то же, что ветка Plus + /// guiDrawCrosshair; цвет - из таблицы игры, его красит Possession.AimTint. + [HarmonyPatch(typeof(EntityPlayerLocal), nameof(EntityPlayerLocal.guiDrawCrosshair))] + public static class Patch_EntityPlayerLocal_guiDrawCrosshair_Possession + { + public static bool Prefix(EntityPlayerLocal __instance, NGuiWdwInGameHUD _guiInGame, bool bModalWindowOpen) + { + if (!Possession.Active || __instance != Possession.Player) + { + return true; + } + if (!_guiInGame.showCrosshair || Event.current.type != EventType.Repaint || __instance.IsDead() || + !GamePrefs.GetBool(EnumGamePrefs.OptionsCrosshairEnabled) || bModalWindowOpen || _guiInGame.CrosshairTexture == null) + { + return false; + } + int i = GamePrefs.GetInt(EnumGamePrefs.OptionsCrosshairColor); + if (i < 0 || i >= EntityPlayerLocal.crosshairColors.Count) + { + i = 0; + } + Color c = EntityPlayerLocal.crosshairColors[i]; + Vector2 center = __instance.GetCrosshairPosition2D(); + center.y = Screen.height - center.y; + float size = Screen.height * 0.059f * GamePrefs.GetFloat(EnumGamePrefs.OptionsCrosshairScale); + Color was = GUI.color; + GUI.color = new Color(c.r, c.g, c.b, GamePrefs.GetFloat(EnumGamePrefs.OptionsCrosshairOpacity)); + GUI.DrawTexture(new Rect(center.x - size / 2f, center.y - size / 2f, size, size), _guiInGame.CrosshairTexture, ScaleMode.StretchToFill); + GUI.color = was; + return false; + } + } + /// Тело оцепенело: ввод игрока не идёт (как у тела в рэгдолле - stopMoving и выход). [HarmonyPatch(typeof(PlayerMoveController), nameof(PlayerMoveController.Update))] public static class Patch_PlayerMoveController_Update_Possession diff --git a/HarmonySrc/RatLoot.cs b/HarmonySrc/RatLoot.cs index 3bd8244..12c6b33 100644 --- a/HarmonySrc/RatLoot.cs +++ b/HarmonySrc/RatLoot.cs @@ -17,6 +17,11 @@ namespace NecromancerTome /// переполнены»): звук ui_denied и xuiInventoryFullForPickup; /// - окно не закрывается из-за дальности до ТЕЛА: XUiC_LootWindow.Update меряет её от персонажа. /// Подсказку «E: обыскать» рисует управление игрока, а оно в одержимости выключено - выводим сами. + /// + /// МЕШКИ (указание 09.10: «лутать жёлтые мешочки, и синие, и красные, падающие с зомби»). Мешок - сущность + /// EntityLootContainer, открывается той же цепочкой замка (LockRequestLocal -> Entity.OnLockResponseLocal), но + /// окно другое - XUiC_BagStorageWindowGroup; для него те же отличия: рюкзак закрыт, ячейки заперты, дальность + /// от крысы. Рюкзак погибшего игрока (EntityBackpack) - не мешок, его не трогаем. /// public static class RatLoot { @@ -29,6 +34,41 @@ namespace NecromancerTome static float _spareUntil; /// Досягаемость контейнера от крысы, м. const float Reach = 2.5f; + /// Мешок - не дальше стольких градусов от луча камеры (мешок мал, точно навести трудно). + const float BagAimDeg = 25f; + /// Открытый на просмотр мешок - для щелчков по ячейкам и «Взять всё». + static EntityLootContainer _viewBag; + + /// Кто осматривает - для лога (у волка тот же обыск). + static string Who => Possession.Zombie != null && WolfControl.Is(Possession.Zombie) ? "волк" : "крыса"; + + /// Мешок перед крысой: ближе Reach к ней и ближе всех к лучу камеры, не дальше BagAimDeg. + public static EntityLootContainer FindBag(World _world, EntityPlayerLocal _player, EntityAlive _rat) + { + Transform cam = _player.cameraTransform; + if (cam == null) + { + return null; + } + Vector3 from = cam.position + Origin.position; + EntityLootContainer best = null; + float bestDeg = BagAimDeg; + for (int i = 0; i < _world.Entities.list.Count; i++) + { + if (!(_world.Entities.list[i] is EntityLootContainer bag) || bag.IsMarkedForUnload() || bag.bag == null || + (bag.position - _rat.position).sqrMagnitude > Reach * Reach) + { + continue; + } + float deg = Vector3.Angle(cam.forward, bag.position + Vector3.up * 0.2f - from); + if (deg < bestDeg) + { + bestDeg = deg; + best = bag; + } + } + return best; + } /// Контейнер перед крысой по лучу камеры: блок с хранилищем ближе Reach к крысе; иначе null. public static TEFeatureStorage Find(World _world, EntityPlayerLocal _player, EntityAlive _rat, out Vector3i _pos, out BlockValue _bv) @@ -76,21 +116,39 @@ namespace NecromancerTome return st; } - /// Каждый кадр одержимости крысой: подсказка и E - открыть. - public static void Tick(World _world, EntityPlayerLocal _player, EntityAlive _rat, bool _activate, bool _take = false) + /// Каждый кадр одержимости крысой: подсказка и E - открыть. true - нажатие E ушло на контейнер или + /// мешок (волку тогда не открывать свою сумку). + public static bool Tick(World _world, EntityPlayerLocal _player, EntityAlive _rat, bool _activate, bool _take = false) { LocalPlayerUI ui = _player.PlayerUI; if (ui == null || Viewing || ui.windowManager.IsModalWindowOpen()) { - return; - } - TEFeatureStorage st = Find(_world, _player, _rat, out Vector3i pos, out BlockValue bv); - if (st == null) - { - return; + return false; } PlayerActionsLocal input = _player.playerInput; string key = input.Activate.GetBindingXuiMarkupString() + input.PermanentActions.Activate.GetBindingXuiMarkupString(); + TEFeatureStorage st = Find(_world, _player, _rat, out Vector3i pos, out BlockValue bv); + if (st == null) + { + EntityLootContainer bag = FindBag(_world, _player, _rat); + if (bag == null) + { + return false; + } + XUiC_InteractionPrompt.SetText(ui, string.Format(Localization.Get("necroPossessSearchBag"), key, bag.LocalizedEntityName)); + if (!_activate) + { + return false; + } + Viewing = true; + Taking = _take; + _viewBag = bag; + XUiC_InteractionPrompt.SetText(ui, null); + _lockRequest = _lockRequest ?? AccessTools.Method(typeof(LockManager), nameof(LockManager.LockRequestLocal), new[] { typeof(ILockTarget), typeof(ushort) }); + _lockRequest.Invoke(LockManager.Instance, new object[] { bag, (ushort)0 }); + Debug.Log("[NecromancerTome] RatLoot: " + Who + " осматривает мешок " + EntityClass.GetEntityClassName(bag.entityClass) + " (" + bag.entityId + ")"); + return true; + } string text = st.GetActivationText(_world, pos, bv, _rat, key, bv.Block.GetLocalizedBlockName()); if (!string.IsNullOrEmpty(text)) { @@ -98,7 +156,7 @@ namespace NecromancerTome } if (!_activate) { - return; + return false; } bool locked = st.lockpickFeature != null && st.lockpickFeature.NeedsLockpicking() || st.lockFeature != null && st.lockFeature.IsLocked() && !st.lockFeature.IsUserAllowed(Platform.PlatformManager.InternalLocalUserIdentifier) || @@ -106,7 +164,7 @@ namespace NecromancerTome if (locked) { Audio.Manager.BroadcastPlayByLocalPlayer(pos.ToVector3() + Vector3.one * 0.5f, "Misc/locked"); - return; + return true; } Viewing = true; Taking = _take; @@ -116,7 +174,8 @@ namespace NecromancerTome // Через отражение: у LockRequestLocal есть перегрузка с ReadOnlySpan, которой нет в справочных сборках проекта. _lockRequest = _lockRequest ?? AccessTools.Method(typeof(LockManager), nameof(LockManager.LockRequestLocal), new[] { typeof(ILockTarget), typeof(ushort) }); _lockRequest.Invoke(LockManager.Instance, new object[] { st, (ushort)0 }); - Debug.Log("[NecromancerTome] RatLoot: крыса осматривает " + bv.Block.GetBlockName() + " " + pos); + Debug.Log("[NecromancerTome] RatLoot: " + Who + " осматривает " + bv.Block.GetBlockName() + " " + pos); + return true; } static System.Reflection.MethodInfo _lockRequest; @@ -141,12 +200,17 @@ namespace NecromancerTome LocalPlayerUI ui = LocalPlayerUI.GetUIForPrimaryPlayer(); ui?.windowManager.CloseAllOpenModalWindows(); Viewing = false; + _viewBag = null; _spareUntil = Time.time + 5f; } static void LockSlots(XUiC_LootWindow _w, bool _lock) { - XUiC_LootContainer grid = _w?.lootContainer; + LockSlots(_w?.lootContainer, _lock); + } + + static void LockSlots(XUiC_ItemStackGrid grid, bool _lock) + { if (grid?.itemControllers == null) { return; @@ -263,6 +327,90 @@ namespace NecromancerTome } } + /// Окно мешка открылось на просмотр - рюкзак закрыть, ячейки запереть (как у ящика). + [HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.OnOpen))] + public static class Patch_Bag_OnOpen + { + public static void Postfix(XUiC_BagStorageWindowGroup __instance) + { + if (!Viewing || _viewBag == null) + { + return; + } + __instance.xui.playerUI.windowManager.Close("backpack"); + LockSlots(__instance.containerWindow, true); + } + } + + [HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.OnClose))] + public static class Patch_Bag_OnClose + { + public static void Prefix(XUiC_BagStorageWindowGroup __instance) + { + LockSlots(__instance.containerWindow, false); // всегда: запертые ячейки не должны достаться персонажу + if (Viewing && _viewBag != null) + { + Viewing = false; + _viewBag = null; + } + } + } + + /// Таймер обыска мешка прерван - просмотра нет. + [HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.CancelOpen))] + public static class Patch_Bag_CancelOpen + { + public static void Postfix() + { + if (Viewing && _viewBag != null) + { + Viewing = false; + _viewBag = null; + } + } + } + + /// Замок мешка не дан (занят другим) - просмотра нет. + [HarmonyPatch(typeof(Entity), nameof(Entity.OnLockResponseLocal))] + public static class Patch_Entity_OnLockResponseLocal + { + public static void Postfix(Entity __instance, bool __result) + { + if (!__result && Viewing && _viewBag != null && __instance == _viewBag) + { + Viewing = false; + _viewBag = null; + } + } + } + + /// Дальность окна мешка - от крысы: проверка окна меряет её до entityPlayer, поэтому на время Update + /// персонаж «стоит» на месте крысы, как у окна ящика. + [HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.Update))] + public static class Patch_Bag_Update + { + public static void Prefix(XUiC_BagStorageWindowGroup __instance, out Vector3? __state) + { + __state = null; + EntityPlayerLocal p = __instance.xui.playerUI.entityPlayer; + EntityAlive rat = Possession.Zombie; + if (!Viewing || _viewBag == null || p == null || rat == null) + { + return; + } + __state = p.position; + p.position = rat.position; + } + + public static void Finalizer(XUiC_BagStorageWindowGroup __instance, Vector3? __state) + { + if (__state.HasValue) + { + __instance.xui.playerUI.entityPlayer.position = __state.Value; + } + } + } + [HarmonyPatch(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveAll))] public static class Patch_MoveAll { @@ -277,6 +425,10 @@ namespace NecromancerTome { WolfBag.TakeAll(te); } + else if (Taking && _viewBag != null) + { + WolfBag.TakeAll(_viewBag); + } else { Deny(); @@ -300,21 +452,29 @@ namespace NecromancerTome { public static void Postfix(XUiC_ItemStack __instance) { - if (!Viewing || !__instance.isOver || __instance.ItemStack == null || __instance.ItemStack.IsEmpty() || - __instance.GetParentByType() == null) + if (!Viewing || !__instance.isOver || __instance.ItemStack == null || __instance.ItemStack.IsEmpty()) + { + return; + } + // Ячейка контейнера-блока (XUiC_LootContainer) или мешка (XUiC_BagContainer), не рюкзака. + XUiC_ItemStackGrid grid = (XUiC_ItemStackGrid)__instance.GetParentByType() ?? __instance.GetParentByType(); + if (grid == null) { return; } CursorControllerAbs cur = __instance.xui.playerUI.CursorController; if (cur.GetMouseButtonDown(UICamera.MouseButton.LeftButton) || cur.GetMouseButtonDown(UICamera.MouseButton.RightButton)) { - XUiC_LootContainer grid = __instance.GetParentByType(); TEFeatureStorage te = __instance.GetParentByType()?.te; - int idx = grid?.itemControllers != null ? System.Array.IndexOf(grid.itemControllers, __instance) : -1; + int idx = grid.itemControllers != null ? System.Array.IndexOf(grid.itemControllers, __instance) : -1; if (Taking && te != null && idx >= 0) { WolfBag.TakeFrom(te, idx); } + else if (Taking && _viewBag != null && grid is XUiC_BagContainer && idx >= 0) + { + WolfBag.TakeFrom(_viewBag, idx); + } else { Deny(); diff --git a/HarmonySrc/WolfBag.cs b/HarmonySrc/WolfBag.cs index cae0bbe..9c009c8 100644 --- a/HarmonySrc/WolfBag.cs +++ b/HarmonySrc/WolfBag.cs @@ -11,7 +11,12 @@ namespace NecromancerTome /// но щелчок по предмету (и «Взять всё») кладёт его в сумку волка; места нет - «Сумка полна»; /// - выгрузка - принести к телу: рядом с телом E перекладывает сумку в рюкзак персонажа, что не /// влезло - мешком на землю рядом; - /// - волк погиб или отозван - сумка выпадает мешком на месте. + /// - волк погиб или отозван - сумка выпадает мешком на месте; при отзыве (указание 09.10) на карте метка + /// «Сумка волка», чтобы за ним прийти; мешок подобран или истлел - метка снята (Patch_removeBackpack); + /// - «багажник» (указание 09.10: «почти как у пространственного хранилища»): E не на контейнер и не у тела - + /// окно сумки, то же ванильное окно, что у браслета (XUiC_BagStorageWindowGroup, шаблон roboticDrone), но + /// без рюкзака персонажа: внутри перекладывать, вытащить за окно - выбросить у волка (Patch_GetDropPosition); + /// - мешки с зомби волк лутает так же, как ящики (RatLoot, TakeFrom(EntityLootContainer)). /// /// ХРАНЕНИЕ. Сумку сущности игра не сохраняет (Entity.bag нет в Entity.Write), поэтому она своя. /// Упаковка - та же, что у пространственного хранилища (SpatialVaultPersistence.BuildPayload/ParsePayload, @@ -25,6 +30,11 @@ namespace NecromancerTome public const int Slots = 3; const string FileName = "NecromancerTome_wolfbag.bin"; public const float UnloadRange = 3f; + /// Метка выпавшей сумки: имя - ключ локализации, по нему и находим свою метку среди меток игрока. + public const string WaypointName = "necroWolfBagWaypoint"; + public const string WaypointIcon = "ui_game_symbol_backpack"; + /// Открыто окно сумки («багажник»). + public static bool WindowOpen; static Bag _bag; static string _dir; @@ -112,11 +122,41 @@ namespace NecromancerTome /// в пустую ячейку; в контейнере остаётся ровно то, что не влезло. public static bool TakeFrom(TEFeatureStorage _te, int _idx) { - ItemStack src = _te.ItemGrid.GetItem(_idx); - if (src == null || src.IsEmpty()) + ItemStack rest = Take(_te.ItemGrid.GetItem(_idx)); + if (rest == null) { return false; } + _te.UpdateSlot(_idx, rest); + _te.SetModified(); + _te.Parent?.NotifyListeners(); // окно контейнера перерисует ячейку (как ванильная сортировка) + return true; + } + + /// То же из мешка, упавшего с зомби. Окно мешка перерисуется по событию сумки (SetSlot). + public static bool TakeFrom(EntityLootContainer _src, int _idx) + { + if (_idx >= _src.bag.SlotCount) + { + return false; + } + ItemStack rest = Take(_src.bag.ItemGrid.items[_idx]); + if (rest == null) + { + return false; + } + _src.bag.SetSlot(_idx, rest); + _src.OnBagModified(); // на клиенте - отправить сумку мешка серверу, как делает окно + return true; + } + + /// Переложить стопку в сумку; что осталось в источнике (пустая стопка, если всё), или null - не влезло. + static ItemStack Take(ItemStack src) + { + if (src == null || src.IsEmpty()) + { + return null; + } Bag b = Get(); ItemStack moving = src.Clone(); int before = moving.count; @@ -130,17 +170,15 @@ namespace NecromancerTome { Audio.Manager.PlayInsidePlayerHead("ui_denied"); Show("necroWolfBagFull"); - return false; + return null; } ItemStack rest = src.Clone(); rest.count = moving.count; - _te.UpdateSlot(_idx, rest.count > 0 ? rest : ItemStack.Empty.Clone()); - _te.SetModified(); - _te.Parent?.NotifyListeners(); // окно контейнера перерисует ячейку (как ванильная сортировка) Save(); Audio.Manager.PlayInsidePlayerHead("UseActions/takeall1"); Show("necroWolfBagTook"); - return true; + Debug.Log("[NecromancerTome] WolfBag: в сумку " + src.itemValue.ItemClass.GetItemName() + " x" + moved + " - " + Describe()); + return rest.count > 0 ? rest : ItemStack.Empty.Clone(); } /// «Взять всё» - по ячейкам, пока есть место. @@ -161,6 +199,54 @@ namespace NecromancerTome } } + /// «Взять всё» из мешка - по ячейкам, пока есть место. + public static void TakeAll(EntityLootContainer _src) + { + bool any = false; + ItemStack[] slots = _src.bag.ItemGrid.items; + for (int i = 0; i < slots.Length; i++) + { + if (!slots[i].IsEmpty() && Get().CanTakeItem(slots[i])) + { + any |= TakeFrom(_src, i); + } + } + if (!any) + { + Audio.Manager.PlayInsidePlayerHead("ui_denied"); + Show("necroWolfBagFull"); + } + } + + /// «Багажник»: окно сумки, как у пространственного браслета (шаблон дрона), без рюкзака персонажа. + /// Перекладка внутри пишется сразу (Save на каждое изменение и на закрытие). + public static void OpenWindow(EntityPlayerLocal _player, EntityAlive _wolf) + { + LocalPlayerUI ui = LocalPlayerUI.GetUIForPlayer(_player); + if (ui == null) + { + return; + } + WindowOpen = true; + XUiC_BagStorageWindowGroup.Open(ui.xui, _wolf, Get(), LootContainer.GetLootContainer("roboticDrone"), + Localization.Get("necroWolfBagWindowTitle"), Save, () => + { + WindowOpen = false; + Save(); + }); + Debug.Log("[NecromancerTome] WolfBag: окно сумки - " + Describe()); + } + + /// Выход из одержимости при открытом окне сумки - закрыть его. + public static void CloseWindow() + { + if (WindowOpen) + { + LocalPlayerUI.GetUIForPrimaryPlayer()?.windowManager.CloseAllOpenModalWindows(); + WindowOpen = false; + } + } + /// E рядом с телом: сумка - в рюкзак, что не влезло - мешком у тела. public static bool TryUnload(EntityPlayerLocal _body, EntityAlive _wolf) { @@ -228,9 +314,61 @@ namespace NecromancerTome b.Clear(); Save(); Drop(_wolf.position, items); + if (_why == "отозван") + { + AddWaypoint(_wolf.position); + } Debug.Log("[NecromancerTome] WolfBag: волк " + _why + " - сумка выпала мешком (" + items.Count + ")"); } + /// Метка на карте и компасе - как игра ставит метку транспорта (XUiC_MapArea.RefreshVehiclePositionWaypoint), + /// но обычная, не «последнее место сущности»: номер мешка после перезахода другой. Метка - в списке меток игрока, + /// сохраняется игрой вместе с ним; удалить можно и вручную. + static void AddWaypoint(Vector3 _at) + { + EntityPlayerLocal p = GameManager.Instance?.World?.GetPrimaryPlayer(); + if (p?.Waypoints == null) + { + return; + } + Waypoint w = new Waypoint(); + w.pos = World.worldToBlockPos(_at); + w.icon = WaypointIcon; + w.ownerId = Platform.PlatformManager.InternalLocalUserIdentifier; + w.name.Update(WaypointName, Platform.PlatformManager.MultiPlatform.User.PlatformUserId); + w.bUsingLocalizationId = true; + p.Waypoints.Collection.Add(w); + w.navObject = NavObjectManager.Instance.RegisterNavObject("waypoint", w.pos.ToVector3(), w.icon); + w.navObject.IsActive = false; + w.navObject.name = w.name.Text; + w.navObject.usingLocalizationId = true; + Debug.Log("[NecromancerTome] WolfBag: метка сумки на карте " + w.pos); + } + + /// Мешок исчез (опустел или истлел) - снять метку сумки, стоящую рядом с ним. + public static void RemoveWaypointAt(Vector3 _at) + { + EntityPlayerLocal p = GameManager.Instance?.World?.GetPrimaryPlayer(); + if (p?.Waypoints == null) + { + return; + } + List list = p.Waypoints.Collection.list; + for (int i = list.Count - 1; i >= 0; i--) + { + Waypoint w = list[i]; + if (w.name.Text == WaypointName && (w.pos.ToVector3() - _at).sqrMagnitude < 3f * 3f) + { + p.Waypoints.Collection.Remove(w); + if (w.navObject != null) + { + NavObjectManager.Instance.UnRegisterNavObject(w.navObject); + } + Debug.Log("[NecromancerTome] WolfBag: мешок сумки подобран - метка снята " + w.pos); + } + } + } + static void Drop(Vector3 _at, List _items) { GameManager.Instance.DropContentInLootContainerServer(-1, "DroppedLootContainer", _at + Vector3.up * 0.5f, _items.ToArray()); @@ -244,6 +382,47 @@ namespace NecromancerTome } } + /// Мешок исчез (пустой после обыска - «empty», или истлел - «old») - снять метку сумки волка рядом. + [HarmonyPatch(typeof(EntityContainerAbs), nameof(EntityContainerAbs.removeBackpack))] + public static class Patch_EntityContainerAbs_removeBackpack_WolfBag + { + public static void Postfix(EntityContainerAbs __instance) + { + if (__instance is EntityLootContainer) + { + WolfBag.RemoveWaypointAt(__instance.position); + } + } + } + + /// «Багажник» без рюкзака персонажа: окно мешка открывает его само (OnOpen), закрываем сразу же. + [HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.OnOpen))] + public static class Patch_XUiC_BagStorageWindowGroup_OnOpen_WolfBag + { + public static void Postfix(XUiC_BagStorageWindowGroup __instance) + { + if (WolfBag.WindowOpen && Possession.Active) + { + __instance.xui.playerUI.windowManager.Close("backpack"); + } + } + } + + /// Предмет, вытащенный за окно сумки волка, падает у волка, а не у тела (XUiM_PlayerInventory.DropItem + /// берёт место у игрока - GetDropPosition). + [HarmonyPatch(typeof(EntityPlayerLocal), nameof(EntityPlayerLocal.GetDropPosition))] + public static class Patch_EntityPlayerLocal_GetDropPosition_WolfBag + { + public static void Postfix(EntityPlayerLocal __instance, ref Vector3 __result) + { + EntityAlive wolf = Possession.Zombie; + if (WolfBag.WindowOpen && Possession.Active && __instance == Possession.Player && wolf != null) + { + __result = wolf.position + wolf.GetForwardVector() * 0.8f + Vector3.up * 0.5f; + } + } + } + [HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.OnEntityDeath))] public static class Patch_EntityAlive_OnEntityDeath_WolfBag { diff --git a/NecromancerHarmony.dll b/NecromancerHarmony.dll index 7c68ffc..c106871 100644 Binary files a/NecromancerHarmony.dll and b/NecromancerHarmony.dll differ