Чертоги разума, хваты пустой руки, одержимость, книга с оглавлением
- Чертоги разума: дух в префабе chertogi под землёй, книга памяти, нож и плакат; кровать префаба - мебель necroPalaceBed (иначе становилась точкой возрождения); охрана выбрасывает и игроков (их нет в EntityAlives). - Хваты пустой руки по R: «отрицание» и «пиф-паф» - поза рук костями поверх аниматора, остальные кости заморожены, ЛКМ - заклинание, прицел краснеет на зомби. - «Отрицание»: зомби в ужасе бежит дальше 40 м и теряет игрока как цель. - «Пиф-паф»: одержимость зомби-гуманоидом - бег, прыжок, удар, дальняя атака, подрыв, крик крикуньи; тело цепенеет, зомби танкуют одержимого; область вокруг тела сжата; полоска здоровья; спавн и прорисовка у одержимого. - Книга с оглавлением (окно necroTome), глава «Некроэнергия». - Прорисовка дверей у змеи и одержимого (CheckLODs от камеры, а не от тела). - Почтовый ящик консоли и команды necrobones/necropose/necroshot для разработки. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -189,6 +189,14 @@ necroSnakeModeHuman,items,Item,,,,Human,,Mensch,Humano,Humain,Umano,人間,인
|
||||
necroSnakeModeSnake,items,Item,,,,Snake,,Schlange,Serpiente,Serpent,Serpente,蛇,뱀,Wąż,Cobra,Змея,Yılan,蛇,蛇
|
||||
necroSnakeBodyDanger,UI,Label,,,,Enemy approaching your body,,Ein Feind nähert sich deinem Körper,Un enemigo se acerca a tu cuerpo,Un ennemi approche de ton corps,Un nemico si avvicina al tuo corpo,敵が体に近づいている,적이 몸에 다가오고 있다,Wróg zbliża się do twojego ciała,Um inimigo se aproxima do seu corpo,Враг приближается к телу,Bir düşman bedenine yaklaşıyor,敌人正在接近你的身体,敵人正在接近你的身體
|
||||
necroSnakeModeDay,items,Item,,,,It is daytime,,Es ist Tag,Es de día,Il fait jour,È giorno,今は昼だ,지금은 낮이다,Jest dzień,Agora é dia,Сейчас день,Şu an gündüz,现在是白天,現在是白天
|
||||
necroHoldNone,items,Item,,,,Bare hand,,Bloße Hand,Mano desnuda,Main nue,Mano nuda,素手,맨손,Goła ręka,Mão nua,Обычная рука,Çıplak el,空手,空手
|
||||
necroHoldDeny,items,Item,,,,Denial,,Verleugnung,Negación,Déni,Negazione,拒絶,거부,Zaprzeczenie,Negação,Отрицание,İnkâr,否认,否認
|
||||
necroHoldPifPaf,items,Item,,,,Bang-bang,,Peng-peng,Pum-pum,Pan-pan,Bang-bang,バンバン,빵야빵야,Pif-paf,Pou-pou,Пиф-паф,Bang-bang,砰砰,砰砰
|
||||
necroPossessHintBase,items,Item,,,,"LMB - strike, R - leave the body",,"LMT - Schlag, R - Körper verlassen","LMB: golpe, R: salir del cuerpo","Clic gauche : frapper, R : quitter le corps","LMB - colpo, R - lascia il corpo",左クリック: 攻撃、R: 体から出る,"좌클릭 - 공격, R - 몸에서 나가기","LPM - cios, R - opuść ciało","LMB - golpe, R - sair do corpo","ЛКМ - удар, R - выйти из тела","Sol tık - vuruş, R - bedenden çık",左键 - 攻击,R - 离开身体,左鍵 - 攻擊,R - 離開身體
|
||||
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),右键 - 尖叫(召唤亡者),右鍵 - 尖叫(召喚亡者)
|
||||
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ış,空格 - 远跳,空白鍵 - 遠跳
|
||||
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.
|
||||
@@ -279,3 +287,20 @@ Dört modifikasyon yuvasına takılan kan küreleriyle çalışır: küreler sı
|
||||
necroBoneSteedNeedBones,UI,Tooltip,,,,You need {0} bones to repair the Bone Steed.,,"Du brauchst {0} Knochen, um das Knochenross zu reparieren.",Necesitas {0} huesos para reparar el Corcel de hueso.,Il te faut {0} os pour réparer le Destrier d'os.,Servono {0} ossa per riparare il Destriero d'ossa.,骨の駿馬の修理には骨が{0}個必要だ。,뼈 군마를 수리하려면 뼈 {0}개가 필요하다.,Do naprawy Kościanego rumaka potrzeba {0} kości.,Você precisa de {0} ossos para reparar o Corcel de osso.,Для ремонта Костяного скакуна нужно {0} костей.,Kemik Atı onarmak için {0} kemik gerekir.,修理骨骸坐骑需要{0}根骨头。,修理骨骸坐騎需要{0}根骨頭。
|
||||
necroBoneSteed,entityclasses,Entity,,,,Bone Steed,,Knochenross,Corcel de hueso,Destrier d'os,Destriero d'ossa,骨の駿馬,뼈 군마,Kościany rumak,Corcel de osso,Костяной скакун,Kemik At,骨骸坐骑,骨骸坐騎
|
||||
zombieEraser,entityclasses,Entity,,,,Eraser,,Radierer,Borrador,Effaceur,Cancellatore,イレイサー,이레이저,Wymazywacz,Apagador,Стиратель,Silici,抹除者,抹除者
|
||||
necroMemoryBook,blocks,Block,,,,Book of Memory,,Buch der Erinnerung,Libro de la memoria,Livre de la mémoire,Libro della memoria,記憶の書,기억의 책,Księga pamięci,Livro da memória,Книга памяти,Hafıza Kitabı,记忆之书,記憶之書
|
||||
necroMemoryBookPrompt,blocks,Block,,,,Press {0} to read,,Drücke {0} zum Lesen,Pulsa {0} para leer,Appuyez sur {0} pour lire,Premi {0} per leggere,{0} で読む,{0} 키를 눌러 읽기,"Naciśnij {0}, aby przeczytać",Pressione {0} para ler,"Нажмите {0}, чтобы прочитать",Okumak için {0} tuşuna basın,按 {0} 阅读,按 {0} 閱讀
|
||||
necroMemoryTitle,XUi,Menu,,,,Memory,,Erinnerung,Memoria,Mémoire,Memoria,記憶,기억,Pamięć,Memória,Память,Hafıza,记忆,記憶
|
||||
necroMemoryText,XUi,Menu,,,,,,,,,,,,,,,,,
|
||||
necroTomeToc,XUi,Menu,,,,Contents,,,,,,,,,,Оглавление,,,
|
||||
necroTomePrev,XUi,Menu,,,,Back,,,,,,,,,,Назад,,,
|
||||
necroTomeNext,XUi,Menu,,,,Next,,,,,,,,,,Далее,,,
|
||||
necroTomeClose,XUi,Menu,,,,Close,,,,,,,,,,Закрыть,,,
|
||||
necroTomeCh1Title,XUi,Menu,,,,Necroenergy,,,,,,,,,,Некроэнергия,,,
|
||||
necroTomeCh1P1,XUi,Menu,,,,"Kvazaŭvivo - necroenergy\n\nElectricity moves electrons. Kvazaŭvivo, necroenergy, moves the necrotic structure - the residual ""pattern of the body"" that survives death. It is not the spirit: the spirit is a separate entity. It is the frame along which a spirit can manifest and a body can move.\n\nElectricity is a current along a wire, and a wire can be bent any way you like. Kvazaŭvivo is a current along a pattern, and the pattern is already set by the shape of the body.\n\nNecroenergy has six quantities of its own.",,,,,,,,,,"Kvazaŭvivo - некроэнергия\n\nЭлектричество движет электроны. Kvazaŭvivo, некроэнергия, движет некротическую структуру - остаточную «схему тела», которая сохраняется после смерти. Это не дух: дух - отдельная сущность. Это каркас, по которому дух может проявиться, а тело - двигаться.\n\nЭлектричество - ток по проводу, и провод можно согнуть как угодно. Kvazaŭvivo - ток по шаблону, а шаблон уже задан формой тела.\n\nУ некроэнергии шесть собственных величин.",,,
|
||||
necroTomeCh1P2,XUi,Menu,,,,"Density - denso (ds)\nHow much quasi-life is packed into the structure. Too little and the body barely stirs; a lot and it is almost indistinguishable from the living, but burns out fast.\n\nDepth - profundo (pf)\nHow deep quasi-life reaches. 1-5 pf: muscles only. 6-15 pf: nerves, complex tasks and speech. 16-30 pf: personality - almost alive, and the rarest of all.\n\nCoherence - kohero (kh)\nHow consistently the structure is held. Low coherence: jerky movements, falling limbs. High: smoothness and precision.",,,,,,,,,,"Плотность - denso (ds)\nСколько квазижизни упаковано в структуру. Мало - тело еле шевелится; много - почти неотличимо от живого, но быстро выгорает.\n\nГлубина - profundo (pf)\nКак глубоко проникает квазижизнь. 1-5 pf - только мышцы. 6-15 pf - нервы, сложные задачи и речь. 16-30 pf - личность: почти живой, и это редчайшее.\n\nКогерентность - kohero (kh)\nНасколько согласованно держится структура. Низкая - движения дёрганые, конечности отваливаются. Высокая - плавность и точность.",,,
|
||||
necroTomeCh1P3,XUi,Menu,,,,"Endurance - daŭro (dr)\nHow long quasi-life holds without feeding. A fresh corpse: 50-100 dr. An old one: 5-20 dr. A mummy: 200+ dr, but it needs an enormous initial density.\n\nResonance with the spirit - rezo (rz)\nHow well quasi-life sounds in unison with the spirit in the body. Below zero, the spirit resists and the energy is burnt on suppressing it; zero, there is no spirit; above zero, the spirit helps.\n\nSaturation - saturo (st)\nThe capacity limit of the structure. Exceed it and the body burns out and collapses. Bone has a high limit, soft tissue a low one, nerves a medium but critical one.",,,,,,,,,,"Устойчивость - daŭro (dr)\nСколько квазижизнь держится без подпитки. Свежий труп - 50-100 dr. Старый - 5-20 dr. Мумия - 200+ dr, но ей нужна огромная начальная плотность.\n\nРезонанс с духом - rezo (rz)\nНасколько квазижизнь звучит в унисон с духом в теле. Ниже нуля - дух сопротивляется, и энергия уходит на его подавление; ноль - духа нет; выше нуля - дух помогает.\n\nНасыщение - saturo (st)\nПредел ёмкости структуры. Превысишь - тело выгорает и рушится. У костей предел высокий, у мягких тканей низкий, у нервов средний, но критичный.",,,
|
||||
necroTomeCh1P4,XUi,Menu,,,,"The law of quasi-life\n\ndaŭro = denso × kohero × (1 + rezo) / profundo\n\nThe deeper the animation, the faster the structure is spent. Density and coherence lengthen the life of a quasi; so does a willing spirit, while a hostile one shortens it.\n\nElectricity gives power. Kvazaŭvivo gives the likeness of life. These are different forces, and a necromancer never confuses them.",,,,,,,,,,"Закон квазижизни\n\ndaŭro = denso × kohero × (1 + rezo) / profundo\n\nЧем глубже анимация, тем быстрее расходуется структура. Плотность и когерентность продлевают жизнь кваза, согласный дух - тоже, враждебный - сокращает.\n\nЭлектричество даёт силу. Kvazaŭvivo даёт подобие жизни. Это разные силы, и некромант их не путает.",,,
|
||||
necroMindModePalace,XUi,Menu,,,,Mind Palace,,Gedächtnispalast,Palacio mental,Palais mental,Palazzo della mente,記憶の宮殿,기억의 궁전,Pałac umysłu,Palácio da mente,Чертоги разума,Zihin Sarayı,思维宫殿,思維宮殿
|
||||
necroKnifeDecor,blocks,Block,,,,Necromancer's Knife,,Nekromantenmesser,Cuchillo del nigromante,Couteau du nécromancien,Coltello del negromante,ネクロマンサーのナイフ,강령술사의 칼,Nóż nekromanty,Faca do necromante,Нож некроманта,Nekromansör Bıçağı,死灵法师之刃,死靈法師之刃
|
||||
necroPalaceBed,blocks,Block,,,,"White Bunk Bed, Made",Stackable metal framed bed with sheets neatly made proper.,"Weißes Etagenbett, gemacht","Litera blanca, tendida","Couchette blanche, faite","Letto a castello bianco, Fatto",二段ベッド(メイク済み・白),"흰색 이층 침대, 깔끔",Biała prycza — posłane,"Beliche branco, arrumado","Двухъярусная белая кровать, заправленная","Beyaz Ranza, Yapılmış",已整理的白色上下铺床,白色上下舖,已整理
|
||||
necroPosterAnatomy,blocks,Block,,,,Chart: Structure of the Body,,Schautafel: Aufbau des Körpers,Lámina: estructura del cuerpo,Planche : structure du corps,Tavola: struttura del corpo,図表:身体の構造,도표: 신체의 구조,Plansza: budowa ciała,Prancha: estrutura do corpo,Схема: строение тела,Şema: Vücudun yapısı,图表:身体结构,圖表:身體結構
|
||||
|
||||
|
@@ -116,6 +116,33 @@
|
||||
<sprite name="blackout" depth="0" sprite="menu_empty" type="sliced" color="[black]" anchor_left="#cam,0,-10" anchor_right="#cam,1,10" anchor_bottom="#cam,0,-10" anchor_top="#cam,1,10" globalopacitymod="0"/>
|
||||
</window>
|
||||
</append>
|
||||
<!-- Книга с оглавлением (03.10.2026, BACKLOG «Книга с оглавлением и главами»): оглавление кнопками,
|
||||
глава по страницам, «Назад/Далее», «Оглавление», «Закрыть». Содержание и условия открытия глав -
|
||||
в HarmonySrc/NecroTome.cs, тексты - Localization.csv (necroTome*). Открывает книга памяти в
|
||||
чертогах (BlockNecroMemoryBook.Open) и консольная команда necrotome. -->
|
||||
<append xpath="/windows">
|
||||
<window name="necroTome" anchor="Center" pos="-650,420" width="1300" height="840" cursor_area="true" depth="1001" lock_navigation="true" controller="NecromancerTome.NecroTome, NecromancerHarmony">
|
||||
<sprite depth="0" name="bg" sprite="menu_empty" color="14,10,10,250" type="sliced" />
|
||||
<sprite depth="1" name="frame" sprite="menu_empty2px" color="120,18,14,255" type="sliced" fillcenter="false" />
|
||||
<label depth="2" name="lblTitle" pos="0,-22" width="1300" height="50" justify="center" font_size="38" color="205,40,30,255" effect="outline" upper_case="false" />
|
||||
<sprite depth="1" name="rule" pos="200,-82" width="900" height="2" sprite="menu_empty" color="120,18,14,255" />
|
||||
<label depth="2" name="lblText" pos="60,-104" width="1180" height="640" justify="left" font_size="27" color="215,205,190,255" overflow="shrinkcontent" upper_case="false" />
|
||||
<simplebutton name="btnCh1" depth="3" pos="350,-130" width="600" height="48" font_size="30" visible="false" />
|
||||
<simplebutton name="btnCh2" depth="3" pos="350,-190" width="600" height="48" font_size="30" visible="false" />
|
||||
<simplebutton name="btnCh3" depth="3" pos="350,-250" width="600" height="48" font_size="30" visible="false" />
|
||||
<simplebutton name="btnCh4" depth="3" pos="350,-310" width="600" height="48" font_size="30" visible="false" />
|
||||
<simplebutton name="btnCh5" depth="3" pos="350,-370" width="600" height="48" font_size="30" visible="false" />
|
||||
<simplebutton name="btnCh6" depth="3" pos="350,-430" width="600" height="48" font_size="30" visible="false" />
|
||||
<simplebutton name="btnCh7" depth="3" pos="350,-490" width="600" height="48" font_size="30" visible="false" />
|
||||
<simplebutton name="btnCh8" depth="3" pos="350,-550" width="600" height="48" font_size="30" visible="false" />
|
||||
<simplebutton name="btnToc" depth="3" pos="40,-790" width="260" height="46" font_size="26" caption_key="necroTomeToc" />
|
||||
<simplebutton name="btnPrev" depth="3" pos="440,-790" width="160" height="46" font_size="26" caption_key="necroTomePrev" />
|
||||
<label depth="2" name="lblPage" pos="600,-772" width="100" height="40" justify="center" font_size="24" color="160,150,140,255" />
|
||||
<simplebutton name="btnNext" depth="3" pos="700,-790" width="160" height="46" font_size="26" caption_key="necroTomeNext" />
|
||||
<simplebutton name="btnClose" depth="3" pos="1000,-790" width="260" height="46" font_size="26" caption_key="necroTomeClose" />
|
||||
</window>
|
||||
</append>
|
||||
|
||||
<!-- ФИОЛЕТОВАЯ ШКАЛА НЕКРОМАНТИИ, 2026-09-17. Продиктовано: "Есть индикатор опыта. Это шкала
|
||||
до уровня. А мы сделаем ещё одну шкалу, которая заполняется каждые 20 зомби... Каждое
|
||||
заполнение индикатора пусть добавляет уровень некромантии, а индикатор обнуляет."
|
||||
|
||||
@@ -42,4 +42,10 @@
|
||||
<window name="necroFinalBlack" />
|
||||
</window_group>
|
||||
</append>
|
||||
<!-- Книга с оглавлением (см. windows.xml, necroTome). -->
|
||||
<append xpath="/xui">
|
||||
<window_group name="necroTome">
|
||||
<window name="necroTome" />
|
||||
</window_group>
|
||||
</append>
|
||||
</xui>
|
||||
|
||||
@@ -232,4 +232,65 @@
|
||||
</property>
|
||||
</block>
|
||||
</append>
|
||||
|
||||
<!-- «Книга памяти» (03.10.2026): единственный интерактивный предмет чертогов разума, по E - окно
|
||||
«Память» (HarmonySrc/BlockNecroMemoryBook.cs). Вид - одиночная книга игры, без хранилища.
|
||||
CreativeMode Dev - чтобы можно было поставить в редакторе префабов. -->
|
||||
<append xpath="/blocks">
|
||||
<block name="necroMemoryBook">
|
||||
<property name="Extends" value="decoEntityClothMaster"/>
|
||||
<property name="Class" value="NecromancerTome.NecroMemoryBook, NecromancerHarmony"/>
|
||||
<property name="CreativeMode" value="Dev"/>
|
||||
<property name="CustomIcon" value="necroMemoryBook"/> <!-- 03.10: свой рендер книги памяти -->
|
||||
<!-- 03.10: своя модель - книга некроманта: чёрная кожа, кроваво-алый рисунок (z-mind), крупнее и толще книг призыва -->
|
||||
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necrobooks?necroBookMindPrefab.prefab"/>
|
||||
<property name="ModelOffset" value="0,0,0"/>
|
||||
<property name="GndAlign" value="1"/>
|
||||
<property name="WaterFlow" value="permitted"/>
|
||||
<property name="CanPickup" value="false"/>
|
||||
</block>
|
||||
</append>
|
||||
|
||||
<!-- Нож-декорация в чертогах разума (03.10.2026): копия ножа некроманта на столе с реактивами,
|
||||
просто декорация, руны светятся (эмиссия материала ножа). Модель - свой бандл necroknife.
|
||||
Поворот задаёт MindPalace.KnifeRotation (подбирается командой necropalace knife N). -->
|
||||
<append xpath="/blocks">
|
||||
<block name="necroKnifeDecor">
|
||||
<property name="Extends" value="decoEntityClothMaster"/>
|
||||
<property name="CreativeMode" value="Dev"/>
|
||||
<property name="CustomIcon" value="NecromancersKnife"/> <!-- 03.10: иконка ножа-предмета (имя предмета иконкой не работает) -->
|
||||
<!-- 03.10: своя модель-декорация - нож уже лежит плашмя, низом на дне клетки (как нижняя обложка
|
||||
книги памяти). Модель ножа-предмета стоит вертикально (для руки), а блоки-модели в 3.x ставятся
|
||||
«реквизитом», для которого код блока не вызывается - поэтому плашмя в самой модели, не в коде. -->
|
||||
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necroknife?necroKnifeDecorPrefab.prefab"/>
|
||||
<property name="ModelOffset" value="0,0,0"/>
|
||||
<property name="WaterFlow" value="permitted"/>
|
||||
<property name="CanPickup" value="false"/>
|
||||
</block>
|
||||
</append>
|
||||
<!-- Кровать чертогов (03.10.2026): копия bunkBedMadeWhite без Class="SleepingBag" - просто мебель.
|
||||
Чертоги ставятся без игрока, и игра делала кровать префаба точкой возрождения: синий камень
|
||||
портала отправлял в чертоги. MindPalace.Expected подменяет ею кровать префаба при постановке. -->
|
||||
<append xpath="/blocks">
|
||||
<block name="necroPalaceBed">
|
||||
<property name="Extends" value="bunkBedMadeWhite" param1="Class,DescriptionKey,DowngradeBlock"/>
|
||||
<property name="CreativeMode" value="None"/>
|
||||
<property name="CanPickup" value="false"/>
|
||||
</block>
|
||||
</append>
|
||||
<!-- Плакаты-схемы в чертогах (03.10.2026): по образцу ванильной pictureFrameGunSchematic, крупнее.
|
||||
Блок 2×1×1, рамка-плакат 1.405×0.95 м (полотно 3:2 под плакат), тонкий багет 2 см; модель - бандл
|
||||
necroposters (на каждый плакат свой префаб с тем же багетом). Исходник рамки:
|
||||
BlenderWork/necroSchemeFramePrefab/build_frame.py. -->
|
||||
<append xpath="/blocks">
|
||||
<block name="necroPosterAnatomy">
|
||||
<property name="Extends" value="pictureFrame_01a"/>
|
||||
<property name="CreativeMode" value="Dev"/>
|
||||
<property name="CustomIcon" value="necroPosterAnatomy"/>
|
||||
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necroposters?necroPosterAnatomyPrefab.prefab"/>
|
||||
<property name="ModelOffset" value="0,0,-0.5"/>
|
||||
<property name="MultiBlockDim" value="2,1,1"/>
|
||||
<property name="CanPickup" value="false"/>
|
||||
</block>
|
||||
</append>
|
||||
</config>
|
||||
|
||||
@@ -896,9 +896,10 @@
|
||||
<property name="DescriptionKey" value="bookSummonZombieBearDesc"/>
|
||||
<property name="DisplayType" value="ammoGrenadeFire"/>
|
||||
<property name="HoldType" value="21"/>
|
||||
<property name="Meshfile" value="@:Other/Items/Misc/bookPrefab.prefab"/>
|
||||
<!-- Своя модель книги (03.10.2026): кожа тёмно-бурая, в тон шкуре зомбомедведя, светящийся фиолетовый рисунок из референса;
|
||||
та же модель, что у книги крысы (бандл necrobooks). -->
|
||||
<property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necrobooks?necroBookBearPrefab.prefab"/>
|
||||
<property name="Material" value="Mpaper"/>
|
||||
<property name="TintColor" value="160, 220, 40"/>
|
||||
<property name="Weight" value="0"/>
|
||||
<property name="Stacknumber" value="10"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
@@ -952,9 +953,10 @@
|
||||
<property name="DescriptionKey" value="bookSummonZombieWolfDesc"/>
|
||||
<property name="DisplayType" value="ammoGrenadeFire"/>
|
||||
<property name="HoldType" value="21"/>
|
||||
<property name="Meshfile" value="@:Other/Items/Misc/bookPrefab.prefab"/>
|
||||
<!-- Своя модель книги (03.10.2026): кожа серая, в тон шкуре зомбоволка, светящийся фиолетовый рисунок из референса;
|
||||
та же модель, что у книги крысы (бандл necrobooks). -->
|
||||
<property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necrobooks?necroBookWolfPrefab.prefab"/>
|
||||
<property name="Material" value="Mpaper"/>
|
||||
<property name="TintColor" value="160, 220, 40"/>
|
||||
<property name="Weight" value="0"/>
|
||||
<property name="Stacknumber" value="10"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// «Книга памяти» в чертогах разума (указание 03.10): единственный предмет чертогов, с которым
|
||||
/// можно взаимодействовать. При наведении - «Нажмите E, чтобы прочитать», по E - окно «Память»
|
||||
/// (потом доделаем, как окна концовки в FinalSlides.cs). Блок подключается в blocks.xml через
|
||||
/// Class="NecromancerTome.NecroMemoryBook, NecromancerHarmony": игра (ReflectionHelpers.
|
||||
/// GetTypeWithPrefix) вставляет приставку Block перед именем после последней точки.
|
||||
/// Вид - одиночная книга игры (bookInvisibleKillerPrefab, как cntBookPile09 на столе префаба
|
||||
/// chertogi), но без хранилища и лута.
|
||||
/// </summary>
|
||||
[Preserve]
|
||||
public class BlockNecroMemoryBook : Block
|
||||
{
|
||||
public const string ReadCommand = "read";
|
||||
|
||||
static readonly BlockActivationCommand[] Commands =
|
||||
{
|
||||
new BlockActivationCommand(ReadCommand, "book", true)
|
||||
};
|
||||
|
||||
public override bool HasBlockActivationCommands(WorldBase _world, BlockValue _blockValue, Vector3i _blockPos, EntityAlive _entityFocusing)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public override BlockActivationCommand[] GetBlockActivationCommands(WorldBase _world, BlockValue _blockValue, Vector3i _blockPos, EntityAlive _entityFocusing)
|
||||
{
|
||||
return Commands;
|
||||
}
|
||||
|
||||
public override string GetActivationText(WorldBase _world, BlockValue _blockValue, Vector3i _blockPos, EntityAlive _entityFocusing)
|
||||
{
|
||||
return Localization.Get("necroMemoryBookPrompt");
|
||||
}
|
||||
|
||||
public override bool OnBlockActivated(string _commandName, WorldBase _world, Vector3i _blockPos, BlockValue _blockValue, EntityPlayerLocal _player)
|
||||
{
|
||||
return _commandName == ReadCommand && OnBlockActivated(_world, _blockPos, _blockValue, _player);
|
||||
}
|
||||
|
||||
public override bool OnBlockActivated(WorldBase _world, Vector3i _blockPos, BlockValue _blockValue, EntityPlayerLocal _player)
|
||||
{
|
||||
Open(_player);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Окно книги (оглавление, главы). Отдельным методом - его же позовёт дух в чертогах (своё E).</summary>
|
||||
public static void Open(EntityPlayerLocal _player)
|
||||
{
|
||||
LocalPlayerUI ui = LocalPlayerUI.GetUIForPlayer(_player);
|
||||
if (ui == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 03.10: вместо окна-сообщения - книга с оглавлением (NecroTome.cs).
|
||||
NecroTomeBook.Open(_player);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- автор на обложке
|
||||
|
||||
/// <summary>Шрифт надписи - тот же готический, что у названия в текстуре (UnifrakturMaguntia, OFL).</summary>
|
||||
const string FontAsset = "#@modfolder(NecromancerTome):Resources/necrobooks?UnifrakturMaguntia-Book.ttf";
|
||||
const string AuthorChild = "NecroAuthor";
|
||||
|
||||
// Место под автора - просвет под рисунком (make_rat_book_textures.py, Mind), в осях корня модели
|
||||
// necroBookMindPrefab: из Blender (x, y, z) -> Unity (-x, z, -y); книга в модели повёрнута на -65°.
|
||||
// Подбирается на ходу командой necroauthor (x y z, размер, ширина).
|
||||
public static Vector3 AuthorPos = new Vector3(0.0944f, 0.0885f, -0.1534f);
|
||||
public static Vector3 AuthorUp = new Vector3(-0.4226f, 0f, 0.9063f);
|
||||
public static float AuthorHeight = 0.030f, AuthorMaxWidth = 0.21f;
|
||||
public static Color AuthorColor = new Color(0.85f, 0.06f, 0.05f, 1f);
|
||||
|
||||
static Font _font;
|
||||
|
||||
public override void OnBlockEntityTransformAfterActivated(WorldBase _world, Vector3i _blockPos, BlockValue _blockValue, BlockEntityData _ebcd)
|
||||
{
|
||||
base.OnBlockEntityTransformAfterActivated(_world, _blockPos, _blockValue, _ebcd);
|
||||
try
|
||||
{
|
||||
if (_ebcd?.transform != null)
|
||||
{
|
||||
PlaceAuthor(_ebcd.transform);
|
||||
}
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] книга памяти: автор не встал - " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ник игрока на обложке, как подпись на ящике: TextMesh поверх модели (модель общая,
|
||||
/// а ник у каждого свой, в текстуру его не запечь). Чертоги у каждого свои - пишем ник своего игрока.</summary>
|
||||
public static void PlaceAuthor(Transform _root)
|
||||
{
|
||||
EntityPlayerLocal player = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||||
string name = player != null ? player.PlayerDisplayName : null;
|
||||
Transform old = _root.Find(AuthorChild);
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
if (old != null) Object.Destroy(old.gameObject);
|
||||
return;
|
||||
}
|
||||
if (_font == null)
|
||||
{
|
||||
_font = DataLoader.LoadAsset<Font>(FontAsset);
|
||||
}
|
||||
GameObject go = old != null ? old.gameObject : new GameObject(AuthorChild);
|
||||
go.layer = _root.gameObject.layer;
|
||||
Transform t = go.transform;
|
||||
t.SetParent(_root, false);
|
||||
t.localPosition = AuthorPos;
|
||||
// лицом вверх (обложка), низ строки - к нижнему краю обложки
|
||||
t.localRotation = Quaternion.LookRotation(Vector3.down, AuthorUp.normalized);
|
||||
t.localScale = Vector3.one;
|
||||
TextMesh tm = go.GetComponent<TextMesh>() ?? go.AddComponent<TextMesh>();
|
||||
MeshRenderer mr = go.GetComponent<MeshRenderer>();
|
||||
if (_font != null)
|
||||
{
|
||||
tm.font = _font;
|
||||
mr.sharedMaterial = _font.material;
|
||||
if (mr.sharedMaterial != null && (mr.sharedMaterial.shader == null || !mr.sharedMaterial.shader.isSupported))
|
||||
{
|
||||
Shader sh = Shader.Find("GUI/Text Shader");
|
||||
if (sh != null) mr.sharedMaterial.shader = sh;
|
||||
}
|
||||
}
|
||||
tm.text = name;
|
||||
tm.anchor = TextAnchor.MiddleCenter;
|
||||
tm.alignment = TextAlignment.Center;
|
||||
tm.fontSize = 96;
|
||||
tm.color = AuthorColor;
|
||||
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
mr.receiveShadows = false;
|
||||
tm.characterSize = 1f;
|
||||
// Размер подгоняется кадром позже: меш надписи TextMesh строит не сразу.
|
||||
NecroAuthorFit fit = go.GetComponent<NecroAuthorFit>() ?? go.AddComponent<NecroAuthorFit>();
|
||||
fit.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Подгонка надписи автора: высота строки AuthorHeight, длинный ник ужимается до AuthorMaxWidth.</summary>
|
||||
public class NecroAuthorFit : MonoBehaviour
|
||||
{
|
||||
void LateUpdate()
|
||||
{
|
||||
MeshRenderer mr = GetComponent<MeshRenderer>();
|
||||
Bounds b = mr != null ? mr.localBounds : default;
|
||||
if (b.size.y < 0.0001f)
|
||||
{
|
||||
return; // меш ещё не построен
|
||||
}
|
||||
float k = BlockNecroMemoryBook.AuthorHeight / b.size.y;
|
||||
if (b.size.x * k > BlockNecroMemoryBook.AuthorMaxWidth)
|
||||
{
|
||||
k = BlockNecroMemoryBook.AuthorMaxWidth / b.size.x;
|
||||
}
|
||||
transform.localScale = new Vector3(k, k, k);
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>necroauthor [x y z] [h N] [w N] - подобрать место и размер ника на книге памяти на ходу.</summary>
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroAuthor : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necroauthor" };
|
||||
public override string getDescription() => "NecromancerTome: ник на книге памяти - x y z | h N | w N";
|
||||
|
||||
public override void Execute(System.Collections.Generic.List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
System.Globalization.CultureInfo ci = System.Globalization.CultureInfo.InvariantCulture;
|
||||
if (_params.Count == 3)
|
||||
{
|
||||
BlockNecroMemoryBook.AuthorPos = new Vector3(float.Parse(_params[0], ci), float.Parse(_params[1], ci), float.Parse(_params[2], ci));
|
||||
}
|
||||
else if (_params.Count == 2 && _params[0] == "h")
|
||||
{
|
||||
BlockNecroMemoryBook.AuthorHeight = float.Parse(_params[1], ci);
|
||||
}
|
||||
else if (_params.Count == 2 && _params[0] == "w")
|
||||
{
|
||||
BlockNecroMemoryBook.AuthorMaxWidth = float.Parse(_params[1], ci);
|
||||
}
|
||||
int n = 0;
|
||||
foreach (NecroAuthorFit f in Object.FindObjectsOfType<NecroAuthorFit>(true))
|
||||
{
|
||||
BlockNecroMemoryBook.PlaceAuthor(f.transform.parent);
|
||||
n++;
|
||||
}
|
||||
Vector3 p = BlockNecroMemoryBook.AuthorPos;
|
||||
SdtdConsole.Instance.Output(string.Format(ci, "автор: {0:0.####} {1:0.####} {2:0.####}, h {3}, w {4}; надписей {5}",
|
||||
p.x, p.y, p.z, BlockNecroMemoryBook.AuthorHeight, BlockNecroMemoryBook.AuthorMaxWidth, n));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,9 @@ namespace NecromancerTome
|
||||
/// 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).
|
||||
///
|
||||
/// ОДЕРЖИМОСТЬ (03.10, тест: «здоровье противника не снижалось»): то же самое, если атакующий -
|
||||
/// одержимый зомби или цель - одержимый зомби (зомби бьют его, когда он их танкует).
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(EntityAlive), "DamageEntity")]
|
||||
public static class Patch_EntityAlive_DamageEntity_CharmedZombieVsZombie
|
||||
@@ -34,8 +37,8 @@ namespace NecromancerTome
|
||||
{
|
||||
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.Buffs != null && attackerZombie.Buffs.HasBuff(Patch_EntityBuffs_AddBuff_DeviatorCharm.CharmBuffName)
|
||||
|| Possession.IsPossessed(attackerZombie) || Possession.IsPossessed(__instance))
|
||||
&& (attackerZombie.entityFlags & EntityFlags.Zombie) != EntityFlags.None)
|
||||
{
|
||||
attackerZombie.entityFlags &= ~EntityFlags.Zombie;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Хитбокс наших декораций (03.10: «Ты не добавляешь к декору хитбокс? А как удалять, если не туда
|
||||
/// поставил?»). В префабах книги памяти, ножа-декора и плакатов на корне стоит BoxCollider, но игра
|
||||
/// понимает, что луч попал в блок, только по игровому тегу на объекте с коллайдером. В нашем
|
||||
/// Unity-проекте такого тега нет (AssetRipper знает его как unknown_20004) - при сборке он стал
|
||||
/// Untagged. Поэтому при первой загрузке модели корню ставим тег и слой корня ванильной рамки
|
||||
/// (postersFramedGunSchematicPrefab - у неё коллайдер тоже на корне). Префаб из бандла общий,
|
||||
/// правка одна на все экземпляры. Тот же приём, что у оссуария (GeoOssuaryPatch.CopyTags).
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(BlockShapeModelEntity), nameof(BlockShapeModelEntity.getPrefab))]
|
||||
public static class Patch_BlockShapeModelEntity_getPrefab_DecorColliderTag
|
||||
{
|
||||
const string VanillaModel = "@:Entities/Decor/postersFramedGunSchematicPrefab.prefab";
|
||||
static readonly System.Collections.Generic.HashSet<string> done = new System.Collections.Generic.HashSet<string>();
|
||||
|
||||
static bool Ours(string _name)
|
||||
{
|
||||
return _name == "necroBookMindPrefab" || _name == "necroKnifeDecorPrefab" || _name.StartsWith("necroPoster");
|
||||
}
|
||||
|
||||
public static void Postfix(Transform __result)
|
||||
{
|
||||
if (__result == null || !Ours(__result.name) || !done.Add(__result.name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Transform vanilla = DataLoader.LoadAsset<Transform>(VanillaModel);
|
||||
if (vanilla == null)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] DecorCollider: ванильная рамка не загрузилась - тег не поставлен");
|
||||
return;
|
||||
}
|
||||
__result.tag = vanilla.tag;
|
||||
__result.gameObject.layer = vanilla.gameObject.layer;
|
||||
Debug.Log("[NecromancerTome] DecorCollider: '" + __result.name + "' тег " + vanilla.tag + ", слой " + vanilla.gameObject.layer +
|
||||
", коллайдер " + (__result.GetComponent<Collider>() != null ? "есть" : "НЕТ"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
using GUI_2;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Хваты пустой руки (03.10, BACKLOG «Заклинания с руки и жесты»): на R - меню из трёх строк:
|
||||
/// обычная рука, «отрицание», «пиф-паф». Пустая рука - предмет meleeHandPlayer с DynamicMelee,
|
||||
/// меню на R игра открывает ей так же, как ножу (PlayerMoveController: HasRadial).
|
||||
///
|
||||
/// Выбор НЕ сохраняется (указание 03.10: «после перезахода будет снова обычная рука») - просто поле
|
||||
/// в памяти. Ни cvar, ни предмет не трогаем. На выходе из мира (нет мира или сменился игрок) всё
|
||||
/// сбрасывается, и ссылки на кости, снимок заморозки и цель отпускаются (HandPose.ResetAll) - чтобы
|
||||
/// за сессию ничего не копилось (указание: «не допустить утечку памяти на сохранение состояний»).
|
||||
/// В хвате рука стоит позой (HandPose: кости после аниматора + заморозка остальных), а кнопки мыши
|
||||
/// не бьют: ExecuteAction у DynamicMelee пустой руки перехвачен.
|
||||
/// ЛКМ в «отрицании» - зомби под прицелом в леденящем ужасе: бежит от игрока с ускорением и теряет
|
||||
/// его как цель (NecroFleeSnakeTask.ScareFrom). Звук - как у празднующего убийства Twitch.
|
||||
/// ЛКМ в «пиф-пафе» - одержимость зомби-гуманоида под прицелом (Possession.cs).
|
||||
/// ПКМ в хватах - ничего.
|
||||
/// </summary>
|
||||
public static class HandHolds
|
||||
{
|
||||
public const string HandItem = "meleeHandPlayer";
|
||||
public const int None = 0, Deny = 1, PifPaf = 2;
|
||||
|
||||
/// <summary>Дальность и конус выбора зомби для «отрицания».</summary>
|
||||
public const float DenyRange = 25f;
|
||||
public const float DenyConeDeg = 12f;
|
||||
/// <summary>Бежит, пока игрок не дальше этого: зрение зомби до 30 м, у Чака 40 (entityclasses.xml,
|
||||
/// SightRange) - указание 03.10: «убегать он должен дальше 40 м, чтобы терять из прицела».</summary>
|
||||
public const float DenyFarDistance = 40f;
|
||||
/// <summary>Предел бегства на случай, если застрял и не может уйти на 40 м.</summary>
|
||||
public const float DenyMaxSeconds = 25f;
|
||||
/// <summary>Скорость бегства - выше бега зомби-людей (1.5), как у медведя (указание: «дать ему ускорение»).</summary>
|
||||
public const float DenySpeed = 2.2f;
|
||||
public const float Cooldown = 0.6f;
|
||||
public const string DenySound = "twitch_celebrate";
|
||||
/// <summary>«Пиф-паф» (указание 03.10): не вселился - звон разбитого стекла, вселился - щелчок
|
||||
/// удавшейся отмычки (sounds.xml: glassdestroy, lockpick_success).</summary>
|
||||
public const string PifPafFailSound = "glassdestroy";
|
||||
public const string PifPafPossessSound = "lockpick_success";
|
||||
|
||||
/// <summary>Выбранный хват - только на время сессии в мире.</summary>
|
||||
public static int Hold;
|
||||
static int _applied = -1;
|
||||
static int _playerId = -1;
|
||||
static float _nextCast;
|
||||
|
||||
public static bool IsHand(ItemClass _item) => _item != null && _item.Name == HandItem;
|
||||
|
||||
public static int Current(EntityPlayerLocal _player)
|
||||
{
|
||||
if (_player == null || _player.Buffs == null || !IsHand(_player.inventory?.holdingItem) ||
|
||||
SnakeMode.Active || MindPalace.Active || Possession.Active || _player.IsDead())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
return Hold;
|
||||
}
|
||||
|
||||
/// <summary>Из HandPoseDriver.LateUpdate - до HandPose.Apply. Позу трогаем только при смене
|
||||
/// хвата: ручная настройка командой necropose держится до следующей смены.</summary>
|
||||
public static void Update()
|
||||
{
|
||||
EntityPlayerLocal player = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||||
int id = player != null ? player.entityId : -1;
|
||||
if (id != _playerId)
|
||||
{
|
||||
// Вышли из мира, зашли в другой или перезашли - обычная рука и пустые ссылки.
|
||||
_playerId = id;
|
||||
Hold = None;
|
||||
_applied = None;
|
||||
_target = null;
|
||||
_targetFrame = -1;
|
||||
HandPose.ResetAll();
|
||||
return;
|
||||
}
|
||||
int want = Current(player);
|
||||
if (want == _applied)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_applied = want;
|
||||
if (want == Deny)
|
||||
{
|
||||
HandPose.PresetDeny();
|
||||
}
|
||||
else if (want == PifPaf)
|
||||
{
|
||||
HandPose.PresetPifPaf();
|
||||
}
|
||||
else
|
||||
{
|
||||
HandPose.Enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void OnRadialCommand(XUiC_Radial _sender, int _commandIndex, XUiC_Radial.RadialContextAbs _context)
|
||||
{
|
||||
if (_commandIndex >= None && _commandIndex <= PifPaf)
|
||||
{
|
||||
Hold = _commandIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>ЛКМ в хвате. True - нажатие наше, удар не выполнять.</summary>
|
||||
public static void Cast(EntityPlayerLocal _player, int _hold)
|
||||
{
|
||||
if (Time.time < _nextCast)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_nextCast = Time.time + Cooldown;
|
||||
if (_hold == PifPaf)
|
||||
{
|
||||
// Одержимость (решение 03.10): «пиф-паф» по зомби-гуманоиду - управление им.
|
||||
EntityAlive victim = Target(_player);
|
||||
if (Possession.CanPossess(victim) && Possession.Enter(_player, victim))
|
||||
{
|
||||
_player.PlayOneShot(PifPafPossessSound);
|
||||
}
|
||||
else
|
||||
{
|
||||
_player.PlayOneShot(PifPafFailSound);
|
||||
Debug.Log("[NecromancerTome] HandHolds: пиф-паф - " + (victim == null ? "в прицеле нет зомби" : victim.EntityName + " не гуманоид"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
_player.PlayOneShot(DenySound);
|
||||
World world = _player.world;
|
||||
if (world.IsRemote())
|
||||
{
|
||||
return; // ИИ живёт на сервере; клиент в мультиплеере - решать отдельно
|
||||
}
|
||||
EntityAlive target = Target(_player);
|
||||
if (target != null)
|
||||
{
|
||||
if (target.HasAnyTags(FlyerTag))
|
||||
{
|
||||
NecroFleeSnakeTask.ForgetNow(target, _player); // летает - наземного бегства нет, только теряет цель
|
||||
Debug.Log("[NecromancerTome] HandHolds: стервятник " + target.entityId + " отрицание - потерял цель");
|
||||
}
|
||||
else
|
||||
{
|
||||
NecroFleeSnakeTask.ScareFrom(target, _player, DenyMaxSeconds, DenySpeed, DenyFarDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static readonly FastTags<TagGroup.Global> ZombieTag = FastTags<TagGroup.Global>.Parse("zombie");
|
||||
static readonly FastTags<TagGroup.Global> FlyerTag = FastTags<TagGroup.Global>.Parse("vulture");
|
||||
|
||||
/// <summary>Только зомби (указание 03.10: «кабаны и змеи хоть и противник, но не зомби, кроме
|
||||
/// зомбокабана»; «стервятники тоже зомби, обычных в игре нету») - метка zombie в entityclasses.xml:
|
||||
/// люди-зомби, зомби-собаки, лютые волки, зомби-медведи, зомби-кабаны, стервятники.</summary>
|
||||
public static bool IsZombie(EntityAlive _e)
|
||||
{
|
||||
return _e != null && _e.HasAnyTags(ZombieTag);
|
||||
}
|
||||
|
||||
static int _targetFrame = -1;
|
||||
static EntityAlive _target;
|
||||
|
||||
/// <summary>Зомби под прицелом (раз за кадр): и для заклинания, и для красного прицела.</summary>
|
||||
public static EntityAlive Target(EntityPlayerLocal _player)
|
||||
{
|
||||
if (_targetFrame != Time.frameCount)
|
||||
{
|
||||
_targetFrame = Time.frameCount;
|
||||
_target = FindTarget(_player.world, _player);
|
||||
}
|
||||
return _target;
|
||||
}
|
||||
|
||||
/// <summary>Прямая видимость своим лучом: между глазом и точкой нет блоков и земли (сущности на луче
|
||||
/// не мешают). Не EntityAlive.CanSee: у игрока нет ИИ, и его кэш видимости (seeCache) не обновляется -
|
||||
/// тест 03.10: Арлин захватывалась, зомби-коп нет.</summary>
|
||||
static bool Visible(Vector3 _eye, Vector3 _to)
|
||||
{
|
||||
Vector3 d = _to - _eye;
|
||||
return !Physics.Raycast(_eye - Origin.position, d.normalized, d.magnitude - 0.3f, SnakeControl.WallMask, QueryTriggerInteraction.Ignore);
|
||||
}
|
||||
|
||||
/// <summary>Зомби ближе всех к линии прицела в конусе DenyConeDeg, не дальше DenyRange,
|
||||
/// с прямой видимостью.</summary>
|
||||
static EntityAlive FindTarget(World _world, EntityPlayerLocal _player)
|
||||
{
|
||||
Transform cam = _player.cameraTransform;
|
||||
if (cam == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Vector3 eye = cam.position + Origin.position;
|
||||
Vector3 fwd = cam.forward;
|
||||
EntityAlive best = null;
|
||||
float bestAngle = DenyConeDeg;
|
||||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||||
{
|
||||
EntityAlive e = _world.EntityAlives[i];
|
||||
if (e == null || e.IsDead() || !IsZombie(e))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Vector3 to = e.getChestPosition() - eye;
|
||||
float d = to.magnitude;
|
||||
if (d > DenyRange || d < 0.1f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
float angle = Vector3.Angle(fwd, to);
|
||||
if (angle < bestAngle && Visible(eye, e.getChestPosition()))
|
||||
{
|
||||
bestAngle = angle;
|
||||
best = e;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Меню на R у пустой руки - как у ножа (Patch_ItemAction_HasRadial_SnakeKnife).</summary>
|
||||
[HarmonyPatch(typeof(ItemAction), nameof(ItemAction.HasRadial))]
|
||||
public static class Patch_ItemAction_HasRadial_HandHolds
|
||||
{
|
||||
public static void Postfix(ItemAction __instance, ref bool __result)
|
||||
{
|
||||
if (!__result && !SnakeMode.Active && !MindPalace.Active && HandHolds.IsHand(__instance.item))
|
||||
{
|
||||
__result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(ItemAction), nameof(ItemAction.SetupRadial))]
|
||||
public static class Patch_ItemAction_SetupRadial_HandHolds
|
||||
{
|
||||
public static bool Prefix(ItemAction __instance, XUiC_Radial _xuiRadialWindow, EntityPlayerLocal _epl)
|
||||
{
|
||||
if (!HandHolds.IsHand(__instance.item))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
int cur = HandHolds.Hold;
|
||||
_xuiRadialWindow.ResetRadialEntries();
|
||||
_xuiRadialWindow.CreateRadialEntry(HandHolds.None, "ui_game_symbol_hand", "UIAtlas", "",
|
||||
Localization.Get("necroHoldNone"), cur == HandHolds.None);
|
||||
_xuiRadialWindow.CreateRadialEntry(HandHolds.Deny, "ui_game_symbol_run", "UIAtlas", "",
|
||||
Localization.Get("necroHoldDeny"), cur == HandHolds.Deny);
|
||||
_xuiRadialWindow.CreateRadialEntry(HandHolds.PifPaf, "ui_game_symbol_pistol", "UIAtlas", "",
|
||||
Localization.Get("necroHoldPifPaf"), cur == HandHolds.PifPaf);
|
||||
_xuiRadialWindow.SetCommonData(UIUtils.GetButtonIconForAction(_epl.playerInput.Reload),
|
||||
HandHolds.OnRadialCommand, null, cur);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>В хвате кнопки мыши не бьют: ЛКМ - заклинание хвата, ПКМ - ничего (03.10: «руки
|
||||
/// деформируются, пытаясь совершить старую анимацию»).</summary>
|
||||
[HarmonyPatch(typeof(ItemActionDynamicMelee), nameof(ItemActionDynamicMelee.ExecuteAction))]
|
||||
public static class Patch_ItemActionDynamicMelee_ExecuteAction_HandHolds
|
||||
{
|
||||
public static bool Prefix(ItemActionData _actionData, bool _bReleased)
|
||||
{
|
||||
if (!(_actionData?.invData?.holdingEntity is EntityPlayerLocal player))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
int hold = HandHolds.Current(player);
|
||||
if (hold == HandHolds.None || !HandHolds.IsHand(_actionData.invData.item))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!_bReleased && _actionData.indexInEntityOfAction == 0)
|
||||
{
|
||||
HandHolds.Cast(player, hold);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Прицел краснеет, когда в нём зомби, в обоих хватах (указание 03.10). Цвет прицела игра
|
||||
/// берёт из EntityPlayerLocal.crosshairColors[настройка] - на время рисования подменяем его красным.</summary>
|
||||
[HarmonyPatch(typeof(EntityPlayerLocal), nameof(EntityPlayerLocal.guiDrawCrosshair))]
|
||||
public static class Patch_EntityPlayerLocal_guiDrawCrosshair_HandHolds
|
||||
{
|
||||
static readonly Color Red = new Color(1f, 0.15f, 0.1f);
|
||||
|
||||
public static void Prefix(EntityPlayerLocal __instance, out Color? __state)
|
||||
{
|
||||
__state = null;
|
||||
int hold = HandHolds.Current(__instance);
|
||||
EntityAlive target = hold == HandHolds.None ? null : HandHolds.Target(__instance);
|
||||
// «Пиф-паф» берёт только зомби-гуманоидов - на зверях-зомби прицел не краснеет.
|
||||
if (target == null || hold == HandHolds.PifPaf && !Possession.CanPossess(target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
int i = GamePrefs.GetInt(EnumGamePrefs.OptionsCrosshairColor);
|
||||
if (i < 0 || i >= EntityPlayerLocal.crosshairColors.Count)
|
||||
{
|
||||
i = 0;
|
||||
}
|
||||
__state = EntityPlayerLocal.crosshairColors[i];
|
||||
EntityPlayerLocal.crosshairColors[i] = Red;
|
||||
}
|
||||
|
||||
public static void Postfix(Color? __state)
|
||||
{
|
||||
if (__state == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int i = GamePrefs.GetInt(EnumGamePrefs.OptionsCrosshairColor);
|
||||
if (i < 0 || i >= EntityPlayerLocal.crosshairColors.Count)
|
||||
{
|
||||
i = 0;
|
||||
}
|
||||
EntityPlayerLocal.crosshairColors[i] = __state.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// necrobones - разведка к жестам (BACKLOG «Заклинания с руки и жесты», 1.7.0). Свой хват -
|
||||
/// это неподвижная поза рук от первого лица, как у браслета (кулак к противнику, без движения
|
||||
/// при действии): кости рига ставятся в нужную комбинацию после аниматора. Сначала надо знать
|
||||
/// кости: команда пишет в лог дерево рига рук от первого лица (EModelSDCS.baseRigFP, его создаёт
|
||||
/// SDCSUtils.CreateVizFP под Camera/UFPSRoot) с локальными поворотами и позициями, и параметры
|
||||
/// аниматора FPPlayerController (WeaponHoldType и прочие) с текущими значениями.
|
||||
/// </summary>
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroBones : ConsoleCmdAbstract
|
||||
{
|
||||
const string Tag = "[NecromancerTome] necrobones: ";
|
||||
|
||||
public override string[] getCommands() => new[] { "necrobones" };
|
||||
public override string getDescription() => "NecromancerTome: дерево костей рук от первого лица и параметры аниматора - в лог";
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
EntityPlayerLocal player = GameManager.Instance.World?.GetPrimaryPlayer();
|
||||
if (player == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!(player.emodel is EModelSDCS sdcs) || sdcs.baseRigFP == null)
|
||||
{
|
||||
SdtdConsole.Instance.Output("риг рук от первого лица не найден (EModelSDCS.baseRigFP)");
|
||||
return;
|
||||
}
|
||||
Transform root = sdcs.baseRigFP.transform;
|
||||
ItemClass held = player.inventory?.holdingItem;
|
||||
Debug.Log(Tag + "в руке " + (held != null ? held.GetItemName() + ", HoldType " + held.HoldType.Value : "ничего") + ", вид от первого лица " + sdcs.IsFPV);
|
||||
Debug.Log(Tag + "путь рига: " + PathOf(root));
|
||||
int count = 0;
|
||||
var sb = new StringBuilder();
|
||||
Dump(root, 0, sb, ref count);
|
||||
foreach (string line in sb.ToString().Split('\n'))
|
||||
{
|
||||
if (line.Length > 0)
|
||||
{
|
||||
Debug.Log(Tag + line);
|
||||
}
|
||||
}
|
||||
Animator anim = root.GetComponentInChildren<Animator>(true) ?? root.GetComponentInParent<Animator>();
|
||||
if (anim != null)
|
||||
{
|
||||
Debug.Log(Tag + "аниматор на " + anim.name + ", контроллер " + (anim.runtimeAnimatorController != null ? anim.runtimeAnimatorController.name : "нет"));
|
||||
foreach (AnimatorControllerParameter p in anim.parameters)
|
||||
{
|
||||
string v = p.type == AnimatorControllerParameterType.Float ? anim.GetFloat(p.nameHash).ToString("0.###")
|
||||
: p.type == AnimatorControllerParameterType.Int ? anim.GetInteger(p.nameHash).ToString()
|
||||
: p.type == AnimatorControllerParameterType.Bool ? anim.GetBool(p.nameHash).ToString()
|
||||
: "trigger";
|
||||
Debug.Log(Tag + " параметр " + p.name + " (" + p.type + ") = " + v);
|
||||
}
|
||||
for (int l = 0; l < anim.layerCount; l++)
|
||||
{
|
||||
AnimatorClipInfo[] clips = anim.GetCurrentAnimatorClipInfo(l);
|
||||
string names = "";
|
||||
foreach (AnimatorClipInfo c in clips)
|
||||
{
|
||||
names += (names.Length > 0 ? ", " : "") + (c.clip != null ? c.clip.name : "?") + " x" + c.weight.ToString("0.##");
|
||||
}
|
||||
Debug.Log(Tag + " слой " + l + " " + anim.GetLayerName(l) + " вес " + anim.GetLayerWeight(l).ToString("0.##") + ": " + names);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log(Tag + "аниматор не найден");
|
||||
}
|
||||
SdtdConsole.Instance.Output("костей рук: " + count + " - дерево и параметры аниматора в логе (строки necrobones)");
|
||||
}
|
||||
|
||||
static void Dump(Transform _t, int _depth, StringBuilder _sb, ref int _count)
|
||||
{
|
||||
_count++;
|
||||
Vector3 e = _t.localEulerAngles;
|
||||
Vector3 p = _t.localPosition;
|
||||
_sb.Append(new string(' ', _depth * 2)).Append(_t.name)
|
||||
.Append(_t.gameObject.activeSelf ? "" : " [выкл]")
|
||||
.Append(" пов. ").Append(e.x.ToString("0.#")).Append(' ').Append(e.y.ToString("0.#")).Append(' ').Append(e.z.ToString("0.#"))
|
||||
.Append(" поз. ").Append(p.x.ToString("0.###")).Append(' ').Append(p.y.ToString("0.###")).Append(' ').Append(p.z.ToString("0.###"))
|
||||
.Append('\n');
|
||||
for (int i = 0; i < _t.childCount; i++)
|
||||
{
|
||||
Dump(_t.GetChild(i), _depth + 1, _sb, ref _count);
|
||||
}
|
||||
}
|
||||
|
||||
static string PathOf(Transform _t)
|
||||
{
|
||||
string s = _t.name;
|
||||
for (Transform p = _t.parent; p != null; p = p.parent)
|
||||
{
|
||||
s = p.name + "/" + s;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Свой хват рук от первого лица (BACKLOG «Заклинания с руки и жесты», 1.7.0). Хват - неподвижная
|
||||
/// поза: после аниматора (LateUpdate) кости рига EModelSDCS.baseRigFP ставятся в заданные локальные
|
||||
/// повороты. Аниматор игры не трогаем. Снимки necrobones 03.10: кулак браслета (FP_Block_Hold,
|
||||
/// HoldType 7) - суставы пальцев повёрнуты по локальной Z на 77-92°, у пустой руки 8-38°;
|
||||
/// направление руки - RightArm, RightArmRoll, RightForeArm (сгиб по Y), RightHand.
|
||||
/// Пока - инструмент подбора позы (команда necropose); готовую позу потом зашить как хват.
|
||||
/// </summary>
|
||||
public static class HandPose
|
||||
{
|
||||
public static bool Enabled;
|
||||
public static readonly Dictionary<string, Vector3> Overrides = new Dictionary<string, Vector3>();
|
||||
|
||||
static GameObject _rig;
|
||||
static readonly Dictionary<string, Transform> _bones = new Dictionary<string, Transform>();
|
||||
static GameObject _host;
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
if (_host != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_host = new GameObject("NecroHandPose");
|
||||
Object.DontDestroyOnLoad(_host);
|
||||
_host.AddComponent<HandPoseDriver>();
|
||||
}
|
||||
|
||||
public static Transform Bone(string _name)
|
||||
{
|
||||
EntityPlayerLocal player = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||||
GameObject rig = (player?.emodel as EModelSDCS)?.baseRigFP;
|
||||
if (rig == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (rig != _rig)
|
||||
{
|
||||
_rig = rig;
|
||||
_bones.Clear();
|
||||
foreach (Transform t in rig.GetComponentsInChildren<Transform>(true))
|
||||
{
|
||||
if (!_bones.ContainsKey(t.name))
|
||||
{
|
||||
_bones[t.name] = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_bones.TryGetValue(_name, out Transform b) && b == null)
|
||||
{
|
||||
_rig = null; // кость уничтожена (риг пересобран) - перечитать в следующий раз
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
/// <summary>Кости, повёрнутые относительно КАМЕРЫ, а не родителя: ставятся после Overrides
|
||||
/// (03.10: локальные углы кисти «плывут» при каждом изменении локтя - подбор вслепую).</summary>
|
||||
public static readonly Dictionary<string, Quaternion> CamOverrides = new Dictionary<string, Quaternion>();
|
||||
|
||||
public static Transform Camera()
|
||||
{
|
||||
return GameManager.Instance?.World?.GetPrimaryPlayer()?.cameraTransform;
|
||||
}
|
||||
|
||||
/// <summary>Заморозка (03.10: «при нажатии на кнопки руки деформируются, пытаясь совершить старую
|
||||
/// анимацию»). Поза задаёт 20-30 костей, а удар двигает и остальные - позвоночник, ключицы, вспомогательные
|
||||
/// кости предплечья и кожи (*_sup_*, *Roll*). Поэтому при включении хвата в покое снимается локальное
|
||||
/// положение ВСЕХ костей под Hips, и каждый кадр они возвращаются на место, а поверх - поза.
|
||||
/// Снимок только в покое аниматора (ItemActionIndex = -1), иначе застыл бы кадр удара.</summary>
|
||||
public static bool Freeze = true;
|
||||
static readonly List<Transform> _frozen = new List<Transform>();
|
||||
static readonly List<Vector3> _frozenPos = new List<Vector3>();
|
||||
static readonly List<Quaternion> _frozenRot = new List<Quaternion>();
|
||||
static GameObject _frozenRig;
|
||||
static readonly int ItemActionIndexHash = Animator.StringToHash("ItemActionIndex");
|
||||
|
||||
/// <summary>Выход из мира: всё в исходное, ссылки на кости рига отпустить (HandHolds.Update).</summary>
|
||||
public static void ResetAll()
|
||||
{
|
||||
Enabled = false;
|
||||
Overrides.Clear();
|
||||
CamOverrides.Clear();
|
||||
Unfreeze();
|
||||
_bones.Clear();
|
||||
_rig = null;
|
||||
}
|
||||
|
||||
public static void Unfreeze()
|
||||
{
|
||||
_frozen.Clear();
|
||||
_frozenPos.Clear();
|
||||
_frozenRot.Clear();
|
||||
_frozenRig = null;
|
||||
}
|
||||
|
||||
static void FreezeBones()
|
||||
{
|
||||
Transform hips = Bone("Hips");
|
||||
if (hips == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_frozenRig != _rig || _frozen.Count == 0 || _frozen[0] == null)
|
||||
{
|
||||
Unfreeze();
|
||||
Animator anim = _rig.GetComponent<Animator>();
|
||||
if (anim != null && (anim.GetInteger(ItemActionIndexHash) != -1 || anim.IsInTransition(0)))
|
||||
{
|
||||
return; // идёт действие - снимок позже, в покое
|
||||
}
|
||||
foreach (Transform t in hips.GetComponentsInChildren<Transform>(true))
|
||||
{
|
||||
_frozen.Add(t);
|
||||
_frozenPos.Add(t.localPosition);
|
||||
_frozenRot.Add(t.localRotation);
|
||||
}
|
||||
_frozenRig = _rig;
|
||||
Debug.Log("[NecromancerTome] HandPose: заморожено костей " + _frozen.Count);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < _frozen.Count; i++)
|
||||
{
|
||||
Transform t = _frozen[i];
|
||||
if (t != null)
|
||||
{
|
||||
t.localPosition = _frozenPos[i];
|
||||
t.localRotation = _frozenRot[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Apply()
|
||||
{
|
||||
if (!Enabled || Overrides.Count == 0 && CamOverrides.Count == 0)
|
||||
{
|
||||
if (_frozen.Count > 0)
|
||||
{
|
||||
Unfreeze();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Freeze)
|
||||
{
|
||||
FreezeBones();
|
||||
}
|
||||
foreach (var kv in Overrides)
|
||||
{
|
||||
Transform b = Bone(kv.Key);
|
||||
if (b != null)
|
||||
{
|
||||
b.localRotation = Quaternion.Euler(kv.Value);
|
||||
}
|
||||
}
|
||||
Transform cam = CamOverrides.Count > 0 ? Camera() : null;
|
||||
if (cam != null)
|
||||
{
|
||||
foreach (var kv in CamOverrides)
|
||||
{
|
||||
Transform b = Bone(kv.Key);
|
||||
if (b != null)
|
||||
{
|
||||
b.rotation = cam.rotation * kv.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Кисть в осях камеры. Оси кисти правой руки (лог necrobones 03.10): пальцы - локальная +X
|
||||
/// (суставы пальцев лежат по +X), ладонь - +Y (сгиб пальцев +Z ведёт +X к +Y), +Z - к мизинцу,
|
||||
/// большой палец - к -Z. Нулевая поза: пальцы вверх, ладонь от игрока, тыльная сторона к камере,
|
||||
/// большой палец влево. tilt - наклон пальцев влево (+) вокруг взгляда; back - кисть «от себя»
|
||||
/// (+, верх пальцев уходит вперёд); turn - разворот кисти вокруг вертикали (+ ладонью вправо).</summary>
|
||||
public static Quaternion PalmToCamera(float _tilt, float _back, float _turn)
|
||||
{
|
||||
Quaternion basis = Quaternion.LookRotation(Vector3.right, Vector3.forward);
|
||||
return Quaternion.AngleAxis(_turn, Vector3.up) * Quaternion.AngleAxis(_back, Vector3.right) * Quaternion.AngleAxis(_tilt, Vector3.forward) * basis;
|
||||
}
|
||||
|
||||
static readonly string[] Fingers = { "Index", "Middle", "Ring", "Pinky" };
|
||||
|
||||
/// <summary>Все суставы четырёх пальцев: сгиб по Z (curl), веер по Y у первого сустава
|
||||
/// (указательный +spread, средний +spread/3, безымянный -spread/3, мизинец -spread).</summary>
|
||||
public static void SetFingers(string _side, float _curl, float _spread)
|
||||
{
|
||||
float[] fan = { 1f, 1f / 3f, -1f / 3f, -1f };
|
||||
for (int f = 0; f < Fingers.Length; f++)
|
||||
{
|
||||
for (int j = 1; j <= 3; j++)
|
||||
{
|
||||
Overrides[_side + "Hand" + Fingers[f] + j] = new Vector3(0f, j == 1 ? _spread * fan[f] : 0f, _curl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Заготовка «раскрытая ладонь»: рука - как у браслета (снимок 03.10), пальцы прямые.</summary>
|
||||
public static void PresetOpen()
|
||||
{
|
||||
Overrides.Clear();
|
||||
Overrides["RightArm"] = new Vector3(330.8f, 34.9f, 24.3f);
|
||||
Overrides["RightArmRoll"] = new Vector3(349.7f, 0f, 0f);
|
||||
Overrides["RightForeArm"] = new Vector3(0f, 88.4f, 0f);
|
||||
Overrides["RightHand"] = new Vector3(13.3f, 325.8f, 7.7f);
|
||||
SetFingers("Right", 3f, 0f);
|
||||
Overrides["RightHandThumb1"] = new Vector3(55f, 68.6f, 40.6f);
|
||||
Overrides["RightHandThumb2"] = new Vector3(0f, 0f, 0f);
|
||||
Overrides["RightHandThumb3"] = new Vector3(0f, 0f, 0f);
|
||||
Enabled = true;
|
||||
}
|
||||
|
||||
/// <summary>Левая рука вдоль туловища, вне кадра (общая для хватов «отрицание» и «пиф-паф», 03.10).</summary>
|
||||
static void LeftArmDown()
|
||||
{
|
||||
Overrides["LeftArm"] = new Vector3(30f, 13.6f, 57.3f);
|
||||
}
|
||||
|
||||
/// <summary>Хват «отрицание» (утверждён пользователем 03.10): рука вперёд, как у браслета, ниже прицела,
|
||||
/// кисть в линию с предплечьем, пальцы раскрыты.</summary>
|
||||
public static void PresetDeny()
|
||||
{
|
||||
Overrides.Clear();
|
||||
CamOverrides.Clear();
|
||||
Overrides["RightArm"] = new Vector3(345.8f, 34.9f, 24.3f);
|
||||
Overrides["RightArmRoll"] = new Vector3(349.7f, 0f, 0f);
|
||||
Overrides["RightForeArm"] = new Vector3(0f, 88.4f, 0f);
|
||||
Overrides["RightHand"] = new Vector3(45f, 0f, 0f);
|
||||
SetFingers("Right", 3f, 12f);
|
||||
Overrides["RightHandThumb1"] = new Vector3(55f, 68.6f, 40.6f);
|
||||
Overrides["RightHandThumb2"] = new Vector3(0f, 0f, 0f);
|
||||
Overrides["RightHandThumb3"] = new Vector3(0f, 0f, 0f);
|
||||
LeftArmDown();
|
||||
Unfreeze();
|
||||
Enabled = true;
|
||||
}
|
||||
|
||||
/// <summary>Хват «пиф-паф» (утверждён 03.10, «Стоп. Замри»): пистолет из пальцев - указательный и средний
|
||||
/// сомкнуты и смотрят к прицелу, безымянный и мизинец поджаты, большой вверх. Кисть - от камеры.</summary>
|
||||
public static void PresetPifPaf()
|
||||
{
|
||||
Overrides.Clear();
|
||||
CamOverrides.Clear();
|
||||
Overrides["RightArm"] = new Vector3(330.8f, 34.9f, 24.3f);
|
||||
Overrides["RightArmRoll"] = new Vector3(349.7f, 0f, 0f);
|
||||
Overrides["RightForeArm"] = new Vector3(0f, 88.4f, 0f);
|
||||
CamOverrides["RightHand"] = PalmToCamera(-90f, 0f, -119f);
|
||||
for (int j = 1; j <= 3; j++)
|
||||
{
|
||||
Overrides["RightHandIndex" + j] = new Vector3(0f, j == 1 ? -3f : 0f, 3f);
|
||||
Overrides["RightHandMiddle" + j] = new Vector3(0f, j == 1 ? 2f : 0f, 3f);
|
||||
float curl = j == 3 ? 55f : 80f;
|
||||
Overrides["RightHandRing" + j] = new Vector3(0f, 0f, curl);
|
||||
Overrides["RightHandPinky" + j] = new Vector3(0f, 0f, curl);
|
||||
}
|
||||
Overrides["RightHandThumb1"] = new Vector3(55f, 68.6f, 20f);
|
||||
Overrides["RightHandThumb2"] = new Vector3(0f, 0f, 0f);
|
||||
Overrides["RightHandThumb3"] = new Vector3(0f, 0f, 0f);
|
||||
LeftArmDown();
|
||||
Unfreeze();
|
||||
Enabled = true;
|
||||
}
|
||||
|
||||
public static string F(float _v) => _v.ToString("0.#", CultureInfo.InvariantCulture);
|
||||
public static string F(Vector3 _v) => F(_v.x) + " " + F(_v.y) + " " + F(_v.z);
|
||||
}
|
||||
|
||||
public class HandPoseDriver : MonoBehaviour
|
||||
{
|
||||
void LateUpdate()
|
||||
{
|
||||
HandHolds.Update();
|
||||
HandPose.Apply();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// necropose - подбор позы рук от первого лица:
|
||||
/// on | off | clear | open (заготовка: ладонь, рука как у браслета)
|
||||
/// <Кость> x y z - локальный поворот кости (градусы)
|
||||
/// add <Кость> dx dy dz - добавить к текущему (заданному или от аниматора)
|
||||
/// fingers Right|Left сгиб [веер] - все суставы четырёх пальцев
|
||||
/// show <Кость> - текущий поворот кости
|
||||
/// save - вся поза в лог строками кода
|
||||
/// </summary>
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroPose : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necropose" };
|
||||
public override string getDescription() => "NecromancerTome: поза рук от первого лица - on|off|clear|deny|pifpaf|freeze [off]|open|<кость> x y z|aim <кость> наклон от_себя разворот|unaim <кость>|add <кость> dx dy dz|fingers Right|Left сгиб [веер]|show <кость>|save";
|
||||
|
||||
static bool Num(string _s, out float _v) => float.TryParse(_s.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out _v);
|
||||
|
||||
void Out(string _s)
|
||||
{
|
||||
SdtdConsole.Instance.Output(_s);
|
||||
Debug.Log("[NecromancerTome] necropose: " + _s);
|
||||
}
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
string cmd = _params.Count > 0 ? _params[0] : "";
|
||||
switch (cmd)
|
||||
{
|
||||
case "on":
|
||||
HandPose.Enabled = true;
|
||||
Out("поза включена, костей " + HandPose.Overrides.Count);
|
||||
return;
|
||||
case "off":
|
||||
HandPose.Enabled = false;
|
||||
Out("поза выключена");
|
||||
return;
|
||||
case "clear":
|
||||
HandPose.Overrides.Clear();
|
||||
HandPose.CamOverrides.Clear();
|
||||
Out("поза очищена");
|
||||
return;
|
||||
case "deny":
|
||||
HandPose.PresetDeny();
|
||||
Out("хват «отрицание»");
|
||||
return;
|
||||
case "pifpaf":
|
||||
HandPose.PresetPifPaf();
|
||||
Out("хват «пиф-паф»");
|
||||
return;
|
||||
case "freeze":
|
||||
HandPose.Freeze = _params.Count < 2 || _params[1] != "off";
|
||||
HandPose.Unfreeze();
|
||||
Out("заморозка остальных костей " + (HandPose.Freeze ? "включена (снимок - в покое)" : "выключена"));
|
||||
return;
|
||||
case "open":
|
||||
HandPose.PresetOpen();
|
||||
Out("заготовка «ладонь» включена, костей " + HandPose.Overrides.Count);
|
||||
return;
|
||||
case "save":
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var kv in HandPose.Overrides)
|
||||
{
|
||||
Out("Overrides[\"" + kv.Key + "\"] = new Vector3(" + HandPose.F(kv.Value.x) + "f, " + HandPose.F(kv.Value.y) + "f, " + HandPose.F(kv.Value.z) + "f);");
|
||||
}
|
||||
foreach (var kv in HandPose.CamOverrides)
|
||||
{
|
||||
Transform cb = HandPose.Bone(kv.Key);
|
||||
if (cb != null)
|
||||
{
|
||||
Out("Overrides[\"" + kv.Key + "\"] = new Vector3(" + HandPose.F(cb.localEulerAngles.x) + "f, " + HandPose.F(cb.localEulerAngles.y) + "f, " + HandPose.F(cb.localEulerAngles.z) + "f); // от камеры (aim)");
|
||||
}
|
||||
}
|
||||
Out("всего костей " + HandPose.Overrides.Count + " + от камеры " + HandPose.CamOverrides.Count + ", включена " + HandPose.Enabled);
|
||||
return;
|
||||
}
|
||||
case "show":
|
||||
{
|
||||
if (_params.Count < 2)
|
||||
{
|
||||
break;
|
||||
}
|
||||
Transform b = HandPose.Bone(_params[1]);
|
||||
Out(b == null ? "нет кости " + _params[1] : _params[1] + " = " + HandPose.F(b.localEulerAngles) + (HandPose.Overrides.ContainsKey(_params[1]) ? " (задана)" : " (от аниматора)"));
|
||||
return;
|
||||
}
|
||||
case "fingers":
|
||||
{
|
||||
if (_params.Count >= 3 && (_params[1] == "Right" || _params[1] == "Left") && Num(_params[2], out float curl))
|
||||
{
|
||||
float spread = 0f;
|
||||
if (_params.Count >= 4)
|
||||
{
|
||||
Num(_params[3], out spread);
|
||||
}
|
||||
HandPose.SetFingers(_params[1], curl, spread);
|
||||
HandPose.Enabled = true;
|
||||
Out("пальцы " + _params[1] + ": сгиб " + HandPose.F(curl) + ", веер " + HandPose.F(spread));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "aim":
|
||||
{
|
||||
// aim <Кость> [наклон] [от_себя] [разворот] - кисть в осях камеры (см. HandPose.PalmToCamera)
|
||||
if (_params.Count >= 2)
|
||||
{
|
||||
float tilt = 0f, back = 0f, turn = 0f;
|
||||
if (_params.Count >= 3) Num(_params[2], out tilt);
|
||||
if (_params.Count >= 4) Num(_params[3], out back);
|
||||
if (_params.Count >= 5) Num(_params[4], out turn);
|
||||
if (HandPose.Bone(_params[1]) == null)
|
||||
{
|
||||
Out("нет кости " + _params[1]);
|
||||
return;
|
||||
}
|
||||
HandPose.Overrides.Remove(_params[1]);
|
||||
HandPose.CamOverrides[_params[1]] = HandPose.PalmToCamera(tilt, back, turn);
|
||||
HandPose.Enabled = true;
|
||||
Out(_params[1] + " от камеры: наклон " + HandPose.F(tilt) + ", от себя " + HandPose.F(back) + ", разворот " + HandPose.F(turn));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "unaim":
|
||||
{
|
||||
if (_params.Count >= 2 && HandPose.CamOverrides.Remove(_params[1]))
|
||||
{
|
||||
Out(_params[1] + " - снова от родителя (от аниматора или заданная)");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "add":
|
||||
{
|
||||
if (_params.Count >= 5 && Num(_params[2], out float dx) && Num(_params[3], out float dy) && Num(_params[4], out float dz))
|
||||
{
|
||||
string name = _params[1];
|
||||
Transform b = HandPose.Bone(name);
|
||||
if (b == null)
|
||||
{
|
||||
Out("нет кости " + name);
|
||||
return;
|
||||
}
|
||||
Vector3 cur = HandPose.Overrides.TryGetValue(name, out Vector3 v) ? v : b.localEulerAngles;
|
||||
HandPose.Overrides[name] = cur + new Vector3(dx, dy, dz);
|
||||
HandPose.Enabled = true;
|
||||
Out(name + " = " + HandPose.F(HandPose.Overrides[name]));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
if (_params.Count >= 4 && Num(_params[1], out float x) && Num(_params[2], out float y) && Num(_params[3], out float z))
|
||||
{
|
||||
if (HandPose.Bone(cmd) == null)
|
||||
{
|
||||
Out("нет кости " + cmd);
|
||||
return;
|
||||
}
|
||||
HandPose.CamOverrides.Remove(cmd);
|
||||
HandPose.Overrides[cmd] = new Vector3(x, y, z);
|
||||
HandPose.Enabled = true;
|
||||
Out(cmd + " = " + HandPose.F(HandPose.Overrides[cmd]));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
Out(getDescription());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Дух в чертогах разума - камера без сущности (указание 03.10: «тело можно вообще не делать,
|
||||
/// нужен только прицел»). Ходит по полу как человек: столкновения - по тем же слоям, что у змеи
|
||||
/// (SnakeControl.WallMask / TerrainMask), гравитация и ступенька. Взаимодействие одно - книга
|
||||
/// памяти по E; остальное в чертогах неприкосновенно. Камеру ставят патчи камеры режима змеи
|
||||
/// (SnakeControl.cs), звук и мир вокруг считаются от духа через SnakeAudioSwap.
|
||||
///
|
||||
/// СОН (указание: «тёплый фильтр, это как бы сон», эффекты - на моё усмотрение): готовые эффекты
|
||||
/// экрана игры - Hot (тёплый оттенок), Blur (мягкое размытие), Drunk (медленное покачивание).
|
||||
/// Силы крутятся командой necrodream на ходу.
|
||||
/// </summary>
|
||||
public static class MindControl
|
||||
{
|
||||
public const float EyeHeight = 1.6f;
|
||||
public const float Radius = 0.3f;
|
||||
public const float Height = 1.75f;
|
||||
public const float WalkSpeed = 2.2f;
|
||||
public const float RunSpeed = 3.6f;
|
||||
public const float Gravity = 20f;
|
||||
public const float JumpSpeed = 6f;
|
||||
public const float StepUp = 0.6f;
|
||||
public const float ReachDistance = 3f;
|
||||
|
||||
public static float Hot = 0.45f;
|
||||
public static float Blur = 0.25f;
|
||||
public static float Drunk = 0.15f;
|
||||
|
||||
public static bool Controlling;
|
||||
static Vector3 _feet;
|
||||
static Vector3 _spawn;
|
||||
static float _yaw, _pitch, _vy;
|
||||
static bool _grounded;
|
||||
public static bool HasFocus;
|
||||
static EntityPlayerLocal _player;
|
||||
|
||||
public static Vector3 FeetPos => _feet;
|
||||
|
||||
public static void Begin(EntityPlayerLocal _p, Vector3 _at, float _atYaw)
|
||||
{
|
||||
_player = _p;
|
||||
_feet = _at;
|
||||
_spawn = _at;
|
||||
_yaw = _atYaw;
|
||||
_pitch = 0f;
|
||||
_vy = 0f;
|
||||
_grounded = false;
|
||||
HasFocus = false;
|
||||
Controlling = true;
|
||||
_p.PlayerUI?.windowManager.Close(SnakeControl.ToolbeltWindow);
|
||||
Dream(_p, true);
|
||||
Debug.Log("[NecromancerTome] MindControl: дух в чертогах, " + _at);
|
||||
}
|
||||
|
||||
public static void End(EntityPlayerLocal _p)
|
||||
{
|
||||
if (!Controlling)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Controlling = false;
|
||||
HasFocus = false;
|
||||
Dream(_p ?? _player, false);
|
||||
_player = null;
|
||||
}
|
||||
|
||||
public static void Dream(EntityPlayerLocal _p, bool _on)
|
||||
{
|
||||
ScreenEffects fx = _p?.ScreenEffectManager;
|
||||
if (fx == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
fx.SetScreenEffect("Hot", _on ? Hot : 0f, _on ? 2f : 0.5f);
|
||||
fx.SetScreenEffect("Blur", _on ? Blur : 0f, _on ? 2f : 0.5f);
|
||||
fx.SetScreenEffect("Drunk", _on ? Drunk : 0f, _on ? 2f : 0.5f);
|
||||
}
|
||||
|
||||
static bool Blocked(Vector3 _p)
|
||||
{
|
||||
Vector3 a = _p + Vector3.up * (Radius + 0.05f) - Origin.position;
|
||||
Vector3 b = _p + Vector3.up * (Height - Radius) - Origin.position;
|
||||
return Physics.CheckCapsule(a, b, Radius, SnakeControl.WallMask, QueryTriggerInteraction.Ignore);
|
||||
}
|
||||
|
||||
static bool TryMove(Vector3 _d)
|
||||
{
|
||||
Vector3 c = _feet + _d;
|
||||
if (!Blocked(c))
|
||||
{
|
||||
_feet = c;
|
||||
return true;
|
||||
}
|
||||
// Ступенька: на полблока выше свободно - шагаем вверх.
|
||||
if (_grounded && !Blocked(c + Vector3.up * StepUp))
|
||||
{
|
||||
_feet = c + Vector3.up * StepUp;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void Update(EntityPlayerLocal _p)
|
||||
{
|
||||
if (!Controlling)
|
||||
{
|
||||
return;
|
||||
}
|
||||
float dt = Time.deltaTime;
|
||||
bool gui = _p.PlayerUI != null && _p.PlayerUI.windowManager.IsModalWindowOpen();
|
||||
PlayerActionsLocal input = _p.playerInput;
|
||||
Vector2 move = Vector2.zero;
|
||||
bool run = false, jump = false;
|
||||
if (!gui && input != null)
|
||||
{
|
||||
PlayerMoveController pmc = PlayerMoveController.Instance;
|
||||
Vector2 sens = pmc != null ? pmc.mouseLookSensitivity : Vector2.one * 5f;
|
||||
int invert = pmc != null && pmc.invertMouse != 0 ? pmc.invertMouse : 1;
|
||||
_yaw += Input.GetAxis("Mouse X") * sens.x;
|
||||
_pitch = Mathf.Clamp(_pitch + Input.GetAxis("Mouse Y") * sens.y * invert, -85f, 85f);
|
||||
move = new Vector2(input.Move.X, input.Move.Y);
|
||||
run = input.Run.IsPressed;
|
||||
jump = input.Jump.WasPressed;
|
||||
if (HasFocus && (input.Activate.WasPressed || input.PermanentActions.Activate.WasPressed))
|
||||
{
|
||||
BlockNecroMemoryBook.Open(_p);
|
||||
}
|
||||
}
|
||||
Quaternion yawRot = Quaternion.Euler(0f, _yaw, 0f);
|
||||
Vector3 dir = yawRot * new Vector3(move.x, 0f, move.y);
|
||||
if (dir.sqrMagnitude > 1f)
|
||||
{
|
||||
dir.Normalize();
|
||||
}
|
||||
if (dir.sqrMagnitude > 0.0001f)
|
||||
{
|
||||
Vector3 delta = dir * (run ? RunSpeed : WalkSpeed) * dt;
|
||||
int steps = Mathf.Max(1, Mathf.CeilToInt(delta.magnitude / 0.1f));
|
||||
Vector3 d = delta / steps;
|
||||
for (int i = 0; i < steps; i++)
|
||||
{
|
||||
if (!TryMove(d) && !TryMove(new Vector3(d.x, 0f, 0f)) && !TryMove(new Vector3(0f, 0f, d.z)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Падение и прыжок: земля - луч вниз по слою ландшафта и блоков.
|
||||
if (_grounded && jump)
|
||||
{
|
||||
_vy = JumpSpeed;
|
||||
_grounded = false;
|
||||
}
|
||||
_vy -= Gravity * dt;
|
||||
float dy = _vy * dt;
|
||||
if (dy > 0f && Blocked(_feet + Vector3.up * dy))
|
||||
{
|
||||
_vy = 0f;
|
||||
dy = 0f;
|
||||
}
|
||||
if (Physics.Raycast(_feet + Vector3.up * 0.5f - Origin.position, Vector3.down, out RaycastHit hit, 0.5f - dy + 0.05f, SnakeControl.TerrainMask, QueryTriggerInteraction.Ignore) && dy <= 0f)
|
||||
{
|
||||
_feet.y = hit.point.y + Origin.position.y;
|
||||
_vy = 0f;
|
||||
_grounded = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_feet.y += dy;
|
||||
_grounded = false;
|
||||
}
|
||||
// Провалился (коллизии свежепоставленных блоков ещё не построены) - назад на точку появления.
|
||||
if (_feet.y < _spawn.y - 3f)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] MindControl: дух провалился - назад на точку появления");
|
||||
_feet = _spawn + Vector3.up * 0.3f;
|
||||
_vy = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Из патчей камеры режима змеи: глаза духа и поиск книги под прицелом.</summary>
|
||||
public static void PlaceCamera(Transform _camera)
|
||||
{
|
||||
Vector3 eye = _feet + Vector3.up * EyeHeight - Origin.position;
|
||||
Quaternion rot = Quaternion.Euler(-_pitch, _yaw, 0f);
|
||||
_camera.SetPositionAndRotation(eye, rot);
|
||||
HasFocus = FindBook(_camera.position + Origin.position, _camera.forward);
|
||||
}
|
||||
|
||||
/// <summary>Шагаем по лучу на ReachDistance: первая встреченная книга памяти - цель; твёрдый
|
||||
/// блок раньше неё - нет цели (сквозь стены не читаем).</summary>
|
||||
static bool FindBook(Vector3 _from, Vector3 _dir)
|
||||
{
|
||||
World world = GameManager.Instance?.World;
|
||||
if (world == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Vector3i last = new Vector3i(int.MinValue, 0, 0);
|
||||
for (float t = 0.1f; t <= ReachDistance; t += 0.05f)
|
||||
{
|
||||
Vector3i b = World.worldToBlockPos(_from + _dir * t);
|
||||
if (b == last)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
last = b;
|
||||
Block block = world.GetBlock(b).Block;
|
||||
if (block is BlockNecroMemoryBook)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (block != null && block.IsCollideMovement && !block.shape.IsTerrain() && block.shape.IsSolidCube)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Прицел духа и подсказка у книги (OnGUI, как прицел змеи).</summary>
|
||||
public class MindHud : MonoBehaviour
|
||||
{
|
||||
static GUIStyle _style;
|
||||
|
||||
public static void Create()
|
||||
{
|
||||
var go = new GameObject("NecroMindHud");
|
||||
DontDestroyOnLoad(go);
|
||||
go.AddComponent<MindHud>();
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (!MindControl.Controlling || Event.current.type != EventType.Repaint)
|
||||
{
|
||||
return;
|
||||
}
|
||||
EntityPlayerLocal player = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||||
if (player == null || (player.PlayerUI != null && player.PlayerUI.windowManager.IsModalWindowOpen()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
float s = 4f * Screen.height / 1080f;
|
||||
Color old = GUI.color;
|
||||
GUI.color = MindControl.HasFocus ? new Color(1f, 0.85f, 0.5f, 1f) : new Color(1f, 0.95f, 0.85f, 0.7f);
|
||||
GUI.DrawTexture(new Rect(Screen.width * 0.5f - s * 0.5f, Screen.height * 0.5f - s * 0.5f, s, s), Texture2D.whiteTexture);
|
||||
if (MindControl.HasFocus)
|
||||
{
|
||||
if (_style == null)
|
||||
{
|
||||
_style = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontStyle = FontStyle.Bold };
|
||||
}
|
||||
_style.fontSize = Mathf.RoundToInt(26f * Screen.height / 1080f);
|
||||
// E в игре привязана к PermanentActions.Activate; у обычного Activate привязок нет - он даёт «Нет».
|
||||
string key = KeyName(player.playerInput?.PermanentActions?.Activate) ?? KeyName(player.playerInput?.Activate) ?? "E";
|
||||
string text = string.Format(Localization.Get("necroMemoryBookPrompt"), key);
|
||||
Rect r = new Rect(0f, Screen.height * 0.5f + 30f * Screen.height / 1080f, Screen.width, 40f * Screen.height / 1080f);
|
||||
GUI.color = new Color(0f, 0f, 0f, 0.8f);
|
||||
GUI.Label(new Rect(r.x + 2f, r.y + 2f, r.width, r.height), text, _style);
|
||||
GUI.color = new Color(1f, 0.9f, 0.7f, 1f);
|
||||
GUI.Label(r, text, _style);
|
||||
}
|
||||
GUI.color = old;
|
||||
}
|
||||
|
||||
private static string KeyName(InControl.PlayerAction action)
|
||||
{
|
||||
if (action == null || action.Bindings.Count == 0) return null;
|
||||
string name = action.GetBindingString(false);
|
||||
return string.IsNullOrEmpty(name) ? null : name;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>necrodream hot|blur|drunk N - подобрать силу сонных эффектов на ходу.</summary>
|
||||
[UnityEngine.Scripting.Preserve]
|
||||
public class ConsoleCmdNecroDream : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrodream" };
|
||||
public override string getDescription() => "NecromancerTome: сила эффектов сна - hot|blur|drunk N (0-1)";
|
||||
|
||||
public override void Execute(System.Collections.Generic.List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
if (_params.Count == 2 && float.TryParse(_params[1], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float v))
|
||||
{
|
||||
switch (_params[0])
|
||||
{
|
||||
case "hot": MindControl.Hot = v; break;
|
||||
case "blur": MindControl.Blur = v; break;
|
||||
case "drunk": MindControl.Drunk = v; break;
|
||||
}
|
||||
if (MindControl.Controlling)
|
||||
{
|
||||
MindControl.Dream(GameManager.Instance.World.GetPrimaryPlayer(), true);
|
||||
}
|
||||
}
|
||||
SdtdConsole.Instance.Output("сон: hot " + MindControl.Hot + ", blur " + MindControl.Blur + ", drunk " + MindControl.Drunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,892 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Ментальная форма - «Чертоги разума» (решения пользователя 02-03.10, BACKLOG «Чертоги разума»).
|
||||
/// Вход - третья строка меню ножа некроманта на R, только ночью, здоровье не тратит. Тело
|
||||
/// падает рэгдоллом и лежит беззащитным, как в режиме змеи; дух - камера без сущности
|
||||
/// (MindControl.cs): ходит по полу, видит только своё, с другими игроками не встречается.
|
||||
/// Выход - R, утро или смерть тела.
|
||||
///
|
||||
/// ГДЕ ЧЕРТОГИ. Под землёй, в радиационном поясе у северо-восточного угла карты: сначала от
|
||||
/// угла по диагонали ищется пояс (biomeProvider.GetRadiationAt > 2.5); пояс шире префаба -
|
||||
/// чертоги в его середине, уже (Navezgane: 16 блоков) - в 45 блоках от края. Своё место у каждого
|
||||
/// игрока - вдоль северного края с шагом SlotStep (дальше дальности показа сущностей). Места
|
||||
/// пишутся в файл в папке сохранения мира, чтобы смена ширины пояса в новой версии игры их не
|
||||
/// сдвинула.
|
||||
///
|
||||
/// КАК СТАВЯТСЯ. Префаб chertogi (PalacePrefab/, нарисован пользователем в редакторе игры)
|
||||
/// грузится Prefab.Load и ставится поблочно через World.SetBlocksRPC - НЕ Prefab.CopyIntoLocal:
|
||||
/// тот перед постановкой вычищает весь столб над префабом до неба от не-земляных блоков,
|
||||
/// и над чертогами вырубило бы деревья (видно сверху). Заодно: внешний слой оболочки
|
||||
/// (woodShapes:cube) -> terrBedrock (в меню творчества его нет, рисовали деревом),
|
||||
/// одиночная книга на столе -> necroMemoryBook, на стол с реактивами - нож-декорация,
|
||||
/// кровать (SleepingBag) -> necroPalaceBed: иначе игра делает её точкой возрождения игрока.
|
||||
/// Построены ли - по книге: нет её - ставим заново (самопочинка).
|
||||
///
|
||||
/// ОХРАНА. Внутри чертогов игра считает место «зоной торговца» (патч IsWithinTraderArea):
|
||||
/// блоки не ломаются ни ударом, ни взрывом, строить нельзя. Чужие сущности в коробке чертогов
|
||||
/// раз в секунду выбрасываются: змея развеивается, игрок - на поверхность, прочие - удаляются.
|
||||
///
|
||||
/// ОГРАНИЧЕНИЕ: одиночная игра и хост. Клиенту в мультиплеере вход пока не даётся - чертоги ставит
|
||||
/// сервер, а камера живёт у клиента (решать отдельно).
|
||||
/// </summary>
|
||||
public static class MindPalace
|
||||
{
|
||||
public const string PrefabName = "chertogi";
|
||||
public const string BookBlock = "necroMemoryBook";
|
||||
public const string KnifeBlock = "necroKnifeDecor";
|
||||
/// <summary>Кровать без класса SleepingBag - ею заменяется кровать префаба (bunkBedMadeWhite).</summary>
|
||||
public const string BedBlock = "necroPalaceBed";
|
||||
public const string ShellFrom = "woodShapes:cube";
|
||||
public const string ShellTo = "terrBedrock";
|
||||
/// <summary>Одиночная книга на обеденном столе (cntBookPile09 в префабе).</summary>
|
||||
public static readonly Vector3i BookAt = new Vector3i(12, 2, 9);
|
||||
/// <summary>Место ножа на столе с реактивами (указание пользователя).</summary>
|
||||
public static readonly Vector3i KnifeAt = new Vector3i(4, 2, 11);
|
||||
/// <summary>Входная дверь чертогов - во внутреннюю оболочку, никуда не ведёт: остаётся закрытой
|
||||
/// (указание 03.10). Остальные четыре - между комнатами - открываются.</summary>
|
||||
public static readonly Vector3i EntranceDoorAt = new Vector3i(8, 1, 3);
|
||||
/// <summary>Где появляется дух: комната с книгой, свободная клетка у стола.</summary>
|
||||
public static readonly Vector3 SpawnAt = new Vector3(11.5f, 1.05f, 11.5f);
|
||||
public const float SpawnYaw = 180f;
|
||||
public const int SlotStep = 300;
|
||||
public const int FallbackInset = 45;
|
||||
public const int FloorY = 3;
|
||||
/// <summary>Рамка вокруг чертогов, которая заливается камнем перед постановкой.</summary>
|
||||
public const int LooseMargin = 3;
|
||||
public const float RadiationThreshold = 2.5f;
|
||||
/// <summary>Поворот ножа-декорации (0-23): 9 - плашмя на столешнице (подобрано пользователем
|
||||
/// 03.10 командой necropalace knife N).</summary>
|
||||
public static int KnifeRotation = 0;
|
||||
|
||||
public static bool Active;
|
||||
static Vector3i _origin;
|
||||
static Prefab _prefab;
|
||||
static ChunkManager.ChunkObserver _observer;
|
||||
enum Stage { None, Loading, Settling, Ready }
|
||||
static Stage _stage;
|
||||
static float _deadline;
|
||||
static float _settleUntil;
|
||||
static float _ragdollAt;
|
||||
static bool _waitStandUp;
|
||||
static float _standUpDeadline;
|
||||
static float _guardCheck;
|
||||
|
||||
static readonly Dictionary<string, Vector3i> _slots = new Dictionary<string, Vector3i>();
|
||||
static string _slotsFor;
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
ModEvents.UnityUpdate.RegisterHandler(OnUnityUpdate);
|
||||
MindHud.Create();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- места
|
||||
|
||||
static string SlotsFile => Path.Combine(GameIO.GetSaveGameDir(), "NecromancerTome_palaces.txt");
|
||||
|
||||
static void LoadSlots()
|
||||
{
|
||||
string dir = GameIO.GetSaveGameDir();
|
||||
if (_slotsFor == dir)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_slotsFor = dir;
|
||||
_slots.Clear();
|
||||
try
|
||||
{
|
||||
if (File.Exists(SlotsFile))
|
||||
{
|
||||
foreach (string line in File.ReadAllLines(SlotsFile))
|
||||
{
|
||||
string[] p = line.Split(' ');
|
||||
if (p.Length == 4 && int.TryParse(p[1], out int x) && int.TryParse(p[2], out int y) && int.TryParse(p[3], out int z))
|
||||
{
|
||||
_slots[p[0]] = new Vector3i(x, y, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] MindPalace: не прочитать места - " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
static void SaveSlots()
|
||||
{
|
||||
try
|
||||
{
|
||||
var lines = new List<string>();
|
||||
foreach (var kv in _slots)
|
||||
{
|
||||
lines.Add(kv.Key + " " + kv.Value.x + " " + kv.Value.y + " " + kv.Value.z);
|
||||
}
|
||||
File.WriteAllLines(SlotsFile, lines);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] MindPalace: не записать места - " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
static string PlayerKey(EntityPlayer _player)
|
||||
{
|
||||
string id = GameManager.Instance.persistentLocalPlayer?.PrimaryId?.CombinedString;
|
||||
return string.IsNullOrEmpty(id) ? "local" : id.Replace(' ', '_');
|
||||
}
|
||||
|
||||
static Vector3i GetSlot(World _world, EntityPlayer _player)
|
||||
{
|
||||
LoadSlots();
|
||||
string key = PlayerKey(_player);
|
||||
if (_slots.TryGetValue(key, out Vector3i o))
|
||||
{
|
||||
return o;
|
||||
}
|
||||
o = ComputeSlot(_world, _slots.Count);
|
||||
_slots[key] = o;
|
||||
SaveSlots();
|
||||
Debug.Log("[NecromancerTome] MindPalace: место " + _slots.Count + " для " + key + " - " + o);
|
||||
return o;
|
||||
}
|
||||
|
||||
/// <summary>Отступ от края: середина радиационного пояса, если он шире префаба, иначе 45.</summary>
|
||||
static int Inset(World _world, Vector3i _min, Vector3i _max, int _size)
|
||||
{
|
||||
IBiomeProvider biomes = _world.ChunkCache?.ChunkProvider?.GetBiomeProvider();
|
||||
int start = -1, end = -1;
|
||||
if (biomes != null)
|
||||
{
|
||||
for (int t = 0; t < 600; t += 4)
|
||||
{
|
||||
bool rad = biomes.GetRadiationAt(_max.x - t, _max.z - t) > RadiationThreshold;
|
||||
if (rad && start < 0) start = t;
|
||||
if (!rad && start >= 0) { end = t; break; }
|
||||
}
|
||||
}
|
||||
if (start >= 0 && end > start && end - start >= _size + 20)
|
||||
{
|
||||
return Mathf.Max(FallbackInset, (start + end) / 2 - _size / 2);
|
||||
}
|
||||
return FallbackInset;
|
||||
}
|
||||
|
||||
static Vector3i ComputeSlot(World _world, int _index)
|
||||
{
|
||||
_world.GetWorldExtent(out Vector3i min, out Vector3i max);
|
||||
Prefab p = LoadPrefab();
|
||||
int size = p != null ? Mathf.Max(p.size.x, p.size.z) : 17;
|
||||
int inset = Inset(_world, min, max, size);
|
||||
int perEdge = Mathf.Max(1, (max.x - min.x - 2 * inset - size) / SlotStep);
|
||||
int x, z;
|
||||
if (_index < perEdge)
|
||||
{
|
||||
x = max.x - inset - size - _index * SlotStep;
|
||||
z = max.z - inset - size;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = max.x - inset - size;
|
||||
z = max.z - inset - size - (_index - perEdge + 1) * SlotStep;
|
||||
}
|
||||
x = Mathf.FloorToInt(x / 16f) * 16;
|
||||
z = Mathf.FloorToInt(z / 16f) * 16;
|
||||
return new Vector3i(x, FloorY, z);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- префаб и постройка
|
||||
|
||||
static string ModDir => Path.GetDirectoryName(typeof(MindPalace).Assembly.Location);
|
||||
|
||||
static Prefab LoadPrefab()
|
||||
{
|
||||
if (_prefab != null)
|
||||
{
|
||||
return _prefab;
|
||||
}
|
||||
string path = Path.Combine(ModDir, "PalacePrefab", PrefabName + ".tts");
|
||||
var p = new Prefab();
|
||||
if (!p.Load(new PathAbstractions.AbstractedLocation(PathAbstractions.EAbstractedLocationType.Mods, PrefabName, path, null, false)))
|
||||
{
|
||||
Debug.LogError("[NecromancerTome] MindPalace: префаб не загрузился - " + path);
|
||||
return null;
|
||||
}
|
||||
_prefab = p;
|
||||
Debug.Log("[NecromancerTome] MindPalace: префаб " + PrefabName + " " + p.size);
|
||||
return _prefab;
|
||||
}
|
||||
|
||||
static bool ChunksLoaded(World _world, Vector3i _o, Vector3i _size)
|
||||
{
|
||||
for (int cx = World.toChunkXZ(_o.x); cx <= World.toChunkXZ(_o.x + _size.x - 1); cx++)
|
||||
{
|
||||
for (int cz = World.toChunkXZ(_o.z); cz <= World.toChunkXZ(_o.z + _size.z - 1); cz++)
|
||||
{
|
||||
// Не просто «есть в памяти», а сгенерирован и украшен: иначе генерация досыпала
|
||||
// землю поверх поставленного (тест 03.10: «внутри блоки камня»).
|
||||
Chunk c = _world.GetChunkSync(cx, cz) as Chunk;
|
||||
if (c == null || !c.IsInitialized || c.NeedsDecoration)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Стоит ли блок уже в самом префабе (03.10: пользователь расставляет книгу и нож в редакторе
|
||||
/// сам) - тогда старые принудительные замены (книга в BookAt, нож в KnifeAt) не делаем.</summary>
|
||||
static bool PrefabHas(Prefab _p, string _block)
|
||||
{
|
||||
if (_hasCache == null || _hasCacheFor != _p)
|
||||
{
|
||||
_hasCache = new Dictionary<string, bool>();
|
||||
_hasCacheFor = _p;
|
||||
}
|
||||
if (_hasCache.TryGetValue(_block, out bool has))
|
||||
{
|
||||
return has;
|
||||
}
|
||||
int type = Block.GetBlockValue(_block).type;
|
||||
for (int y = 0; y < _p.size.y && !has; y++)
|
||||
for (int z = 0; z < _p.size.z && !has; z++)
|
||||
for (int x = 0; x < _p.size.x && !has; x++)
|
||||
{
|
||||
has = _p.GetBlock(x, y, z).type == type;
|
||||
}
|
||||
_hasCache[_block] = has;
|
||||
return has;
|
||||
}
|
||||
|
||||
static Dictionary<string, bool> _hasCache;
|
||||
static Prefab _hasCacheFor;
|
||||
|
||||
/// <summary>Что должно стоять в клетке префаба после постановки (с заменами). False - клетку
|
||||
/// не трогаем (дочерняя часть многоблочного предмета).</summary>
|
||||
static bool Expected(Prefab _p, int _x, int _y, int _z, out BlockValue _bv, out sbyte _d)
|
||||
{
|
||||
_bv = _p.GetBlock(_x, _y, _z);
|
||||
_d = _p.GetDensity(_x, _y, _z);
|
||||
if (_bv.ischild)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Vector3i lp = new Vector3i(_x, _y, _z);
|
||||
// Пол (y 0) - НЕ bedrock: в префабе он же пол комнат, а bedrock - блок-земля, её сглаженная
|
||||
// поверхность выпирала из пола в комнаты каменными наплывами (тест 03.10: «в помещении
|
||||
// блоки камня»). Защита снизу - отдельный слой bedrock под префабом (FloorShield).
|
||||
bool outer = _x == 0 || _z == 0 || _x == _p.size.x - 1 || _z == _p.size.z - 1 || _y == _p.size.y - 1;
|
||||
if (outer && !_bv.isair && _bv.Block.GetBlockName() == ShellFrom)
|
||||
{
|
||||
_bv = Block.GetBlockValue(ShellTo);
|
||||
_d = MarchingCubes.DensityTerrain;
|
||||
}
|
||||
else if (lp == BookAt && !PrefabHas(_p, BookBlock))
|
||||
{
|
||||
BlockValue b = Block.GetBlockValue(BookBlock);
|
||||
b.rotation = _bv.rotation;
|
||||
_bv = b;
|
||||
_d = MarchingCubes.DensityAir;
|
||||
}
|
||||
else if (lp == KnifeAt && !PrefabHas(_p, KnifeBlock))
|
||||
{
|
||||
BlockValue k = Block.GetBlockValue(KnifeBlock);
|
||||
k.rotation = (byte)KnifeRotation;
|
||||
_bv = k;
|
||||
_d = MarchingCubes.DensityAir;
|
||||
}
|
||||
else if (!_bv.isair && _bv.Block is BlockSleepingBag)
|
||||
{
|
||||
// Кровать префаба - просто мебель (тест 03.10: синий камень портала отправил в чертоги).
|
||||
// Чертоги ставятся SetBlocksRPC без игрока, а GameManager.ChangeBlocks тогда считает
|
||||
// ставившим локального игрока и любую SleepingBag-кровать делает его точкой возрождения.
|
||||
BlockValue bed = Block.GetBlockValue(BedBlock);
|
||||
bed.rotation = _bv.rotation;
|
||||
_bv = bed;
|
||||
_d = MarchingCubes.DensityAir;
|
||||
}
|
||||
else if (_bv.isair || !_bv.Block.shape.IsTerrain())
|
||||
{
|
||||
_d = MarchingCubes.DensityAir;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Диагностика (03.10, «нож пропал со стола»): что стоит в клетке и есть ли у блока модель.</summary>
|
||||
static void LogCell(World _world, Vector3i _wp, string _what)
|
||||
{
|
||||
BlockValue bv = _world.GetBlock(_wp);
|
||||
string s = "[NecromancerTome] MindPalace: " + _what + " " + _wp + " - " + bv.Block.GetBlockName() + " пов. " + bv.rotation;
|
||||
Chunk c = _world.GetChunkFromWorldPos(_wp) as Chunk;
|
||||
BlockEntityData be = c?.GetBlockEntity(_wp);
|
||||
if (be == null)
|
||||
{
|
||||
s += ", модели нет (нет BlockEntity)";
|
||||
}
|
||||
else if (!be.bHasTransform || be.transform == null)
|
||||
{
|
||||
s += ", модель ещё не создана";
|
||||
}
|
||||
else
|
||||
{
|
||||
Renderer[] rs = be.transform.GetComponentsInChildren<Renderer>(true);
|
||||
int on = 0;
|
||||
foreach (Renderer r in rs) if (r.enabled && r.gameObject.activeInHierarchy) on++;
|
||||
s += ", модель " + be.transform.name + " в " + (be.transform.position + Origin.position) + ", активна " + be.transform.gameObject.activeInHierarchy + ", рендеров " + on + "/" + rs.Length;
|
||||
}
|
||||
Debug.Log(s);
|
||||
}
|
||||
|
||||
/// <summary>Сколько клеток в мире не совпадает с префабом по типу блока (0 - чертоги целы).</summary>
|
||||
public static int Mismatches(World _world, Vector3i _o, List<string> _report = null)
|
||||
{
|
||||
Prefab p = LoadPrefab();
|
||||
if (p == null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
int n = 0;
|
||||
int shieldType = Block.GetBlockValue(ShellTo).type;
|
||||
for (int z = 0; z < p.size.z; z++)
|
||||
for (int x = 0; x < p.size.x; x++)
|
||||
{
|
||||
if (_world.GetBlock(_o + new Vector3i(x, -1, z)).type != shieldType)
|
||||
{
|
||||
n++;
|
||||
}
|
||||
}
|
||||
for (int y = 0; y < p.size.y; y++)
|
||||
for (int z = 0; z < p.size.z; z++)
|
||||
for (int x = 0; x < p.size.x; x++)
|
||||
{
|
||||
Vector3i wp = _o + new Vector3i(x, y, z);
|
||||
if (!Expected(p, x, y, z, out BlockValue want, out sbyte wantD))
|
||||
{
|
||||
// Дочерняя клетка: там не должно быть земли/камня.
|
||||
BlockValue c = _world.GetBlock(wp);
|
||||
if (c.Block.shape.IsTerrain() || _world.GetDensity(wp.x, wp.y, wp.z) < 0)
|
||||
{
|
||||
n++;
|
||||
if (_report != null && _report.Count < 40)
|
||||
{
|
||||
_report.Add("(" + x + "," + y + "," + z + ") дочерняя клетка, стоит " + c.Block.GetBlockName() + " плотн. " + _world.GetDensity(wp.x, wp.y, wp.z));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
BlockValue have = _world.GetBlock(wp);
|
||||
// Плотность тоже: тип блока - воздух, а плотность земли - и клетка рисуется камнем
|
||||
// (тест 03.10: «камень под столом, в дверном проёме, под потолком»).
|
||||
sbyte haveD = _world.GetDensity(wp.x, wp.y, wp.z);
|
||||
bool densityBad = (wantD < 0) != (haveD < 0);
|
||||
// И покраска (указание 03.10: «покраску обязательно перенеси из префаба»).
|
||||
bool paintBad = !want.isair && want.type == p.GetBlock(x, y, z).type &&
|
||||
_world.GetTextureFullArray(wp.x, wp.y, wp.z) != p.GetTexture(x, y, z);
|
||||
if (have.type != want.type || densityBad || paintBad)
|
||||
{
|
||||
n++;
|
||||
if (_report != null && _report.Count < 40)
|
||||
{
|
||||
_report.Add("(" + x + "," + y + "," + z + ") ждали " + want.Block.GetBlockName() + " плотн. " + wantD +
|
||||
", стоит " + have.Block.GetBlockName() + " плотн. " + haveD + (paintBad ? ", не та покраска" : ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
public static bool IsBuilt(World _world, Vector3i _o)
|
||||
{
|
||||
return Mismatches(_world, _o) == 0;
|
||||
}
|
||||
|
||||
/// <summary>Все двери чертогов - открыты (в редакторе сохранились закрытыми, а дух дверей не
|
||||
/// открывает). Состояние двери - в её TEFeatureDoor, не в блоке.</summary>
|
||||
public static void OpenDoors(World _world, Vector3i _o)
|
||||
{
|
||||
Vector3i s = Size;
|
||||
int opened = 0;
|
||||
for (int y = 0; y < s.y; y++)
|
||||
for (int z = 0; z < s.z; z++)
|
||||
for (int x = 0; x < s.x; x++)
|
||||
{
|
||||
if (_world.GetTileEntity(_o + new Vector3i(x, y, z)) is TileEntityComposite tec)
|
||||
{
|
||||
TEFeatureDoor door = tec.GetFeature<TEFeatureDoor>();
|
||||
// Входная - закрыть (могла остаться открытой от прежних входов), остальные - открыть.
|
||||
bool wantOpen = new Vector3i(x, y, z) != EntranceDoorAt;
|
||||
if (door != null && door.IsOpen() != wantOpen)
|
||||
{
|
||||
door.SetOpen(wantOpen);
|
||||
opened++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (opened > 0)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] MindPalace: дверей переключено " + opened);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Stamp(World _world, Vector3i _o)
|
||||
{
|
||||
Prefab p = LoadPrefab();
|
||||
if (p == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// ПОРЯДОК ВАЖЕН (тест 03.10: при очистке всей коробки разом на миг открывалась пустота
|
||||
// 17x8x17, земля над ней и края теряли опору - обвал, падающие блоки ушли под мир):
|
||||
// 1) слой bedrock под чертогами;
|
||||
// 2) оболочка - пол (y 0), стены (края по x/z) и крыша - сразу на место земли, без очистки;
|
||||
// 3) очистка внутренности (её уже держит оболочка) - в том числе дочерних клеток стола,
|
||||
// дверей, шкафов, где иначе оставался камень;
|
||||
// 4) всё внутреннее по префабу.
|
||||
var list = new List<BlockChangeInfo>();
|
||||
// 0) Рамка LooseMargin блоков вокруг чертогов, со всех сторон и сверху, - сплошь камнем
|
||||
// (указание 03.10: «заполни сначала камнем, на 3 блока во все стороны»): земля, песок,
|
||||
// гравий (осыпаются без опоры) и пустоты пещер -> terrStone. Не земля и не воздух
|
||||
// (чужие постройки) - не трогаем.
|
||||
BlockValue stone = Block.GetBlockValue("terrStone");
|
||||
int loose = 0;
|
||||
for (int y = -1 - LooseMargin; y < p.size.y + LooseMargin; y++)
|
||||
for (int z = -LooseMargin; z < p.size.z + LooseMargin; z++)
|
||||
for (int x = -LooseMargin; x < p.size.x + LooseMargin; x++)
|
||||
{
|
||||
bool inside = y >= -1 && y < p.size.y && x >= 0 && x < p.size.x && z >= 0 && z < p.size.z;
|
||||
if (inside)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Vector3i wp = _o + new Vector3i(x, y, z);
|
||||
if (wp.y < 1)
|
||||
{
|
||||
continue; // ниже 1 игра блоки не ставит (дно мира)
|
||||
}
|
||||
BlockValue have = _world.GetBlock(wp);
|
||||
Block b = have.Block;
|
||||
if (have.type != stone.type && (have.isair || b != null && b.shape.IsTerrain()))
|
||||
{
|
||||
list.Add(new BlockChangeInfo(wp, stone, MarchingCubes.DensityTerrain));
|
||||
loose++;
|
||||
}
|
||||
}
|
||||
if (loose > 0)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] MindPalace: рамка вокруг залита камнем - " + loose + " клеток");
|
||||
}
|
||||
BlockValue shield = Block.GetBlockValue(ShellTo);
|
||||
for (int z = 0; z < p.size.z; z++)
|
||||
for (int x = 0; x < p.size.x; x++)
|
||||
{
|
||||
list.Add(new BlockChangeInfo(_o + new Vector3i(x, -1, z), shield, MarchingCubes.DensityTerrain));
|
||||
}
|
||||
for (int pass = 0; pass < 3; pass++)
|
||||
for (int yi = 0; yi < p.size.y; yi++)
|
||||
for (int z = 0; z < p.size.z; z++)
|
||||
for (int x = 0; x < p.size.x; x++)
|
||||
{
|
||||
// Очистка (проход 1) - СВЕРХУ ВНИЗ: снизу вверх убранный слой оставлял над собой
|
||||
// песок/гравий без опоры, тот падал и рушил уже поставленные стены (тест 03.10,
|
||||
// necropalace test: 30 расхождений сразу, 309 - при следующем входе). Постановка -
|
||||
// снизу вверх, на опору.
|
||||
int y = pass == 1 ? p.size.y - 1 - yi : yi;
|
||||
bool shellCell = y == 0 || y == p.size.y - 1 || x == 0 || z == 0 || x == p.size.x - 1 || z == p.size.z - 1;
|
||||
Vector3i wp = _o + new Vector3i(x, y, z);
|
||||
if (pass == 0 && shellCell || pass == 2 && !shellCell)
|
||||
{
|
||||
if (Expected(p, x, y, z, out BlockValue bv, out sbyte d))
|
||||
{
|
||||
// С покраской из префаба (указание 03.10).
|
||||
list.Add(new BlockChangeInfo(wp, bv, d, p.GetTexture(x, y, z)));
|
||||
}
|
||||
}
|
||||
else if (pass == 1 && !shellCell)
|
||||
{
|
||||
list.Add(new BlockChangeInfo(wp, BlockValue.Air, MarchingCubes.DensityAir));
|
||||
}
|
||||
}
|
||||
_world.SetBlocksRPC(list);
|
||||
Debug.Log("[NecromancerTome] MindPalace: чертоги поставлены в " + _o + ", блоков " + list.Count);
|
||||
}
|
||||
|
||||
static Vector3i Size => _prefab != null ? _prefab.size : new Vector3i(17, 8, 17);
|
||||
|
||||
/// <summary>Проверка постановки в нетронутую землю (указание 03.10): залить камнем коробку на
|
||||
/// 2 блока шире чертогов во все стороны (включая слой bedrock снизу), подождать, поставить
|
||||
/// чертоги заново, выставить двери. Сразу заливкой, без предварительной очистки - очистка и
|
||||
/// есть та пустота без опоры, из-за которой был обвал.</summary>
|
||||
public static System.Collections.IEnumerator StressTest(World _world, Vector3i _o)
|
||||
{
|
||||
Vector3i s = Size;
|
||||
// Гравием - худший случай: сыпучее, падает без опоры (указание 03.10).
|
||||
BlockValue stone = Block.GetBlockValue("terrGravel");
|
||||
var list = new List<BlockChangeInfo>();
|
||||
for (int y = -3; y < s.y + 2; y++)
|
||||
for (int z = -2; z < s.z + 2; z++)
|
||||
for (int x = -2; x < s.x + 2; x++)
|
||||
{
|
||||
list.Add(new BlockChangeInfo(_o + new Vector3i(x, y, z), stone, MarchingCubes.DensityTerrain));
|
||||
}
|
||||
_world.SetBlocksRPC(list);
|
||||
Debug.Log("[NecromancerTome] necropalace test: залито гравием " + list.Count + " клеток, ждём 2 с");
|
||||
yield return new WaitForSeconds(2f);
|
||||
Stamp(_world, _o);
|
||||
yield return new WaitForSeconds(1f);
|
||||
OpenDoors(_world, _o);
|
||||
Debug.Log("[NecromancerTome] necropalace test: готово, расхождений с префабом " + Mismatches(_world, _o));
|
||||
}
|
||||
|
||||
public static bool InAnyPalace(Vector3i _pos, int _margin = 0)
|
||||
{
|
||||
if (_slots.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Vector3i s = Size;
|
||||
foreach (Vector3i o in _slots.Values)
|
||||
{
|
||||
if (_pos.x >= o.x - _margin && _pos.x < o.x + s.x + _margin &&
|
||||
_pos.y >= o.y - 1 - _margin && _pos.y < o.y + s.y + _margin &&
|
||||
_pos.z >= o.z - _margin && _pos.z < o.z + s.z + _margin)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- вход и выход
|
||||
|
||||
public static void Enter(EntityPlayerLocal _player)
|
||||
{
|
||||
if (Active || SnakeMode.Active || _player == null || _player.IsDead() || _player.AttachedToEntity != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!SnakeMode.IsNight())
|
||||
{
|
||||
Debug.Log("[NecromancerTome] MindPalace: днём чертоги недоступны");
|
||||
return;
|
||||
}
|
||||
World world = GameManager.Instance.World;
|
||||
if (world.IsRemote())
|
||||
{
|
||||
Debug.Log("[NecromancerTome] MindPalace: в мультиплеере у клиента пока недоступно");
|
||||
return;
|
||||
}
|
||||
if (LoadPrefab() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_origin = GetSlot(world, _player);
|
||||
Vector3 center = _origin.ToVector3() + new Vector3(Size.x * 0.5f, 0f, Size.z * 0.5f);
|
||||
// Дальность как у змеи: при радиусе 2 игра не строила коллизии участка (тест 03.10:
|
||||
// «чертоги не построились вовремя» - ждали коллизий 30 с).
|
||||
_observer = GameManager.Instance.AddChunkObserver(center, true, GameUtils.GetViewDistance(), -1);
|
||||
_stage = Stage.Loading;
|
||||
_deadline = Time.time + 30f;
|
||||
Active = true;
|
||||
_player.ScreenEffectManager?.SetScreenEffect("FadeToBlack", 1f, 0.6f);
|
||||
_player.SetFirstPersonView(false, true);
|
||||
_ragdollAt = Time.time + SnakeMode.RagdollDelay;
|
||||
_waitStandUp = false;
|
||||
Debug.Log("[NecromancerTome] MindPalace: вход, чертоги в " + _origin);
|
||||
}
|
||||
|
||||
public static void Exit(EntityPlayerLocal _player)
|
||||
{
|
||||
if (!Active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Active = false;
|
||||
_stage = Stage.None;
|
||||
MindControl.End(_player);
|
||||
RemoveObserver();
|
||||
_player?.ScreenEffectManager?.SetScreenEffect("FadeToBlack", 0f, 1f);
|
||||
if (_player == null || _player.IsDead())
|
||||
{
|
||||
return;
|
||||
}
|
||||
_ragdollAt = 0f;
|
||||
_player.emodel.ragdollTime = _player.emodel.ragdollDuration;
|
||||
_waitStandUp = true;
|
||||
_standUpDeadline = Time.time + 5f;
|
||||
Debug.Log("[NecromancerTome] MindPalace: выход");
|
||||
}
|
||||
|
||||
static void RemoveObserver()
|
||||
{
|
||||
if (_observer != null && GameManager.Instance?.World != null)
|
||||
{
|
||||
GameManager.Instance.RemoveChunkObserver(_observer);
|
||||
}
|
||||
_observer = null;
|
||||
}
|
||||
|
||||
static void OnUnityUpdate(ref ModEvents.SUnityUpdateData _data)
|
||||
{
|
||||
World world = GameManager.Instance?.World;
|
||||
if (world == null)
|
||||
{
|
||||
Active = false;
|
||||
_stage = Stage.None;
|
||||
_observer = null;
|
||||
MindControl.End(null);
|
||||
_slotsFor = null;
|
||||
return;
|
||||
}
|
||||
EntityPlayerLocal player = world.GetPrimaryPlayer();
|
||||
Guard(world);
|
||||
if (!Active)
|
||||
{
|
||||
if (_waitStandUp && player != null && (!player.emodel.IsRagdollActive || Time.time > _standUpDeadline))
|
||||
{
|
||||
_waitStandUp = false;
|
||||
player.SetFirstPersonView(true, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (player == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (player.IsDead())
|
||||
{
|
||||
Debug.Log("[NecromancerTome] MindPalace: тело убито");
|
||||
Exit(player);
|
||||
return;
|
||||
}
|
||||
if (!SnakeMode.IsNight())
|
||||
{
|
||||
Debug.Log("[NecromancerTome] MindPalace: наступило утро - обратно в тело");
|
||||
Exit(player);
|
||||
return;
|
||||
}
|
||||
Body(player);
|
||||
switch (_stage)
|
||||
{
|
||||
case Stage.Loading:
|
||||
if (ChunksLoaded(world, _origin, Size))
|
||||
{
|
||||
var report = new List<string>();
|
||||
int bad = Mismatches(world, _origin, report);
|
||||
if (bad != 0)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] MindPalace: расхождений с префабом " + bad + " - ставим заново");
|
||||
for (int i = 0; i < report.Count && i < 10; i++)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] MindPalace: " + report[i]);
|
||||
}
|
||||
Stamp(world, _origin);
|
||||
}
|
||||
_stage = Stage.Settling;
|
||||
_settleUntil = Time.time + 1.5f;
|
||||
}
|
||||
else if (Time.time > _deadline)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] MindPalace: участки чертогов не загрузились");
|
||||
Exit(player);
|
||||
}
|
||||
break;
|
||||
case Stage.Settling:
|
||||
Vector3 spawn = _origin.ToVector3() + SpawnAt;
|
||||
// Коллизии готовы - или прошло 5 с: тогда всё равно пускаем, от провала страхует
|
||||
// MindControl (возврат на точку появления).
|
||||
if (Time.time > _settleUntil && (world.IsChunkAreaCollidersLoaded(spawn) || Time.time > _settleUntil + 5f))
|
||||
{
|
||||
Debug.Log("[NecromancerTome] MindPalace: коллизии " + (world.IsChunkAreaCollidersLoaded(spawn) ? "готовы" : "НЕ готовы, пускаем по таймеру"));
|
||||
_stage = Stage.Ready;
|
||||
OpenDoors(world, _origin);
|
||||
LogCell(world, _origin + KnifeAt, "нож");
|
||||
LogCell(world, _origin + BookAt, "книга");
|
||||
MindControl.Begin(player, spawn, SpawnYaw);
|
||||
player.ScreenEffectManager?.SetScreenEffect("FadeToBlack", 0f, 1.5f);
|
||||
}
|
||||
else if (Time.time > _deadline)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] MindPalace: чертоги не построились вовремя");
|
||||
Exit(player);
|
||||
}
|
||||
break;
|
||||
case Stage.Ready:
|
||||
MindControl.Update(player);
|
||||
break;
|
||||
}
|
||||
// Выход по R - когда тело уже лежит и не открыто окно (как у змеи).
|
||||
PlayerActionsLocal input = player.playerInput;
|
||||
bool gui = player.PlayerUI != null && player.PlayerUI.windowManager.IsModalWindowOpen();
|
||||
if (_ragdollAt <= 0f && player.emodel.IsRagdollActive && input != null && !gui &&
|
||||
(input.Reload.WasPressed || input.PermanentActions.Reload.WasPressed))
|
||||
{
|
||||
Exit(player);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Тело падает и лежит - как в режиме змеи (SnakeMode.OnUnityUpdate).</summary>
|
||||
static void Body(EntityPlayerLocal _player)
|
||||
{
|
||||
if (_ragdollAt > 0f)
|
||||
{
|
||||
if (Time.time >= _ragdollAt)
|
||||
{
|
||||
_ragdollAt = 0f;
|
||||
_player.emodel.DoRagdoll(EModelBase.RagdollMode.Default, 999999f, EnumBodyPartHit.None, Vector3.zero, Vector3.zero, false);
|
||||
}
|
||||
}
|
||||
else if (!_player.emodel.IsRagdollActive)
|
||||
{
|
||||
_player.emodel.DoRagdoll(EModelBase.RagdollMode.Default, 999999f, EnumBodyPartHit.None, Vector3.zero, Vector3.zero, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Раз в секунду: всё живое в коробке чертогов - вон. Дух сущности не имеет.</summary>
|
||||
static void Guard(World _world)
|
||||
{
|
||||
_guardCheck -= Time.deltaTime;
|
||||
if (_guardCheck > 0f || _world.IsRemote())
|
||||
{
|
||||
return;
|
||||
}
|
||||
_guardCheck = 1f;
|
||||
LoadSlots();
|
||||
if (_slots.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Игроков в EntityAlives нет (World.SpawnEntityInWorld кладёт туда только не-игроков) - их
|
||||
// отдельно, по Players.list (тест 03.10: камень портала привёл в чертоги, охрана молчала).
|
||||
for (int i = _world.Players.list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
EntityPlayer pl = _world.Players.list[i];
|
||||
if (pl == null || pl.IsDead() || !InAnyPalace(World.worldToBlockPos(pl.position)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Vector3i b = World.worldToBlockPos(pl.position);
|
||||
Vector3 up = new Vector3(pl.position.x, _world.GetTerrainHeight(b.x, b.z) + 2f, pl.position.z);
|
||||
Debug.Log("[NecromancerTome] MindPalace: игрок " + pl.entityId + " в чертогах - выброшен на " + up);
|
||||
if (pl is EntityPlayerLocal)
|
||||
{
|
||||
// Как камень портала (PortalStonePatch.TeleportToBedroll).
|
||||
NetPackageManager.GetPackage<NetPackageTeleportPlayer>().Setup(up, null).ProcessPackage(_world, GameManager.Instance);
|
||||
}
|
||||
else
|
||||
{
|
||||
pl.Teleport(up);
|
||||
}
|
||||
}
|
||||
for (int i = _world.EntityAlives.Count - 1; i >= 0; i--)
|
||||
{
|
||||
EntityAlive e = _world.EntityAlives[i];
|
||||
if (e == null || e.IsDead() || !InAnyPalace(World.worldToBlockPos(e.position)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
EntityAlive snake = SnakeMode.Snake(_world);
|
||||
if (snake != null && e == snake)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] MindPalace: змея в чертогах - развеяна");
|
||||
SnakeMode.Exit(_world.GetPrimaryPlayer());
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("[NecromancerTome] MindPalace: " + e.EntityName + " в чертогах - удалён");
|
||||
_world.RemoveEntity(e.entityId, EnumRemoveEntityReason.Despawned);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>В чертогах и в рамке камня вокруг ничего не падает (указание 03.10: «почему не
|
||||
/// отключить просчёт физики в той области»): World.AddFallingBlock - единственная очередь
|
||||
/// падающих блоков (туда же игра сама не пускает блоки со StabilityIgnore). Тест гравием
|
||||
/// 03.10: после постановки нижний/верхний ряд внутренней стены всё равно обрушивался.</summary>
|
||||
[HarmonyPatch(typeof(World), nameof(World.AddFallingBlock))]
|
||||
public static class Patch_World_AddFallingBlock_MindPalace
|
||||
{
|
||||
public static bool Prefix(Vector3i _blockPos)
|
||||
{
|
||||
return !MindPalace.InAnyPalace(_blockPos, MindPalace.LooseMargin);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Чертоги - «зона торговца»: блоки не ломаются ударом и взрывом, строить нельзя
|
||||
/// (ItemActionAttack, Explosion, Block.DamageBlock проверяют IsWithinTraderArea).</summary>
|
||||
[HarmonyPatch(typeof(World), nameof(World.IsWithinTraderArea), new[] { typeof(Vector3i) })]
|
||||
public static class Patch_World_IsWithinTraderArea_MindPalace
|
||||
{
|
||||
public static void Postfix(Vector3i _worldBlockPos, ref bool __result)
|
||||
{
|
||||
if (!__result && MindPalace.InAnyPalace(_worldBlockPos))
|
||||
{
|
||||
__result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// necropalace - отладка чертогов: where (где мои), rebuild (поставить заново, если участки
|
||||
/// загружены), knife N (поворот ножа 0-23 и переставить его).
|
||||
/// </summary>
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroPalace : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necropalace" };
|
||||
public override string getDescription() => "NecromancerTome: чертоги разума - where | scan | rebuild | test | knife N";
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
World world = GameManager.Instance.World;
|
||||
EntityPlayerLocal player = world?.GetPrimaryPlayer();
|
||||
if (player == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Vector3i o = Traverse.Create(typeof(MindPalace)).Method("GetSlot", world, (EntityPlayer)player).GetValue<Vector3i>();
|
||||
string cmd = _params.Count > 0 ? _params[0] : "where";
|
||||
if (cmd == "knife" && _params.Count > 1 && int.TryParse(_params[1], out int r))
|
||||
{
|
||||
MindPalace.KnifeRotation = Mathf.Clamp(r, 0, 3);
|
||||
BlockValue k = Block.GetBlockValue(MindPalace.KnifeBlock);
|
||||
k.rotation = (byte)MindPalace.KnifeRotation;
|
||||
world.SetBlockRPC(o + MindPalace.KnifeAt, k, MarchingCubes.DensityAir);
|
||||
SdtdConsole.Instance.Output("нож: поворот " + MindPalace.KnifeRotation);
|
||||
}
|
||||
else if (cmd == "scan")
|
||||
{
|
||||
var report = new List<string>();
|
||||
int n = MindPalace.Mismatches(world, o, report);
|
||||
SdtdConsole.Instance.Output("расхождений с префабом: " + n);
|
||||
foreach (string line in report)
|
||||
{
|
||||
SdtdConsole.Instance.Output(line);
|
||||
Debug.Log("[NecromancerTome] necropalace scan: " + line);
|
||||
}
|
||||
}
|
||||
else if (cmd == "test")
|
||||
{
|
||||
GameManager.Instance.StartCoroutine(MindPalace.StressTest(world, o));
|
||||
SdtdConsole.Instance.Output("проверка: заливка гравием, через 2 с - постановка (итог - в логе)");
|
||||
}
|
||||
else if (cmd == "rebuild")
|
||||
{
|
||||
MindPalace.Stamp(world, o);
|
||||
SdtdConsole.Instance.Output("чертоги поставлены заново в " + o);
|
||||
}
|
||||
else
|
||||
{
|
||||
SdtdConsole.Instance.Output("чертоги: " + o + ", построены: " + MindPalace.IsBuilt(world, o));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,10 @@ namespace NecromancerTome
|
||||
// hooks a spawn event - see that file for why the SDCS-built trader forces it.
|
||||
GhostTraderPatch.Init();
|
||||
SnakeMode.Init();
|
||||
MindPalace.Init();
|
||||
HandPose.Init();
|
||||
NecroMailbox.Init();
|
||||
Possession.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.
|
||||
|
||||
@@ -37,6 +37,15 @@ namespace NecromancerTome
|
||||
|
||||
public static Vector3 SnakePos;
|
||||
public float Until;
|
||||
/// <summary>Своя угроза вместо змеи (хват «отрицание», 03.10): бежать от этой точки.</summary>
|
||||
public bool HasThreat;
|
||||
public Vector3 Threat;
|
||||
public float Speed = FleeSpeed;
|
||||
/// <summary>Кого забыть, когда бегство кончится (хват «отрицание»: «должен терять меня как цель»).
|
||||
/// Он же угроза: бежать от его ТЕКУЩЕГО места.</summary>
|
||||
public EntityAlive Forget;
|
||||
/// <summary>Больше нуля - бежать, пока Forget ближе этого (и не дольше Until).</summary>
|
||||
public float FarDistance;
|
||||
Vector3 targetPos;
|
||||
int retargetTicks;
|
||||
|
||||
@@ -49,7 +58,7 @@ namespace NecromancerTome
|
||||
|
||||
public override bool CanExecute()
|
||||
{
|
||||
return Time.time < Until && FindFleePos();
|
||||
return Time.time < Until && !FarEnough() && FindFleePos();
|
||||
}
|
||||
|
||||
/// <summary>Сколько криков тревоги уже было в этом кадре: у толпы кричат не все.</summary>
|
||||
@@ -78,13 +87,18 @@ namespace NecromancerTome
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
if (Forget != null)
|
||||
{
|
||||
ForgetNow(theEntity, Forget);
|
||||
Forget = null;
|
||||
}
|
||||
theEntity.moveHelper.Stop();
|
||||
theEntity.SetLookPosition(Vector3.zero);
|
||||
}
|
||||
|
||||
public override bool Continue()
|
||||
{
|
||||
return Time.time < Until && !theEntity.IsDead();
|
||||
return Time.time < Until && !theEntity.IsDead() && !FarEnough();
|
||||
}
|
||||
|
||||
float _diagTime;
|
||||
@@ -104,7 +118,7 @@ namespace NecromancerTome
|
||||
Debug.Log("[NecromancerTome] NecroFleeSnakeTask: " + theEntity.entityId + " поз " + theEntity.position.ToString("F0") +
|
||||
" цель " + targetPos.ToString("F0") + " путь " + (path == null ? "нет" : path.NodeCountRemaining() + " узл.") +
|
||||
" считается " + PathFinderThread.Instance.IsCalculatingPath(theEntity.entityId) +
|
||||
" скор " + FleeSpeed.ToString("F2"));
|
||||
" скор " + Speed.ToString("F2"));
|
||||
}
|
||||
if (--retargetTicks > 0 || PathFinderThread.Instance.IsCalculatingPath(theEntity.entityId))
|
||||
{
|
||||
@@ -113,7 +127,7 @@ namespace NecromancerTome
|
||||
retargetTicks = RetargetTicks;
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
FindFleePos();
|
||||
theEntity.FindPath(targetPos, FleeSpeed, true, this);
|
||||
theEntity.FindPath(targetPos, Speed, true, this);
|
||||
if (sw.Elapsed.TotalMilliseconds > 3.0)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] NecroFleeSnakeTask: долгий перерасчёт у " + theEntity.entityId + " - " + sw.Elapsed.TotalMilliseconds.ToString("F1") + " мс");
|
||||
@@ -122,7 +136,11 @@ namespace NecromancerTome
|
||||
|
||||
bool FindFleePos()
|
||||
{
|
||||
Vector3 away = theEntity.position - SnakePos;
|
||||
if (Forget != null && FarDistance > 0f)
|
||||
{
|
||||
Threat = Forget.position;
|
||||
}
|
||||
Vector3 away = theEntity.position - (HasThreat ? Threat : SnakePos);
|
||||
away.y = 0f;
|
||||
if (away.sqrMagnitude < 0.01f)
|
||||
{
|
||||
@@ -166,9 +184,58 @@ namespace NecromancerTome
|
||||
_e.aiManager.tasks.AddTask(Priority, task);
|
||||
Debug.Log("[NecromancerTome] NecroFleeSnakeTask: зомби " + _e.entityId + " в ужасе");
|
||||
}
|
||||
task.HasThreat = false;
|
||||
task.Speed = FleeSpeed;
|
||||
task.Forget = null;
|
||||
task.FarDistance = 0f;
|
||||
task.Until = Time.time + FearSeconds;
|
||||
}
|
||||
|
||||
/// <summary>Хват «отрицание» (03.10): напугать зомби жестом игрока - бежит от игрока быстрее обычного
|
||||
/// _seconds секунд, сразу и в конце бегства теряет игрока как цель.</summary>
|
||||
bool FarEnough()
|
||||
{
|
||||
return FarDistance > 0f && Forget != null && (theEntity.position - Forget.position).sqrMagnitude > FarDistance * FarDistance;
|
||||
}
|
||||
|
||||
public static void ScareFrom(EntityAlive _e, EntityAlive _threat, float _seconds, float _speed, float _farDistance = 0f)
|
||||
{
|
||||
if (_e.aiManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_e.ConditionalTriggerSleeperWakeUp();
|
||||
NecroFleeSnakeTask task = _e.aiManager.tasks.GetTask<NecroFleeSnakeTask>();
|
||||
if (task == null)
|
||||
{
|
||||
task = new NecroFleeSnakeTask();
|
||||
task.Init(_e);
|
||||
_e.aiManager.tasks.AddTask(Priority, task);
|
||||
}
|
||||
task.HasThreat = true;
|
||||
task.Threat = _threat.position;
|
||||
task.Speed = _speed;
|
||||
task.Forget = _threat;
|
||||
task.FarDistance = _farDistance;
|
||||
task.Until = Time.time + _seconds;
|
||||
ForgetNow(_e, _threat);
|
||||
// Сетка поиска пути строится вокруг игроков - здесь она есть; но зомби может быть на краю.
|
||||
AstarManager.Instance?.AddLocation(_e.position, 40);
|
||||
Debug.Log("[NecromancerTome] NecroFleeSnakeTask: зомби " + _e.entityId + " отрицание - бежит до " + _farDistance + " м (не дольше " + _seconds + " с), скор " + _speed);
|
||||
}
|
||||
|
||||
public static void ForgetNow(EntityAlive _e, EntityAlive _who)
|
||||
{
|
||||
if (_e.GetAttackTarget() == _who)
|
||||
{
|
||||
_e.SetAttackTarget(null, 0);
|
||||
}
|
||||
if (_e.GetRevengeTarget() == _who)
|
||||
{
|
||||
_e.SetRevengeTarget(null);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return base.ToString() + ", flee to " + targetPos + ", " + (Until - Time.time).ToString("F1") + " s";
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Почтовый ящик консоли - только для разработки (03.10: ассистент подбирает позу рук сам, без
|
||||
/// клавиатуры). Работает, лишь если есть папка %APPDATA%\7DaysToDie\NecroMailbox - у игроков её
|
||||
/// нет. Раз в полсекунды: команды из cmd.txt (по строке) выполняются в консоли игры, как набранные
|
||||
/// вручную, файл удаляется, ответ - в лог строками «[NecromancerTome] mailbox:».
|
||||
/// </summary>
|
||||
public static class NecroMailbox
|
||||
{
|
||||
public static string Dir => Path.Combine(GameIO.GetUserGameDataDir(), "NecroMailbox");
|
||||
static string CmdFile => Path.Combine(Dir, "cmd.txt");
|
||||
static float _next;
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
ModEvents.UnityUpdate.RegisterHandler(OnUnityUpdate);
|
||||
}
|
||||
|
||||
static void OnUnityUpdate(ref ModEvents.SUnityUpdateData _data)
|
||||
{
|
||||
if (Time.unscaledTime < _next)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_next = Time.unscaledTime + 0.5f;
|
||||
if (!File.Exists(CmdFile) || SdtdConsole.Instance == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
string[] lines;
|
||||
try
|
||||
{
|
||||
lines = File.ReadAllLines(CmdFile);
|
||||
File.Delete(CmdFile);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return; // файл ещё пишется - в следующий раз
|
||||
}
|
||||
foreach (string raw in lines)
|
||||
{
|
||||
string line = raw.Trim();
|
||||
if (line.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Debug.Log("[NecromancerTome] mailbox: > " + line);
|
||||
try
|
||||
{
|
||||
List<string> res = SdtdConsole.Instance.ExecuteSync(line, null);
|
||||
if (res != null)
|
||||
{
|
||||
foreach (string r in res)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] mailbox: " + r);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] mailbox: ошибка - " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>necroshot [имя] - снимок экрана игры в %APPDATA%\7DaysToDie\NecroMailbox\shots\имя.png
|
||||
/// (как F9, только в известное место). Файл появляется в конце кадра.</summary>
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroShot : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necroshot" };
|
||||
public override string getDescription() => "NecromancerTome: снимок экрана в NecroMailbox/shots/<имя>.png";
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
string dir = Path.Combine(NecroMailbox.Dir, "shots");
|
||||
Directory.CreateDirectory(dir);
|
||||
string name = _params.Count > 0 ? _params[0] : System.DateTime.Now.ToString("HHmmss");
|
||||
string path = Path.Combine(dir, name + ".png");
|
||||
ScreenCapture.CaptureScreenshot(path);
|
||||
SdtdConsole.Instance.Output("снимок: " + path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Книга с оглавлением (03.10.2026, BACKLOG «Книга с оглавлением и главами»). Окно necroTome
|
||||
/// (Config/XUi_InGame/windows.xml): оглавление - кнопки глав (до восьми), глава - страницы
|
||||
/// с «Назад/Далее», «Оглавление» возвращает к списку, «Закрыть» и Esc закрывают.
|
||||
///
|
||||
/// СОДЕРЖАНИЕ - список Chapters ниже: ключ названия, ключи страниц (Localization.csv,
|
||||
/// necroTome*), условие открытия. Закрытая глава в оглавлении не показывается. Условия брать
|
||||
/// из того, что игра уже хранит (уровень некромантии, рецепты, испытания), своё хранение - только
|
||||
/// для чистых событий, по cvar на событие.
|
||||
/// </summary>
|
||||
public static class NecroTomeBook
|
||||
{
|
||||
public const string WindowGroup = "necroTome";
|
||||
|
||||
public class Chapter
|
||||
{
|
||||
public string TitleKey;
|
||||
public string[] PageKeys;
|
||||
public Func<EntityPlayerLocal, bool> Unlocked;
|
||||
}
|
||||
|
||||
public static readonly List<Chapter> Chapters = new List<Chapter>
|
||||
{
|
||||
// Глава 1 (указание 03.10): статья о некроэнергии, сокращённая - без примеров и бытовой речи.
|
||||
new Chapter
|
||||
{
|
||||
TitleKey = "necroTomeCh1Title",
|
||||
PageKeys = new[] { "necroTomeCh1P1", "necroTomeCh1P2", "necroTomeCh1P3", "necroTomeCh1P4" },
|
||||
Unlocked = _p => true
|
||||
},
|
||||
};
|
||||
|
||||
public static void Open(EntityPlayerLocal _player)
|
||||
{
|
||||
LocalPlayerUI ui = LocalPlayerUI.GetUIForPlayer(_player);
|
||||
ui?.windowManager.Open(WindowGroup, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class XUiC_NecroTome : XUiController
|
||||
{
|
||||
const int MaxButtons = 8;
|
||||
|
||||
XUiV_Label _title, _text, _page;
|
||||
XUiC_SimpleButton _toc, _prev, _next, _close;
|
||||
readonly XUiC_SimpleButton[] _chapterButtons = new XUiC_SimpleButton[MaxButtons];
|
||||
/// <summary>Глава по кнопке оглавления (только открытые главы, по порядку).</summary>
|
||||
readonly int[] _buttonChapter = new int[MaxButtons];
|
||||
int _chapter = -1, _pageIndex;
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
base.Init();
|
||||
_title = GetChildById("lblTitle")?.ViewComponent as XUiV_Label;
|
||||
_text = GetChildById("lblText")?.ViewComponent as XUiV_Label;
|
||||
_page = GetChildById("lblPage")?.ViewComponent as XUiV_Label;
|
||||
_toc = GetChildById("btnToc") as XUiC_SimpleButton;
|
||||
_prev = GetChildById("btnPrev") as XUiC_SimpleButton;
|
||||
_next = GetChildById("btnNext") as XUiC_SimpleButton;
|
||||
_close = GetChildById("btnClose") as XUiC_SimpleButton;
|
||||
if (_toc != null) _toc.OnPressed += (_s, _m) => ShowToc();
|
||||
if (_prev != null) _prev.OnPressed += (_s, _m) => Turn(-1);
|
||||
if (_next != null) _next.OnPressed += (_s, _m) => Turn(1);
|
||||
if (_close != null) _close.OnPressed += (_s, _m) => xui.playerUI.windowManager.Close(NecroTomeBook.WindowGroup);
|
||||
for (int i = 0; i < MaxButtons; i++)
|
||||
{
|
||||
int idx = i;
|
||||
_chapterButtons[i] = GetChildById("btnCh" + (i + 1)) as XUiC_SimpleButton;
|
||||
if (_chapterButtons[i] != null)
|
||||
{
|
||||
_chapterButtons[i].OnPressed += (_s, _m) => ShowChapter(_buttonChapter[idx], 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnOpen()
|
||||
{
|
||||
base.OnOpen();
|
||||
ShowToc();
|
||||
}
|
||||
|
||||
void ShowToc()
|
||||
{
|
||||
_chapter = -1;
|
||||
SetLabel(_title, Localization.Get("necroTomeToc"));
|
||||
SetLabel(_text, "");
|
||||
SetLabel(_page, "");
|
||||
EntityPlayerLocal player = xui.playerUI.entityPlayer;
|
||||
int b = 0;
|
||||
for (int c = 0; c < NecroTomeBook.Chapters.Count && b < MaxButtons; c++)
|
||||
{
|
||||
NecroTomeBook.Chapter ch = NecroTomeBook.Chapters[c];
|
||||
if (ch.Unlocked != null && !ch.Unlocked(player))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
_buttonChapter[b] = c;
|
||||
if (_chapterButtons[b] != null)
|
||||
{
|
||||
_chapterButtons[b].Text = Localization.Get(ch.TitleKey);
|
||||
_chapterButtons[b].ViewComponent.IsVisible = true;
|
||||
}
|
||||
b++;
|
||||
}
|
||||
for (; b < MaxButtons; b++)
|
||||
{
|
||||
if (_chapterButtons[b] != null)
|
||||
{
|
||||
_chapterButtons[b].ViewComponent.IsVisible = false;
|
||||
}
|
||||
}
|
||||
SetVisible(_toc, false);
|
||||
SetVisible(_prev, false);
|
||||
SetVisible(_next, false);
|
||||
}
|
||||
|
||||
void ShowChapter(int _c, int _p)
|
||||
{
|
||||
if (_c < 0 || _c >= NecroTomeBook.Chapters.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
NecroTomeBook.Chapter ch = NecroTomeBook.Chapters[_c];
|
||||
_chapter = _c;
|
||||
_pageIndex = Mathf.Clamp(_p, 0, ch.PageKeys.Length - 1);
|
||||
foreach (XUiC_SimpleButton btn in _chapterButtons)
|
||||
{
|
||||
SetVisible(btn, false);
|
||||
}
|
||||
SetLabel(_title, Localization.Get(ch.TitleKey));
|
||||
SetLabel(_text, Localization.Get(ch.PageKeys[_pageIndex]));
|
||||
SetLabel(_page, (_pageIndex + 1) + " / " + ch.PageKeys.Length);
|
||||
SetVisible(_toc, true);
|
||||
SetVisible(_prev, true);
|
||||
SetVisible(_next, true);
|
||||
if (_prev != null) _prev.Enabled = _pageIndex > 0;
|
||||
if (_next != null) _next.Enabled = _pageIndex < ch.PageKeys.Length - 1;
|
||||
}
|
||||
|
||||
void Turn(int _d)
|
||||
{
|
||||
if (_chapter >= 0)
|
||||
{
|
||||
ShowChapter(_chapter, _pageIndex + _d);
|
||||
}
|
||||
}
|
||||
|
||||
static void SetLabel(XUiV_Label _l, string _s)
|
||||
{
|
||||
if (_l != null)
|
||||
{
|
||||
_l.Text = _s;
|
||||
}
|
||||
}
|
||||
|
||||
static void SetVisible(XUiController _c, bool _v)
|
||||
{
|
||||
if (_c?.ViewComponent != null)
|
||||
{
|
||||
_c.ViewComponent.IsVisible = _v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>necrotome - открыть книгу (проверка окна без чертогов, в том числе через почтовый ящик).</summary>
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroTome : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrotome" };
|
||||
public override string getDescription() => "NecromancerTome: открыть книгу с оглавлением";
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
EntityPlayerLocal player = GameManager.Instance.World?.GetPrimaryPlayer();
|
||||
if (player != null)
|
||||
{
|
||||
NecroTomeBook.Open(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,10 @@
|
||||
<HintPath>..\..\..\7DaysToDie_Data\Managed\Unity.Addressables.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.ScreenCaptureModule">
|
||||
<HintPath>..\..\..\7DaysToDie_Data\Managed\UnityEngine.ScreenCaptureModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="UnityEngine.CoreModule">
|
||||
<HintPath>..\..\..\7DaysToDie_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
|
||||
@@ -153,6 +153,11 @@ namespace NecromancerTome
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Одержимый зомби, пока в нём игрок, в некромантию не идёт (решение 03.10).
|
||||
if (Possession.IsPossessed(killedEntity))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float kills = AddKillsCVar(__instance);
|
||||
SyncNecromancyLevel(__instance, kills);
|
||||
|
||||
@@ -0,0 +1,688 @@
|
||||
using GamePath;
|
||||
using System.Collections.Generic;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Одержимость - заклинание хвата «пиф-паф» (решения пользователя 03.10, BACKLOG «Одержимость»).
|
||||
/// ЛКМ по зомби-гуманоиду (EntityZombie и наследники; не звери и не стервятники) - игрок перехватывает
|
||||
/// управление им: бегает, прыгает, бьёт. Тело игрока не падает, а цепенеет и стоит (аниматор на паузе,
|
||||
/// ввод снят) - его могут атаковать, это и есть цена. Любое время суток, и в кровавую луну.
|
||||
/// Остальные зомби, увидев одержимого, бросаются на него, как на игрока, - можно танковать.
|
||||
/// Выход - R, гибель одержимого или смерть тела. После выхода зомби пугается и убегает, как при
|
||||
/// «отрицании». Убийства одержимым игроку не засчитываются (урон от его имени), его собственная
|
||||
/// смерть - тоже (NecromancyKillCreditPatch).
|
||||
///
|
||||
/// КАК ВЕДЁМ ЗОМБИ. Его ИИ выключен (EntityAlive.updateTasks пропущен), а движение - родное:
|
||||
/// SetMoveForwardWithModifiers, как делает сама игра при DebugStopEnemiesMoving и EntityMoveHelper;
|
||||
/// прыжок - Jumping + SetJumpDistance, как EntityMoveHelper.StartJump; удар - Attack(false/true),
|
||||
/// как EAIApproachAndAttackTarget. Походка, анимация и урон - самого зомби.
|
||||
///
|
||||
/// КАК СТОИТ ТЕЛО. PlayerMoveController.Update пропущен (и сам вызывает stopMoving, как у тела в
|
||||
/// рэгдолле), вид - от третьего лица, аниматор модели на паузе. Камера - за головой зомби (постфикс
|
||||
/// к vp_FPCamera.LateUpdate, как у змеи).
|
||||
///
|
||||
/// ПРИЁМЫ ЗОМБИ (указание 03.10: «у копа есть плевок и подрыв - чтобы такие спеллы становились доступны»):
|
||||
/// ПКМ - дальняя атака, если у зомби есть задача ИИ EAIRangedAttackTarget (Ранчер, Чак, коп, мутант):
|
||||
/// её параметры берутся из задачи захваченного зомби, а шаги повторяют её Start/Update/Reset -
|
||||
/// замах (StartAnimAction startAnimType+3000), отпуск (ContinueAnimAction +1), выстрел родным
|
||||
/// оружием (UseHoldingItem itemType), пока держишь кнопку и не вышло attackDuration. Направление -
|
||||
/// точка под прицелом камеры (SetLookPosition), как у ИИ - голова цели.
|
||||
/// Пробел - прыжок на собственную дальность зомби (jumpMaxDistance из JumpMaxDistance): у паука 7-9 м,
|
||||
/// как его EAILeap, у остальных их 1-4 м.
|
||||
/// ПКМ у крикуньи - её крик (SoundAlert): все зомби в ScreamRange бросаются на неё, даже не видя (её
|
||||
/// ванильный зов орды - часть разведки AIScoutHordeSpawner, напрямую не вызываем).
|
||||
/// E - подрыв у EntityZombieCop (коп, мутант, подрывник): HandlePrimingDetonator, через несколько
|
||||
/// секунд взрыв от имени зомби - он гибнет, игрок возвращается в тело.
|
||||
///
|
||||
/// ПАМЯТЬ. Ничего не сохраняется. Ссылки - только на зомби, тело и замороженные аниматоры, на время
|
||||
/// одержимости; на выходе и при выходе из мира всё обнуляется.
|
||||
/// </summary>
|
||||
public static class Possession
|
||||
{
|
||||
public static bool Active;
|
||||
public static EntityAlive Zombie;
|
||||
|
||||
/// <summary>Камера - позади и выше головы зомби.</summary>
|
||||
public const float CamBack = 2.6f;
|
||||
public const float CamUp = 0.6f;
|
||||
/// <summary>Как далеко зомби замечают одержимого: зрение зомби - 30 м (entityclasses.xml SightRange).</summary>
|
||||
public const float TauntRange = 30f;
|
||||
public const float TauntPeriod = 1f;
|
||||
public const int TauntTicks = 1200;
|
||||
/// <summary>Сколько чанков держать вокруг тела, пока игрок в зомби (указание 03.10: «сильно ограничить
|
||||
/// видимость у тела - ему вообще нет смысла что-либо видеть»). Тест 03.10: две полные области обзора
|
||||
/// (тело + одержимый) - память росла с каждым шагом зомби, а выход давал подвисание ~50 с. Трёх чанков
|
||||
/// хватает, чтобы тело стояло на земле и зомби могли к нему подойти.</summary>
|
||||
public const int BodyViewDim = 3;
|
||||
static int _bodyViewDim = -1;
|
||||
/// <summary>Полный радиус области игрока (до сжатия) - для систем, рисующих мир у одержимого
|
||||
/// (SnakeAudioSwap: дальний ландшафт, Dynamic Mesh, детализация зданий). -1 - не сжата.</summary>
|
||||
public static int FullViewDim => _bodyViewDim;
|
||||
public const float ScreamRange = 50f;
|
||||
public const float ScreamCooldown = 10f;
|
||||
static float _nextScream;
|
||||
|
||||
static EntityPlayerLocal _player;
|
||||
static float _yaw, _pitch;
|
||||
/// <summary>Куда повёрнуто тело зомби. Шага вбок и назад у зомби нет (тест 03.10: «не работают A, D и S») -
|
||||
/// при движении тело разворачивается в сторону шага и идёт вперёд, без движения - смотрит туда же, куда камера.</summary>
|
||||
static float _bodyYaw;
|
||||
public const float TurnSpeed = 540f;
|
||||
static float _nextAttack;
|
||||
static float _nextTaunt;
|
||||
static ChunkManager.ChunkObserver _observer;
|
||||
static readonly List<Animator> _frozen = new List<Animator>();
|
||||
static readonly List<float> _frozenSpeed = new List<float>();
|
||||
static bool _loggedCamera;
|
||||
// Дальняя атака захваченного зомби (копия параметров его EAIRangedAttackTarget).
|
||||
static bool _hasRanged;
|
||||
static int _rItem, _rStartAnim;
|
||||
static float _rRelease, _rDuration, _rCooldown;
|
||||
static string _rSoundStart, _rSoundRelease;
|
||||
enum RState { Idle, StartAnim, Release, Fire }
|
||||
static RState _rState;
|
||||
static float _rTime, _rNext;
|
||||
|
||||
static GameObject _hud;
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
ModEvents.UnityUpdate.RegisterHandler(OnUnityUpdate);
|
||||
if (_hud == null)
|
||||
{
|
||||
_hud = new GameObject("NecroPossessionHud");
|
||||
Object.DontDestroyOnLoad(_hud);
|
||||
_hud.AddComponent<PossessionHud>();
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsPossessed(Entity _e) => Active && _e != null && Zombie != null && _e.entityId == Zombie.entityId;
|
||||
|
||||
/// <summary>Только зомби-гуманоиды (решение 03.10: «одержимая собака или стервятник - такое себе»).</summary>
|
||||
public static bool CanPossess(EntityAlive _e) => _e is EntityZombie && !_e.IsDead() && HandHolds.IsZombie(_e);
|
||||
|
||||
/// <summary>True - вселились (для звука «пиф-пафа»).</summary>
|
||||
public static bool Enter(EntityPlayerLocal _player0, EntityAlive _zombie)
|
||||
{
|
||||
if (Active || SnakeMode.Active || MindPalace.Active || _player0 == null || _player0.IsDead() ||
|
||||
_player0.AttachedToEntity != null || !CanPossess(_zombie) || _player0.world.IsRemote())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Active = true;
|
||||
_player = _player0;
|
||||
Zombie = _zombie;
|
||||
_yaw = _player0.rotation.y;
|
||||
_bodyYaw = _zombie.rotation.y;
|
||||
_pitch = 0f;
|
||||
_nextAttack = 0f;
|
||||
_nextTaunt = 0f;
|
||||
_nextScream = 0f;
|
||||
_loggedCamera = false;
|
||||
|
||||
// Зомби - в руки игрока: цели, путь и шаги ИИ сброшены.
|
||||
_zombie.SetAttackTarget(null, 0);
|
||||
_zombie.SetRevengeTarget(null);
|
||||
PathFinderThread.Instance.RemovePathsFor(_zombie.entityId);
|
||||
_zombie.navigator?.clearPath();
|
||||
_zombie.moveHelper?.Stop();
|
||||
_zombie.ConditionalTriggerSleeperWakeUp();
|
||||
_zombie.SetRotation(new Vector3(0f, _yaw, 0f));
|
||||
|
||||
// Тело цепенеет: вид от третьего лица, аниматоры модели на паузе.
|
||||
_player0.SetFirstPersonView(false, true);
|
||||
FreezeBody(_player0, true);
|
||||
_observer = GameManager.Instance.AddChunkObserver(_zombie.position, true, GameUtils.GetViewDistance(), -1);
|
||||
ChunkManager.ChunkObserver body = _player0.ChunkObserver;
|
||||
if (body != null && body.viewDim > BodyViewDim)
|
||||
{
|
||||
_bodyViewDim = body.viewDim;
|
||||
body.viewDim = BodyViewDim;
|
||||
}
|
||||
ReadRanged(_zombie);
|
||||
Hint(_player0, _zombie);
|
||||
Debug.Log("[NecromancerTome] Possession: вселение в " + _zombie.EntityName + " (" + _zombie.entityId + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void Exit(string _why)
|
||||
{
|
||||
if (!Active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Active = false;
|
||||
EntityAlive z = Zombie;
|
||||
EntityPlayerLocal player = _player;
|
||||
if (_rState != RState.Idle && z != null && !z.IsDead())
|
||||
{
|
||||
StopRanged(z);
|
||||
}
|
||||
_rState = RState.Idle;
|
||||
_hasRanged = false;
|
||||
Zombie = null;
|
||||
_player = null;
|
||||
Debug.Log("[NecromancerTome] Possession: выход - " + _why);
|
||||
FreezeBody(player, false);
|
||||
if (_bodyViewDim > 0 && player?.ChunkObserver != null)
|
||||
{
|
||||
player.ChunkObserver.viewDim = _bodyViewDim;
|
||||
}
|
||||
_bodyViewDim = -1;
|
||||
if (player != null && !player.IsDead())
|
||||
{
|
||||
player.SetFirstPersonView(true, true);
|
||||
}
|
||||
if (_observer != null && GameManager.Instance?.World != null)
|
||||
{
|
||||
GameManager.Instance.RemoveChunkObserver(_observer);
|
||||
}
|
||||
_observer = null;
|
||||
World world = GameManager.Instance?.World;
|
||||
if (world == null || z == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Кто гнался за одержимым - отпускает его.
|
||||
for (int i = 0; i < world.EntityAlives.Count; i++)
|
||||
{
|
||||
EntityAlive e = world.EntityAlives[i];
|
||||
if (e != null && e != z && e.GetAttackTarget() == z)
|
||||
{
|
||||
e.SetAttackTarget(null, 0);
|
||||
}
|
||||
}
|
||||
// Сам зомби в ужасе бежит от тела игрока (решение 03.10: «пугается и убегает как при отрицании»).
|
||||
if (!z.IsDead() && player != null)
|
||||
{
|
||||
z.SetMoveForwardWithModifiers(0f, 0f, 0f, false);
|
||||
NecroFleeSnakeTask.ScareFrom(z, player, HandHolds.DenyMaxSeconds, HandHolds.DenySpeed, HandHolds.DenyFarDistance);
|
||||
}
|
||||
}
|
||||
|
||||
static void FreezeBody(EntityPlayerLocal _p, bool _freeze)
|
||||
{
|
||||
if (_freeze)
|
||||
{
|
||||
_frozen.Clear();
|
||||
_frozenSpeed.Clear();
|
||||
Transform model = _p?.emodel?.GetModelTransform();
|
||||
if (model == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (Animator a in model.GetComponentsInChildren<Animator>(true))
|
||||
{
|
||||
_frozen.Add(a);
|
||||
_frozenSpeed.Add(a.speed);
|
||||
a.speed = 0f;
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < _frozen.Count; i++)
|
||||
{
|
||||
if (_frozen[i] != null)
|
||||
{
|
||||
_frozen[i].speed = _frozenSpeed[i];
|
||||
}
|
||||
}
|
||||
_frozen.Clear();
|
||||
_frozenSpeed.Clear();
|
||||
}
|
||||
|
||||
static void OnUnityUpdate(ref ModEvents.SUnityUpdateData _data)
|
||||
{
|
||||
World world = GameManager.Instance?.World;
|
||||
if (world == null)
|
||||
{
|
||||
if (Active || Zombie != null || _frozen.Count > 0)
|
||||
{
|
||||
Active = false;
|
||||
Zombie = null;
|
||||
_player = null;
|
||||
_observer = null;
|
||||
_bodyViewDim = -1;
|
||||
_frozen.Clear();
|
||||
_frozenSpeed.Clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!Active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
EntityAlive z = Zombie;
|
||||
if (_player == null || _player.IsDead())
|
||||
{
|
||||
Exit("тело погибло");
|
||||
return;
|
||||
}
|
||||
if (z == null || z.IsDead() || z.IsMarkedForUnload())
|
||||
{
|
||||
Exit("одержимый погиб");
|
||||
return;
|
||||
}
|
||||
PlayerActionsLocal input = _player.playerInput;
|
||||
bool gui = _player.PlayerUI != null && _player.PlayerUI.windowManager.IsModalWindowOpen();
|
||||
if (input != null && !gui && (input.Reload.WasPressed || input.PermanentActions.Reload.WasPressed))
|
||||
{
|
||||
Exit("R");
|
||||
return;
|
||||
}
|
||||
Vector2 move = Vector2.zero;
|
||||
bool run = false, jump = false, attack = false, ranged = false, rangedHeld = false, detonate = false;
|
||||
if (input != null && !gui)
|
||||
{
|
||||
Look();
|
||||
move = new Vector2(input.Move.X, input.Move.Y);
|
||||
run = input.Run.IsPressed;
|
||||
jump = input.Jump.WasPressed;
|
||||
attack = input.Primary.WasPressed;
|
||||
ranged = input.Secondary.WasPressed;
|
||||
rangedHeld = input.Secondary.IsPressed;
|
||||
// E на клавиатуре - в PlayerActionsPermanent.Activate; у PlayerActionsLocal.Activate только
|
||||
// кнопка геймпада (тест 03.10: «подрыва нету»).
|
||||
detonate = input.Activate.WasPressed || input.PermanentActions.Activate.WasPressed;
|
||||
}
|
||||
if (detonate && z is EntityZombieCop cop && !cop.isPrimed)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Possession: подрыв " + z.EntityName);
|
||||
cop.HandlePrimingDetonator();
|
||||
}
|
||||
Ranged(z, ranged, rangedHeld);
|
||||
|
||||
bool moving = move.sqrMagnitude > 0.01f;
|
||||
float wantYaw = moving ? _yaw + Mathf.Atan2(move.x, move.y) * Mathf.Rad2Deg : _yaw;
|
||||
_bodyYaw = Mathf.MoveTowardsAngle(_bodyYaw, wantYaw, TurnSpeed * Time.deltaTime);
|
||||
z.SetRotation(new Vector3(0f, _bodyYaw, 0f));
|
||||
z.rotation.x = -_pitch; // наклон головы - для луча удара
|
||||
if (moving)
|
||||
{
|
||||
z.SetMoveForwardWithModifiers(run ? z.GetMoveSpeedAggro() : z.GetMoveSpeed(), 1f, 0f, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
z.SetMoveForwardWithModifiers(0f, 0f, 0f, false);
|
||||
}
|
||||
if (jump && z.onGround && !z.Jumping)
|
||||
{
|
||||
z.Jumping = true;
|
||||
z.SetJumpDistance(Mathf.Max(1.5f, z.jumpMaxDistance), 1.2f);
|
||||
}
|
||||
if (ranged && IsScreamer(z))
|
||||
{
|
||||
Scream(world, z);
|
||||
}
|
||||
if (attack && Time.time >= _nextAttack && z.Attack(false))
|
||||
{
|
||||
z.Attack(true);
|
||||
_nextAttack = Time.time + Mathf.Max(0.3f, z.GetAttackTimeoutTicks() / 20f);
|
||||
}
|
||||
_observer?.SetPosition(z.position);
|
||||
Taunt(world, z);
|
||||
}
|
||||
|
||||
static void ReadRanged(EntityAlive _z)
|
||||
{
|
||||
_hasRanged = false;
|
||||
_rState = RState.Idle;
|
||||
_rNext = 0f;
|
||||
List<EAIRangedAttackTarget> tasks = _z.aiManager?.GetTasks<EAIRangedAttackTarget>();
|
||||
if (tasks == null || tasks.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
EAIRangedAttackTarget t = tasks[0];
|
||||
_hasRanged = true;
|
||||
_rItem = t.itemActionType;
|
||||
_rStartAnim = t.startAnimType;
|
||||
_rRelease = t.releaseDelay;
|
||||
_rDuration = t.attackDuration > 0f ? t.attackDuration : 3f;
|
||||
// Вдвое быстрее ИИ (тест 03.10: «перезаряд медленный»).
|
||||
_rCooldown = Mathf.Max(1f, t.baseCooldown * 0.5f);
|
||||
_rSoundStart = t.soundStartName;
|
||||
_rSoundRelease = t.soundReleaseName;
|
||||
Debug.Log("[NecromancerTome] Possession: дальняя атака - оружие " + _rItem + ", замах " + _rStartAnim + ", длит. " + _rDuration + " с, откат " + _rCooldown + " с");
|
||||
}
|
||||
|
||||
/// <summary>Подсказка при вселении: что умеет этот зомби.</summary>
|
||||
static void Hint(EntityPlayerLocal _p, EntityAlive _z)
|
||||
{
|
||||
string text = Localization.Get("necroPossessHintBase");
|
||||
if (_hasRanged)
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintRanged");
|
||||
}
|
||||
if (IsScreamer(_z))
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintScream");
|
||||
}
|
||||
if (_z.jumpMaxDistance >= 5f)
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintLeap");
|
||||
}
|
||||
if (_z is EntityZombieCop)
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintDetonate");
|
||||
}
|
||||
GameManager.ShowTooltip(_p, text);
|
||||
}
|
||||
|
||||
/// <summary>Точка под прицелом камеры (в мировых координатах), до 50 м.</summary>
|
||||
static Vector3 AimPoint()
|
||||
{
|
||||
Transform cam = _player?.cameraTransform;
|
||||
if (cam == null)
|
||||
{
|
||||
return Zombie.position + Quaternion.Euler(-_pitch, _yaw, 0f) * Vector3.forward * 30f;
|
||||
}
|
||||
if (Physics.Raycast(cam.position, cam.forward, out RaycastHit hit, 50f, -5, QueryTriggerInteraction.Ignore) &&
|
||||
hit.distance > CamBack + 0.5f)
|
||||
{
|
||||
return hit.point + Origin.position;
|
||||
}
|
||||
return cam.position + Origin.position + cam.forward * 50f;
|
||||
}
|
||||
|
||||
/// <summary>ПКМ - дальняя атака зомби по шагам ванильной EAIRangedAttackTarget.</summary>
|
||||
static void Ranged(EntityAlive _z, bool _pressed, bool _held)
|
||||
{
|
||||
if (!_hasRanged)
|
||||
{
|
||||
return;
|
||||
}
|
||||
float dt = Time.deltaTime;
|
||||
switch (_rState)
|
||||
{
|
||||
case RState.Idle:
|
||||
if (!_pressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Time.time < _rNext || !_z.IsAttackValid())
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Possession: дальняя атака не готова - откат " + Mathf.Max(0f, _rNext - Time.time).ToString("F1") + " с, атака " + (_z.IsAttackValid() ? "можно" : "нельзя"));
|
||||
return;
|
||||
}
|
||||
RearmRanged(_z);
|
||||
_z.emodel.avatarController.hitWeightMax = 0.5f;
|
||||
_rTime = 0f;
|
||||
if (!string.IsNullOrEmpty(_rSoundStart))
|
||||
{
|
||||
Audio.Manager.BroadcastPlay(_z, _rSoundStart);
|
||||
}
|
||||
if (_rStartAnim >= 0)
|
||||
{
|
||||
_z.StartAnimAction(_rStartAnim + 3000);
|
||||
_rState = RState.StartAnim;
|
||||
}
|
||||
else
|
||||
{
|
||||
_rState = RState.Fire;
|
||||
}
|
||||
return;
|
||||
case RState.StartAnim:
|
||||
_z.SetLookPosition(AimPoint());
|
||||
if (_z.GetAnimActionState() != AvatarController.ActionState.Ready)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_z.ContinueAnimAction(_rStartAnim + 1 + 3000);
|
||||
if (!string.IsNullOrEmpty(_rSoundRelease))
|
||||
{
|
||||
Audio.Manager.BroadcastPlay(_z, _rSoundRelease);
|
||||
}
|
||||
_rState = RState.Release;
|
||||
_rTime = 0f;
|
||||
return;
|
||||
case RState.Release:
|
||||
_z.SetLookPosition(AimPoint());
|
||||
_rTime += dt;
|
||||
if (_rTime >= _rRelease)
|
||||
{
|
||||
_rState = RState.Fire;
|
||||
_rTime = 0f;
|
||||
}
|
||||
return;
|
||||
case RState.Fire:
|
||||
_z.SetLookPosition(AimPoint());
|
||||
_rTime += dt;
|
||||
_z.UseHoldingItem(_rItem, false);
|
||||
// Доводим до конца по одному нажатию: у плевка сначала 1-3 предупреждения по 1.2 с
|
||||
// (ItemActionVomit), и с отпущенной кнопкой он сбрасывался (тест 03.10: «если зажать
|
||||
// ПКМ - плюёт»). Конец - оружие закончило или вышла длительность задачи.
|
||||
if (_rTime > 0.2f && !_z.IsHoldingItemInUse(_rItem) || _rTime >= _rDuration)
|
||||
{
|
||||
StopRanged(_z);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Перед новой дальней атакой - оружие в исходное (тест 03.10: «плюёт, но один раз»).
|
||||
/// У ИИ это делает его собственный цикл; здесь - сброс очереди выстрелов (ResetBurstShot), отпущенный
|
||||
/// курок, отменённая перезарядка и патрон в магазине (Infinite_ammo, магазин 1).</summary>
|
||||
static void RearmRanged(EntityAlive _z)
|
||||
{
|
||||
ItemInventoryData inv = _z.inventory?.holdingItemData;
|
||||
if (inv == null || _rItem >= inv.actionData.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ItemValue iv = _z.inventory.holdingItemItemValue;
|
||||
string before = "патронов " + (iv != null ? iv.Meta.ToString() : "?");
|
||||
if (inv.actionData[_rItem] is ItemActionRanged.ItemActionDataRanged d && _z.inventory.holdingItem.Actions[_rItem] is ItemActionRanged ranged)
|
||||
{
|
||||
before += ", очередь " + d.curBurstCount + ", перезарядка " + d.isReloading;
|
||||
ranged.ResetBurstShot(d);
|
||||
d.bReleased = true;
|
||||
d.bPressed = false;
|
||||
d.isReloading = false;
|
||||
d.isReloadRequested = false;
|
||||
}
|
||||
// Плевок: без лишних «позывов» - сразу последнее предупреждение (ItemActionVomit: 1-3 по 1.2 с).
|
||||
if (inv.actionData[_rItem] is ItemActionVomit.ItemActionDataVomit dv && _z.inventory.holdingItem.Actions[_rItem] is ItemActionVomit vomit)
|
||||
{
|
||||
dv.numWarningsPlayed = Mathf.Max(0, Traverse.Create(vomit).Field("warningMax").GetValue<int>() - 1);
|
||||
dv.warningTime = 0f;
|
||||
}
|
||||
if (iv != null && iv.Meta < 1)
|
||||
{
|
||||
iv.Meta = 1;
|
||||
}
|
||||
Debug.Log("[NecromancerTome] Possession: дальняя атака - взвод (" + before + ")");
|
||||
}
|
||||
|
||||
static void StopRanged(EntityAlive _z)
|
||||
{
|
||||
_z.ShowHoldingItem(false);
|
||||
_z.UseHoldingItem(_rItem, true);
|
||||
_z.StartAnimAction(9999);
|
||||
_z.SetLookPosition(Vector3.zero);
|
||||
_z.emodel.avatarController.hitWeightMax = 1f;
|
||||
_rState = RState.Idle;
|
||||
_rNext = Time.time + _rCooldown;
|
||||
}
|
||||
|
||||
static bool IsScreamer(EntityAlive _z)
|
||||
{
|
||||
return EntityClass.list.TryGetValue(_z.entityClass, out EntityClass c) && c.entityClassName.StartsWith("zombieScreamer");
|
||||
}
|
||||
|
||||
/// <summary>Крик одержимой крикуньи: её звук тревоги и все зомби в ScreamRange - на неё (танкование).</summary>
|
||||
static void Scream(World _world, EntityAlive _z)
|
||||
{
|
||||
if (Time.time < _nextScream)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_nextScream = Time.time + ScreamCooldown;
|
||||
string snd = _z.GetSoundAlert();
|
||||
if (!string.IsNullOrEmpty(snd))
|
||||
{
|
||||
Audio.Manager.BroadcastPlay(_z, snd);
|
||||
}
|
||||
int n = 0;
|
||||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||||
{
|
||||
EntityAlive e = _world.EntityAlives[i];
|
||||
if (e == null || e == _z || e.IsDead() || !(e is EntityZombie) || e.aiManager == null ||
|
||||
PetFollowPatch.FindOwnerOfPet(e.entityId) != null || (e.position - _z.position).sqrMagnitude > ScreamRange * ScreamRange)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
e.ConditionalTriggerSleeperWakeUp();
|
||||
AllowZombieTarget(e);
|
||||
e.SetAttackTarget(_z, TauntTicks);
|
||||
n++;
|
||||
}
|
||||
Debug.Log("[NecromancerTome] Possession: крик крикуньи - на зов идут " + n);
|
||||
}
|
||||
|
||||
static void Look()
|
||||
{
|
||||
PlayerMoveController pmc = PlayerMoveController.Instance;
|
||||
Vector2 sens = pmc != null ? pmc.mouseLookSensitivity : Vector2.one * 5f;
|
||||
int invert = pmc != null && pmc.invertMouse != 0 ? pmc.invertMouse : 1;
|
||||
_yaw += Input.GetAxis("Mouse X") * sens.x;
|
||||
_pitch = Mathf.Clamp(_pitch + Input.GetAxis("Mouse Y") * sens.y * invert, -70f, 70f);
|
||||
}
|
||||
|
||||
/// <summary>Раз в секунду: зомби, которые видят одержимого, бросаются на него (решение 03.10:
|
||||
/// «зомби начинают считать этого зомби ещё одним игроком»). Ванильный подход к цели принимает
|
||||
/// только свои классы (EntityPlayer...) - добавляем EntityZombie, как CharmPatch; это безопасно:
|
||||
/// цель-зомби им ставим только мы (поиск цели EAISetNearestEntityAsTarget не трогаем).</summary>
|
||||
static void Taunt(World _world, EntityAlive _z)
|
||||
{
|
||||
if (Time.time < _nextTaunt)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_nextTaunt = Time.time + TauntPeriod;
|
||||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||||
{
|
||||
EntityAlive e = _world.EntityAlives[i];
|
||||
if (e == null || e == _z || e.IsDead() || !(e is EntityZombie) || e.GetAttackTarget() == _z ||
|
||||
e.aiManager == null || PetFollowPatch.FindOwnerOfPet(e.entityId) != null ||
|
||||
(e.position - _z.position).sqrMagnitude > TauntRange * TauntRange || !e.CanSee(_z))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
AllowZombieTarget(e);
|
||||
e.SetAttackTarget(_z, TauntTicks);
|
||||
}
|
||||
}
|
||||
|
||||
static void AllowZombieTarget(EntityAlive _e)
|
||||
{
|
||||
List<EAIApproachAndAttackTarget> tasks = _e.aiManager.GetTasks<EAIApproachAndAttackTarget>();
|
||||
if (tasks == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (EAIApproachAndAttackTarget t in tasks)
|
||||
{
|
||||
bool has = false;
|
||||
foreach (EAIApproachAndAttackTarget.TargetClass c in t.targetClasses)
|
||||
{
|
||||
has |= c.type == typeof(EntityZombie);
|
||||
}
|
||||
if (!has)
|
||||
{
|
||||
t.targetClasses.Add(new EAIApproachAndAttackTarget.TargetClass { type = typeof(EntityZombie), chaseTimeMax = 0f });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Из постфикса vp_FPCamera.LateUpdate: камера позади и выше головы зомби, не сквозь стены.</summary>
|
||||
public static void PlaceCamera(Transform _camera)
|
||||
{
|
||||
EntityAlive z = Zombie;
|
||||
if (z == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Transform head = z.emodel != null ? z.emodel.GetHeadTransform() : null;
|
||||
Vector3 anchor = head != null ? head.position : z.position - Origin.position + Vector3.up * 1.6f;
|
||||
Quaternion rot = Quaternion.Euler(-_pitch, _yaw, 0f);
|
||||
Vector3 eye = anchor + Vector3.up * CamUp - rot * Vector3.forward * CamBack;
|
||||
Vector3 to = eye - anchor;
|
||||
if (Physics.Raycast(anchor, to.normalized, out RaycastHit hit, to.magnitude + 0.2f, SnakeControl.WallMask, QueryTriggerInteraction.Ignore))
|
||||
{
|
||||
eye = anchor + to.normalized * Mathf.Max(0.2f, hit.distance - 0.2f);
|
||||
}
|
||||
_camera.SetPositionAndRotation(eye, rot);
|
||||
if (!_loggedCamera)
|
||||
{
|
||||
_loggedCamera = true;
|
||||
Debug.Log("[NecromancerTome] Possession: камера за " + (head != null ? "головой " + head.name : "корнем (головы нет)"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Одержимый зомби без своего ИИ: шаги ИИ, путь и moveHelper не идут, движение задаём мы.</summary>
|
||||
[HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.updateTasks))]
|
||||
public static class Patch_EntityAlive_updateTasks_Possession
|
||||
{
|
||||
public static bool Prefix(EntityAlive __instance)
|
||||
{
|
||||
return !Possession.IsPossessed(__instance);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Тело оцепенело: ввод игрока не идёт (как у тела в рэгдолле - stopMoving и выход).</summary>
|
||||
[HarmonyPatch(typeof(PlayerMoveController), nameof(PlayerMoveController.Update))]
|
||||
public static class Patch_PlayerMoveController_Update_Possession
|
||||
{
|
||||
public static bool Prefix(PlayerMoveController __instance)
|
||||
{
|
||||
if (!Possession.Active)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
__instance.stopMoving();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Полоска здоровья одержимого (тест 03.10: «а индикатор здоровья у одержимого где?») -
|
||||
/// внизу по центру, над поясом, с именем зомби. Своё здоровье игрок видит как обычно (тело беззащитно).
|
||||
/// OnGUI, как прицел змеи (SnakeCrosshair), - без своего окна XUi.</summary>
|
||||
public class PossessionHud : MonoBehaviour
|
||||
{
|
||||
const float Width = 420f, Height = 16f, Bottom = 150f;
|
||||
static readonly Color Back = new Color(0f, 0f, 0f, 0.6f);
|
||||
static readonly Color Fill = new Color(0.55f, 0.08f, 0.06f, 0.95f);
|
||||
static GUIStyle _style;
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
EntityAlive z = Possession.Active ? Possession.Zombie : null;
|
||||
if (z == null || Event.current.type != EventType.Repaint)
|
||||
{
|
||||
return;
|
||||
}
|
||||
float scale = Screen.height / 1080f;
|
||||
float w = Width * scale, h = Height * scale;
|
||||
Rect r = new Rect((Screen.width - w) / 2f, Screen.height - Bottom * scale - h, w, h);
|
||||
float frac = Mathf.Clamp01(z.Health / (float)Mathf.Max(1, z.GetMaxHealth()));
|
||||
Color prev = GUI.color;
|
||||
GUI.color = Back;
|
||||
GUI.DrawTexture(r, Texture2D.whiteTexture);
|
||||
GUI.color = Fill;
|
||||
GUI.DrawTexture(new Rect(r.x + 2f, r.y + 2f, (r.width - 4f) * frac, r.height - 4f), Texture2D.whiteTexture);
|
||||
GUI.color = prev;
|
||||
if (_style == null)
|
||||
{
|
||||
_style = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.LowerCenter };
|
||||
_style.normal.textColor = new Color(0.9f, 0.85f, 0.8f);
|
||||
}
|
||||
_style.fontSize = Mathf.RoundToInt(18f * scale);
|
||||
string name = Localization.Get(z.EntityName);
|
||||
GUI.Label(new Rect(r.x, r.y - 26f * scale, r.width, 24f * scale), name + " " + z.Health + " / " + z.GetMaxHealth(), _style);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -876,6 +876,11 @@ namespace NecromancerTome
|
||||
{
|
||||
public static bool Prefix(EntityPlayerLocal __instance)
|
||||
{
|
||||
if (MindControl.Controlling)
|
||||
{
|
||||
MindControl.PlaceCamera(__instance.cameraTransform);
|
||||
return false;
|
||||
}
|
||||
if (!SnakeControl.Controlling)
|
||||
{
|
||||
return true;
|
||||
@@ -923,6 +928,14 @@ namespace NecromancerTome
|
||||
{
|
||||
SnakeControl.PlaceCamera(__instance.transform);
|
||||
}
|
||||
else if (Possession.Active)
|
||||
{
|
||||
Possession.PlaceCamera(__instance.transform);
|
||||
}
|
||||
else if (MindControl.Controlling)
|
||||
{
|
||||
MindControl.PlaceCamera(__instance.transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1111,13 +1124,10 @@ namespace NecromancerTome
|
||||
public static void Prefix(out Vector3 __state)
|
||||
{
|
||||
__state = Vector3.zero;
|
||||
if (!SnakeControl.Controlling)
|
||||
{
|
||||
return;
|
||||
}
|
||||
World world = GameManager.Instance.World;
|
||||
// И у одержимого зомби (03.10: «если одержимый отошёл подальше, новые не спавнятся возле него»).
|
||||
EntityAlive snake = SnakeControl.Controlling ? SnakeMode.Snake(world) : Possession.Active ? Possession.Zombie : null;
|
||||
EntityPlayerLocal player = world?.GetPrimaryPlayer();
|
||||
EntityAlive snake = SnakeMode.Snake(world);
|
||||
if (player == null || snake == null)
|
||||
{
|
||||
return;
|
||||
@@ -1150,12 +1160,18 @@ namespace NecromancerTome
|
||||
{
|
||||
public static bool Prefix(EntityAlive __instance, ref int ___ticksNoPlayerAdjacent)
|
||||
{
|
||||
if (!SnakeControl.Controlling)
|
||||
EntityAlive snake = SnakeControl.Controlling ? SnakeMode.Snake(__instance.world as World) : Possession.Active ? Possession.Zombie : null;
|
||||
if (snake == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
EntityAlive snake = SnakeMode.Snake(__instance.world as World);
|
||||
if (snake == null || snake == __instance || (__instance.position - snake.position).sqrMagnitude > 6400f)
|
||||
// Одержимый сам не исчезает, как бы далеко от тела ни ушёл.
|
||||
if (snake == __instance)
|
||||
{
|
||||
___ticksNoPlayerAdjacent = 0;
|
||||
return !Possession.IsPossessed(__instance);
|
||||
}
|
||||
if ((__instance.position - snake.position).sqrMagnitude > 6400f)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -1172,7 +1188,7 @@ namespace NecromancerTome
|
||||
{
|
||||
public static bool Prefix(GUIWindow _w)
|
||||
{
|
||||
return !(SnakeControl.Controlling && _w != null && _w.Id == SnakeControl.ToolbeltWindow);
|
||||
return !((SnakeControl.Controlling || MindControl.Controlling) && _w != null && _w.Id == SnakeControl.ToolbeltWindow);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1182,8 +1198,61 @@ namespace NecromancerTome
|
||||
/// змеи. На время вызова переносим позицию игрока к змее.</summary>
|
||||
public static class SnakeAudioSwap
|
||||
{
|
||||
/// <summary>Вложенность подмены: CheckLODs зовётся и изнутри WorldEnvironment.Update, где позиция уже
|
||||
/// подменена. Подменяет и возвращает только внешний вызов, вложенные ничего не трогают.</summary>
|
||||
static int _swapped, _swapFrame;
|
||||
|
||||
public static Vector3 Prefix()
|
||||
{
|
||||
if (_swapped > 0 && _swapFrame != Time.frameCount)
|
||||
{
|
||||
_swapped = 0; // подмена не вернулась (исключение посреди вызова) - не держим её дольше кадра
|
||||
}
|
||||
if (_swapped > 0)
|
||||
{
|
||||
_swapped++;
|
||||
return Vector3.zero;
|
||||
}
|
||||
Vector3 body = PrefixOuter();
|
||||
if (body != Vector3.zero)
|
||||
{
|
||||
_swapped = 1;
|
||||
_swapFrame = Time.frameCount;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
static Vector3 PrefixOuter()
|
||||
{
|
||||
if (MindControl.Controlling)
|
||||
{
|
||||
// Дух чертогов: звук, мир и компас - от него (MindControl.cs).
|
||||
EntityPlayerLocal p = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||||
if (p == null)
|
||||
{
|
||||
return Vector3.zero;
|
||||
}
|
||||
Vector3 bodyPos = p.position;
|
||||
p.position = MindControl.FeetPos;
|
||||
return bodyPos;
|
||||
}
|
||||
if (Possession.Active && Possession.Zombie != null)
|
||||
{
|
||||
// Одержимость (тест 03.10: «без звуков») - звуки мерятся от одержимого зомби, а не от тела.
|
||||
EntityPlayerLocal pp = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||||
if (pp == null)
|
||||
{
|
||||
return Vector3.zero;
|
||||
}
|
||||
Vector3 bodyAt = pp.position;
|
||||
pp.position = Possession.Zombie.position;
|
||||
// Область тела сжата (Possession.BodyViewDim) - этим системам радиус полный, они рисуют у зомби.
|
||||
if (Possession.FullViewDim > 0 && pp.ChunkObserver != null)
|
||||
{
|
||||
pp.ChunkObserver.viewDim = Possession.FullViewDim;
|
||||
}
|
||||
return bodyAt;
|
||||
}
|
||||
if (!SnakeControl.Controlling)
|
||||
{
|
||||
return Vector3.zero;
|
||||
@@ -1204,16 +1273,44 @@ namespace NecromancerTome
|
||||
{
|
||||
if (_body == Vector3.zero)
|
||||
{
|
||||
if (_swapped > 1)
|
||||
{
|
||||
_swapped--;
|
||||
}
|
||||
return;
|
||||
}
|
||||
_swapped = 0;
|
||||
EntityPlayerLocal player = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||||
if (player != null)
|
||||
{
|
||||
player.position = _body;
|
||||
if (Possession.Active && Possession.FullViewDim > 0 && player.ChunkObserver != null)
|
||||
{
|
||||
player.ChunkObserver.viewDim = Possession.BodyViewDim;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Полная модель блоков (двери, люки, мебель) ближе 41 м, дальше - упрощённая замена:
|
||||
/// ChunkGameObject.CheckLODs меряет расстояние до ИГРОКА - тела. Его зовёт не только WorldEnvironment.Update
|
||||
/// (там позиция уже подменена), но и сам чанк при появлении - тест 03.10: «змея и одержимый видят вместо
|
||||
/// дверей деревянные блоки». Подменяем позицию и здесь: змея, одержимый, дух чертогов.</summary>
|
||||
[HarmonyPatch(typeof(ChunkGameObject), nameof(ChunkGameObject.CheckLODs))]
|
||||
public static class Patch_ChunkGameObject_CheckLODs_Snake
|
||||
{
|
||||
public static void Prefix(out Vector3 __state) { __state = SnakeAudioSwap.Prefix(); }
|
||||
public static void Postfix(Vector3 __state) { SnakeAudioSwap.Postfix(__state); }
|
||||
}
|
||||
|
||||
/// <summary>Детализация зданий (POI LOD) - вокруг одержимого, с полным радиусом (03.10, сжатая область тела).</summary>
|
||||
[HarmonyPatch(typeof(PrefabLODManager), nameof(PrefabLODManager.FrameUpdate))]
|
||||
public static class Patch_PrefabLODManager_FrameUpdate_Possession
|
||||
{
|
||||
public static void Prefix(out Vector3 __state) { __state = Possession.Active ? SnakeAudioSwap.Prefix() : Vector3.zero; }
|
||||
public static void Postfix(Vector3 __state) { SnakeAudioSwap.Postfix(__state); }
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Audio.Manager), nameof(Audio.Manager.Play), new[] { typeof(Vector3), typeof(string), typeof(int), typeof(bool), typeof(float) })]
|
||||
public static class Patch_AudioManager_PlayAt_Snake
|
||||
{
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace NecromancerTome
|
||||
|
||||
public static void Enter(EntityPlayerLocal _player)
|
||||
{
|
||||
if (Active || _player == null || _player.IsDead() || _player.AttachedToEntity != null)
|
||||
if (Active || MindPalace.Active || _player == null || _player.IsDead() || _player.AttachedToEntity != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -318,6 +318,10 @@ namespace NecromancerTome
|
||||
{
|
||||
Enter(_sender.xui.playerUI.entityPlayer);
|
||||
}
|
||||
else if (_commandIndex == 2)
|
||||
{
|
||||
MindPalace.Enter(_sender.xui.playerUI.entityPlayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +330,7 @@ namespace NecromancerTome
|
||||
{
|
||||
public static void Postfix(ItemAction __instance, ref bool __result)
|
||||
{
|
||||
if (!__result && !SnakeMode.Active && SnakeMode.IsKnife(__instance.item))
|
||||
if (!__result && !SnakeMode.Active && !MindPalace.Active && SnakeMode.IsKnife(__instance.item))
|
||||
{
|
||||
__result = true;
|
||||
}
|
||||
@@ -336,6 +340,9 @@ namespace NecromancerTome
|
||||
[HarmonyPatch(typeof(ItemAction), nameof(ItemAction.SetupRadial))]
|
||||
public static class Patch_ItemAction_SetupRadial_SnakeKnife
|
||||
{
|
||||
/// <summary>Книга (указание 03.10: «может лучше книга?» - с неё всё и начинается).</summary>
|
||||
public const string MindPalaceIcon = "ui_game_symbol_book";
|
||||
|
||||
public static bool Prefix(ItemAction __instance, XUiC_Radial _xuiRadialWindow, EntityPlayerLocal _epl)
|
||||
{
|
||||
if (!SnakeMode.IsKnife(__instance.item))
|
||||
@@ -355,6 +362,17 @@ namespace NecromancerTome
|
||||
_xuiRadialWindow.CreateRadialEntry(1, SnakeMode.DayIcon, "UIAtlas", "",
|
||||
Localization.Get("necroSnakeModeDay"), _highlighted: false);
|
||||
}
|
||||
// Чертоги разума (указание 03.10: «вход тоже через нож») - третья строка, тоже только ночью.
|
||||
if (SnakeMode.IsNight())
|
||||
{
|
||||
_xuiRadialWindow.CreateRadialEntry(2, MindPalaceIcon, "UIAtlas", "",
|
||||
Localization.Get("necroMindModePalace"), _highlighted: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_xuiRadialWindow.CreateRadialEntry(2, SnakeMode.DayIcon, "UIAtlas", "",
|
||||
Localization.Get("necroSnakeModeDay"), _highlighted: false);
|
||||
}
|
||||
_xuiRadialWindow.SetCommonData(UIUtils.GetButtonIconForAction(_epl.playerInput.Reload),
|
||||
SnakeMode.OnRadialCommand, null, 0);
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<prefab>
|
||||
<property name="CopyAirBlocks" value="True" />
|
||||
<property name="ExcludeDistantPOIMesh" value="False" />
|
||||
<property name="ExcludePOICulling" value="False" />
|
||||
<property name="DistantPOIYOffset" value="0" />
|
||||
<property name="AllowDecorations" value="True" />
|
||||
<property name="DifficultyTier" value="0" />
|
||||
<property name="ShowQuestClearCount" value="1" />
|
||||
<property name="TraderArea" value="False" />
|
||||
<property name="PrefabSize" value="17, 8, 17" />
|
||||
<property name="RotationToFaceNorth" value="2" />
|
||||
<property name="Zoning" value="none" />
|
||||
<property class="IndexedBlockOffsets">
|
||||
<property class="POILight">
|
||||
<property name="0" value="11, 3, 9" />
|
||||
<property name="1" value="12, 3, 9" />
|
||||
<property name="2" value="8, 4, 10" />
|
||||
<property name="3" value="12, 4, 10" />
|
||||
<property name="4" value="12, 4, 12" />
|
||||
<property name="5" value="9, 3, 13" />
|
||||
</property>
|
||||
<property class="challenge_sinks">
|
||||
<property name="0" value="11, 1, 3" />
|
||||
<property name="1" value="13, 1, 4" />
|
||||
</property>
|
||||
<property class="TraderOnOff">
|
||||
<property name="0" value="8, 1, 8" />
|
||||
<property name="1" value="10, 1, 11" />
|
||||
</property>
|
||||
<property class="challenge_gore">
|
||||
<property name="0" value="3, 2, 9" />
|
||||
</property>
|
||||
<property class="challenge_mech_parts">
|
||||
<property name="0" value="6, 1, 13" />
|
||||
</property>
|
||||
</property>
|
||||
<property class="Stats">
|
||||
<property name="LightsVolume" value="5516.637" />
|
||||
<property name="TotalVertices" value="91813" />
|
||||
<property name="TotalTriangles" value="61958" />
|
||||
<property class="BlockEntities">
|
||||
<property name="Vertices" value="73400" />
|
||||
<property name="Triangles" value="52089" />
|
||||
</property>
|
||||
<property class="ChunkMeshes0">
|
||||
<property name="Vertices" value="18121" />
|
||||
<property name="Triangles" value="9645" />
|
||||
</property>
|
||||
<property class="ChunkMeshes1">
|
||||
<property name="Vertices" value="0" />
|
||||
<property name="Triangles" value="0" />
|
||||
</property>
|
||||
<property class="ChunkMeshes2">
|
||||
<property name="Vertices" value="292" />
|
||||
<property name="Triangles" value="224" />
|
||||
</property>
|
||||
<property class="ChunkMeshes3">
|
||||
<property name="Vertices" value="0" />
|
||||
<property name="Triangles" value="0" />
|
||||
</property>
|
||||
<property class="ChunkMeshes4">
|
||||
<property name="Vertices" value="0" />
|
||||
<property name="Triangles" value="0" />
|
||||
</property>
|
||||
<property class="ChunkMeshes5">
|
||||
<property name="Vertices" value="0" />
|
||||
<property name="Triangles" value="0" />
|
||||
</property>
|
||||
</property>
|
||||
</prefab>
|
||||
@@ -0,0 +1,94 @@
|
||||
Copyright (c) 2010, j. 'mach' wust, with Reserved Font Name UnifrakturMaguntia.
|
||||
Copyright (c) 2009, Peter Wiegel.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 34 KiB |