42 Commits
Author SHA1 Message Date
AlexCubeandClaude Opus 5.5 5691cd62d1 DLL: тело с ножом-для-нитей, necrobody zdance/mimic
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:55:31 +03:00
AlexCubeandClaude Opus 5.5 96da967c61 necroview free: без рук первого лица
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:46:34 +03:00
AlexCubeandClaude Opus 5.5 f7f793b325 Кровавая роза: бутоны и цветы не висят в воздухе (ModelOffset 0,0,0)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:46:16 +03:00
AlexCubeandClaude Opus 5.5 a17f2ff375 necrobody zdance/mimic: зомби танцует, тело повторяет его кости
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:42:26 +03:00
AlexCubeandClaude Opus 5.5 e0411ce3e5 Нож-для-нитей у тела: хват лука, кисть +20 к ладони, нож в кулаке (подбор necrobody)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:38:59 +03:00
AlexCubeandClaude Opus 5.5 a447ba1bbc DLL: necrobody play
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:23:01 +03:00
AlexCubeandClaude Opus 5.5 5fc04fd668 necrobody anims/play: любая загруженная анимация на теле (танцы зомби и т.п.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:22:55 +03:00
AlexCubeandClaude Opus 5.5 31ebbb9bc8 DLL: necrobody
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:21:07 +03:00
AlexCubeandClaude Opus 5.5 6267de5656 necrobody: аниматор тела в игре - параметры, клипы, кости, заморозка, скорость, место предмета
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:21:00 +03:00
AlexCubeandClaude Opus 5.5 ba2fe6c202 DLL: necroview мышью
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:19:04 +03:00
AlexCubeandClaude Opus 5.5 180be18dae Нож-для-нитей: тело держит его хватом ножа (necroheld tphold N)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:18:44 +03:00
AlexCubeandClaude Opus 5.5 87fad62c62 necroview: камера под интерфейсом; режим free - выйти из тела
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:15:56 +03:00
AlexCubeandClaude Opus 5.5 606f9c3816 necroview: мышь облетает руки, персонаж стоит (necroview mouse off - как было)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:12:45 +03:00
AlexCubeandClaude Opus 5.5 1b5c68d6f2 DLL: хват для нитей только от первого лица
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:07:02 +03:00
AlexCubeandClaude Opus 5.5 5a05c86657 Хват для нитей - только от первого лица
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:05:22 +03:00
AlexCubeandClaude Opus 5.5 fc1fa53736 necroview: осмотр рук первого лица со стороны, поза замирает; DLL с хватом для нитей
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:00:51 +03:00
AlexCubeandClaude Opus 5.5 a0e93744ed DLL: логисты вне лимита урн
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 22:59:58 +03:00
AlexCubeandClaude Opus 5.5 ed4015ecee Хват для нитей: основа 68, поза рук мода, нож в левой руке
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 22:58:28 +03:00
AlexCubeandClaude Opus 5.5 bf334c5c44 Урны-логисты не входят в лимит урн
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 22:31:11 +03:00
AlexCubeandClaude Opus 5.5 38a8b3b417 Группы логиста: предметы и блоки мода - в «Некромантию» по его items/blocks.xml; факел - инструменты, гвозди - детали, жестянки с водой - напитки, жир - материалы, корм кур - сырое
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:05:24 +03:00
AlexCubeandClaude Opus 5.5 17a4ab2e96 necroheld hold N - смена хвата предмета в руке на лету; дух не собирает список рендереров каждые 0.5 с
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:03:51 +03:00
AlexCubeandClaude Opus 5.5 f4ae9cbcb8 Урна-логист - отдельный блок: без зоны (ставится вплотную), нити только к ней, зелёные руны и глазницы, своя иконка; рецепт с набором для шитья, лимит общий
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:00:19 +03:00
AlexCubeandClaude Opus 5.5 adc9371a09 DLL: меню ножа и хват для нитей
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:56:52 +03:00
AlexCubeandClaude Opus 5.5 06155ff014 Нити видны только с ножом хватом для нитей в руках
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:55:25 +03:00
AlexCubeandClaude Opus 5.5 f5be0e7f43 Нож: хват для нитей - пункт в прежнем меню ножа (человек/змея/чертоги вернулись); хват стриппера убран - держится как обычный нож
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:53:15 +03:00
AlexCubeandClaude Opus 5.5 0290a73477 Урна-логист: фильтр по 22 группам предметов в «Содержимом» (вместо образца); дух засыпает, когда игрок дальше 40 м
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:40:04 +03:00
AlexCubeandClaude Opus 5.5 d8e7c8c66a Нити цинерария: нож «хватом для нитей» (R), нити урна-ящик как у стриппера, урна-логист перевозит по образцу, зелёные глазницы
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:29:50 +03:00
AlexCubeandClaude Opus 5.5 fad57a3756 Урна: дух встаёт у края станции в свободной клетке со стороны урны и смотрит на неё, а не влетает в центр
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:11:52 +03:00
AlexCubeandClaude Opus 5.5 d7c80341d2 Команда necrogrow: подрастить растения вокруг игрока (для проверки грядок духа урны)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:10:55 +03:00
AlexCubeandClaude Opus 5.5 7e93e19c5f Урна: дух летит к делу лицом, над местом жестикулирует руками, дело - в середине жеста, назад лицом к урне
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:03:53 +03:00
AlexCubeandClaude Opus 5.5 23587d78e0 Урна: дух опускает руки из Т-позы (кости плеч и предплечий при появлении); necrogenie arms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:53:43 +03:00
AlexCubeandClaude Opus 5.5 49c7afe273 Урна: дух - светящаяся дымка (сложение One/One) вместо Fade с пятнами; necrogenie glow/tint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:49:20 +03:00
AlexCubeandClaude Opus 5.5 3ba6572bef Урна: «Содержимое» - родное окно хранилища игры с ячейкой на один череп
Своё окно с ячейкой коллектора не принимало череп ни перетаскиванием,
ни Shift+кликом. Теперь - XUiC_BagStorageWindowGroup (как сумка
транспорта и волка) с сумкой 1x1: перенос - кодом игры; после каждого
изменения в ячейке остаётся один череп, лишнее и чужое - в рюкзак.
Своё окно и группа из XUi удалены.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:34:31 +03:00
AlexCubeandClaude Opus 5.5 e590fab082 Урна: «Содержимое» с иконкой черепа вместо «Багажника»; «Взять» везде, как у транспорта
Команда багажника переименована в «Содержимое» (меню, заголовок окна,
описание - 13 языков), иконка - ui_game_symbol_skull. «Взять» игра даёт
станциям только внутри своего земельного камня; урне - как транспорту:
везде, кроме зоны торговца, хозяину, с пустой сумкой.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:21:11 +03:00
AlexCubeandClaude Opus 5.5 845455c6a0 Урна: окно «Багажник» не добавлялось - xpath /xui вместо /xui/ruleset (в 3.3 узла ruleset нет)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:18:20 +03:00
AlexCubeandClaude Opus 5.5 9f813bb699 Урна-цинерарий: своя модель, свечение по черепу, кровь в рецепте
Модель (бандл necrocinerarium, BlenderWork/necroCinerarium/make_urn.py):
тело вращения в чёрной глазури, по четырём сторонам - вдавленные лицевые
части черепа, наклонённые вниз; руны старшего футарка кровью «SERVU ĜIS
LA LASTA CINDRO» («Служи до последнего пепла»), меандр на ножке, бусины на
шейке, пояс черепов в двух линиях. Руны тлеют красным, глазницы светятся
фиолетовым, только когда в урне череп (_EmissionColor через
MaterialPropertyBlock). Своя иконка. Коллайдер-тег - DecorColliderTagPatch.
Дух: выше на 10 см и на 7 см вперёд в своей системе (хвост не вылезает
сзади), necrogenie fwd. Рецепт: + кровь некроманта, копия на соке розы.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:37:13 +03:00
AlexCubeandClaude Opus 5.5 94bf0a5f5c Некротехнолог: грядки; скакун крафтится 120 с
Дух последним делом собирает созревшее (метка crops, добыча Harvest) с
идеальными бонусами и сразу сажает семя на то же место; засаживает пустые
грядки случайными семенами из ящиков и сумки (можно ли сажать - решает
сам блок семени). Описание урны - про универсального работника, 13 языков.
Костяному скакуну задано craft_time 120 (было - сумма ингредиентов).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:52:47 +03:00
AlexCubeandClaude Opus 5.5 0890ffe1e9 Некротехнолог: работа духа - расходники коллекторов и готовое из станций
Раз в 3 с одно дело: (1) пополнить из ящиков зоны и сумки урны ячейки
расходников любого коллектора (банки, цветы, корм, сферы) и катализатора
(куры, по одной в пустую ячейку); (2) забрать готовое из коллекторов и
станций (верстак, кузня, химстанция, костёр...) в ящики зоны - сначала
к таким же, потом в пустое, ящики полны - в сумку урны; некуда - дух
чернеет и замирает, оживает сам. Модификации, инструменты, топливо и
сырьё станций не трогаются; открытое у игрока и закреплённые ячейки -
тоже. Модель духа плывёт к месту работы и обратно. Грядки - позже.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:43:36 +03:00
AlexCubeandClaude Opus 5.5 0558f46069 Некротехнолог: джинн из урны - появляется по черепу в багажнике
Дух - не сущность, а модель из бандла necrotechnologist, дочерний объект
модели урны (как каркас зоны): без хитбокса, ИИ и сохранения. Кончик
хвоста в горлышке, дух покачивается и медленно поворачивается; тело
полупрозрачное (0.3, как у торговцев), глаза светятся. Появляется и
исчезает с порывом (necroTechGust). Команда necrogenie - подгонка
высоты, поворота, покачивания и прозрачности в игре.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:16:34 +03:00
AlexCubeandClaude Opus 5.5 35313f2e90 Урна: пункт «Сумка» с иконкой мешка вместо «Обыскать» игры, первым в меню
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:13:07 +03:00
AlexCubeandClaude Opus 5.5 bd76360eeb Урна-цинерарий: блок, багажник с черепом, сумка духа, зона, лимит
Урна устроена как транспорт: на E круговое меню - «Багажник» (своё окно
с одной ячейкой под череп), сумка духа 6x2 (хранилище игры), «Починить»
(5 костей из рюкзака), показать/скрыть зону 5x5 от -1 до +4, «Поднять»
(деталь игры TEFeaturePickup; только с пустой сумкой, череп остаётся
внутри - поднятая урна с черепом становится necroCinerariumSkull).
Постановка: зоны урн не пересекаются, урн не больше ceil(убийств / 500);
реестр урн - файл в папке сейва. Разбитая урна роняет череп.
Сохранение - CinerariumWriteHelper, версия первым байтом.
Рецепт - конец Адепта (уровень 15). Модель и иконка временные (кастрюля).
Тексты - 13 языков. Дух-работник - следующий этап.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:10:02 +03:00
AlexCubeandClaude Opus 5.5 87f3aaf295 Крыса: E на зомби рядом переносит дух в него, крыса развеивается
Одержимой крысой E на зомби в прицеле (не дальше 3 м, в прямой видимости,
не во время прохода сквозь постройки) - выход из крысы и вселение в зомби;
крыса отзывается с чёрным облаком. Цепочки нет: после зомби дух в теле.
Подсказка вселения и описание гримуара крысы - 13 языков. ModInfo 1.7.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:51:44 +03:00
43 changed files with 4897 additions and 35 deletions
File diff suppressed because one or more lines are too long
+56
View File
@@ -208,4 +208,60 @@
<sprite depth="9" pos="0,20" height="8" color="[black]" sprite="menu_empty2px" globalopacitymod="0" type="sliced" fillcenter="false" />
<sprite depth="4" pos="0,20" height="8" width="750" color="150,60,220,200" globalopacitymod="0" type="filled" fill="{% cvar('necroNecromancyProgressCVar') / 20 }" />
</append>
<!-- Фильтр урны-логиста (16.10.2026, HarmonySrc/CinerariumFilterWindow.cs, CinerariumGroups.cs): значки групп предметов
в окне «Содержимое» рядом с ячейкой черепа. Окно в группе bagStorage (xui.xml) - контроллер прячет его, если окно
открыто не урной-логистом (сумки транспорта и волка). f0..f21 - рамки, g0..g21 - значки (иконки ставит контроллер). -->
<append xpath="/windows">
<window name="necroUrnFilter" width="348" height="279" depth="5" panel="Left" backgroundcolor="[black]" cursor_area="true" controller="NecromancerTome.NecroUrnFilter, NecromancerHarmony">
<rect name="header" height="43" depth="0">
<sprite depth="1" name="headerbackground" sprite="ui_game_panel_header" width="348" height="43" />
<sprite depth="3" name="windowIcon" style="icon32px" pivot="center" pos="21,-21" sprite="ui_game_symbol_sort" />
<label depth="2" name="title" pos="39,-6" width="303" height="32" text_key="necroUrnFilterTitle" font_size="28" />
</rect>
<sprite depth="2" name="f0" pos="6,-49" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g0" pos="8,-51" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f1" pos="62,-49" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g1" pos="64,-51" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f2" pos="118,-49" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g2" pos="120,-51" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f3" pos="174,-49" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g3" pos="176,-51" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f4" pos="230,-49" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g4" pos="232,-51" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f5" pos="286,-49" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g5" pos="288,-51" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f6" pos="6,-105" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g6" pos="8,-107" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f7" pos="62,-105" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g7" pos="64,-107" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f8" pos="118,-105" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g8" pos="120,-107" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f9" pos="174,-105" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g9" pos="176,-107" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f10" pos="230,-105" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g10" pos="232,-107" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f11" pos="286,-105" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g11" pos="288,-107" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f12" pos="6,-161" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g12" pos="8,-163" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f13" pos="62,-161" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g13" pos="64,-163" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f14" pos="118,-161" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g14" pos="120,-163" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f15" pos="174,-161" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g15" pos="176,-163" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f16" pos="230,-161" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g16" pos="232,-163" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f17" pos="286,-161" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g17" pos="288,-163" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f18" pos="6,-217" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g18" pos="8,-219" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f19" pos="62,-217" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g19" pos="64,-219" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f20" pos="118,-217" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g20" pos="120,-219" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
<sprite depth="2" name="f21" pos="174,-217" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="g21" pos="176,-219" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
</window>
</append>
</windows>
+4
View File
@@ -59,4 +59,8 @@
<window name="windowNonPagingHeader" />
</window_group>
</append>
<!-- Фильтр урны-логиста - в окне хранилища игры (bagStorage), рядом с ячейкой «Содержимого» (windows.xml). -->
<append xpath="/xui/window_group[@name='bagStorage']">
<window name="necroUrnFilter" />
</append>
</xui>
+64
View File
@@ -519,4 +519,68 @@
<property name="ItemTypeIcon" value="all_blocks"/>
</block>
</append>
<!-- Урна-цинерарий (Harvest Cinerarium), 1.7.0, 16.10.2026: этап 2 некротехнолога (BACKLOG «Некротехнолог v2»).
Составной блок, как земельный камень: TEFeatureStorage - сумка духа (окно хранилища 6x2, «Открыть»),
TEFeatureCinerarium (HarmonySrc/Cinerarium.cs) - череп (положить/забрать), показ зоны 5x5, -1..+4,
лимит ceil(убийств / 500) и запрет пересечения зон при постановке. Всё - в радиальном меню на E (решение 16.10).
Модель (16.10) - своя: бандл necrocinerarium (BlenderWork/necroCinerarium, make_urn.py): чёрная глазурь, черепа по
четырём сторонам, руны футарка кровью «SERVU ĜIS LA LASTA CINDRO», меандр, бусины; свечение рун и глазниц
включает TEFeatureCinerarium по черепу. Иконка - рендер той же модели (make_icon.py). -->
<append xpath="/blocks">
<block name="necroCinerarium">
<property name="CreativeMode" value="Player"/>
<property name="DescriptionKey" value="necroCinerariumDesc"/>
<property name="UnlockedBy" value="craftingNecroNecromancy"/>
<property name="Class" value="CompositeTileEntity"/>
<property class="CompositeFeatures">
<property class="TEFeatureStorage">
<property name="LootList" value="necroCinerariumBag"/>
</property>
<property class="TEFeatureCinerarium"/>
<property class="TEFeaturePickup">
<property name="TakeDelay" value="2"/>
</property>
</property>
<property name="Material" value="Mstone"/>
<property name="Shape" value="ModelEntity"/>
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necrocinerarium?necroCinerariumPrefab.prefab"/>
<property name="ModelOffset" value="0,0,0"/> <!-- меш стоит основанием на y=0; без этого ModelEntity поднимает на полклетки -->
<property name="CustomIcon" value="necroCinerarium"/>
<property name="WaterFlow" value="permitted"/>
<property name="OnlySimpleRotations" value="true"/>
<property name="Collide" value="movement,melee,bullet,arrow,rocket"/>
<property name="StabilitySupport" value="false"/>
<property name="MaxDamage" value="500"/>
<property name="EconomicValue" value="0"/>
<property name="SellableToTrader" value="false"/>
<property name="FilterTags" value="MC_playerBlocks,SC_decor"/>
<property class="RepairItems">
<property name="resourceBone" value="5"/>
</property>
</block>
<!-- Урна, поднятая с черепом внутри (как транспорт держит модификации): тот же блок, при постановке череп
уже в багажнике. Получается только подъёмом (Patch_Block_OnBlockPickedUp_Cinerarium). -->
<block name="necroCinerariumSkull">
<property name="Extends" value="necroCinerarium"/>
<property name="CreativeMode" value="None"/>
<property name="DescriptionKey" value="necroCinerariumDesc"/>
</block>
<!-- Урна-логист (16.10.2026): «Я не могу ставить урны рядом. Может нам для логиста вообще сделать отдельные урны?»
Без зоны (ставится вплотную к любым урнам), только нити и перевозка (CinerariumThreads.cs), лимит урн общий.
Модель - та же; отличие - код (TEFeatureCinerarium.SyncGlow): зелёные руны и глазницы
(Resources/necroCinerarium_e_logist.png), зеленоватая глазурь. Иконка - своя (зелёные руны, нити из горлышка).
Своя модель логиста - позже, в Blender. -->
<block name="necroCinerariumLogist">
<property name="Extends" value="necroCinerarium"/>
<property name="CreativeMode" value="Player"/>
<property name="DescriptionKey" value="necroCinerariumLogistDesc"/>
<property name="CustomIcon" value="necroCinerariumLogist"/>
</block>
<block name="necroCinerariumLogistSkull">
<property name="Extends" value="necroCinerariumLogist"/>
<property name="CreativeMode" value="None"/>
<property name="DescriptionKey" value="necroCinerariumLogistDesc"/>
</block>
</append>
</config>
+17
View File
@@ -2018,4 +2018,21 @@
<property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necroblood?necroJuicePrefab.prefab"/>
</item>
</append>
<!-- Нож некроманта «хватом для нитей» (16.10.2026, HarmonySrc/CinerariumThreads.cs): «нож некроманта, но взятый особым
хватом... как кисточка или как тот же стриппер». Отдельный предмет - класс ножа в сейве менять нельзя; переключение -
меню по R (качество, износ и моды переносятся). Не бьёт: ПКМ - тянуть нить урна-ящик, ЛКМ - снять нити. Удары
перехвачены в ItemClass.ExecuteAction, поэтому действия ножа (Extends) остаются, но не срабатывают. Держится пока как
обычный нож (хват стриппера HoldType 52 не подошёл - 16.10). effect_group через Extends не наследуется -
у этого предмета нет урона, лечения и слотов модов (моды в ItemValue сохраняются и вернутся с обычным хватом). -->
<append xpath="/items">
<item name="necroWpnBladeNecroKnifeThread">
<property name="Extends" value="necroWpnBladeNecroKnife"/>
<property name="CreativeMode" value="None"/>
<!-- Хват для нитей (16.10, подбор в игре): основа - 68 (деревянный лук), поверх - поза рук мода
(HandPose.PresetThread): нож в левой руке, правая раскрытой ладонью. 52 (стриппер) не подошёл. -->
<property name="HoldType" value="68"/>
<property name="DescriptionKey" value="necroWpnBladeNecroKnifeThreadDesc"/>
<property name="CustomIconTint" value="150, 205, 255"/>
</item>
</append>
</config>
+7
View File
@@ -45,4 +45,11 @@
<item group="groupZpackVictim"/>
</lootcontainer>
</append>
<!-- Сумка духа урны-цинерария (1.7.0): пустое хранилище игрока, 6x2. -->
<append xpath="/lootcontainers">
<lootcontainer name="necroCinerariumBag" count="0" size="6,2" sound_open="UseActions/open_backpack" sound_close="UseActions/close_backpack" open_time="0" loot_quality_template="qualBaseTemplate">
<item name="cobweb" count="0"/>
</lootcontainer>
</append>
</config>
+1 -1
View File
@@ -181,7 +181,7 @@
пользуется, вода сидит на necroNecromancyLvl20. -->
<unlock_entry item="necroModKnifeScavengersFeast,bookSummonRatSpirit" unlock_tier="3"/>
<unlock_entry item="necroModKnifeGravesRepose" unlock_tier="4"/>
<unlock_entry item="necroModKnifeDarkSense" unlock_tier="5"/>
<unlock_entry item="necroModKnifeDarkSense,necroCinerarium,necroCinerariumLogist" unlock_tier="5"/>
</display_entry>
<!-- Ступенчатая разблокировка внутри группы. ПЕРЕРАСПРЕДЕЛЕНО 2026-09-09: четыре из
шести модов ножа (вода/еда/покой/чутьё) уехали отсюда в группу 1 - см. большой
+34 -2
View File
@@ -493,7 +493,7 @@
гео-оссуария: транспорт нужен, когда карта уже велика для пеших ходок. Состав - по
указанию пользователя: кости, гнилое мясо, прах, кожа жертвы, два флакона крови. -->
<append xpath="/recipes">
<recipe name="necroBoneSteedPlaceable" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
<recipe name="necroBoneSteedPlaceable" count="1" craft_time="120" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
<ingredient name="resourceBone" count="300"/>
<ingredient name="foodRottingFlesh" count="30"/>
<ingredient name="resourceZombieAsh" count="150"/>
@@ -587,7 +587,7 @@
<ingredient name="resourceWood" count="100"/>
<ingredient name="resourceYuccaFibers" count="50"/>
</recipe>
<recipe name="necroBoneSteedPlaceable" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
<recipe name="necroBoneSteedPlaceable" count="1" craft_time="120" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
<ingredient name="resourceBone" count="300"/>
<ingredient name="foodRottingFlesh" count="30"/>
<ingredient name="resourceZombieAsh" count="150"/>
@@ -605,4 +605,36 @@
<ingredient name="necroSkullDecor" count="1"/>
</recipe>
</append>
<!-- Урна-цинерарий (1.7.0, 16.10): конец Адепта - уровень 15, 300 убийств (решение 06.10). Кровь - 16.10: «Да, кровью» (руны на урне расписаны кровью некроманта). -->
<append xpath="/recipes">
<recipe name="necroCinerarium" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
<ingredient name="resourceClayLump" count="30"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecromancerBlood" count="1"/>
</recipe>
<!-- Копия на соке чёрной розы (правило 05.10: у каждого рецепта на крови - копия; одна банка крови = одна банка сока). -->
<recipe name="necroCinerarium" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
<ingredient name="resourceClayLump" count="30"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
</recipe>
<!-- Урна-логист (16.10: «Рецепт с набором для шитья») - рецепт урны и набор для шитья; копия на соке чёрной розы. -->
<recipe name="necroCinerariumLogist" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
<ingredient name="resourceClayLump" count="30"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecromancerBlood" count="1"/>
<ingredient name="resourceSewingKit" count="1"/>
</recipe>
<recipe name="necroCinerariumLogist" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
<ingredient name="resourceClayLump" count="30"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
<ingredient name="resourceSewingKit" count="1"/>
</recipe>
</append>
</config>
File diff suppressed because it is too large Load Diff
+84
View File
@@ -0,0 +1,84 @@
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Фильтр урны-логиста (16.10.2026): панель значков групп (CinerariumGroups) в окне «Содержимое» - окно necroUrnFilter
/// добавлено в группу окон игры bagStorage рядом с ячейкой черепа (Config/XUi_InGame). Видна только у урны с нитями;
/// у сумок транспорта и волка (та же группа окон) скрыта. Щелчок по значку включает/выключает группу; по умолчанию все
/// включены. Выключенный значок - тёмный.
/// </summary>
[Preserve]
public class XUiC_NecroUrnFilter : XUiController
{
readonly XUiController[] _icons = new XUiController[CinerariumGroups.Count];
readonly XUiController[] _frames = new XUiController[CinerariumGroups.Count];
static readonly Color On = Color.white;
static readonly Color Off = new Color(0.25f, 0.25f, 0.25f, 0.6f);
static readonly Color FrameOn = new Color(0.45f, 0.85f, 0.55f, 1f);
static readonly Color FrameOff = new Color(0.2f, 0.2f, 0.2f, 1f);
public override void Init()
{
base.Init();
for (int i = 0; i < CinerariumGroups.Count; i++)
{
int g = i;
_icons[i] = GetChildById("g" + i);
_frames[i] = GetChildById("f" + i);
if (_icons[i] == null)
{
continue;
}
_icons[i].OnPress += (_s, _m) => Toggle(g);
if (_icons[i].ViewComponent is XUiV_Sprite sp)
{
sp.UIAtlas = "ItemIconAtlas";
sp.SpriteName = CinerariumGroups.Icons[g];
}
}
}
public override void OnOpen()
{
base.OnOpen();
Refresh();
}
void Toggle(int _g)
{
TEFeatureCinerarium urn = CinerariumContents.Urn;
if (urn == null)
{
return;
}
urn.ToggleGroup(_g);
Refresh();
}
void Refresh()
{
TEFeatureCinerarium urn = CinerariumContents.Urn;
bool show = urn != null && urn.IsLogist;
ViewComponent.IsVisible = show;
if (!show)
{
return;
}
for (int i = 0; i < CinerariumGroups.Count; i++)
{
bool on = urn.GroupOn(i);
if (_icons[i]?.ViewComponent is XUiV_Sprite sp)
{
sp.Color = on ? On : Off;
sp.ToolTip = Localization.Get(CinerariumGroups.NameKey(i)) + " - " + Localization.Get(on ? "necroGroupOn" : "necroGroupOff");
}
if (_frames[i]?.ViewComponent is XUiV_Sprite fr)
{
fr.Color = on ? FrameOn : FrameOff;
}
}
}
}
}
+626
View File
@@ -0,0 +1,626 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Дух-работник урны - «джинн из урны» (решение 16.10): кончик хвоста уходит в горлышко, дух вырастает над урной,
/// без работы висит, чуть покачиваясь и медленно поворачиваясь. Этап 3 некротехнолога.
///
/// Не сущность игры, а видимая модель - дочерний объект модели блока урны, как каркас зоны (решение 16.10 вместо
/// плана 15.10 «сущность на префабе медсестры»): хитбокса, ИИ и сохранения у неё нет, поэтому «убить нельзя»,
/// «зомби не замечают», «сквозь стены» выходят сами, а сейвы дух не трогает - он появляется по черепу в урне
/// (TEFeatureCinerarium.HasSkull) и пропадает без него. Сдвиги Origin и выгрузку чанка переживает с моделью урны.
///
/// Модель - бандл necrotechnologist (NecroTechnologistSetup.cs): своя сетка на своём скелете NT_rig в позе покоя,
/// глаза - отдельные рендереры (в имени «eye»). Тело - светящаяся дымка (16.10: Fade давал пятна; сложение One/One,
/// холодный оттенок Tint, своё слабое свечение Glow); текстура уже серая (сделана под обесцвечивание). Глаза - свой
/// непрозрачный светящийся материал из бандла, очередь отрисовки 3100 - после тела.
/// </summary>
public static class CinerariumGenie
{
public const string ModelPath = "#@modfolder(NecromancerTome):Resources/necrotechnologist?necroTechnologistPrefab.prefab";
/// <summary>Высота корня модели над низом блока урны, м. 16.10 в Blender по модели урны: -0.15 («духа чуть приподними»).
/// Подгоняется командой necrogenie.</summary>
public static float OffsetY = -0.15f;
/// <summary>Сдвиг вперёд в системе самого духа, м (16.10: «чуть вперёд, чтобы хвост не просвечивал сквозь урну сзади»
/// - загнутый назад кончик хвоста; в Blender 0.06 - ни одной вершины за стенкой, взято 0.07). Поворачивается вместе
/// с духом, поэтому хвост всегда в горлышке. Знак проверить в игре: necrogenie fwd.</summary>
public static float Forward = 0.07f;
/// <summary>Поворот без работы, градусов в секунду.</summary>
public static float TurnSpeed = 6f;
/// <summary>Покачивание: размах (м) и период (с).</summary>
public static float BobHeight = 0.04f;
public static float BobPeriod = 4f;
public static float Alpha = 0.3f;
/// <summary>Засыпание (16.10): игрок дальше SleepDistance - дух замирает дома, огоньки нитей стоят. Работа идёт.</summary>
public static float SleepDistance = 40f;
public static bool Hidden;
/// <summary>Далеко ли игрок от точки сцены (без Origin). Для засыпания духа и нитей.</summary>
public static bool PlayerFar(Vector3 _scenePos)
{
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
if (pl == null)
{
return true;
}
return (pl.GetPosition() - Origin.position - _scenePos).sqrMagnitude > SleepDistance * SleepDistance;
}
/// <summary>Руки (16.10: «Т-поза бесит»): плечо от вертикали вбок и вперёд, локоть согнут вперёд, градусы.
/// Модель в позе покоя, аниматора нет - кости ставятся один раз при появлении.</summary>
public static float ArmOut = 12f;
public static float ArmFwd = 15f;
public static float Elbow = 35f;
static GameObject _prefab;
static bool _tried;
static readonly Dictionary<Material, Material> _ghostMats = new Dictionary<Material, Material>();
/// <summary>Все джинны на сцене - команда necrogenie правит их сразу.</summary>
public static readonly List<CinerariumGenieMotion> Live = new List<CinerariumGenieMotion>();
static bool Load()
{
if (!_tried)
{
_tried = true;
_prefab = DataLoader.LoadAsset<GameObject>(ModelPath);
Debug.Log("[NecromancerTome] CinerariumGenie: модель " + (_prefab != null ? "загружена" : "НЕ загружена: " + ModelPath));
}
return _prefab != null;
}
public static GameObject Spawn(Transform _urnModel)
{
if (_urnModel == null || !Load())
{
return null;
}
GameObject g = Object.Instantiate(_prefab, _urnModel, false);
g.name = "NecroCinerariumGenie";
foreach (Renderer r in g.GetComponentsInChildren<Renderer>(true))
{
r.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
r.receiveShadows = false;
if (r is SkinnedMeshRenderer smr)
{
smr.updateWhenOffscreen = false; // 16.10: вне экрана скелет не считать (экономия); границы - от сетки
}
if (r.name.Contains("eye"))
{
continue;
}
Material[] mats = r.sharedMaterials;
for (int i = 0; i < mats.Length; i++)
{
mats[i] = Ghost(mats[i]);
}
r.sharedMaterials = mats;
}
foreach (Collider c in g.GetComponentsInChildren<Collider>(true))
{
Object.Destroy(c);
}
g.AddComponent<CinerariumGenieMotion>().Pose();
return g;
}
/// <summary>Полупрозрачная копия материала тела - одна на исходный материал на всю игру.</summary>
static Material Ghost(Material _src)
{
if (_src == null)
{
return null;
}
if (_ghostMats.TryGetValue(_src, out Material m) && m != null)
{
return m;
}
m = new Material(_src) { name = _src.name + "_ghost" };
Shader std = Shader.Find("Standard");
if (std != null && m.shader != std)
{
m.shader = std;
}
// 16.10: Fade (SrcAlpha/OneMinusSrcAlpha) давал «прозрачный, но какими-то пятнами» и «не выглядит бесплотным
// духом»: без записи глубины треугольники одной сетки ложатся в случайном порядке. Свечение (One/One) от порядка
// не зависит: дух только добавляет свет к тому, что за ним, - дымка. Альфа здесь не нужна, яркость - цветом.
m.SetFloat("_Mode", 2f);
m.SetOverrideTag("RenderType", "Transparent");
m.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
m.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.One);
m.SetInt("_ZWrite", 0);
m.DisableKeyword("_ALPHATEST_ON");
m.DisableKeyword("_ALPHABLEND_ON");
m.DisableKeyword("_ALPHAPREMULTIPLY_ON");
if (m.HasProperty("_Glossiness"))
{
m.SetFloat("_Glossiness", 0f); // без бликов: блик при сложении - белое пятно
}
if (m.HasProperty("_Metallic"))
{
m.SetFloat("_Metallic", 0f);
}
if (m.HasProperty("_EmissionMap") && m.HasProperty("_MainTex"))
{
m.SetTexture("_EmissionMap", m.GetTexture("_MainTex")); // своё свечение - чтобы ночью не пропадал
m.EnableKeyword("_EMISSION");
m.globalIlluminationFlags = MaterialGlobalIlluminationFlags.None;
}
m.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
SetLook(m);
_ghostMats[_src] = m;
return m;
}
/// <summary>Холодный бледный оттенок духа (умножается на серую текстуру).</summary>
public static Color Tint = new Color(0.55f, 0.75f, 1f, 1f);
/// <summary>Своё свечение тела, доля от Tint.</summary>
public static float Glow = 0.35f;
static void SetLook(Material _m)
{
if (_m == null)
{
return;
}
if (_m.HasProperty("_Color"))
{
_m.color = new Color(Tint.r * Alpha, Tint.g * Alpha, Tint.b * Alpha, 1f);
}
if (_m.HasProperty("_EmissionColor"))
{
_m.SetColor("_EmissionColor", new Color(Tint.r * Glow, Tint.g * Glow, Tint.b * Glow, 1f));
}
}
public static void ApplyAlpha()
{
foreach (Material m in _ghostMats.Values)
{
SetLook(m);
}
}
static readonly Dictionary<Material, Material> _stuckMats = new Dictionary<Material, Material>();
/// <summary>Почерневший дух (класть некуда): свечением чёрное не нарисовать, поэтому обычная Fade-копия, тёмная.</summary>
public static Material Stuck(Material _src)
{
if (_src == null)
{
return null;
}
if (_stuckMats.TryGetValue(_src, out Material m) && m != null)
{
return m;
}
m = new Material(_src) { name = _src.name + "_stuck" };
Shader std = Shader.Find("Standard");
if (std != null && m.shader != std)
{
m.shader = std;
}
m.SetFloat("_Mode", 2f);
m.SetOverrideTag("RenderType", "Transparent");
m.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
m.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
m.SetInt("_ZWrite", 1); // с глубиной: пятен нет, тёмный и почти плотный
m.DisableKeyword("_ALPHATEST_ON");
m.EnableKeyword("_ALPHABLEND_ON");
m.DisableKeyword("_ALPHAPREMULTIPLY_ON");
m.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
if (m.HasProperty("_Color"))
{
m.color = new Color(0.04f, 0.035f, 0.045f, 0.8f);
}
_stuckMats[_src] = m;
return m;
}
/// <summary>Исходный материал тела по его светящейся копии.</summary>
public static Material Source(Material _ghost)
{
foreach (KeyValuePair<Material, Material> p in _ghostMats)
{
if (p.Value == _ghost)
{
return p.Key;
}
}
return _ghost;
}
}
/// <summary>Покачивание и поворот джинна над урной. Только вид - никакой логики.</summary>
[Preserve]
public class CinerariumGenieMotion : MonoBehaviour
{
float _yaw;
float _phase;
/// <summary>Полёт к месту работы и обратно (16.10: «возвращается задом», «делал действие не сразу, а хоть руками
/// пошевелив»): туда - лицом к месту, над местом - жест руками (дело делается в его середине, см. Visit),
/// обратно - развернувшись лицом к урне. Точка - в координатах модели урны; _t - время с начала полёта, -1 - дома.</summary>
struct Stop
{
public Vector3 Hover, Look; // в осях модели урны
public float Fly;
}
readonly List<Stop> _stops = new List<Stop>();
float _backFly;
float _t = -1f;
/// <summary>Скорость полёта, м/с: время перелёта - не меньше OutSeconds/BackSeconds, дальше - по расстоянию (нити до 15 м).</summary>
public static float FlySpeed = 4f;
public static float OutSeconds = 1.3f;
public static float WorkSeconds = 1.6f;
public static float BackSeconds = 1.3f;
/// <summary>Жест: плечи вперёд до GestureFwd градусов, локти ходят на ±GestureWave вразнобой.</summary>
public static float GestureFwd = 55f;
public static float GestureWave = 30f;
bool _gesturing;
/// <summary>Почернел и замер (готовое класть некуда, решение 06.10).</summary>
bool _stuck;
/// <summary>Кости рук (плечо, предплечье, кисть по сторонам) и их поворот в позе покоя.</summary>
Transform[] _arm;
Quaternion[] _armRest;
static bool _armLogged;
/// <summary>Опустить руки из позы покоя: направление плеча и предплечья задаётся в осях модели (вперёд - +Z),
/// поворот - от текущего направления кости к нужному, так что оси самих костей значения не имеют.</summary>
public void Pose()
{
Pose(CinerariumGenie.ArmFwd, CinerariumGenie.Elbow, CinerariumGenie.Elbow);
}
void Pose(float _fwd, float _elbowL, float _elbowR)
{
if (_arm == null)
{
string[] names = { "LeftArm", "LeftForeArm", "LeftHand", "RightArm", "RightForeArm", "RightHand" };
_arm = new Transform[names.Length];
_armRest = new Quaternion[names.Length];
foreach (Transform t in GetComponentsInChildren<Transform>(true))
{
for (int i = 0; i < names.Length; i++)
{
if (_arm[i] == null && t.name.EndsWith(names[i]))
{
_arm[i] = t;
_armRest[i] = t.localRotation;
}
}
}
if (!_armLogged)
{
_armLogged = true;
System.Text.StringBuilder sb = new System.Text.StringBuilder();
for (int i = 0; i < names.Length; i++)
{
sb.Append(names[i]).Append('=').Append(_arm[i] != null ? "ok" : "НЕТ").Append(' ');
}
Debug.Log("[NecromancerTome] CinerariumGenie: кости рук " + sb);
}
}
for (int i = 0; i < _arm.Length; i++)
{
if (_arm[i] != null)
{
_arm[i].localRotation = _armRest[i];
}
}
for (int side = 0; side < 2; side++)
{
Transform up = _arm[side * 3], fore = _arm[side * 3 + 1], hand = _arm[side * 3 + 2];
if (up == null || fore == null || hand == null)
{
continue;
}
float sign = Mathf.Sign(transform.InverseTransformPoint(up.position).x);
float o = CinerariumGenie.ArmOut * Mathf.Deg2Rad, f = _fwd * Mathf.Deg2Rad;
Vector3 upDir = new Vector3(sign * Mathf.Sin(o), -Mathf.Cos(o) * Mathf.Cos(f), Mathf.Sin(f)).normalized;
Vector3 foreDir = Vector3.RotateTowards(upDir, Vector3.forward, (side == 0 ? _elbowL : _elbowR) * Mathf.Deg2Rad, 0f);
up.rotation = Quaternion.FromToRotation(fore.position - up.position, transform.TransformDirection(upDir)) * up.rotation;
fore.rotation = Quaternion.FromToRotation(hand.position - fore.position, transform.TransformDirection(foreDir)) * fore.rotation;
}
}
/// <summary>Светящиеся материалы рендереров - вернуть после почернения.</summary>
readonly Dictionary<Renderer, Material[]> _glowing = new Dictionary<Renderer, Material[]>();
/// <summary>Облёт мест работы по порядку (у перевозки по нити - ящик «откуда», потом «куда»), у каждого - жест, затем
/// назад к урне. Точки - в координатах сцены (без Origin): stand - где встать (низ клетки), look - низ клетки места,
/// beside - встал сбоку, а не над местом. Возвращает, через сколько секунд дело (середина последнего жеста).</summary>
public float Visit(List<(Vector3 stand, Vector3 look, bool beside)> _list)
{
if (transform.parent == null || _list == null || _list.Count == 0)
{
return 0f;
}
_stops.Clear();
Vector3 home = Home();
Vector3 prev = home;
float delay = 0f;
foreach ((Vector3 stand, Vector3 look, bool beside) in _list)
{
Stop st = new Stop
{
Hover = transform.parent.InverseTransformPoint(stand) + new Vector3(0f, CinerariumGenie.OffsetY + (beside ? 0.15f : 0.8f), 0f),
Look = transform.parent.InverseTransformPoint(look)
};
st.Fly = Mathf.Max(OutSeconds, Vector3.Distance(prev, st.Hover) / FlySpeed);
delay += st.Fly + WorkSeconds;
prev = st.Hover;
_stops.Add(st);
}
_backFly = Mathf.Max(BackSeconds, Vector3.Distance(prev, home) / FlySpeed);
_t = 0f;
return delay - WorkSeconds * 0.5f;
}
Vector3 Home()
{
return new Vector3(0f, CinerariumGenie.OffsetY, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward);
}
/// <summary>Повернуться лицом по направлению _dir (горизонтально), не быстрее 300°/с.</summary>
void Face(Vector3 _dir)
{
if (_dir.x * _dir.x + _dir.z * _dir.z > 0.01f)
{
_yaw = Mathf.MoveTowardsAngle(_yaw, Mathf.Atan2(_dir.x, _dir.z) * Mathf.Rad2Deg, 300f * Time.deltaTime) % 360f;
}
}
void Gesture(float _w)
{
if (_w > 0f)
{
float wave = Mathf.Sin(_t * Mathf.PI * 2f * 1.5f) * GestureWave * _w;
Pose(Mathf.Lerp(CinerariumGenie.ArmFwd, GestureFwd, _w), CinerariumGenie.Elbow + 15f * _w + wave, CinerariumGenie.Elbow + 15f * _w - wave);
_gesturing = true;
}
else if (_gesturing)
{
Pose();
_gesturing = false;
}
}
public void SetStuck(bool _on)
{
if (_stuck == _on)
{
return;
}
_stuck = _on;
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
{
if (r.name.Contains("eye"))
{
continue;
}
if (!_glowing.ContainsKey(r))
{
_glowing[r] = r.sharedMaterials;
}
Material[] glow = _glowing[r];
if (_on)
{
Material[] dark = new Material[glow.Length];
for (int i = 0; i < glow.Length; i++)
{
dark[i] = CinerariumGenie.Stuck(CinerariumGenie.Source(glow[i]));
}
r.sharedMaterials = dark;
}
else
{
r.sharedMaterials = glow;
}
}
}
void OnEnable()
{
CinerariumGenie.Live.Add(this);
_phase = Random.value * 10f;
_yaw = Random.value * 360f;
}
void OnDisable()
{
CinerariumGenie.Live.Remove(this);
}
float _nextSleepCheck;
bool _asleep;
bool _hiddenApplied;
void Update()
{
if (Time.time >= _nextSleepCheck)
{
_nextSleepCheck = Time.time + 0.5f;
bool asleep = CinerariumGenie.PlayerFar(transform.position);
if (asleep && !_asleep)
{
_t = -1f; // полёт прерван - дело всё равно выполнится
Gesture(0f);
transform.localPosition = Home();
}
_asleep = asleep;
if (_hiddenApplied != CinerariumGenie.Hidden)
{
// Только при смене (16.10, проверка на утечки: раньше список рендереров собирался каждые 0.5 с - мусор для GC).
_hiddenApplied = CinerariumGenie.Hidden;
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
{
r.enabled = !_hiddenApplied;
}
}
}
if (_stuck || _asleep)
{
return; // замер как был
}
float yawTarget = _yaw + CinerariumGenie.TurnSpeed * Time.deltaTime;
Vector3 home = Home();
if (_t >= 0f && _stops.Count > 0)
{
_t += Time.deltaTime;
float acc = 0f;
Vector3 from = home;
foreach (Stop st in _stops)
{
if (_t < acc + st.Fly)
{
transform.localPosition = Vector3.Lerp(from, st.Hover, Mathf.SmoothStep(0f, 1f, (_t - acc) / st.Fly));
Face(st.Hover - from);
Gesture(0f);
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
return;
}
acc += st.Fly;
if (_t < acc + WorkSeconds)
{
transform.localPosition = st.Hover;
Face(st.Look - st.Hover); // за работой - лицом к месту
Gesture(Mathf.Sin((_t - acc) / WorkSeconds * Mathf.PI));
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
return;
}
acc += WorkSeconds;
from = st.Hover;
}
if (_t < acc + _backFly)
{
transform.localPosition = Vector3.Lerp(from, home, Mathf.SmoothStep(0f, 1f, (_t - acc) / _backFly));
Face(home - from); // назад - лицом к урне
Gesture(0f);
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
return;
}
_t = -1f;
Gesture(0f);
}
_yaw = yawTarget % 360f;
float bob = CinerariumGenie.BobPeriod > 0.01f
? Mathf.Sin((Time.time + _phase) * Mathf.PI * 2f / CinerariumGenie.BobPeriod) * CinerariumGenie.BobHeight
: 0f;
transform.localPosition = home + new Vector3(0f, bob, 0f);
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
}
}
/// <summary>Подгонка джинна в игре: высота над урной, скорость поворота, покачивание, прозрачность. Не сохраняется -
/// найденные значения переносятся в константы CinerariumGenie.</summary>
[Preserve]
public class ConsoleCmdNecroGenie : ConsoleCmdAbstract
{
public override bool IsExecuteOnClient => true;
public override bool AllowedInMainMenu => false;
public override string[] getCommands()
{
return new string[] { "necrogenie" };
}
public override string getDescription()
{
return "Necromancer's Tome: tune the cinerarium spirit (height over the urn, turning, bobbing, transparency).";
}
public override string getHelp()
{
return "necrogenie - show the current values and how many spirits are on screen\n" +
"necrogenie y <m> - model root height above the urn block's bottom (tail tip is 0.47 m above the root)\n" +
"necrogenie fwd <m> - shift along the spirit's own forward (keeps the curled tail inside the neck)\n" +
"necrogenie turn <deg/s> - idle turning speed\n" +
"necrogenie bob <m> <s> - bobbing height and period\n" +
"necrogenie alpha <0-1> - body brightness (the body adds light, it is not alpha-blended)\n" +
"necrogenie glow <0-1> - body's own glow (visible at night)\n" +
"necrogenie tint <r> <g> <b> - body colour\n" +
"necrogenie arms <out> <fwd> <elbow> - arm pose, degrees: upper arm from vertical sideways and forward, elbow bend\n" +
"necrogenie sleep <m> - spirits and threads freeze when the player is farther (work goes on)\n" +
"necrogenie hide|show - hide all spirits (weak PCs, video), work goes on";
}
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{
SdtdConsole con = SingletonMonoBehaviour<SdtdConsole>.Instance;
if (_params.Count == 1 && (_params[0] == "hide" || _params[0] == "show"))
{
CinerariumGenie.Hidden = _params[0] == "hide";
}
if (_params.Count >= 2 && float.TryParse(_params[1], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float v))
{
switch (_params[0].ToLowerInvariant())
{
case "y":
CinerariumGenie.OffsetY = v;
break;
case "fwd":
CinerariumGenie.Forward = v;
break;
case "turn":
CinerariumGenie.TurnSpeed = v;
break;
case "bob":
CinerariumGenie.BobHeight = v;
if (_params.Count >= 3 && float.TryParse(_params[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float per))
{
CinerariumGenie.BobPeriod = per;
}
break;
case "alpha":
CinerariumGenie.Alpha = Mathf.Clamp01(v);
CinerariumGenie.ApplyAlpha();
break;
case "glow":
CinerariumGenie.Glow = Mathf.Clamp01(v);
CinerariumGenie.ApplyAlpha();
break;
case "sleep":
CinerariumGenie.SleepDistance = Mathf.Max(5f, v);
break;
case "arms":
CinerariumGenie.ArmOut = v;
if (_params.Count >= 3 && float.TryParse(_params[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float af))
{
CinerariumGenie.ArmFwd = af;
}
if (_params.Count >= 4 && float.TryParse(_params[3], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float ae))
{
CinerariumGenie.Elbow = ae;
}
foreach (CinerariumGenieMotion m in CinerariumGenie.Live)
{
m.Pose();
}
break;
case "tint":
if (_params.Count >= 4 &&
float.TryParse(_params[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float g) &&
float.TryParse(_params[3], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float b))
{
CinerariumGenie.Tint = new Color(v, g, b, 1f);
CinerariumGenie.ApplyAlpha();
}
break;
default:
con.Output("necrogenie: unknown '" + _params[0] + "' - see 'help necrogenie'");
return;
}
}
con.Output("necrogenie: y " + CinerariumGenie.OffsetY.ToString("F2") + ", fwd " + CinerariumGenie.Forward.ToString("F2") + ", turn " + CinerariumGenie.TurnSpeed.ToString("F1") +
", bob " + CinerariumGenie.BobHeight.ToString("F2") + " / " + CinerariumGenie.BobPeriod.ToString("F1") + " s, alpha " +
CinerariumGenie.Alpha.ToString("F2") + ", glow " + CinerariumGenie.Glow.ToString("F2") + ", tint " +
CinerariumGenie.Tint.r.ToString("F2") + " " + CinerariumGenie.Tint.g.ToString("F2") + " " + CinerariumGenie.Tint.b.ToString("F2") + ", arms " + CinerariumGenie.ArmOut.ToString("F0") + " " +
CinerariumGenie.ArmFwd.ToString("F0") + " " + CinerariumGenie.Elbow.ToString("F0") + ", sleep " + CinerariumGenie.SleepDistance.ToString("F0") + " m" + (CinerariumGenie.Hidden ? ", HIDDEN" : "") + "; spirits on screen: " + CinerariumGenie.Live.Count);
}
}
}
+218
View File
@@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
namespace NecromancerTome
{
/// <summary>
/// Группы предметов для фильтра урны-логиста (16.10.2026): «пиктограммы, которые по умолчанию выбраны и активны...
/// изображения предмета, символизирующего группу предметов... Дух-логист будет переносить только те предметы, которые
/// активированы в его урне. Это же прямой путь к автоматизации сортировки». Деление игры (свойство Group) грубое -
/// «Ресурсы» смешивают руду, слитки, детали и кур, - поэтому группы свои, а предмет относится к группе по классу, тегам и
/// имени. Предметы других модов разойдутся по тем же правилам; что не подошло никуда - «Прочее».
/// Номер группы = номер бита в TEFeatureCinerarium.GroupMask - порядок НЕ менять (сейвы), новые - только в конец.
/// </summary>
public static class CinerariumGroups
{
public const int Cooked = 0, Canned = 1, Raw = 2, Harvest = 3, Flowers = 4, Seeds = 5, Drinks = 6, Medicine = 7, Ore = 8,
Ingots = 9, Parts = 10, Materials = 11, Chemicals = 12, Tools = 13, Weapons = 14, Ammo = 15, Armor = 16, Mods = 17,
Books = 18, Blocks = 19, Necro = 20, Other = 21;
public const int Count = 22;
public const uint All = (1u << Count) - 1;
/// <summary>Значок группы - иконка типичного предмета (ItemIconAtlas).</summary>
public static readonly string[] Icons =
{
"foodMeatStew", "foodCanChili", "foodRawMeat", "foodCropCorn", "plantedChrysanthemum3Harvest", "plantedCorn1",
"drinkJarBoiledWater", "medicalFirstAidKit", "resourceRockSmall", "resourceForgedSteel", "resourceMechanicalParts",
"resourceWood", "resourceGunPowder", "meleeToolPickT1IronPickaxe", "gunHandgunT1Pistol", "ammo9mmBulletBall",
"armorMinerHelmet", "modGunLaserSight", "bookArtOfMiningLuckyStrike", "cobblestoneMaster", "necroSkull", "oldCash"
};
/// <summary>Ключ названия группы в Localization.csv: necroGroup0..necroGroup21.</summary>
public static string NameKey(int _g) => "necroGroup" + _g;
static readonly HashSet<string> OreNames = new HashSet<string>
{
"resourceRockSmall", "resourceRockSmallBundle", "resourceScrapIron", "resourceScrapIronBundle", "resourceScrapLead",
"resourceLeadBundle", "resourceCoal", "resourceCoalBundle", "resourcePotassiumNitratePowder",
"resourcePotassiumNitratePowderBundle", "resourceOilShale", "resourceOilShaleBundle", "resourceClayLump",
"resourceCrushedSand", "resourceCobblestones", "resourceGoldNugget", "resourceSilverNugget", "resourceRawDiamond",
"resourceSnowBall"
};
static readonly HashSet<string> IngotNames = new HashSet<string> { "resourceForgedIron", "resourceForgedSteel" };
static readonly HashSet<string> ChemNames = new HashSet<string>
{
"resourceGunPowder", "resourceGunPowderBundle", "resourceOil", "ammoGasCan", "ammoGasCanBundle", "resourceAcid",
"resourceGlue", "resourcePaint", "resourceTestosteroneExtract"
};
static readonly HashSet<string> MaterialNames = new HashSet<string>
{
"resourceWood", "resourceWoodBundle", "resourceCloth", "resourceLeather", "resourceBone", "resourceFeather",
"resourceYuccaFibers", "resourcePaper", "resourceCement", "resourceConcreteMix", "resourceBeeswax", "resourceCandleStick",
"resourceAnimalFat"
};
/// <summary>Детали, которые теги отнесли бы не туда (гвозди - патроны гвоздомёта).</summary>
static readonly HashSet<string> PartNames = new HashSet<string> { "resourceNail" };
static readonly HashSet<string> AmmoParts = new HashSet<string>
{
"resourceArrowHeadIron", "resourceArrowHeadSteelAP", "resourceBuckshot", "resourceBulletCasing", "resourceBulletTip",
"resourceRocketCasing", "resourceRocketTip"
};
static readonly HashSet<string> RawNames = new HashSet<string>
{
"foodRawMeat", "foodRawMeatBundle", "foodEgg", "foodHoney", "foodRottingFlesh", "resourceChickenFeed"
};
/// <summary>Предметы и блоки самого мода - по его Config/items.xml и blocks.xml (у многих нет «necro» в имени: книги
/// призыва, браслет, камни портала, прах зомби, кожа жертвы). Читается один раз.</summary>
static HashSet<string> _modNames;
static HashSet<string> ModNames()
{
if (_modNames != null)
{
return _modNames;
}
_modNames = new HashSet<string>();
foreach (string f in new[] { "items.xml", "blocks.xml" })
{
try
{
XmlDocument doc = new XmlDocument();
doc.Load(Path.Combine(ModEntry.Instance.Path, "Config", f));
foreach (XmlNode n in doc.SelectNodes("//item[@name] | //block[@name]"))
{
_modNames.Add(n.Attributes["name"].Value);
}
}
catch (Exception e)
{
UnityEngine.Debug.LogWarning("[NecromancerTome] CinerariumGroups: " + f + " мода не прочитан (" + e.Message + ")");
}
}
return _modNames;
}
static readonly Dictionary<int, int> _cache = new Dictionary<int, int>();
public static int Of(ItemValue _iv)
{
ItemClass ic = _iv?.ItemClass;
if (ic == null)
{
return Other;
}
if (!_cache.TryGetValue(ic.Id, out int g))
{
g = Classify(ic, _iv);
_cache[ic.Id] = g;
}
return g;
}
static bool Tag(ItemClass _ic, string _t)
{
return _ic.ItemTags.Test_AnySet(FastTags<TagGroup.Global>.Parse(_t));
}
static int Classify(ItemClass _ic, ItemValue _iv)
{
string n = _ic.GetItemName() ?? "";
if (n.IndexOf("necro", StringComparison.OrdinalIgnoreCase) >= 0 || ModNames().Contains(n))
{
return Necro;
}
if (_ic is ItemClassModifier)
{
return Mods;
}
if (_ic.IsBlock())
{
return new ItemValue(_ic.Id).ToBlockValue().Block is BlockPlantGrowing ? Seeds : Blocks;
}
if (_ic is ItemClassArmor || n.StartsWith("armor") || n.StartsWith("clothing"))
{
return Armor;
}
if (n.StartsWith("book") || n.Contains("Schematic") || n.Contains("Magazine") || Tag(_ic, "books"))
{
return Books;
}
if (OreNames.Contains(n))
{
return Ore;
}
if (IngotNames.Contains(n) || n.StartsWith("unit_"))
{
return Ingots;
}
if (ChemNames.Contains(n))
{
return Chemicals;
}
if (PartNames.Contains(n))
{
return Parts;
}
if (MaterialNames.Contains(n))
{
return Materials;
}
if (AmmoParts.Contains(n) || n.StartsWith("ammo") || n.StartsWith("thrown") || Tag(_ic, "ammo"))
{
return Ammo;
}
if (n.EndsWith("Parts") || n == "partsMaster" || n.StartsWith("vehicle") && !n.EndsWith("Placeable"))
{
return Parts;
}
if (n.StartsWith("meleeTool"))
{
return Tools; // раньше оружия: у факела тег weapon
}
if (n.StartsWith("gun") || n.StartsWith("meleeWpn") || Tag(_ic, "weapon"))
{
return Weapons;
}
if (n.StartsWith("meleeTool") || n.StartsWith("tool") || n.StartsWith("bucket") || Tag(_ic, "tool"))
{
return Tools;
}
if (n.StartsWith("foodCan"))
{
return Canned;
}
if (n.StartsWith("foodCrop"))
{
return Harvest;
}
if (n.StartsWith("resourceCrop"))
{
return Flowers;
}
if (RawNames.Contains(n) || n.StartsWith("foodRaw"))
{
return Raw;
}
if (n.StartsWith("drink") || n.StartsWith("tinCan"))
{
return Drinks;
}
if (n.StartsWith("medical") || n.StartsWith("drug") || Tag(_ic, "medical"))
{
return Medicine;
}
if (n.StartsWith("food") || Tag(_ic, "food"))
{
return Cooked;
}
if (n.StartsWith("resource") || n == "carBattery" || n == "smallEngine" || n == "solarCell")
{
return Parts;
}
return Other;
}
}
}
+418
View File
@@ -0,0 +1,418 @@
using System.Collections.Generic;
using GUI_2;
using HarmonyLib;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Нити цинерария (16.10.2026, BACKLOG «Нити цинерария»): перевозка между ящиками, как провода электрики. «Ящик на
/// территории одного цинерария и присоединяю к нему другой цинерарий проводом. А затем от цинерария тяну провод до другого
/// ящика. Дух в цинерарии будет забирать всё что лежит в ящике из которого в него тянется провод, и тащить в ящик, в
/// который тянется провод.» Решения: у урны несколько нитей «откуда» и одна «куда»; везётся по образцу (только то, что уже
/// лежит в ящике «куда»; пустой - всё подряд); «куда» полон - дух ждёт; нить призрачная; длина - как у провода (15 м);
/// ящики - любые. Нити - только у отдельного блока «урна-логист» (necroCinerariumLogist, 16.10): без зоны, дух только перевозит,
/// глазницы всегда зелёные.
///
/// Инструмент - нож некроманта «хватом для нитей»: отдельный предмет necroWpnBladeNecroKnifeThread (класс ножа в сейве
/// менять нельзя), переключение - пункт в меню ножа по R (то же меню, что «человек/змея/чертоги», SnakeMode.cs; 16.10:
/// своё меню по R перекрывало его - «ты убрал хваты чертогов памяти и духа змеи»); качество и моды переносятся
/// (ItemValue.Clone со сменой типа). Как у стриппера: ПКМ - начать нить и закончить её (порядок = направление: ящик, потом
/// урна - «откуда»; урна, потом ящик - «куда»), ЛКМ по урне или ящику - снять его нити.
/// Первая версия - одиночная игра и хост (связи меняются на месте, клиентам сети - позже).
/// </summary>
public static class CinerariumThreads
{
public const string KnifeName = Patch_ItemClass_ExecuteAction_KnifePowerCost.KnifeName;
public const string ThreadKnifeName = "necroWpnBladeNecroKnifeThread";
/// <summary>Как у провода электрики: ItemActionConnectPower.maxWireLength = 15.</summary>
public const float MaxLength = 15f;
public const int MaxFrom = 8;
public const int GripNormal = 0, GripThread = 1;
/// <summary>Цвет нити - в цвет духа (CinerariumGenie.Tint).</summary>
public static readonly Color ThreadColor = new Color(0.55f, 0.8f, 1f, 0.45f);
public static readonly Color SparkColor = new Color(0.8f, 0.95f, 1f, 1f);
public static bool IsKnife(ItemClass _i) => _i != null && (_i.Name == KnifeName || _i.Name == ThreadKnifeName);
public static bool IsThreadKnife(ItemClass _i) => _i != null && _i.Name == ThreadKnifeName;
// ------------------------------------------------------------------
// Хват.
// ------------------------------------------------------------------
/// <summary>Нож в руке меняется на _name: тот же ItemValue (качество, износ, моды), другой тип.</summary>
public static void Convert(EntityPlayerLocal _pl, string _name)
{
ItemStack cur = _pl?.inventory?.holdingItemStack;
if (cur == null || cur.IsEmpty() || !IsKnife(cur.itemValue.ItemClass) || cur.itemValue.ItemClass.Name == _name)
{
return;
}
ItemValue target = ItemClass.GetItem(_name);
if (target == null || target.IsEmpty())
{
return;
}
ItemValue iv = cur.itemValue.Clone();
iv.type = target.type;
_pl.inventory.SetItem(_pl.inventory.holdingItemIdx, new ItemStack(iv, cur.count));
Cancel();
}
// ------------------------------------------------------------------
// Соединение.
// ------------------------------------------------------------------
static Vector3i? _start;
static bool _startUrn;
static GameObject _preview;
public static Vector3i? Start => _start;
public static bool StartIsUrn => _startUrn;
public static void Cancel()
{
_start = null;
if (_preview != null)
{
Object.Destroy(_preview);
_preview = null;
}
}
/// <summary>Урна или ящик (составной блок игрока с хранилищем) в клетке _p; _owner - главная клетка блока.</summary>
public static bool Resolve(World _w, Vector3i _p, out Vector3i _owner, out TEFeatureCinerarium _urn)
{
BlockValue bv = _w.GetBlock(_p);
_owner = bv.ischild ? _p + bv.parent : _p;
_urn = null;
if (!(_w.GetTileEntity(_owner) is TileEntityComposite comp))
{
return false;
}
if (comp.TryGetSelfOrFeature<TEFeatureCinerarium>(out _urn))
{
return true;
}
return comp.PlayerPlaced && comp.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage _);
}
public static TEFeatureCinerarium UrnAt(World _w, Vector3i _p)
{
return _w.GetTileEntity(_p) is TileEntityComposite c && c.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium u) ? u : null;
}
/// <summary>Отпущена кнопка с ножом-для-нитей: _connect - ПКМ (тянуть нить), иначе ЛКМ (снять нити).</summary>
public static void Click(EntityPlayerLocal _pl, bool _connect)
{
World w = GameManager.Instance?.World;
WorldRayHitInfo hi = _pl?.HitInfo;
if (w == null || hi == null || !hi.bHitValid || !GameUtils.IsBlockOrTerrain(hi.tag) || hi.hit.distanceSq > 36f)
{
return;
}
bool ok = Resolve(w, hi.hit.blockPos, out Vector3i owner, out TEFeatureCinerarium urn);
if (!_connect)
{
if (_start.HasValue)
{
Cancel();
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadCancel"));
return;
}
if (ok && RemoveAll(w, owner, urn) > 0)
{
_pl.PlayOneShot("wiretool_grab");
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadRemoved"));
}
return;
}
if (!ok)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedPair"));
return;
}
if (!_start.HasValue)
{
if (urn != null && !urn.IsLogist)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedLogist"));
return;
}
_start = owner;
_startUrn = urn != null;
_preview = new GameObject("NecroThreadPreview");
_preview.AddComponent<CinerariumThreadPreview>();
_pl.PlayOneShot("wiretool_grab");
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadStart"));
return;
}
Vector3i s = _start.Value;
bool sUrn = _startUrn;
Cancel();
if (s == owner)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadCancel"));
return;
}
if (sUrn == (urn != null))
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedPair"));
return;
}
TEFeatureCinerarium end = sUrn ? UrnAt(w, s) : urn;
if (end == null || !end.IsLogist)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedLogist"));
return;
}
if (Vector3.Distance(s.ToVector3(), owner.ToVector3()) > MaxLength)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTooFar"));
return;
}
if (sUrn)
{
TEFeatureCinerarium u = UrnAt(w, s);
if (u == null)
{
return;
}
u.SetThreadTo(owner);
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTo"));
}
else
{
if (!urn.AddThreadFrom(s))
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadLimit"));
return;
}
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadFrom"));
}
_pl.PlayOneShot("wiretool_place");
}
/// <summary>Снять все нити урны или все нити, идущие к ящику (у всех урн сейва). Сколько снято.</summary>
static int RemoveAll(World _w, Vector3i _owner, TEFeatureCinerarium _urn)
{
if (_urn != null)
{
return _urn.ClearThreads();
}
int n = 0;
foreach (Vector3i p in CinerariumRegistry.All())
{
TEFeatureCinerarium u = UrnAt(_w, p);
if (u != null)
{
n += u.RemoveThreadsTo(_owner);
}
}
return n;
}
// ------------------------------------------------------------------
// Вид нитей.
// ------------------------------------------------------------------
static Material _mat;
public static Material Mat()
{
if (_mat == null)
{
_mat = new Material(Shader.Find("Sprites/Default")) { name = "NecroThread" };
}
return _mat;
}
/// <summary>Где нить крепится: у урны - горлышко, у ящика - край со стороны урны, чуть выше середины. Сцена (без Origin).</summary>
public static Vector3 UrnPoint(Vector3i _urn) => World.blockToTransformPos(_urn) - Origin.position + Vector3.up * 0.5f;
public static Vector3 BoxPoint(Vector3i _box, Vector3i _urn)
{
Vector3 c = World.blockToTransformPos(_box) - Origin.position + Vector3.up * 0.6f;
Vector3 d = (World.blockToTransformPos(_urn) - World.blockToTransformPos(_box));
d.y = 0f;
if (d.sqrMagnitude > 0.01f)
{
d.Normalize();
c += d * 0.5f;
}
return c;
}
/// <summary>Провисающая нить: точка по доле пути _u.</summary>
public static Vector3 Curve(Vector3 _a, Vector3 _b, float _u)
{
float sag = Mathf.Min(0.6f, Vector3.Distance(_a, _b) * 0.05f);
return Vector3.Lerp(_a, _b, _u) + Vector3.down * (sag * 4f * _u * (1f - _u));
}
public static LineRenderer NewLine(Transform _parent, string _name, float _width, Color _c)
{
GameObject g = new GameObject(_name);
g.transform.SetParent(_parent, false);
LineRenderer lr = g.AddComponent<LineRenderer>();
lr.useWorldSpace = false;
lr.widthMultiplier = _width;
lr.sharedMaterial = Mat();
lr.startColor = _c;
lr.endColor = _c;
lr.numCapVertices = 2;
lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
lr.receiveShadows = false;
return lr;
}
}
/// <summary>Нити одной урны: провисающие линии и бегущие по ним огоньки (направление). Видны, только пока в руках нож
/// хватом для нитей. Дочерний объект модели урны -
/// сдвиги Origin и выгрузка чанка его не теряют; точки - в осях модели.</summary>
[Preserve]
public class CinerariumThreadView : MonoBehaviour
{
struct Thread
{
public Vector3 A, B; // в осях модели урны, огоньки бегут от A к B
public LineRenderer[] Sparks;
}
readonly List<Thread> _threads = new List<Thread>();
const int Segments = 16;
const int SparksPerThread = 3;
public static float SparkSpeed = 1.2f; // м/с
/// <summary>_pairs - концы в координатах сцены (без Origin). Видимость - заново по ножу в руке (Update).</summary>
public void Build(List<(Vector3 a, Vector3 b)> _pairs)
{
foreach (Transform c in transform)
{
Destroy(c.gameObject);
}
_threads.Clear();
Transform t = transform;
foreach ((Vector3 a, Vector3 b) in _pairs)
{
Thread th = new Thread { A = t.InverseTransformPoint(a), B = t.InverseTransformPoint(b), Sparks = new LineRenderer[SparksPerThread] };
LineRenderer lr = CinerariumThreads.NewLine(t, "thread", 0.025f, CinerariumThreads.ThreadColor);
lr.positionCount = Segments + 1;
for (int i = 0; i <= Segments; i++)
{
lr.SetPosition(i, CinerariumThreads.Curve(th.A, th.B, i / (float)Segments));
}
for (int k = 0; k < SparksPerThread; k++)
{
th.Sparks[k] = CinerariumThreads.NewLine(t, "spark", 0.06f, CinerariumThreads.SparkColor);
th.Sparks[k].positionCount = 2;
}
_threads.Add(th);
}
_shown = -1;
}
float _nextSleepCheck;
bool _asleep;
int _shown = -1;
/// <summary>16.10: «Нити должны быть видны, только когда нож в режиме нитей и в руках».</summary>
public static bool ToolInHand()
{
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
return pl != null && CinerariumThreads.IsThreadKnife(pl.inventory?.holdingItem);
}
void Update()
{
if (Time.time >= _nextSleepCheck)
{
_nextSleepCheck = Time.time + 0.25f;
_asleep = CinerariumGenie.PlayerFar(transform.position); // засыпание (16.10): огоньки стоят
int show = ToolInHand() ? 1 : 0;
if (show != _shown)
{
_shown = show;
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
{
r.enabled = show == 1;
}
}
}
if (_asleep || _shown != 1)
{
return;
}
foreach (Thread th in _threads)
{
float len = Mathf.Max(0.5f, Vector3.Distance(th.A, th.B));
float step = 0.2f / len;
for (int k = 0; k < th.Sparks.Length; k++)
{
float u = Mathf.Repeat(Time.time * SparkSpeed / len + k / (float)th.Sparks.Length, 1f);
th.Sparks[k].SetPosition(0, CinerariumThreads.Curve(th.A, th.B, Mathf.Max(0f, u - step)));
th.Sparks[k].SetPosition(1, CinerariumThreads.Curve(th.A, th.B, u));
}
}
}
}
/// <summary>Нить, которую тянут: от начала до руки игрока; дальше 15 м - красная. Нож убрали - нить брошена.</summary>
[Preserve]
public class CinerariumThreadPreview : MonoBehaviour
{
LineRenderer _lr;
void Start()
{
_lr = CinerariumThreads.NewLine(transform, "line", 0.025f, CinerariumThreads.ThreadColor);
_lr.useWorldSpace = true;
_lr.positionCount = 17;
}
void Update()
{
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
if (pl == null || !CinerariumThreads.Start.HasValue || !CinerariumThreads.IsThreadKnife(pl.inventory?.holdingItem))
{
CinerariumThreads.Cancel();
return;
}
if (_lr == null)
{
return;
}
Vector3i s = CinerariumThreads.Start.Value;
Vector3 a = World.blockToTransformPos(s) - Origin.position + Vector3.up * (CinerariumThreads.StartIsUrn ? 0.5f : 0.6f);
Transform cam = pl.playerCamera != null ? pl.playerCamera.transform : null;
Vector3 b = cam != null ? cam.position + cam.forward * 0.6f + cam.right * 0.2f - cam.up * 0.25f : pl.GetPosition() - Origin.position + Vector3.up * 1.2f;
bool far = Vector3.Distance(s.ToVector3(), pl.GetPosition()) > CinerariumThreads.MaxLength;
Color c = far ? new Color(1f, 0.3f, 0.3f, 0.7f) : CinerariumThreads.ThreadColor;
_lr.startColor = c;
_lr.endColor = c;
for (int i = 0; i <= 16; i++)
{
_lr.SetPosition(i, CinerariumThreads.Curve(a, b, i / 16f));
}
}
}
/// <summary>Нож для нитей не бьёт: ЛКМ и ПКМ - нити (на отпускание кнопки).</summary>
[HarmonyPatch(typeof(ItemClass), nameof(ItemClass.ExecuteAction))]
public static class Patch_ItemClass_ExecuteAction_ThreadKnife
{
public static bool Prefix(ItemClass __instance, int _actionIdx, ItemInventoryData _data, bool _bReleased)
{
if (!CinerariumThreads.IsThreadKnife(__instance))
{
return true;
}
if (_bReleased && _data?.holdingEntity is EntityPlayerLocal pl)
{
CinerariumThreads.Click(pl, _actionIdx == 1);
}
return false;
}
}
}
+120
View File
@@ -0,0 +1,120 @@
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// «Содержимое» урны-цинерария - ячейка черепа (решение 16.10: «урна как транспорт»; переименовано из «Багажника»).
/// Окно - родное окно хранилища игры (XUiC_BagStorageWindowGroup, как сумка транспорта и сумка волка) с сумкой на одну
/// ячейку: перетаскивание, Shift+клик и правый клик - кодом игры (16.10: «Сумки, ящики в игре есть» - своё окно с
/// ячейкой коллектора не принимало череп ни перетаскиванием, ни Shift+кликом). Своё здесь одно: после каждого изменения
/// в ячейке остаётся один череп, всё остальное (лишние черепа, чужие предметы) - назад в рюкзак, и урна узнаёт, есть ли
/// в ней череп. Череп - обычный предмет (ItemClass): он уже в сейвах, менять ему класс на модификацию нельзя.
/// </summary>
public static class CinerariumContents
{
public const string LootTemplate = "roboticDrone"; // звуки и время открытия, как у сумки волка
static TEFeatureCinerarium _urn;
/// <summary>Урна, чьё «Содержимое» сейчас открыто (окно фильтра логиста), или null.</summary>
public static TEFeatureCinerarium Urn => _urn;
static Bag _bag;
static EntityPlayerLocal _player;
public static void Open(EntityPlayerLocal _p, TEFeatureCinerarium _u)
{
LocalPlayerUI ui = _p != null ? LocalPlayerUI.GetUIForPlayer(_p) : null;
ItemValue skull = ItemClass.GetItem(TEFeatureCinerarium.SkullItemName);
if (ui == null || _u == null)
{
return;
}
_urn = _u;
_player = _p;
_bag = new Bag(new Vector2i(1, 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null);
if (_u.HasSkull && skull != null && skull.type > 0)
{
_bag.SetSlot(0, new ItemStack(skull, 1));
}
XUiC_BagStorageWindowGroup.Open(ui.xui, _p, _bag, LootContainer.GetLootContainer(LootTemplate),
Localization.Get("necroCinerariumTrunk"), Check, () =>
{
Check();
_urn = null;
_bag = null;
_player = null;
});
Debug.Log("[NecromancerTome] Cinerarium: «Содержимое» урны " + _u.ToWorldPos() + " открыто, череп " + (_u.HasSkull ? "есть" : "нет"));
}
/// <summary>Оставить в ячейке один череп, остальное - игроку; отметить в урне.</summary>
static void Check()
{
if (_bag == null || _urn == null)
{
return;
}
ItemStack s = _bag.ItemGrid.items[0];
bool has = false;
if (s != null && !s.IsEmpty())
{
ItemStack back = null;
if (s.itemValue.ItemClass?.GetItemName() != TEFeatureCinerarium.SkullItemName)
{
back = s.Clone();
_bag.SetSlot(0, ItemStack.Empty.Clone());
}
else
{
has = true;
if (s.count > 1)
{
back = s.Clone();
back.count = s.count - 1;
ItemStack one = s.Clone();
one.count = 1;
_bag.SetSlot(0, one);
}
}
if (back != null)
{
GiveBack(back);
}
}
_urn.SetSkull(has);
}
static void GiveBack(ItemStack _s)
{
if (_player == null)
{
return;
}
if (_player.bag != null && _player.bag.AddItem(_s))
{
return;
}
if (_player.inventory != null && _player.inventory.AddItem(_s))
{
return;
}
_player.world.gameManager.ItemDropServer(_s, _player.GetPosition(), Vector3.zero);
}
/// <summary>Урна убрана или выгружена, пока открыто её «Содержимое», - закрыть окно (череп из ячейки не потеряется:
/// Check успеет отметить его в урне, а урна при разрушении роняет череп).</summary>
public static void CloseIfShowing(TEFeatureCinerarium _u)
{
if (_urn == null || _urn != _u)
{
return;
}
Check();
LocalPlayerUI ui = _player != null ? LocalPlayerUI.GetUIForPlayer(_player) : null;
_urn = null;
if (ui != null && ui.windowManager.IsWindowOpen(XUiC_BagStorageWindowGroup.ID))
{
ui.windowManager.Close(XUiC_BagStorageWindowGroup.ID);
}
}
}
}
+565
View File
@@ -0,0 +1,565 @@
using System.Collections.Generic;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Работа духа урны - этап 4 некротехнолога (указание 16.10: дух - УНИВЕРСАЛЬНЫЙ работник зоны; BACKLOG «Некротехнолог v2»).
/// Раз в StepSeconds - одно дело, по порядку:
/// 1. пополнить расходники коллекторов из ящиков зоны: ячейки топлива (банки, цветы, корм, сферы - что коллектор
/// принимает, GetFuelTypes) и катализатора (куры - GetCatalystTypes; по одной в пустую ячейку). Модификации
/// коллекторов не трогаются;
/// 2. забрать готовое из коллекторов (Items) и станций (TileEntityWorkstation.Output - верстак, кузня, химстанция,
/// костёр, бетономешалка) и сложить в ящики зоны: сначала к таким же предметам, потом в свободное место; ящики полны -
/// в сумку урны; некуда - дух чернеет и замирает над урной, оживает сам, когда место появится;
/// 3. грядки - последними: собрать созревшее (блок с меткой crops и добычей Harvest) с «идеальными бонусами»
/// (06.10: все бонусные доли урожая и семя - всегда), семя сразу посадить на то же место; затем засадить пустые
/// грядки случайными семенами из ящиков и сумки (семя - предмет-блок стадии 1, BlockPlantGrowing; можно ли сажать,
/// решает сам блок семени - CanPlaceBlockAt проверяет плодородие под ним, то есть грядку).
/// Коллектор или станция подходят любые - будущий крольчатник обслуживается тем же кодом. Ящик - составной блок игрока
/// с хранилищем (TEFeatureStorage, PlayerPlaced), кроме урн. Открытое у игрока (IsUserAccessing) не трогается.
/// Работает на сервере: зовётся из TEFeatureCinerarium.UpdateTick.
/// </summary>
public static class CinerariumWork
{
public static float StepSeconds = 3f;
/// <summary>Что в зоне: собирается заново на каждом шаге (150 клеток - дёшево, а ящики могут переставить).</summary>
sealed class Zone
{
public readonly List<TEFeatureStorage> Boxes = new List<TEFeatureStorage>();
public readonly List<TileEntityCollector> Collectors = new List<TileEntityCollector>();
public readonly List<TileEntityWorkstation> Stations = new List<TileEntityWorkstation>();
public readonly Dictionary<TileEntity, Vector3i> Where = new Dictionary<TileEntity, Vector3i>();
public TEFeatureStorage Bag;
public readonly List<Vector3i> Ripe = new List<Vector3i>();
public readonly List<Vector3i> Empty = new List<Vector3i>();
}
static readonly FastTags<TagGroup.Global> CropTag = FastTags<TagGroup.Global>.Parse("crops");
static readonly GameRandom _rnd = GameRandomManager.Instance.CreateGameRandom();
static readonly HashSet<TileEntity> _seen = new HashSet<TileEntity>();
/// <summary>Пробный шаг: найти дело и место, ничего не меняя.</summary>
static bool _dry;
/// <summary>Одно дело. Возвращает, где дух работал (для полёта модели), или null; _stuck - есть что забрать, но некуда.
/// _dryRun - только узнать, где будет дело (дух летит туда заранее), ничего не трогая.</summary>
public static Vector3i? Step(World _world, TEFeatureCinerarium _urn, out bool _stuck, bool _dryRun)
{
_dry = _dryRun;
try
{
return DoStep(_world, _urn, out _stuck);
}
finally
{
_dry = false;
}
}
/// <summary>У перевозки по нити - ящик «откуда» последнего шага (дух летит сначала к нему), иначе null.</summary>
public static Vector3i? LastFrom;
static Vector3i? DoStep(World _world, TEFeatureCinerarium _urn, out bool _stuck)
{
_stuck = false;
LastFrom = null;
// Урна с нитями - логист (16.10: «занимается только перетаскиванием из ящиков в ящики»).
if (_urn.IsLogist)
{
return Ferry(_world, _urn);
}
Zone z = Scan(_world, _urn);
// 1. Расходники.
foreach (TileEntityCollector c in z.Collectors)
{
if (Refill(c, c.FuelSlots, c.GetFuelTypes(), false, z) || Refill(c, c.CatalystSlots, c.GetCatalystTypes(), true, z))
{
if (!_dry)
{
c.SetModified();
}
return z.Where[c];
}
}
// 2. Готовое.
bool blocked = false;
foreach (TileEntityCollector c in z.Collectors)
{
ItemStack[] items = c.Items;
if (Unload(items, z, ref blocked))
{
if (!_dry)
{
c.Items = items;
c.SetModified();
}
return z.Where[c];
}
}
foreach (TileEntityWorkstation w in z.Stations)
{
ItemStack[] output = w.Output;
if (Unload(output, z, ref blocked))
{
if (!_dry)
{
w.Output = output;
}
return z.Where[w];
}
}
// 3. Грядки.
foreach (Vector3i p in z.Ripe)
{
if (Harvest(_world, p, z, ref blocked))
{
return p;
}
}
if (z.Empty.Count > 0)
{
Vector3i? planted = PlantRandom(_world, z);
if (planted.HasValue)
{
return planted;
}
}
_stuck = blocked;
return null;
}
/// <summary>Перевозка по нитям: одна стопка из ящика «откуда» в ящик «куда». Только предметы включённых в урне групп
/// (фильтр логиста, CinerariumGroups; 16.10 заменил прежнее правило «по образцу»); «куда» полон - ждём (дух не чернеет). Открытые у игрока ящики и закреплённые
/// ячейки не трогаются. Возвращает ящик «куда», LastFrom - откуда взято.</summary>
static Vector3i? Ferry(World _world, TEFeatureCinerarium _urn)
{
if (!_urn.HasThreadTo)
{
return null;
}
TEFeatureStorage dst = BoxAt(_world, _urn.ThreadTo);
if (dst == null)
{
return null;
}
foreach (Vector3i f in _urn.ThreadFrom)
{
if (f == _urn.ThreadTo)
{
continue;
}
TEFeatureStorage src = BoxAt(_world, f);
if (src == null)
{
continue;
}
for (int k = 0; k < src.ItemGrid.Length; k++)
{
ItemStack s = src.ItemGrid[k];
if (s == null || s.IsEmpty() || Locked(src, k) || !_urn.GroupOn(CinerariumGroups.Of(s.itemValue)) || !Fits(dst, s))
{
continue;
}
LastFrom = f;
if (_dry)
{
return _urn.ThreadTo;
}
ItemStack moving = s.Clone();
Place(moving, new[] { dst });
int moved = s.count - moving.count;
if (moved <= 0)
{
continue;
}
Take(src, k, moved);
dst.SetModified();
Debug.Log("[NecromancerTome] Cinerarium: дух перенёс " + moved + " " + s.itemValue.ItemClass?.GetItemName() + " из " + f + " в " + _urn.ThreadTo);
return _urn.ThreadTo;
}
}
return null;
}
static TEFeatureStorage BoxAt(World _world, Vector3i _p)
{
return _world.GetTileEntity(_p) is TileEntityComposite c && !c.IsUserAccessing() &&
!c.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium _) &&
c.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage s) ? s : null;
}
static bool IsRipe(Block _b)
{
return _b != null && !(_b is BlockPlantGrowing) && _b.HasAnyFastTags(CropTag) &&
_b.itemsToDrop.TryGetValue(EnumDropEvent.Harvest, out List<Block.SItemDropProb> l) && l != null && l.Count > 0;
}
static bool IsSeed(ItemValue _iv, out BlockValue _bv)
{
_bv = BlockValue.Air;
if (_iv == null || _iv.type == 0 || !_iv.ItemClass.IsBlock())
{
return false;
}
_bv = _iv.ToBlockValue();
return _bv.Block is BlockPlantGrowing;
}
/// <summary>Урожай: все доли добычи Harvest (бонусы - тоже, вероятность только у обычных долей), множители сервера на
/// урожай и семена. Одно семя - сразу в землю на то же место, остальное - в ящики/сумку. Нет места ни для чего -
/// грядку не трогаем, дух чернеет.</summary>
static bool Harvest(World _world, Vector3i _p, Zone _z, ref bool _blocked)
{
Block crop = _world.GetBlock(_p).Block;
List<ItemStack> got = new List<ItemStack>();
BlockValue replant = BlockValue.Air;
foreach (Block.SItemDropProb d in crop.itemsToDrop[EnumDropEvent.Harvest])
{
ItemValue iv = ItemClass.GetItem(d.name);
if (iv == null || iv.type == 0)
{
continue;
}
bool seed = IsSeed(iv, out BlockValue seedBv);
bool bonus = d.tag != null && (d.tag.Contains("Bonus") || d.tag.Contains("bonus"));
if (!bonus && d.prob < 1f && _rnd.RandomFloat > d.prob)
{
continue;
}
int n = _rnd.RandomRange(d.minCount, d.maxCount + 1);
n = Mathf.RoundToInt(n * (seed ? XUiM_Recipes.SeedDropOutputModifier : XUiM_Recipes.CropOutputModifier));
if (seed && replant.isair && n > 0)
{
replant = seedBv;
n--;
}
if (n > 0)
{
got.Add(new ItemStack(iv, n));
}
}
if (got.Count > 0 && !HasRoom(got[0], _z))
{
_blocked = true;
return false;
}
if (_dry)
{
return true;
}
_world.SetBlockRPC(_p, BlockValue.Air);
if (!replant.isair)
{
replant.rotation = (byte)_rnd.RandomRange(0, 4);
_world.SetBlockRPC(_p, replant);
}
foreach (ItemStack s in got)
{
Place(s, _z.Boxes);
if (s.count > 0 && _z.Bag != null)
{
Place(s, new[] { _z.Bag });
}
if (s.count > 0)
{
// Влезло не всё (проверяли только первую долю) - остаток падает у урны, не пропадает.
GameManager.Instance.ItemDropServer(s.Clone(), World.blockToTransformPos(_p) + Vector3.up * 0.5f, Vector3.zero);
}
}
Debug.Log("[NecromancerTome] Cinerarium: дух собрал " + crop.GetBlockName() + " в " + _p + (replant.isair ? "" : ", посадил снова"));
return true;
}
static bool HasRoom(ItemStack _s, Zone _z)
{
foreach (TEFeatureStorage t in _z.Boxes)
{
if (Fits(t, _s))
{
return true;
}
}
return _z.Bag != null && Fits(_z.Bag, _s);
}
static bool Fits(TEFeatureStorage _t, ItemStack _s)
{
for (int i = 0; i < _t.ItemGrid.Length; i++)
{
ItemStack x = _t.ItemGrid[i];
if (x == null || x.IsEmpty())
{
return true;
}
int n = _s.count;
if (x.itemValue.type == _s.itemValue.type && x.CanStackPartly(ref n))
{
return true;
}
}
return false;
}
/// <summary>Засадить одну пустую грядку случайным семенем из сумки и ящиков (06.10: «случайно сажает имеющиеся семена»).</summary>
static Vector3i? PlantRandom(World _world, Zone _z)
{
List<(TEFeatureStorage src, int slot, BlockValue bv)> seeds = new List<(TEFeatureStorage, int, BlockValue)>();
foreach (TEFeatureStorage src in Sources(_z))
{
for (int k = 0; k < src.ItemGrid.Length; k++)
{
ItemStack st = src.ItemGrid[k];
if (st != null && !st.IsEmpty() && !Locked(src, k) && IsSeed(st.itemValue, out BlockValue bv))
{
seeds.Add((src, k, bv));
}
}
}
if (seeds.Count == 0)
{
return null;
}
foreach (Vector3i p in _z.Empty)
{
var pick = seeds[_rnd.RandomRange(0, seeds.Count)];
BlockValue bv = pick.bv;
bv.rotation = (byte)_rnd.RandomRange(0, 4);
if (!bv.Block.CanPlaceBlockAt(_world, p, bv))
{
continue;
}
if (_dry)
{
return p;
}
Take(pick.src, pick.slot, 1);
_world.SetBlockRPC(p, bv);
Debug.Log("[NecromancerTome] Cinerarium: дух посадил " + bv.Block.GetBlockName() + " в " + p);
return p;
}
return null;
}
static Zone Scan(World _world, TEFeatureCinerarium _urn)
{
Zone z = new Zone();
_seen.Clear();
Vector3i c = _urn.ToWorldPos();
if (_urn.Parent != null)
{
_urn.Parent.TryGetSelfOrFeature<TEFeatureStorage>(out z.Bag);
_seen.Add(_urn.Parent);
}
int w = TEFeatureCinerarium.HalfWidth;
for (int dy = -TEFeatureCinerarium.Below; dy <= TEFeatureCinerarium.Above; dy++)
{
for (int dx = -w; dx <= w; dx++)
{
for (int dz = -w; dz <= w; dz++)
{
Vector3i p = new Vector3i(c.x + dx, c.y + dy, c.z + dz);
BlockValue bv = _world.GetBlock(p);
if (bv.isair)
{
// Пустая клетка над плодородным блоком - кандидат в грядку (окончательно решит блок семени).
if (_world.GetBlock(p - Vector3i.up).Block?.blockMaterial?.FertileLevel > 0)
{
z.Empty.Add(p);
}
continue;
}
if (!bv.ischild && IsRipe(bv.Block))
{
z.Ripe.Add(p);
continue;
}
Vector3i owner = bv.ischild ? p + bv.parent : p; // часть составного блока (верстак 2x1) - к главной клетке
TileEntity te = _world.GetTileEntity(owner);
if (te == null || !_seen.Add(te) || te.IsUserAccessing())
{
continue;
}
if (te is TileEntityCollector col)
{
z.Collectors.Add(col);
z.Where[col] = owner;
}
else if (te is TileEntityWorkstation ws)
{
z.Stations.Add(ws);
z.Where[ws] = owner;
}
else if (te is TileEntityComposite comp && comp.PlayerPlaced &&
!comp.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium _) &&
comp.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage box))
{
z.Boxes.Add(box);
z.Where[comp] = owner;
}
}
}
}
return z;
}
/// <summary>Дополнить ячейки коллектора тем, что он принимает, из ящиков и сумки. _single - по одному предмету в
/// пустую ячейку (куры), иначе до полного стака. true - что-то положено.</summary>
static bool Refill(TileEntityCollector _c, ItemStack[] _slots, string[] _allowed, bool _single, Zone _z)
{
if (_slots == null || _allowed == null || _allowed.Length == 0)
{
return false;
}
for (int i = 0; i < _slots.Length; i++)
{
ItemStack slot = _slots[i];
ItemClass want = null;
int room;
if (slot == null || slot.IsEmpty())
{
room = -1; // любой допустимый
}
else
{
if (_single)
{
continue;
}
want = slot.itemValue.ItemClass;
room = want != null ? want.MaxCount - slot.count : 0;
if (room <= 0)
{
continue;
}
}
foreach (TEFeatureStorage src in Sources(_z))
{
for (int k = 0; k < src.ItemGrid.Length; k++)
{
ItemStack s = src.ItemGrid[k];
if (s == null || s.IsEmpty() || Locked(src, k))
{
continue;
}
ItemClass ic = s.itemValue.ItemClass;
if (ic == null || (want != null ? ic != want : System.Array.IndexOf(_allowed, ic.GetItemName()) < 0))
{
continue;
}
int take = _single ? 1 : (room < 0 ? Mathf.Min(s.count, ic.MaxCount) : Mathf.Min(s.count, room));
if (take <= 0)
{
continue;
}
if (_dry)
{
return true;
}
if (want == null)
{
_slots[i] = new ItemStack(s.itemValue.Clone(), take);
}
else
{
_slots[i].count += take;
}
Take(src, k, take);
return true;
}
}
}
return false;
}
/// <summary>Одна стопка готового - в ящики (к таким же, потом в пустое), затем в сумку урны. true - что-то перенесено;
/// _blocked - была стопка, которой не нашлось места.</summary>
static bool Unload(ItemStack[] _items, Zone _z, ref bool _blocked)
{
if (_items == null)
{
return false;
}
for (int i = 0; i < _items.Length; i++)
{
ItemStack s = _items[i];
if (s == null || s.IsEmpty())
{
continue;
}
if (_dry)
{
if (HasRoom(s, _z))
{
return true;
}
_blocked = true;
continue;
}
int before = s.count;
// Сначала ящики зоны (к таким же, потом в пустое), они полны - тогда сумка урны (решение 06.10).
Place(s, _z.Boxes);
if (s.count > 0 && _z.Bag != null)
{
Place(s, new[] { _z.Bag });
}
if (s.count == 0)
{
_items[i] = ItemStack.Empty.Clone();
return true;
}
if (s.count != before)
{
return true;
}
_blocked = true;
}
return false;
}
static void Place(ItemStack _s, IList<TEFeatureStorage> _dst)
{
for (int i = 0; i < _dst.Count && _s.count > 0; i++)
{
_dst[i].TryStackItem(0, _s);
}
for (int i = 0; i < _dst.Count && _s.count > 0; i++)
{
if (_dst[i].AddItem(_s.Clone()))
{
_s.count = 0;
}
}
}
/// <summary>Откуда брать расходники: сумка урны (там то, что дух накопил), потом ящики зоны.</summary>
static IEnumerable<TEFeatureStorage> Sources(Zone _z)
{
if (_z.Bag != null)
{
yield return _z.Bag;
}
foreach (TEFeatureStorage b in _z.Boxes)
{
yield return b;
}
}
/// <summary>Ячейка, закреплённая игроком (замок в окне ящика), - не трогать.</summary>
static bool Locked(TEFeatureStorage _src, int _slot)
{
PackedBoolArray locks = _src.ItemGrid.SlotLocks;
return locks != null && _slot < locks.Length && locks[_slot];
}
static void Take(TEFeatureStorage _src, int _slot, int _count)
{
ItemStack s = _src.ItemGrid[_slot].Clone();
s.count -= _count;
_src.UpdateSlot(_slot, s.count > 0 ? s : ItemStack.Empty.Clone());
_src.SetModified();
}
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ namespace NecromancerTome
static bool Ours(string _name)
{
return _name == "necroBookMindPrefab" || _name == "necroKnifeDecorPrefab" || _name == "necroMortarPrefab" || _name.StartsWith("necroBloodRose") || _name.StartsWith("necroPoster") || _name == "necroBookDoorPrefab" || _name.StartsWith("necroSkullDecor");
return _name == "necroBookMindPrefab" || _name == "necroKnifeDecorPrefab" || _name == "necroMortarPrefab" || _name.StartsWith("necroBloodRose") || _name.StartsWith("necroPoster") || _name == "necroBookDoorPrefab" || _name.StartsWith("necroSkullDecor") || _name == "necroCinerariumPrefab";
}
public static void Postfix(Transform __result)
+1 -3
View File
@@ -319,9 +319,7 @@ namespace NecromancerTome
{
if (_entity is EntityTrader)
{
// Только торговцы самой игры (класс EntityTrader). 16.10 (Nexus, Crozekanov): NPC мода NPCCore построены на
// наследнике EntityTrader и тоже становились призраками - «практически невидимыми».
return _entity.GetType() == typeof(EntityTrader);
return true;
}
EntityClass entityClass = EntityClass.list[_entity.entityClass];
if (entityClass == null || entityClass.entityClassName == null)
+22 -1
View File
@@ -23,7 +23,10 @@ namespace NecromancerTome
public static class HandHolds
{
public const string HandItem = "meleeHandPlayer";
public const int None = 0, Deny = 1, PifPaf = 2;
public const int None = 0, Deny = 1, PifPaf = 2, Thread = 3;
/// <summary>Хват для нитей - не сразу: снимок замороженных костей должен быть в покое хвата 68, после доставания.</summary>
const float ThreadDelay = 0.6f;
static float _threadSince = -1f;
/// <summary>Дальность и конус выбора зомби для «отрицания».</summary>
public const float DenyRange = 25f;
@@ -59,6 +62,19 @@ namespace NecromancerTome
public static int Current(EntityPlayerLocal _player)
{
// Только от первого лица (16.10: «В виде от третьего лица что-то страшное»): поза и перенос ножа - для рук первого
// лица; от третьего лица модель в руке - у тела, и нож улетал к невидимой руке первого лица.
if (_player != null && _player.inventory?.holdingItem?.Name == CinerariumThreads.ThreadKnifeName &&
(_player.emodel as EModelSDCS)?.IsFPV == true && !SnakeMode.Active && !MindPalace.Active && !Possession.Active && !_player.IsDead())
{
if (_threadSince < 0f)
{
_threadSince = Time.time;
}
bool ready = Time.time - _threadSince >= ThreadDelay && !(_player.Hand != null && _player.Hand.isSwitching);
return ready ? Thread : None;
}
_threadSince = -1f;
if (_player == null || _player.Buffs == null || !IsHand(_player.inventory?.holdingItem) ||
SnakeMode.Active || MindPalace.Active || Possession.Active || _player.IsDead())
{
@@ -104,9 +120,14 @@ namespace NecromancerTome
{
HandPose.PresetPifPaf();
}
else if (want == Thread)
{
HandPose.PresetThread();
}
else
{
HandPose.Enabled = false;
HandPose.KnifeToLeft = false;
}
}
+127 -1
View File
@@ -76,6 +76,8 @@ namespace NecromancerTome
/// положение ВСЕХ костей под Hips, и каждый кадр они возвращаются на место, а поверх - поза.
/// Снимок только в покое аниматора (ItemActionIndex = -1), иначе застыл бы кадр удара.</summary>
public static bool Freeze = true;
/// <summary>Замереть без своего хвата - на время осмотра со стороны (NecroView).</summary>
public static bool HoldStill;
static readonly List<Transform> _frozen = new List<Transform>();
static readonly List<Vector3> _frozenPos = new List<Vector3>();
static readonly List<Quaternion> _frozenRot = new List<Quaternion>();
@@ -86,6 +88,8 @@ namespace NecromancerTome
public static void ResetAll()
{
Enabled = false;
KnifeToLeft = false;
NecroView.Stop();
Overrides.Clear();
CamOverrides.Clear();
_jabStart = -1f;
@@ -142,10 +146,18 @@ namespace NecromancerTome
{
if (!Enabled || Overrides.Count == 0 && CamOverrides.Count == 0)
{
if (_frozen.Count > 0)
if (HoldStill)
{
FreezeBones(); // осмотр со стороны (necroview): поза замирает и без своего хвата
}
else if (_frozen.Count > 0)
{
Unfreeze();
}
if (KnifeToLeft && Enabled)
{
ApplyKnifeToLeft();
}
return;
}
if (Freeze)
@@ -160,6 +172,10 @@ namespace NecromancerTome
b.localRotation = Quaternion.Euler(kv.Value);
}
}
if (KnifeToLeft)
{
ApplyKnifeToLeft();
}
Transform cam = CamOverrides.Count > 0 || _jabStart >= 0f ? Camera() : null;
ApplyJab(cam);
if (cam != null)
@@ -265,6 +281,7 @@ namespace NecromancerTome
/// (сгиб - как у поджатых пальцев «пиф-пафа»), указательный и мизинец прямые.</summary>
public static void PresetDeny()
{
KnifeToLeft = false;
Overrides.Clear();
CamOverrides.Clear();
Overrides["RightArm"] = new Vector3(345.8f, 34.9f, 24.3f);
@@ -291,6 +308,7 @@ namespace NecromancerTome
/// сомкнуты и смотрят к прицелу, безымянный и мизинец поджаты, большой вверх. Кисть - от камеры.</summary>
public static void PresetPifPaf()
{
KnifeToLeft = false;
Overrides.Clear();
CamOverrides.Clear();
Overrides["RightArm"] = new Vector3(330.8f, 34.9f, 24.3f);
@@ -313,6 +331,111 @@ namespace NecromancerTome
Enabled = true;
}
/// <summary>Нож в левой кисти (хват для нитей): корень модели в руке ставится каждый кадр от LeftHand. Смещение -
/// снимок хвата 68 (деревянный лук) 16.10, где нож лежал в левой руке («Нож в левой руке»).</summary>
public static bool KnifeToLeft;
static readonly Vector3 KnifeLeftPos = new Vector3(-0.0832f, -0.0354f, 0.0186f);
static readonly Quaternion KnifeLeftRot = new Quaternion(0.56471f, -0.62192f, -0.41297f, 0.35182f);
static void ApplyKnifeToLeft()
{
Transform lh = Bone("LeftHand");
Transform m = HeldTweak.Model();
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
if (lh != null && m != null && (pl?.emodel as EModelSDCS)?.IsFPV == true)
{
m.SetPositionAndRotation(lh.TransformPoint(KnifeLeftPos), lh.rotation * KnifeLeftRot);
}
}
/// <summary>Хват для нитей (16.10, подбор с пользователем: «Нож в левую руку, а правую сделай как в том хвате, где
/// ты говорил что она перебирает нити»): основа - HoldType 68 (деревянный лук, items.xml); левая рука поднята и
/// отведена влево, нож в ней; правая - раскрытая ладонь хвата 64 (бинт), пересчитанная под плечо хвата 68 так,
/// чтобы кисть стояла там же относительно камеры (scratchpad ik.py).</summary>
public static void PresetThread()
{
Overrides.Clear();
CamOverrides.Clear();
Overrides["RightShoulder"] = new Vector3(0.0f, 180.0f, 180.0f);
Overrides["Right_sup_shoulder_01"] = new Vector3(0.0f, 0.0f, 0.0f);
Overrides["Right_sup_shoulder_02"] = new Vector3(0.0f, 0.0f, 0.0f);
Overrides["Right_sup_trap_01"] = new Vector3(0.0f, 0.0f, 0.0f);
Overrides["RightArm"] = new Vector3(311.4f, 24.9f, 39.2f);
Overrides["Right_sup_arm_01"] = new Vector3(0.0f, 0.0f, 358.2f);
Overrides["RightArmRoll"] = new Vector3(357.9f, 0.0f, 0.0f);
Overrides["RightForeArm"] = new Vector3(0.0f, 79.9f, 0.0f);
Overrides["Right_sup_elbow_00"] = new Vector3(0.0f, 332.8f, 0.0f);
Overrides["RightForeArmRoll"] = new Vector3(8.7f, 0.0f, 0.0f);
Overrides["RightHand"] = new Vector3(7.2f, 346.5f, 5.4f);
Overrides["RightHandIndex0"] = new Vector3(0.0f, 0.0f, 0.0f);
Overrides["Right_sup_index_00"] = new Vector3(0.0f, 0.0f, 17.3f);
Overrides["Right_sup_index_00_palm"] = new Vector3(0.0f, 0.0f, 15.6f);
Overrides["RightHandIndex1"] = new Vector3(2.6f, 2.9f, 31.3f);
Overrides["Right_sup_index_01_under"] = new Vector3(0.0f, 0.0f, 3.5f);
Overrides["RightHandIndex2"] = new Vector3(0.0f, 0.0f, 52.8f);
Overrides["Right_sup_index_02"] = new Vector3(0.0f, 0.0f, 333.6f);
Overrides["Right_sup_index_02_under"] = new Vector3(0.0f, 0.0f, 5.1f);
Overrides["RightHandIndex3"] = new Vector3(0.0f, 0.0f, 77.0f);
Overrides["Right_sup_index_03"] = new Vector3(0.0f, 0.0f, 321.5f);
Overrides["RightHandIndex4"] = new Vector3(0.0f, 0.0f, 0.0f);
Overrides["RightHandMiddle0"] = new Vector3(0.0f, 0.0f, 0.0f);
Overrides["Right_sup_middle_00"] = new Vector3(0.0f, 0.0f, 32.1f);
Overrides["Right_sup_middle_00_palm"] = new Vector3(0.0f, 0.0f, 28.8f);
Overrides["RightHandMiddle1"] = new Vector3(0.0f, 0.0f, 57.7f);
Overrides["Right_sup_middle_01_under"] = new Vector3(0.0f, 0.0f, 5.7f);
Overrides["RightHandMiddle2"] = new Vector3(0.0f, 0.0f, 85.0f);
Overrides["Right_sup_middle_02"] = new Vector3(0.0f, 0.0f, 317.5f);
Overrides["Right_sup_middle_02_under"] = new Vector3(0.0f, 0.0f, 5.1f);
Overrides["RightHandMiddle3"] = new Vector3(0.0f, 0.0f, 77.0f);
Overrides["Right_sup_middle_03"] = new Vector3(0.0f, 0.0f, 321.5f);
Overrides["RightHandMiddle4"] = new Vector3(0.0f, 0.0f, 0.0f);
Overrides["RightHandPinky0"] = new Vector3(0.0f, 0.0f, 0.0f);
Overrides["Right_sup_pinky_00"] = new Vector3(0.0f, 0.0f, 45.0f);
Overrides["Right_sup_pinky_00_palm"] = new Vector3(0.0f, 0.0f, 45.0f);
Overrides["RightHandPinky1"] = new Vector3(0.2f, 353.1f, 92.0f);
Overrides["Right_sup_pinky_01_under"] = new Vector3(0.0f, 0.0f, 5.6f);
Overrides["RightHandPinky2"] = new Vector3(0.0f, 0.0f, 84.0f);
Overrides["Right_sup_pinky_02"] = new Vector3(0.0f, 0.0f, 318.0f);
Overrides["Right_sup_pinky_02_under"] = new Vector3(0.0f, 0.0f, 5.2f);
Overrides["RightHandPinky3"] = new Vector3(0.0f, 0.0f, 78.0f);
Overrides["Right_sup_pinky_03"] = new Vector3(0.0f, 0.0f, 321.0f);
Overrides["RightHandPinky4"] = new Vector3(0.0f, 0.0f, 0.0f);
Overrides["RightHandRing0"] = new Vector3(0.0f, 0.0f, 0.0f);
Overrides["Right_sup_ring_00"] = new Vector3(0.0f, 0.0f, 33.0f);
Overrides["Right_sup_ring_00_palm"] = new Vector3(0.0f, 0.0f, 33.0f);
Overrides["RightHandRing1"] = new Vector3(358.6f, 355.2f, 66.1f);
Overrides["Right_sup_ring_01_under"] = new Vector3(0.0f, 0.0f, 5.5f);
Overrides["RightHandRing2"] = new Vector3(0.0f, 0.0f, 83.0f);
Overrides["Right_sup_ring_02"] = new Vector3(0.0f, 0.0f, 318.5f);
Overrides["Right_sup_ring_02_under"] = new Vector3(0.0f, 0.0f, 5.1f);
Overrides["RightHandRing3"] = new Vector3(0.0f, 0.0f, 77.0f);
Overrides["Right_sup_ring_03"] = new Vector3(0.0f, 0.0f, 321.5f);
Overrides["RightHandRing4"] = new Vector3(0.0f, 0.0f, 0.0f);
Overrides["RightHandThumb1"] = new Vector3(27.9f, 75.4f, 33.2f);
Overrides["RightHandThumb2"] = new Vector3(0.0f, 0.0f, 15.8f);
Overrides["Right_sup_thumb_01"] = new Vector3(0.0f, 0.0f, 352.1f);
Overrides["Right_sup_thumb_01_under"] = new Vector3(0.0f, 0.0f, 1.2f);
Overrides["RightHandThumb3"] = new Vector3(0.0f, 0.0f, 17.9f);
Overrides["Right_sup_thumb_02"] = new Vector3(0.0f, 0.0f, 351.0f);
Overrides["RightHandThumb4"] = new Vector3(0.0f, 0.0f, 0.0f);
Overrides["necroKnifePrefab(Clone)"] = new Vector3(0.0f, 0.0f, 0.0f);
Overrides["boneShiv_LOD0"] = new Vector3(0.0f, 0.0f, 0.0f);
Overrides["boneShiv_LOD1"] = new Vector3(0.0f, 0.0f, 0.0f);
Overrides["boneShiv_LOD2"] = new Vector3(0.0f, 0.0f, 0.0f);
Overrides["RightForeArmRoll1"] = new Vector3(3.5f, 0.0f, 0.0f);
Overrides["RightForeArmRoll2"] = new Vector3(7.0f, 0.0f, 0.0f);
Overrides["RightForeArmRoll3"] = new Vector3(10.5f, 0.0f, 0.0f);
Overrides["RightForeArmRoll4"] = new Vector3(14.0f, 0.0f, 0.0f);
Overrides["Right_sup_wrist_00"] = new Vector3(0.0f, 0.0f, 357.5f);
Overrides["Right_sup_wrist_01"] = new Vector3(0.0f, 347.6f, 0.0f);
Overrides["Right_sup_wrist_02"] = new Vector3(0.0f, 0.0f, 356.8f);
Overrides["Right_sup_wrist_03"] = new Vector3(0.0f, 347.6f, 0.0f);
Overrides["LeftArm"] = new Vector3(329.6f, 42.5f, 8.5f);
KnifeToLeft = true;
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);
}
@@ -324,6 +447,9 @@ namespace NecromancerTome
HandHolds.Update();
HandPose.Apply();
HeldTweak.Apply();
NecroView.Update();
NecroBody.LateUpdate();
ThreadKnifeBody.LateUpdate();
}
}
+37 -2
View File
@@ -72,19 +72,44 @@ namespace NecromancerTome
}
}
/// <summary>Сменить хват (HoldType) предмета в руке и пересобрать его в руке, как BloodTide (16.10: «давай попробуем в
/// режиме нитей разные хваты. Ты будешь перебирать их по моей просьбе»). Меняется класс предмета - до перезапуска
/// игры или следующей команды; итог переносится в items.xml.</summary>
public static string SetHold(int _hold)
{
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
Inventory inv = pl?.inventory;
ItemInventoryData held = inv?.holdingItemData;
if (held?.item == null)
{
return "в руке ничего";
}
if (pl.Hand != null && pl.Hand.isSwitching || inv.IsHoldingItemActionRunning())
{
return "рука занята - повторите";
}
int was = held.item.HoldType.Value;
held.item.HoldType.Value = _hold;
ItemValue pv = (held.physicalItemValue ?? held.itemValue).Clone();
pv.Seed = (ushort)(pv.Seed + 1); // «собранный» предмет устарел - соберётся заново (NeedsRebuild)
held.physicalItemValue = pv;
inv.SetHoldingItemIdx(inv.holdingItemIdx);
return held.item.Name + ": хват " + was + " -> " + _hold;
}
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);
}
/// <summary>
/// necroheld - посадка модели в руке:
/// pos x y z (см) | add x y z (см) | rot x y z (градусы) | scale s | off | show
/// pos x y z (см) | add x y z (см) | rot x y z (градусы) | scale s | hold N (хват, HoldType) | off | show
/// </summary>
[Preserve]
public class ConsoleCmdNecroHeld : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necroheld" };
public override string getDescription() => "NecromancerTome: посадка модели в руке - pos x y z (см)|add x y z (см)|rot x y z|scale s|off|show";
public override string getDescription() => "NecromancerTome: посадка модели в руке - pos x y z (см)|add x y z (см)|rot x y z|scale s|hold N|off|show";
static bool Num(List<string> _p, int _i, out float _v)
{
@@ -117,6 +142,16 @@ namespace NecromancerTome
case "scale" when Num(_params, 1, out float s) && s > 0f:
HeldTweak.Scale = s;
break;
case "tphold" when Num(_params, 1, out float th):
ThreadKnifeBody.TpHold = (int)th;
Out("хват тела для ножа-для-нитей: " + ThreadKnifeBody.TpHold + (ThreadKnifeBody.TpHold > 0 ? "" : " (выкл - как у рук)") + "; место в руке - при следующем доставании");
return;
case "hold" when Num(_params, 1, out float h):
Out(HeldTweak.SetHold((int)h));
return;
case "hold":
Out("хват предмета в руке: " + (GameManager.Instance?.World?.GetPrimaryPlayer()?.inventory?.holdingItem?.HoldType.Value.ToString() ?? "нет"));
return;
case "off":
HeldTweak.Enabled = false;
HeldTweak.Pos = Vector3.zero;
+694
View File
@@ -0,0 +1,694 @@
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using HarmonyLib;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Правка аниматора ТЕЛА игрока (вид от третьего лица, его же видят другие) прямо в игре (16.10: «Добавь все необходимые
/// команды для аниматора тела, чтобы можно было править прямо в игре»). Пара к necropose/necrobones, которые - для рук
/// первого лица. Всё держится до necrobody off или выхода из мира; итог переносится в код (necrobody save).
/// Параметры ставятся после Update аватара (иначе игра перезапишет их до расчёта аниматора), кости - в LateUpdate.
/// </summary>
public static class NecroBody
{
/// <summary>Параметры аниматора тела, которые держим своими (имя -> значение; bool - 0/1).</summary>
public static readonly Dictionary<string, float> Params = new Dictionary<string, float>();
/// <summary>Локальные повороты костей тела поверх анимации.</summary>
public static readonly Dictionary<string, Vector3> Pose = new Dictionary<string, Vector3>();
public static bool Freeze;
public static float Speed = 1f;
/// <summary>Место предмета в руке тела (поверх смещения игры по хвату), если задано.</summary>
public static Vector3? ItemPos, ItemRot;
static Animator _anim;
static readonly Dictionary<string, Transform> _bones = new Dictionary<string, Transform>();
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 int _playerId = -1;
/// <summary>Любая загруженная анимация на теле (16.10: «Может и танцевать сможешь заставить?.. сильно бы помогло в
/// создании контента»): клип играется поверх контроллера через Playables, по кругу. Танцы есть у зомби (twitch_dance) -
/// их клипы в памяти, пока в мире есть зомби с таким аниматором.</summary>
static PlayableGraph _graph;
static AnimationClipPlayable _clip;
public static string Playing;
public static List<AnimationClip> FindClips(string _filter)
{
var list = new List<AnimationClip>();
var seen = new HashSet<string>();
foreach (AnimationClip c in Resources.FindObjectsOfTypeAll<AnimationClip>())
{
if (c != null && (_filter.Length == 0 || c.name.IndexOf(_filter, System.StringComparison.OrdinalIgnoreCase) >= 0) && seen.Add(c.name))
{
list.Add(c);
}
}
list.Sort((x, y) => string.CompareOrdinal(x.name, y.name));
return list;
}
public static string Play(string _name, float _speed)
{
Animator a = Anim();
if (a == null)
{
return "нет аниматора тела";
}
AnimationClip clip = null;
foreach (AnimationClip c in FindClips(_name))
{
if (c.name == _name)
{
clip = c;
break;
}
clip = clip ?? c;
}
if (clip == null)
{
return "нет загруженной анимации «" + _name + "» (necrobody anims слово)";
}
StopPlay();
_graph = PlayableGraph.Create("NecroBodyPlay");
_graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
var output = AnimationPlayableOutput.Create(_graph, "body", a);
_clip = AnimationClipPlayable.Create(_graph, clip);
_clip.SetSpeed(_speed);
output.SetSourcePlayable(_clip);
_graph.Play();
Playing = clip.name;
return "играет " + clip.name + " (" + NecroBody.F(clip.length) + " с" + (clip.isLooping ? ", петля" : "") + (clip.humanMotion ? ", гуманоид" : ", не гуманоид - может лечь криво") + "), скорость " + NecroBody.F(_speed);
}
/// <summary>Тело повторяет движения другого существа (16.10: танец зомби не лёг на тело игрока через Playables - клип
/// гуманоидный, а тело игрока - нет). Каждый кадр локальные повороты костей источника копируются на кости тела с теми же
/// именами (Hips, Spine, LeftArm...); у бёдер - ещё и сдвиг. Источник - зомби, которого necrobody zdance заставляет танцевать.</summary>
static EntityAlive _mimic;
static readonly List<Transform> _mimicSrc = new List<Transform>();
static readonly List<Transform> _mimicDst = new List<Transform>();
public static EntityAlive Nearest(float _radius, bool _zombiesOnly)
{
EntityPlayerLocal pl = Player();
if (pl == null)
{
return null;
}
EntityAlive best = null;
float bestD = _radius * _radius;
foreach (Entity e in GameManager.Instance.World.Entities.list)
{
if (e is EntityAlive a && a != pl && !a.IsDead() && (!_zombiesOnly || a is EntityZombie))
{
float d = (a.position - pl.position).sqrMagnitude;
if (d < bestD)
{
bestD = d;
best = a;
}
}
}
return best;
}
static Transform ModelRoot(EntityAlive _e)
{
if (_e == null || _e.emodel == null)
{
return null;
}
if (_e.emodel.avatarController is AvatarCharacterController ac && ac.CharacterBody?.Animator != null)
{
return ac.CharacterBody.Animator.transform;
}
return _e.emodel.GetModelTransform();
}
public static string Mimic(EntityAlive _src)
{
StopMimic();
Animator a = Anim();
Transform src = ModelRoot(_src);
if (a == null || src == null)
{
return "нет тела игрока или источника";
}
var byName = new Dictionary<string, Transform>();
foreach (Transform t in src.GetComponentsInChildren<Transform>(true))
{
if (!byName.ContainsKey(t.name))
{
byName[t.name] = t;
}
}
foreach (Transform t in a.GetComponentsInChildren<Transform>(true))
{
if (t != a.transform && byName.TryGetValue(t.name, out Transform s2) && s2 != src)
{
_mimicDst.Add(t);
_mimicSrc.Add(s2);
}
}
_mimic = _src;
return "тело повторяет за " + _src.EntityName + " (id " + _src.entityId + "): общих костей " + _mimicDst.Count;
}
public static void StopMimic()
{
_mimic = null;
_mimicSrc.Clear();
_mimicDst.Clear();
}
static void ApplyMimic()
{
if (_mimic == null)
{
return;
}
if (_mimic.IsDead() || _mimicSrc.Count == 0 || _mimicSrc[0] == null)
{
StopMimic();
return;
}
for (int i = 0; i < _mimicDst.Count; i++)
{
Transform s = _mimicSrc[i], d = _mimicDst[i];
if (s == null || d == null)
{
continue;
}
d.localRotation = s.localRotation;
if (d.name == "Hips")
{
d.localPosition = s.localPosition;
}
}
}
public static void StopPlay()
{
if (_graph.IsValid())
{
_graph.Destroy();
}
Playing = null;
}
/// <summary>Петля для клипов без своей петли.</summary>
static void LoopPlay()
{
if (!_graph.IsValid())
{
return;
}
AnimationClip c = _clip.GetAnimationClip();
if (c != null && !c.isLooping && _clip.GetTime() >= c.length)
{
_clip.SetTime(0);
}
}
public static EntityPlayerLocal Player() => GameManager.Instance?.World?.GetPrimaryPlayer();
public static BodyAnimator Body()
{
return (Player()?.emodel?.avatarController as AvatarCharacterController)?.CharacterBody;
}
public static Animator Anim() => Body()?.Animator;
public static bool Active => Params.Count > 0 || Pose.Count > 0 || Freeze || Speed != 1f || ItemPos.HasValue || ItemRot.HasValue;
public static Transform Bone(string _name)
{
Animator a = Anim();
if (a == null)
{
return null;
}
if (a != _anim)
{
_anim = a;
_bones.Clear();
_frozen.Clear();
foreach (Transform t in a.GetComponentsInChildren<Transform>(true))
{
if (!_bones.ContainsKey(t.name))
{
_bones[t.name] = t;
}
}
}
return _bones.TryGetValue(_name, out Transform b) ? b : null;
}
public static void Reset()
{
StopPlay();
StopMimic();
Params.Clear();
Pose.Clear();
Freeze = false;
_frozen.Clear();
ItemPos = null;
ItemRot = null;
if (Speed != 1f)
{
Speed = 1f;
Animator a = Anim();
if (a != null)
{
a.speed = 1f;
}
}
}
public static void Unfreeze()
{
_frozen.Clear();
_frozenPos.Clear();
_frozenRot.Clear();
}
/// <summary>Параметр по имени: тип берётся из аниматора. false - нет такого.</summary>
public static bool SetParam(Animator _a, string _name, float _v)
{
foreach (AnimatorControllerParameter p in _a.parameters)
{
if (p.name != _name)
{
continue;
}
switch (p.type)
{
case AnimatorControllerParameterType.Float: _a.SetFloat(p.nameHash, _v); break;
case AnimatorControllerParameterType.Int: _a.SetInteger(p.nameHash, (int)_v); break;
case AnimatorControllerParameterType.Bool: _a.SetBool(p.nameHash, _v != 0f); break;
case AnimatorControllerParameterType.Trigger: _a.SetTrigger(p.nameHash); break;
}
return true;
}
return false;
}
/// <summary>После Update аватара (заплатка ниже) - до расчёта аниматора в этом кадре.</summary>
public static void ApplyParams()
{
if (Params.Count == 0 && Speed == 1f)
{
return;
}
Animator a = Anim();
if (a == null)
{
return;
}
foreach (KeyValuePair<string, float> kv in Params)
{
SetParam(a, kv.Key, kv.Value);
}
a.speed = Speed;
}
/// <summary>Из HandPoseDriver.LateUpdate.</summary>
public static void LateUpdate()
{
EntityPlayerLocal pl = Player();
int id = pl != null ? pl.entityId : -1;
if (id != _playerId)
{
_playerId = id;
Reset();
_anim = null;
_bones.Clear();
return;
}
LoopPlay();
ApplyMimic();
if (!Active || Bone("") == null && _anim == null)
{
return;
}
if (Freeze)
{
if (_frozen.Count == 0 || _frozen[0] == null)
{
Unfreeze();
foreach (Transform t in _anim.GetComponentsInChildren<Transform>(true))
{
_frozen.Add(t);
_frozenPos.Add(t.localPosition);
_frozenRot.Add(t.localRotation);
}
Debug.Log("[NecromancerTome] necrobody: заморожено костей " + _frozen.Count);
}
else
{
for (int i = 0; i < _frozen.Count; i++)
{
if (_frozen[i] != null)
{
_frozen[i].localPosition = _frozenPos[i];
_frozen[i].localRotation = _frozenRot[i];
}
}
}
}
foreach (KeyValuePair<string, Vector3> kv in Pose)
{
Transform b = Bone(kv.Key);
if (b != null)
{
b.localRotation = Quaternion.Euler(kv.Value);
}
}
if ((ItemPos.HasValue || ItemRot.HasValue) && pl.emodel != null && !pl.emodel.IsFPV)
{
Transform item = (pl.emodel.avatarController as AvatarMultiBodyController)?.heldItemTransform;
if (item != null)
{
if (ItemPos.HasValue)
{
item.localPosition = ItemPos.Value;
}
if (ItemRot.HasValue)
{
item.localEulerAngles = ItemRot.Value;
}
}
}
}
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);
}
[HarmonyPatch(typeof(AvatarMultiBodyController), "Update")]
public static class Patch_AvatarMultiBodyController_Update_NecroBody
{
public static void Postfix(AvatarMultiBodyController __instance)
{
if (NecroBody.Params.Count > 0 || NecroBody.Speed != 1f)
{
EntityPlayerLocal pl = NecroBody.Player();
if (pl != null && __instance.Entity == pl)
{
NecroBody.ApplyParams();
}
}
}
}
/// <summary>
/// necrobody - аниматор тела игрока (вид от третьего лица):
/// params [фильтр] - параметры аниматора и значения
/// clips - что играет в каждом слое (клип, вес слоя)
/// set &lt;параметр&gt; &lt;число&gt; - держать значение (bool 0/1); unset &lt;параметр&gt;; trigger &lt;параметр&gt;
/// speed &lt;k&gt; - скорость аниматора (0 - пауза)
/// freeze [off] - замереть в текущем кадре (все кости)
/// bones [фильтр] - кости тела с локальными поворотами в лог
/// show &lt;кость&gt;; pose &lt;кость&gt; x y z; add &lt;кость&gt; dx dy dz; unpose &lt;кость&gt;
/// item pos x y z (см) | rot x y z | off | show - место предмета в руке тела
/// holdoffset [N] - смещение игры для хвата N (по умолчанию - предмета в руке)
/// save - всё заданное строками в лог; off - всё снять
/// </summary>
[Preserve]
public class ConsoleCmdNecroBody : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necrobody" };
public override string getDescription() => "NecromancerTome: аниматор тела - params [ф]|clips|set п v|unset п|trigger п|speed k|freeze [off]|bones [ф]|show к|pose к x y z|add к dx dy dz|unpose к|item pos|rot|off|show|holdoffset [N]|anims [слово]|play имя [скорость]|play off|zdance [off]|mimic [id]|mimic off|save|off";
static bool Num(List<string> _p, int _i, out float _v)
{
_v = 0f;
return _p.Count > _i && float.TryParse(_p[_i].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out _v);
}
static bool Vec(List<string> _p, int _i, out Vector3 _v)
{
_v = Vector3.zero;
if (Num(_p, _i, out float x) && Num(_p, _i + 1, out float y) && Num(_p, _i + 2, out float z))
{
_v = new Vector3(x, y, z);
return true;
}
return false;
}
static void Out(string _s)
{
SdtdConsole.Instance.Output(_s);
foreach (string line in _s.Split('\n'))
{
Debug.Log("[NecromancerTome] necrobody: " + line);
}
}
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{
Animator a = NecroBody.Anim();
if (a == null)
{
Out("нет аниматора тела (игрок не в мире?)");
return;
}
string cmd = _params.Count > 0 ? _params[0] : "";
string arg = _params.Count > 1 ? _params[1] : "";
switch (cmd)
{
case "params":
{
var sb = new StringBuilder("аниматор " + a.name + ", контроллер " + (a.runtimeAnimatorController != null ? a.runtimeAnimatorController.name : "нет"));
foreach (AnimatorControllerParameter p in a.parameters)
{
if (arg.Length > 0 && p.name.IndexOf(arg, System.StringComparison.OrdinalIgnoreCase) < 0)
{
continue;
}
string v = p.type == AnimatorControllerParameterType.Float ? NecroBody.F(a.GetFloat(p.nameHash))
: p.type == AnimatorControllerParameterType.Int ? a.GetInteger(p.nameHash).ToString()
: p.type == AnimatorControllerParameterType.Bool ? (a.GetBool(p.nameHash) ? "1" : "0") : "trigger";
sb.Append("\n ").Append(p.name).Append(" (").Append(p.type).Append(") = ").Append(v)
.Append(NecroBody.Params.ContainsKey(p.name) ? " [держим]" : "");
}
Out(sb.ToString());
return;
}
case "clips":
{
var sb = new StringBuilder("слоёв " + a.layerCount);
for (int l = 0; l < a.layerCount; l++)
{
string names = "";
foreach (AnimatorClipInfo c in a.GetCurrentAnimatorClipInfo(l))
{
names += (names.Length > 0 ? ", " : "") + (c.clip != null ? c.clip.name : "?") + " x" + c.weight.ToString("0.##");
}
sb.Append("\n ").Append(l).Append(' ').Append(a.GetLayerName(l)).Append(" вес ").Append(a.GetLayerWeight(l).ToString("0.##")).Append(": ").Append(names);
}
Out(sb.ToString());
return;
}
case "set" when Num(_params, 2, out float v):
if (!NecroBody.SetParam(a, arg, v))
{
Out("нет параметра " + arg + " (necrobody params)");
return;
}
NecroBody.Params[arg] = v;
Out(arg + " = " + NecroBody.F(v) + " (держим; unset - отпустить)");
return;
case "unset":
Out(NecroBody.Params.Remove(arg) ? arg + " отпущен" : arg + " не был задан");
return;
case "trigger":
Out(NecroBody.SetParam(a, arg, 1f) ? "сработал " + arg : "нет параметра " + arg);
return;
case "speed" when Num(_params, 1, out float k):
NecroBody.Speed = k;
a.speed = k;
Out("скорость аниматора тела " + NecroBody.F(k));
return;
case "freeze":
NecroBody.Freeze = arg != "off";
NecroBody.Unfreeze();
Out("заморозка тела " + (NecroBody.Freeze ? "включена (снимок - в этом кадре)" : "выключена"));
return;
case "bones":
{
var sb = new StringBuilder();
int n = 0;
Dump(a.transform, 0, arg, sb, ref n);
Out("костей " + n + (arg.Length > 0 ? " (фильтр " + arg + ")" : "") + sb);
return;
}
case "show":
{
Transform b = NecroBody.Bone(arg);
Out(b == null ? "нет кости " + arg : arg + " = " + NecroBody.F(b.localEulerAngles) + (NecroBody.Pose.ContainsKey(arg) ? " (задана)" : " (от аниматора)") + ", поз. " + NecroBody.F(b.localPosition));
return;
}
case "pose" when Vec(_params, 2, out Vector3 r):
if (NecroBody.Bone(arg) == null)
{
Out("нет кости " + arg);
return;
}
NecroBody.Pose[arg] = r;
Out(arg + " = " + NecroBody.F(r));
return;
case "add" when Vec(_params, 2, out Vector3 d):
{
Transform b = NecroBody.Bone(arg);
if (b == null)
{
Out("нет кости " + arg);
return;
}
Vector3 cur = NecroBody.Pose.TryGetValue(arg, out Vector3 was) ? was : b.localEulerAngles;
NecroBody.Pose[arg] = cur + d;
Out(arg + " = " + NecroBody.F(NecroBody.Pose[arg]));
return;
}
case "unpose":
Out(NecroBody.Pose.Remove(arg) ? arg + " отпущена" : arg + " не была задана");
return;
case "item":
{
Transform it = (NecroBody.Player()?.emodel?.avatarController as AvatarMultiBodyController)?.heldItemTransform;
if (arg == "pos" && Vec(_params, 2, out Vector3 p))
{
NecroBody.ItemPos = p / 100f;
}
else if (arg == "rot" && Vec(_params, 2, out Vector3 r2))
{
NecroBody.ItemRot = r2;
}
else if (arg == "off")
{
NecroBody.ItemPos = null;
NecroBody.ItemRot = null;
Out("место предмета - как у игры (при следующем доставании)");
return;
}
Out("предмет в руке тела " + (it != null ? it.name + ": поз. (см) " + NecroBody.F(it.localPosition * 100f) + ", пов. " + NecroBody.F(it.localEulerAngles) : "нет") +
(NecroBody.ItemPos.HasValue || NecroBody.ItemRot.HasValue ? " [держим]" : ""));
return;
}
case "holdoffset":
{
int h = Num(_params, 1, out float hv) ? (int)hv : NecroBody.Player()?.inventory?.holdingItem?.HoldType.Value ?? 0;
var o = AnimationGunjointOffsetData.AnimationGunjointOffset[h];
Out("хват " + h + ": смещение игры поз. (см) " + NecroBody.F(o.position * 100f) + ", пов. " + NecroBody.F(o.rotation));
return;
}
case "save":
{
var sb = new StringBuilder("заданное:");
foreach (var kv in NecroBody.Params)
{
sb.Append("\n param ").Append(kv.Key).Append(" = ").Append(NecroBody.F(kv.Value));
}
foreach (var kv in NecroBody.Pose)
{
sb.Append("\n Pose[\"").Append(kv.Key).Append("\"] = new Vector3(").Append(NecroBody.F(kv.Value.x)).Append("f, ")
.Append(NecroBody.F(kv.Value.y)).Append("f, ").Append(NecroBody.F(kv.Value.z)).Append("f);");
}
if (NecroBody.ItemPos.HasValue)
{
sb.Append("\n item pos (см) ").Append(NecroBody.F(NecroBody.ItemPos.Value * 100f));
}
if (NecroBody.ItemRot.HasValue)
{
sb.Append("\n item rot ").Append(NecroBody.F(NecroBody.ItemRot.Value));
}
sb.Append("\n speed ").Append(NecroBody.F(NecroBody.Speed)).Append(", freeze ").Append(NecroBody.Freeze);
Out(sb.ToString());
return;
}
case "anims":
{
List<AnimationClip> l = NecroBody.FindClips(arg);
var sb = new StringBuilder("анимаций в памяти" + (arg.Length > 0 ? " со словом " + arg : "") + ": " + l.Count);
int shown = 0;
foreach (AnimationClip c in l)
{
if (shown++ >= 200)
{
sb.Append("\n ... (уточните слово)");
break;
}
sb.Append("\n ").Append(c.name).Append(" ").Append(NecroBody.F(c.length)).Append(" с").Append(c.humanMotion ? "" : " [не гуманоид]");
}
Out(sb.ToString());
return;
}
case "zdance":
{
EntityAlive z = NecroBody.Nearest(30f, true);
if (z == null)
{
Out("рядом (30 м) нет зомби");
return;
}
z.SetDancing(arg != "off");
Out(z.EntityName + " (id " + z.entityId + ") " + (arg != "off" ? "танцует" : "перестал танцевать"));
return;
}
case "mimic" when arg == "off":
NecroBody.StopMimic();
Out("тело больше не повторяет");
return;
case "mimic":
{
EntityAlive src = null;
if (Num(_params, 1, out float mid))
{
src = GameManager.Instance.World.GetEntity((int)mid) as EntityAlive;
}
else
{
src = NecroBody.Nearest(30f, false);
}
Out(src == null ? "нет источника (id или существо в 30 м)" : NecroBody.Mimic(src));
return;
}
case "play" when arg == "off":
NecroBody.StopPlay();
Out("анимация снята");
return;
case "play" when arg.Length > 0:
Out(NecroBody.Play(arg, Num(_params, 2, out float ps) ? ps : 1f));
return;
case "off":
NecroBody.Reset();
Out("тело - как у игры");
return;
}
Out(getDescription());
}
static void Dump(Transform _t, int _depth, string _filter, StringBuilder _sb, ref int _n)
{
if (_filter.Length == 0 || _t.name.IndexOf(_filter, System.StringComparison.OrdinalIgnoreCase) >= 0)
{
_n++;
_sb.Append('\n').Append(new string(' ', _depth * 2)).Append(_t.name).Append(_t.gameObject.activeSelf ? "" : " [выкл]")
.Append(" пов. ").Append(NecroBody.F(_t.localEulerAngles)).Append(" поз. ").Append(NecroBody.F(_t.localPosition));
}
for (int i = 0; i < _t.childCount; i++)
{
Dump(_t.GetChild(i), _depth + 1, _filter, _sb, ref _n);
}
}
}
}
+93
View File
@@ -0,0 +1,93 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Подрастить растения вокруг игрока (16.10, для проверки работы духа урны на грядках): каждое растущее растение
/// (BlockPlantGrowing) в радиусе переходит на следующие стадии родным BlockPlantGrowing.ForceNextGrowStage.
/// </summary>
[Preserve]
public class ConsoleCmdNecroGrow : ConsoleCmdAbstract
{
public override bool IsExecuteOnClient => false;
public override bool AllowedInMainMenu => false;
public override string[] getCommands()
{
return new string[] { "necrogrow" };
}
public override string getDescription()
{
return "Necromancer's Tome: grow the plants around you by one or more stages (testing).";
}
public override string getHelp()
{
return "necrogrow [radius] [stages] - advance every growing plant within radius (default 8) by stages (default 9 = ripe)";
}
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{
SdtdConsole con = SingletonMonoBehaviour<SdtdConsole>.Instance;
World world = GameManager.Instance.World;
EntityPlayer player = null;
if (_senderInfo.RemoteClientInfo != null)
{
world.Players.dict.TryGetValue(_senderInfo.RemoteClientInfo.entityId, out player);
}
else
{
player = world.GetPrimaryPlayer();
}
if (player == null)
{
con.Output("necrogrow: no player");
return;
}
int radius = 8, stages = 9;
if (_params.Count >= 1)
{
int.TryParse(_params[0], out radius);
}
if (_params.Count >= 2)
{
int.TryParse(_params[1], out stages);
}
radius = Mathf.Clamp(radius, 1, 32);
Vector3i c = World.worldToBlockPos(player.GetPosition());
int plants = 0, steps = 0;
for (int dx = -radius; dx <= radius; dx++)
{
for (int dz = -radius; dz <= radius; dz++)
{
for (int dy = -4; dy <= 4; dy++)
{
Vector3i p = new Vector3i(c.x + dx, c.y + dy, c.z + dz);
BlockValue bv = world.GetBlock(p);
if (bv.ischild || !(bv.Block is BlockPlantGrowing))
{
continue;
}
plants++;
for (int s = 0; s < stages && bv.Block is BlockPlantGrowing g && !g.nextPlant.isair; s++)
{
BlockValue next = g.ForceNextGrowStage(world, p, bv);
if (next.type == bv.type)
{
break;
}
world.SetBlockRPC(p, next);
bv = next;
steps++;
}
}
}
}
con.Output("necrogrow: " + plants + " growing plant(s) within " + radius + ", " + steps + " stage(s) advanced");
}
}
}
+271
View File
@@ -0,0 +1,271 @@
using System.Collections.Generic;
using System.Globalization;
using HarmonyLib;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Осмотр рук от первого лица со стороны (16.10: «зафиксировать командой позу игрока и осматривать его камерой... хват
/// от первого лица не позволяет видеть всех нюансов»). Руки первого лица (baseRigFP) - отдельная модель под камерой
/// игрока, в виде от третьего лица их нет, а у тела свои анимации. Поэтому вид остаётся от первого лица, а поверх
/// рисует своя камера, которая облетает руки. Её место задано в осях камеры игрока: повернёшь голову - вид на руки
/// не меняется. Поза замирает (снимок всех костей, как у хватов мода).
/// Управление - цифровой блок: 4/6 вокруг, 8/2 выше/ниже, +/- ближе/дальше, 5 - сначала; или командой necroview.
/// </summary>
public static class NecroView
{
public static bool On;
public static float Yaw, Pitch, Dist = 0.6f;
/// <summary>Точка, вокруг которой летает камера (между кистями), - в осях камеры игрока.</summary>
static Vector3 _target;
static Camera _cam;
const float TurnSpeed = 60f, ZoomSpeed = 0.4f;
/// <summary>Мышь облетает руки, персонаж стоит (16.10: «я поворачиваю мышь, а туловище проворачивается следом, и я
/// вижу тело только с одной позиции»). Поворот игрока каждый кадр возвращается к снимку при включении.</summary>
public static bool Mouse = true;
public static float MouseSpeed = 3f;
static Vector3 _lockRot;
static Quaternion _lockCam;
static bool _mouseBroken;
/// <summary>«Выйти из тела» (16.10: «временно мог выйти из тела, когда попрошу? Чтобы мог ходить вокруг тела и
/// осматривать»): камера летает свободно - WASD, мышь, пробел/Ctrl - выше/ниже, Shift - быстрее; тело стоит
/// (PlayerMoveController.Update пропускается, как при одержимости), поза рук замерла. Выход - End или necroview off.</summary>
public static bool Free;
static Vector3 _freePos;
static float _freeYaw, _freePitch;
const float FlySpeed = 1.5f;
public static string StartFree()
{
string r = Start(0f, 0f, 0.6f);
Transform pc = HandPose.Camera();
if (!On || pc == null)
{
return r;
}
Free = true;
// Руки первого лица (слой HoldingItem) вне тела не рисуем: 16.10 они висели над телом без головы.
_cam.cullingMask &= ~(1 << LayerMask.NameToLayer("HoldingItem"));
_freePos = pc.position;
Vector3 e = pc.eulerAngles;
_freeYaw = e.y;
_freePitch = e.x > 180f ? e.x - 360f : e.x;
return "вне тела: WASD, мышь, пробел/Ctrl - выше/ниже, Shift - быстрее, End - вернуться";
}
static void UpdateFree(float _dt)
{
PlayerMoveController pmc = PlayerMoveController.Instance;
Vector2 sens = pmc != null ? pmc.mouseLookSensitivity : Vector2.one * 5f;
if (!_mouseBroken)
{
try
{
_freeYaw += Input.GetAxis("Mouse X") * sens.x;
_freePitch = Mathf.Clamp(_freePitch - Input.GetAxis("Mouse Y") * sens.y, -89f, 89f);
}
catch (System.Exception e)
{
_mouseBroken = true;
Debug.LogWarning("[NecromancerTome] necroview: мышь не читается (" + e.Message + ")");
}
}
Quaternion rot = Quaternion.Euler(_freePitch, _freeYaw, 0f);
Vector3 move = Vector3.zero;
if (Input.GetKey(KeyCode.W)) move += Vector3.forward;
if (Input.GetKey(KeyCode.S)) move += Vector3.back;
if (Input.GetKey(KeyCode.A)) move += Vector3.left;
if (Input.GetKey(KeyCode.D)) move += Vector3.right;
Vector3 world = rot * move;
if (Input.GetKey(KeyCode.Space)) world += Vector3.up;
if (Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.C)) world += Vector3.down;
float speed = FlySpeed * (Input.GetKey(KeyCode.LeftShift) ? 4f : 1f);
_freePos += world * speed * _dt;
_cam.transform.SetPositionAndRotation(_freePos, rot);
}
public static string Start(float _yaw, float _pitch, float _dist)
{
Transform pc = HandPose.Camera();
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
if (pc == null || pl?.playerCamera == null)
{
return "нет камеры игрока";
}
Transform l = HandPose.Bone("LeftHand"), r = HandPose.Bone("RightHand");
Vector3 mid = l != null && r != null ? (l.position + r.position) * 0.5f : pc.position + pc.forward * 0.4f;
_target = pc.InverseTransformPoint(mid);
Yaw = _yaw;
Pitch = _pitch;
Dist = _dist;
if (_cam == null)
{
GameObject go = new GameObject("NecroViewCam");
Object.DontDestroyOnLoad(go);
_cam = go.AddComponent<Camera>();
}
_cam.CopyFrom(pl.playerCamera);
// Сразу над камерой игрока, но ПОД камерами интерфейса (16.10: с +50 рисовала поверх меню - «мне из игры не выйти даже»).
float above = float.MaxValue;
foreach (Camera c in Camera.allCameras)
{
if (c != _cam && c.depth > pl.playerCamera.depth && c.depth < above)
{
above = c.depth;
}
}
_cam.depth = above < float.MaxValue ? (pl.playerCamera.depth + above) * 0.5f : pl.playerCamera.depth + 1f;
_cam.nearClipPlane = 0.01f;
_cam.gameObject.SetActive(true);
HandPose.HoldStill = true;
_lockRot = pl.rotation;
_lockCam = pc.localRotation;
On = true;
return "осмотр включён: вокруг " + Yaw + ", высота " + Pitch + ", расстояние " + Dist;
}
public static void Stop()
{
On = false;
Free = false;
HandPose.HoldStill = false;
if (_cam != null)
{
Object.Destroy(_cam.gameObject);
_cam = null;
}
}
/// <summary>Из HandPoseDriver.LateUpdate - после позы.</summary>
public static void Update()
{
if (!On)
{
return;
}
Transform pc = HandPose.Camera();
if (pc == null || _cam == null)
{
Stop();
return;
}
float dt = Time.unscaledDeltaTime;
if (Free)
{
if (Input.GetKeyDown(KeyCode.End))
{
Stop();
return;
}
UpdateFree(dt);
return;
}
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
if (Mouse && pl != null && !_mouseBroken)
{
try
{
Yaw += Input.GetAxis("Mouse X") * MouseSpeed;
Pitch = Mathf.Clamp(Pitch - Input.GetAxis("Mouse Y") * MouseSpeed, -85f, 85f);
}
catch (System.Exception e)
{
_mouseBroken = true; // нет оси мыши в старом вводе - остаётся цифровой блок
Debug.LogWarning("[NecromancerTome] necroview: мышь не читается (" + e.Message + ")");
}
pl.rotation = _lockRot;
pc.localRotation = _lockCam;
}
if (Input.GetKey(KeyCode.Keypad4)) Yaw -= TurnSpeed * dt;
if (Input.GetKey(KeyCode.Keypad6)) Yaw += TurnSpeed * dt;
if (Input.GetKey(KeyCode.Keypad8)) Pitch = Mathf.Min(85f, Pitch + TurnSpeed * dt);
if (Input.GetKey(KeyCode.Keypad2)) Pitch = Mathf.Max(-85f, Pitch - TurnSpeed * dt);
if (Input.GetKey(KeyCode.KeypadPlus)) Dist = Mathf.Max(0.1f, Dist - ZoomSpeed * dt);
if (Input.GetKey(KeyCode.KeypadMinus)) Dist = Mathf.Min(3f, Dist + ZoomSpeed * dt);
if (Input.GetKeyDown(KeyCode.Keypad5))
{
Yaw = 0f;
Pitch = 0f;
Dist = 0.6f;
}
// Yaw 0, Pitch 0 - смотрим на руки со стороны глаз игрока (сзади), 180 - спереди.
Quaternion local = Quaternion.Euler(Pitch, Yaw, 0f);
Vector3 offset = local * new Vector3(0f, 0f, -Dist);
Vector3 t = pc.TransformPoint(_target);
_cam.transform.SetPositionAndRotation(t + pc.rotation * offset, pc.rotation * local);
}
public static string F(float _v) => _v.ToString("0.#", CultureInfo.InvariantCulture);
}
/// <summary>necroview on [вокруг высота расстояние] | off | set вокруг высота расстояние | show</summary>
[Preserve]
public class ConsoleCmdNecroView : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necroview" };
public override string getDescription() => "NecromancerTome: осмотр рук со стороны - on [вокруг высота расстояние]|off|set вокруг высота расстояние|free (выйти из тела, End - назад)|mouse [off]|show; мышь или цифровой блок 4/6 8/2 +/- 5";
static bool Num(List<string> _p, int _i, out float _v)
{
_v = 0f;
return _p.Count > _i && float.TryParse(_p[_i].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out _v);
}
void Out(string _s)
{
SdtdConsole.Instance.Output(_s);
Debug.Log("[NecromancerTome] necroview: " + _s);
}
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{
string cmd = _params.Count > 0 ? _params[0] : "";
switch (cmd)
{
case "on":
Out(NecroView.Start(Num(_params, 1, out float y) ? y : 150f, Num(_params, 2, out float p) ? p : 10f, Num(_params, 3, out float d) ? d : 0.6f));
return;
case "off":
NecroView.Stop();
Out("осмотр выключен");
return;
case "set" when NecroView.On && Num(_params, 1, out float y2) && Num(_params, 2, out float p2) && Num(_params, 3, out float d2):
NecroView.Yaw = y2;
NecroView.Pitch = p2;
NecroView.Dist = d2;
break;
case "free":
Out(NecroView.StartFree());
return;
case "mouse":
NecroView.Mouse = !(_params.Count > 1 && _params[1] == "off");
Out("мышь " + (NecroView.Mouse ? "облетает руки" : "крутит персонажа, облёт - цифровой блок"));
return;
case "show":
break;
default:
Out(getDescription());
return;
}
Out(NecroView.On ? "вокруг " + NecroView.F(NecroView.Yaw) + ", высота " + NecroView.F(NecroView.Pitch) + ", расстояние " + NecroView.F(NecroView.Dist) : "осмотр выключен");
}
}
/// <summary>Вне тела (NecroView.Free) тело не ходит и не крутится - как при одержимости.</summary>
[HarmonyPatch(typeof(PlayerMoveController), nameof(PlayerMoveController.Update))]
public static class Patch_PlayerMoveController_Update_NecroView
{
public static bool Prefix(PlayerMoveController __instance)
{
if (!NecroView.Free)
{
return true;
}
__instance.stopMoving();
return false;
}
}
}
+43 -1
View File
@@ -825,6 +825,11 @@ namespace NecromancerTome
}
if (IsPet && RatModel.Is(z))
{
// E на зомби в прицеле - дух переходит в него, крыса развеивается (указание 16.10).
if (detonate && TryRatJump(world, z))
{
return;
}
// E - осмотреть контейнер без права взять (указание 08.10: крыса - разведчик).
RatLoot.Tick(world, _player, z, detonate);
// ПКМ (держать) - сквозь постройки, без полёта (BACKLOG, «Могильная крыса» п.4).
@@ -1524,6 +1529,42 @@ namespace NecromancerTome
return best;
}
/// <summary>Перенос духа из крысы в зомби под прицелом (решения 16.10): E, зомби не дальше RatJumpRange от
/// крысы и в прямой видимости (GroundAimTarget не видит сквозь стены, во время прохода - не работает);
/// цена - как у «пиф-пафа» (никакой). Крыса отзывается и развеивается; цепочки нет - после зомби дух
/// вернётся в тело ГГ (Exit). True - E израсходована: перенос сделан или начат.</summary>
static bool TryRatJump(World _world, EntityAlive _rat)
{
if (RatPhase.On)
{
return false;
}
EntityAlive victim = GroundAimTarget(_world, _rat);
if (victim == null || !CanPossess(victim) ||
(victim.position - _rat.position).sqrMagnitude > RatJumpRange * RatJumpRange)
{
return false;
}
EntityPlayerLocal player = _player;
int ratId = _rat.entityId;
Exit("крыса: дух переходит в " + victim.EntityName + " (" + victim.entityId + ")");
if (!Enter(player, victim))
{
Debug.Log("[NecromancerTome] Possession: перенос из крысы не удался - дух в теле, крыса осталась");
return true;
}
if (NecroBurst.EnsureRegistered())
{
ParticleEffect pe = new ParticleEffect(NecroBurst.ParticleName, _rat.position,
_world.GetLightBrightness(_rat.GetBlockPosition()), Color.white, null, null, _OLDCreateColliders: false);
GameManager.Instance.SpawnParticleEffectServer(pe, ratId);
}
Patch_ItemActionSpawnEntity_Spawn_PetLimit.RecallPet(player, ratId, null, null); // без подсказки «отозвана» - её место у подсказки вселения
return true;
}
public const float RatJumpRange = 3f;
public const float GroundAimRange = 30f; // зрение зомби, как TauntRange
public const float GroundAimRadius = 0.6f;
@@ -1678,7 +1719,8 @@ namespace NecromancerTome
}
if (IsPet && RatModel.Is(_z))
{
text += ", " + Localization.Get("necroPossessHintLook") + ", " + Localization.Get("necroPossessHintPhase");
text += ", " + Localization.Get("necroPossessHintLook") + ", " + Localization.Get("necroPossessHintRatJump") + ", " +
Localization.Get("necroPossessHintPhase");
}
if (IsPet && DogSense.Is(_z))
{
+31 -1
View File
@@ -356,8 +356,22 @@ namespace NecromancerTome
}
}
public const int GripThreadIdx = 3;
public static void OnRadialCommand(XUiC_Radial _sender, int _commandIndex, XUiC_Radial.RadialContextAbs _context)
{
EntityPlayerLocal pl = _sender.xui.playerUI.entityPlayer;
// Хват ножа (16.10, нити цинерария): 3 - хват для нитей; 0 у ножа-для-нитей - обычный хват.
if (_commandIndex == GripThreadIdx)
{
CinerariumThreads.Convert(pl, CinerariumThreads.ThreadKnifeName);
return;
}
if (_commandIndex == 0 && CinerariumThreads.IsThreadKnife(pl?.inventory?.holdingItem))
{
CinerariumThreads.Convert(pl, CinerariumThreads.KnifeName);
return;
}
if (_commandIndex == 1)
{
Enter(_sender.xui.playerUI.entityPlayer);
@@ -374,7 +388,8 @@ namespace NecromancerTome
{
public static void Postfix(ItemAction __instance, ref bool __result)
{
if (!__result && !SnakeMode.Active && !MindPalace.Active && SnakeMode.IsKnife(__instance.item))
if (!__result && !SnakeMode.Active && !MindPalace.Active &&
(SnakeMode.IsKnife(__instance.item) || CinerariumThreads.IsThreadKnife(__instance.item)))
{
__result = true;
}
@@ -389,6 +404,18 @@ namespace NecromancerTome
public static bool Prefix(ItemAction __instance, XUiC_Radial _xuiRadialWindow, EntityPlayerLocal _epl)
{
if (CinerariumThreads.IsThreadKnife(__instance.item))
{
// Нож хватом для нитей: вернуть обычный хват (0) или остаться (3).
_xuiRadialWindow.ResetRadialEntries();
_xuiRadialWindow.CreateRadialEntry(0, "NecromancersKnife", "ItemIconAtlas", "",
Localization.Get("necroKnifeGripNormal"), _highlighted: false);
_xuiRadialWindow.CreateRadialEntry(SnakeMode.GripThreadIdx, "resourceSewingKit", "ItemIconAtlas", "",
Localization.Get("necroKnifeGripThread"), _highlighted: true);
_xuiRadialWindow.SetCommonData(UIUtils.GetButtonIconForAction(_epl.playerInput.Reload),
SnakeMode.OnRadialCommand, null, SnakeMode.GripThreadIdx);
return false;
}
if (!SnakeMode.IsKnife(__instance.item))
{
return true;
@@ -417,6 +444,9 @@ namespace NecromancerTome
_xuiRadialWindow.CreateRadialEntry(2, SnakeMode.DayIcon, "UIAtlas", "",
Localization.Get("necroSnakeModeDay"), _highlighted: false);
}
// Хват для нитей (16.10: «Есть в игре пиктограмма ниток, катушки?» - набор для шитья) - в любое время суток.
_xuiRadialWindow.CreateRadialEntry(SnakeMode.GripThreadIdx, "resourceSewingKit", "ItemIconAtlas", "",
Localization.Get("necroKnifeGripThread"), _highlighted: false);
_xuiRadialWindow.SetCommonData(UIUtils.GetButtonIconForAction(_epl.playerInput.Reload),
SnakeMode.OnRadialCommand, null, 0);
return false;
+1 -1
View File
@@ -562,7 +562,7 @@ namespace NecromancerTome
{
GiveBackSummonItem(owner, summonItemName);
}
if (world != null)
if (world != null && recalledTooltipKey != null) // null - без подсказки (перенос духа крысой, 16.10)
{
GameManager.ShowTooltip(GameManager.Instance.World.GetPrimaryPlayer(), recalledTooltipKey);
}
+138
View File
@@ -0,0 +1,138 @@
using System.Collections.Generic;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Тело (вид от третьего лица и для других игроков) держит нож-для-нитей обычным хватом ножа, а не луком (16.10, скрин
/// от третьего лица: «у персонажа запястье вывихнуто» - анимация деревянного лука тянет правую кисть с ножом). Хват
/// предмета один на все модели (HoldType 68 нужен рукам первого лица - основа позы HandPose.PresetThread), поэтому
/// подменяется только параметр аниматора WeaponHoldType/WeaponCarry у тел, кроме рук первого лица, и место ножа в
/// руке тела. Подбор на лету: necroheld tphold N (0 - выключить подмену).
/// </summary>
public static class ThreadKnifeBody
{
/// <summary>0 - тело держит хватом предмета (68, лук). 16.10 в игре (necrobody) пользователь выбрал именно его: нож в левой
/// руке тела, «Нож был нормальным», нужна только поправка кисти и места ножа (ниже). Подмена на 36 - на пробу командой.</summary>
public static int TpHold = 0;
/// <summary>Поправка тела, подобранная 16.10 командами necrobody: левая кисть к ладони на 20° («лучезапястный сустав
/// выгнуло... словно перелом»: LeftHand Z 302 -> 322), нож в кулаке - item pos 3 -4 -2 см («Вот так идеально»).</summary>
public static float WristBend = 20f;
public static Vector3 ItemPos = new Vector3(0.03f, -0.04f, -0.02f);
static readonly Dictionary<Animator, Transform> _leftHand = new Dictionary<Animator, Transform>();
/// <summary>Из HandPoseDriver.LateUpdate: у всех игроков в мире с ножом-для-нитей, кроме своего вида от первого лица.</summary>
public static void LateUpdate()
{
World w = GameManager.Instance?.World;
if (w?.Players?.list == null || TpHold > 0)
{
return;
}
foreach (EntityPlayer p in w.Players.list)
{
if (p == null || p.inventory?.holdingItem?.Name != CinerariumThreads.ThreadKnifeName || p.emodel == null ||
p.emodel.IsFPV && !p.isEntityRemote)
{
continue;
}
AvatarCharacterController ac = p.emodel.avatarController as AvatarCharacterController;
Animator a = ac?.CharacterBody?.Animator;
if (a == null)
{
continue;
}
if (!_leftHand.TryGetValue(a, out Transform lh) || lh == null)
{
lh = null;
foreach (Transform t in a.GetComponentsInChildren<Transform>(true))
{
if (t.name == "LeftHand")
{
lh = t;
break;
}
}
if (_leftHand.Count > 16)
{
_leftHand.Clear();
}
_leftHand[a] = lh;
}
if (lh != null)
{
lh.localRotation *= Quaternion.Euler(0f, 0f, WristBend);
}
if (ac.heldItemTransform != null && !NecroBody.ItemPos.HasValue)
{
ac.heldItemTransform.localPosition = ItemPos;
}
}
}
public static bool Applies(EntityAlive _e)
{
return TpHold > 0 && _e != null && _e.inventory?.holdingItem?.Name == CinerariumThreads.ThreadKnifeName;
}
}
[HarmonyPatch(typeof(AvatarMultiBodyController), nameof(AvatarMultiBodyController._setInt), new[] { typeof(int), typeof(int), typeof(bool) })]
public static class Patch_AvatarMultiBodyController_setInt_ThreadKnife
{
public static bool Prefix(AvatarMultiBodyController __instance, int _propertyHash, int _value)
{
bool hold = _propertyHash == AvatarController.weaponHoldTypeHash;
if (!hold && _propertyHash != AvatarController.weaponCarryHash || !ThreadKnifeBody.Applies(__instance.Entity))
{
return true;
}
BodyAnimator fp = (__instance as AvatarLocalPlayerController)?.FPSArms;
int tp = hold ? ThreadKnifeBody.TpHold : AnimationDelayData.AnimationDelay[ThreadKnifeBody.TpHold].Carry;
foreach (BodyAnimator b in __instance.BodyAnimators)
{
Animator a = b.Animator;
int v = b == fp ? _value : tp;
if (a != null && a.GetInteger(_propertyHash) != v)
{
a.SetInteger(_propertyHash, v);
}
}
return false; // без рассылки по сети: у других игроков мод ставит то же сам
}
}
/// <summary>Место ножа в руке тела - по хвату ножа (игра берёт смещение по HoldType предмета).</summary>
[HarmonyPatch(typeof(AvatarMultiBodyController), nameof(AvatarMultiBodyController.SetInRightHand))]
public static class Patch_AvatarMultiBodyController_SetInRightHand_ThreadKnife
{
public static void Postfix(AvatarMultiBodyController __instance, Transform _transform)
{
EntityAlive e = __instance.Entity;
if (_transform == null || !ThreadKnifeBody.Applies(e) || e.emodel.IsFPV && !e.isEntityRemote)
{
return;
}
AnimationGunjointOffsetData.AnimationGunjointOffsets o = AnimationGunjointOffsetData.AnimationGunjointOffset[ThreadKnifeBody.TpHold];
_transform.localPosition = o.position;
_transform.localRotation = Quaternion.Euler(o.rotation);
}
}
[HarmonyPatch(typeof(AvatarMultiBodyController), nameof(AvatarMultiBodyController.SwitchModelAndView))]
public static class Patch_AvatarMultiBodyController_SwitchModelAndView_ThreadKnife
{
public static void Postfix(AvatarMultiBodyController __instance, bool _bFPV)
{
Transform t = __instance.heldItemTransform;
if (t == null || _bFPV || !ThreadKnifeBody.Applies(__instance.Entity))
{
return;
}
AnimationGunjointOffsetData.AnimationGunjointOffsets o = AnimationGunjointOffsetData.AnimationGunjointOffset[ThreadKnifeBody.TpHold];
t.localPosition = o.position;
t.localEulerAngles = o.rotation;
}
}
}
+1 -1
View File
@@ -4,6 +4,6 @@
<DisplayName value="Necromancer's Tome" />
<Description value="A dark necromancy progression for 7 Days to Die 3.3: a skill that grows from the dead you put to rest, a bone knife, undead servants, possession of zombies and beasts, the Snake Spirit, the Mind Palace, the Blood Tide and a story-ending Black Portal. Fully localized into 13 languages. Single-player; requires EAC off." />
<Author value="Alex Cube" />
<Version value="1.6.2" />
<Version value="1.7.0" />
<Website value="https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/" />
</xml>
-2
View File
@@ -1,2 +0,0 @@
FIX: blood rose buds and blooming roses floated half a block above the planter. The plant model was lifted by the game's default model offset; now it stands on the soil.
Nothing else changed. Drop-in over 1.6.0 or 1.5.x: saves carry over, roses already planted fix themselves.
-3
View File
@@ -1,3 +0,0 @@
FIX: compatibility with NPC mods. The ghost look (grey and translucent) was given to every entity built on the game's trader type, so NPCs from other mods such as NPCCore became nearly invisible. Now only the game's own traders look like ghosts. Thanks to Crozekanov for the report.
Also in this file: the 1.6.1 fix - blood rose buds and blooming roses no longer float half a block above the planter.
Nothing else changed. Drop-in over 1.6.0, 1.6.1 or 1.5.x: saves carry over.
+1 -1
View File
@@ -42,7 +42,7 @@ Updating from 1.5.x: replace the folder, saves carry over. To uninstall, delete
[size=5][b]Requirements[/b][/size]
[list]
[*][b]7 Days to Die 3.3: version 1.6.2. 7 Days to Die 3.2: version 1.4.3.[/b] Both are on the Files tab. Each build tells you in the main menu if it is started on the wrong game version.
[*][b]7 Days to Die 3.3: version 1.6.0. 7 Days to Die 3.2: version 1.4.3.[/b] Both are on the Files tab. Each build tells you in the main menu if it is started on the wrong game version.
[*][b]EasyAntiCheat must be off[/b] - the mod uses Harmony patches.
[*][b]No other mods required.[/b] Harmony itself ships with the game.
[*][b]Single-player.[/b] The mod was made for single-player from the start. It reworks a number of the game's own mechanics - corpses, loot, zombie behaviour - and a server isn't necessarily built for that, so in multiplayer some things may not work. The story scenes also play there without stopping time, since vanilla pauses only in single-player.
-1
View File
@@ -1 +0,0 @@
Fix for 1.6.0: blood rose buds and flowers no longer float half a block above the planter. Nothing else changed - drop-in over 1.6.0 or 1.5.x, saves carry over. For game 3.3. Single-player, EAC off.
-1
View File
@@ -1 +0,0 @@
Fixes for 1.6.0: NPCs from other mods (e.g. NPCCore) no longer turn into translucent ghosts - only the game's own traders do; blood rose buds and flowers no longer float. Drop-in over 1.6.x or 1.5.x, saves carry over. Game 3.3, single-player, EAC off.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4 -8
View File
@@ -2,7 +2,7 @@
*English version below — scroll past the Russian half.*
**Версия 1.6.2** — для 7 Days to Die 3.3. Для игры 3.2 — версия 1.4.3. Каждая сборка на чужой версии игры сама скажет, что несовместима. Автор: Alex Cube.
**Версия 1.6.0** — для 7 Days to Die 3.3. Для игры 3.2 — версия 1.4.3. Каждая сборка на чужой версии игры сама скажет, что несовместима. Автор: Alex Cube.
- Страница мода: https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/
- Nexus Mods: https://www.nexusmods.com/7daystodie/mods/12547
@@ -63,7 +63,7 @@
`%APPDATA%/7DaysToDie/` (подходят обе, если её нет — создайте).
3. Запустите игру. Навык «Некромантия» появится на панели навыков крафта сразу.
Версия 1.6.2 — для игры 3.3, версия 1.4.3 — для игры 3.2 (обе на Nexus во вкладке Files).
Версия 1.6.0 — для игры 3.3, версия 1.4.3 — для игры 3.2 (обе на Nexus во вкладке Files).
Обновление поверх 1.5.x: сейвы переносятся.
Рассчитан на одиночную игру. Мод переделывает ряд механик самой игры (трупы, добычу, поведение
@@ -75,8 +75,6 @@
## История версий
- **1.6.2** — совместимость: призрачный вид только у торговцев самой игры; NPC других модов (например, NPCCore) больше не становятся полупрозрачными.
- **1.6.1** — исправление: бутоны и цветы кровавой розы висели над грядкой на полклетки.
- **1.6.0** — дух змеи, одержимость зомби и своих питомцев, чертоги разума, книга некроманта с
главами, слуги охраняют хозяина (один за раз), могильная крыса, костяной скакун, гео-оссуарий,
кровавая роза и ступка, кровавый прилив и нож-меч, останки красных трупов, череп и
@@ -100,7 +98,7 @@
# Necromancer's Tome (English)
**Version 1.6.2** — for 7 Days to Die 3.3. For game 3.2 use version 1.4.3. Each build tells you if it is started on the wrong game version. Author: Alex Cube.
**Version 1.6.0** — for 7 Days to Die 3.3. For game 3.2 use version 1.4.3. Each build tells you if it is started on the wrong game version. Author: Alex Cube.
- Mod page: https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/
- Nexus Mods: https://www.nexusmods.com/7daystodie/mods/12547
@@ -163,7 +161,7 @@ Polish, Brazilian Portuguese, Russian, Turkish, Simplified and Traditional Chine
`%APPDATA%/7DaysToDie/` (either works; create it if it is not there).
3. Start the game. The Necromancy skill appears in the crafting skills panel right away.
Version 1.6.2 is for game 3.3, version 1.4.3 is for game 3.2 (both on the Nexus Files tab).
Version 1.6.0 is for game 3.3, version 1.4.3 is for game 3.2 (both on the Nexus Files tab).
Drop-in over 1.5.x: saves carry over.
Built for single-player. The mod reworks a number of the game's own mechanics (corpses, loot,
@@ -175,8 +173,6 @@ To uninstall, delete the folder. The mod's items and blocks will be gone from th
## Version history
- **1.6.2** - compatibility: only the game's own traders get the ghost look; NPCs from other mods (for example NPCCore) no longer turn translucent.
- **1.6.1** - fix: blood rose buds and flowers floated half a block above the planter.
- **1.6.0** - the Snake Spirit, possession of zombies and of your own servants, the Mind Palace, the
Necromancer's Tome with chapters, servants that guard their master (one at a time), the Grave
Rat, the Bone Steed, the Geo-Ossuary, the blood rose and the mortar, the Blood Tide and the
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.
+2 -2
View File
@@ -38,7 +38,7 @@
<li>Запустите игру. Навык «Некромантия» появится на панели навыков крафта сразу.</li>
</ol>
Версия 1.6.2 — для игры 3.3, версия 1.4.3 — для игры 3.2 (обе на Nexus во вкладке Files). Обновление поверх 1.5.x: сейвы переносятся.
Версия 1.6.0 — для игры 3.3, версия 1.4.3 — для игры 3.2 (обе на Nexus во вкладке Files). Обновление поверх 1.5.x: сейвы переносятся.
Рассчитан на одиночную игру. Мод переделывает ряд механик самой игры (трупы, добычу, поведение зомби), а сервер не обязательно на такое рассчитан, поэтому в мультиплеере часть вещей может не работать. Сюжетные сцены там проиграются без остановки времени: пауза игры работает только в сингле.
@@ -97,7 +97,7 @@ Fully localized into 13 languages: English, German, Spanish, French, Italian, Ja
<li>Start the game. The Necromancy skill appears in the crafting skills panel right away.</li>
</ol>
Version 1.6.2 is for game 3.3, version 1.4.3 is for game 3.2 (both on the Nexus Files tab). Drop-in over 1.5.x: saves carry over.
Version 1.6.0 is for game 3.3, version 1.4.3 is for game 3.2 (both on the Nexus Files tab). Drop-in over 1.5.x: saves carry over.
Built for single-player. The mod reworks a number of the game's own mechanics (corpses, loot, zombie behaviour), and a server isn't necessarily built for that, so in multiplayer some things may not work. The story scenes also play there without stopping time, since the game pauses only in single-player.
+28
View File
@@ -0,0 +1,28 @@
namespace NecromancerTome
{
/// <summary>
/// Запись состояния урны-цинерария (TEFeatureCinerarium, HarmonySrc/Cinerarium.cs). Здесь, а не в
/// основном проекте, по той же причине, что и PyramidWardWriteHelper: PooledBinaryWriter.Write
/// оттуда не компилируется (см. NecromancerTEPersistence.csproj). Первым байтом - версия, чтобы
/// поля урны (дух, его работа) можно было дописывать, не ломая старые сейвы.
/// </summary>
public static class CinerariumWriteHelper
{
public static void Write(PooledBinaryWriter _bw, byte _version, bool _hasSkull, bool _zoneShown)
{
_bw.Write(_version);
_bw.Write(_hasSkull);
_bw.Write(_zoneShown);
}
/// <summary>Версия 2 (16.10, нити): число, затем сами числа - координаты нитей (см. TEFeatureCinerarium.Write).</summary>
public static void WriteInts(PooledBinaryWriter _bw, int[] _values)
{
_bw.Write(_values.Length);
foreach (int v in _values)
{
_bw.Write(v);
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB