17 Commits
Author SHA1 Message Date
AlexCubeandClaude Opus 5.5 64fc0c060f Главы книги некроманта на 11 языках (#39)
Все 46 строк книги (оглавление, кнопки, восемь глав) переведены на немецкий,
испанский, французский, итальянский, японский, корейский, польский,
бразильский португальский, турецкий и оба китайских. Названия предметов,
кнопок урн и термины взяты из уже переведённых строк мода. В английском
убран лишний «\» перед апострофом на четырёх страницах.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:18:11 +03:00
AlexCubeandClaude Opus 5.5 9d54cab98f Резная кость: отдельный инструмент нитей цинерариев (#86)
Новый предмет necroCarvedBone: бедренная кость с железным колпачком, кольцом
и светящимися рунами; в руке как нож некроманта, хват всегда нитяной и делает
всё то же. Рецепт: кость, 10 кованого железа, щепоть праха; открывается вместе
с цинерариями. Иконка, лист книги, тексты на 13 языках.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:24:15 +03:00
AlexCubeandClaude Opus 5.5 96d930e5bb Ремонтник и лутер: зона из 3x3 квадратов, лишние выключаются (#84)
Сетка «Где работать» в «Содержимом» урны, квадраты очерчены линиями при
показе границ; выключенные квадраты обход пропускает. Сохраняется пятым
числом v6 (старая DLL его пропустит). Консоль: necrourn x y z sectors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 10:21:39 +03:00
AlexCubeandClaude Opus 5.5 9a567b6ac0 DLL: черепа в урне (#82), скорость змеи на Shift
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:58:16 +03:00
AlexCubeandClaude Opus 5.5 95d5117cfc Змея: скорость на Shift 16.5 -> 12.5 (на четверть меньше)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:51:59 +03:00
AlexCubeandClaude Opus 5.5 3cd0e3d6cb Урна: черепа не пропадают при Shift+клике стопкой (#82); рецепты из кузни на верстак, не больше 8 компонентов, долгие ÷4 (#83)
Проверка «Содержимого» урны - в следующем кадре: игра звала onModified посреди перекладки,
лишние черепа возвращались в исходную ячейку рюкзака и затирались пустотой.
Кузня делает только из вплавленного - урны логиста, лутера, старателя и гео-оссуарий на верстаке
(на ломе). Зомбособака 9 -> 7 компонентов (окно показывает 8). Время долгих вещей вчетверо меньше.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:47:45 +03:00
AlexCubeandClaude Opus 5.5 b5a4ec4a2c Пыль и хранилища магии: свои модели, бандл necromagic, хват книги (#40)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 14:25:41 +03:00
AlexCubeandClaude Opus 5.5 cf61d98bb7 Одержимый зомби: оружие на поясе по ремеслу (#37), карта по M (#36)
- Пояс - нижний ряд сумки зомби; оружие ближнего боя в руку, если этот тип зомби
  им владеет (ZombieWeapons: таблица умений, утверждена 25.10). Удар как у персонажа:
  ItemActionAttack.Hit, урон предмета без перков зомби, износ, темп оружия.
  Модель на кость RightWeapon, хват WeaponHoldType; иконки и износ на поясе HUD.
  Неумелое или сломанное - кулаки и подсказка (13 языков). Выключатель
  necroarms weapon on|off; dev-команда necropossess для проверок без мыши.
- M в зомби и волке открывал сумку вместо карты (перехват windowpaging) - исправлено;
  туман карты и карта на одержимом - у любого одержимого зомби, как у стервятника.
- Глава «Одержимость» (EN/RU): пояс - оружие, M - карта.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 14:26:55 +03:00
AlexCubeandClaude Opus 5.5 5ebd09dacd Ремонтник: сначала то, на что есть материал; ячейка нехватки видна всегда
- Блоки без материалов откладываются (_lacking) и не держат очередь;
  к ним ремонтник возвращается, когда чинить больше нечего, и чинит, как
  только материал появился. Зона не останавливается, пока они есть.
- Кнопка жёлтая, только когда кроме ждущих чинить нечего; ячейка «не
  хватает» видна, пока хоть один блок ждёт.
- «Запустить» повторно отпускает старые взятые блоки (Claimed) - раньше
  они не попадали в обход заново.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 15:47:13 +03:00
AlexCubeandClaude Opus 5.5 3d08511e5b Ремонтник: «с привязкой к урне» вместо «с нитью», «!» и число поверх иконки
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 15:35:07 +03:00
AlexCubeandClaude Opus 5.5 7452eba0d9 Ремонтник: в «Содержимом» урны ячейка с недостающим материалом и «!»
Пока ремонтник ждёт материал, под блоком лимита урн видна ячейка: иконка
материала, сколько доложить в ящики с нитями, красный «!» и подпись.
Только показ, не ячейка сумки.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 15:32:22 +03:00
AlexCubeandClaude Opus 5.5 c208e3b9f9 Урна рабочего: подкладка сырья в кузню до изготовления (#19)
После изготовления очередь до подкладки не доходила: переплавленного
каждый шаг хватало на один цемент. Теперь подкладка первой, но только
в пустую или наполовину опустевшую ячейку входа.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 15:07:52 +03:00
AlexCubeandClaude Opus 5.5 cc9c0366f0 Урна рабочего: вход сырья кузни держать полным (#19)
Сырьё подкладывалось, только когда переплавленного не хватало на одну
штуку - кузня плавила остатки, цемент шёл по одному. Теперь отдельное
дело: есть место во входе под материал рецепта образца - доложить сырьё
до стака (после изготовления).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 15:01:57 +03:00
AlexCubeandClaude Opus 5.5 b4335a0448 Урна рабочего: в кузню только сырьё, булыжник не плавить (#19)
Проверка «нет рецепта» не отличала камни от булыжника: у камня тоже есть
рецепты (разбор, выплавка из переплавленного), и дух брал то, что лежит
в ящике раньше. Теперь «сделано руками» = есть рецепт изготовления, не
разбор и не выплавка; такое в кузню не кладётся.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 14:56:59 +03:00
AlexCubeandClaude Opus 5.5 fb9c50cdf9 Урна рабочего: партия до стака, одна ячейка очереди на предмет, сланец не плавить (#19)
- Рецепт ставится в очередь сразу на столько, на сколько хватает всех
  ингредиентов, не больше стака любого ингредиента и стака готового
  (было по одной штуке за шаг - «очень медленно»).
- Пока партия предмета в очереди станции, второй такой не ставится -
  прочие ячейки остаются другим урнам на той же станции.
- Горючее сырьё (нефтяной сланец) в кузню не кладётся.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 14:45:10 +03:00
AlexCubeandClaude Opus 5.5 3938f1786a Игра 3.4: аналитика пояса падала на кинжале некроманта - нож не переключался (#79)
Inventory.SetStackAt до записи в слот зовёт LogToolBeltUsageEvent, та считает
passive_effect без сущности и падает на значении из CVar (@$necroKnifeDamageCVar;
в игре так же у мода modMeleeFortifyingGrip и админской одежды). Исключение
телеметрии глотается финализатором, слот пишется как обычно.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 14:21:09 +03:00
AlexCubeandClaude Opus 5.5 a747f5aa8d Порт на игру 3.4: замок ячеек, бросок на пустое место окна, проверка версии (#79)
- 3.4 убрала XUiC_ContainerStandardControls.ToggleLockMode: замок при обыске
  крысой/волком и у сумки волка запрещается в сеттере UserLockMode.
- 3.4 кладёт предмет, брошенный на пустое место окна, прямо в ящик/мешок/
  рюкзак (dropItemToGrid) мимо запертых ячеек - при обыске отказ.
- GameVersionGuard: мод собран под 3.4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 13:57:50 +03:00
32 changed files with 1447 additions and 233 deletions
+61 -52
View File
File diff suppressed because one or more lines are too long
+45
View File
@@ -272,6 +272,38 @@
<sprite depth="3" name="g25" pos="64,-275" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" /> <sprite depth="3" name="g25" pos="64,-275" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
</window> </window>
</append> </append>
<!-- Квадраты зоны ремонтника и лутера (#84, 28.10.2026, HarmonySrc/CinerariumZoneWindow.cs): сетка 3x3 в окне «Содержимое»,
север сверху, урна в среднем. Окно в группе bagStorage (xui.xml), вне урн с большой зоной скрыто. s0..s8 - квадраты
(столбец + строка * 3, строка 0 - юг), f0..f8 - их рамки; цвета ставит контроллер. -->
<append xpath="/windows">
<window name="necroUrnZone" width="228" height="296" depth="5" panel="Left" backgroundcolor="[black]" cursor_area="true" controller="NecromancerTome.NecroUrnZone, NecromancerHarmony">
<rect name="header" height="43" depth="0">
<sprite depth="1" name="headerbackground" sprite="ui_game_panel_header" width="228" height="43" />
<sprite depth="3" name="windowIcon" style="icon32px" pivot="center" pos="21,-21" sprite="ui_game_symbol_frames" />
<label depth="2" name="title" pos="39,-6" width="183" height="32" text_key="necroUrnZoneTitle" font_size="26" />
</rect>
<label depth="2" name="north" pos="0,-49" width="228" height="22" justify="center" font_size="20" color="200,200,205" text_key="necroUrnZoneNorth" />
<sprite depth="2" name="f6" pos="10,-74" width="68" height="68" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="s6" pos="12,-76" width="64" height="64" sprite="menu_empty2px" type="sliced" on_press="true" sound="[paging_click]" />
<sprite depth="2" name="f7" pos="80,-74" width="68" height="68" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="s7" pos="82,-76" width="64" height="64" sprite="menu_empty2px" type="sliced" on_press="true" sound="[paging_click]" />
<sprite depth="2" name="f8" pos="150,-74" width="68" height="68" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="s8" pos="152,-76" width="64" height="64" sprite="menu_empty2px" type="sliced" on_press="true" sound="[paging_click]" />
<sprite depth="2" name="f3" pos="10,-144" width="68" height="68" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="s3" pos="12,-146" width="64" height="64" sprite="menu_empty2px" type="sliced" on_press="true" sound="[paging_click]" />
<sprite depth="2" name="f4" pos="80,-144" width="68" height="68" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="s4" pos="82,-146" width="64" height="64" sprite="menu_empty2px" type="sliced" on_press="true" sound="[paging_click]" />
<sprite depth="2" name="f5" pos="150,-144" width="68" height="68" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="s5" pos="152,-146" width="64" height="64" sprite="menu_empty2px" type="sliced" on_press="true" sound="[paging_click]" />
<sprite depth="2" name="f0" pos="10,-214" width="68" height="68" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="s0" pos="12,-216" width="64" height="64" sprite="menu_empty2px" type="sliced" on_press="true" sound="[paging_click]" />
<sprite depth="2" name="f1" pos="80,-214" width="68" height="68" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="s1" pos="82,-216" width="64" height="64" sprite="menu_empty2px" type="sliced" on_press="true" sound="[paging_click]" />
<sprite depth="2" name="f2" pos="150,-214" width="68" height="68" sprite="menu_empty2px" type="sliced" fillcenter="false" />
<sprite depth="3" name="s2" pos="152,-216" width="64" height="64" sprite="menu_empty2px" type="sliced" on_press="true" sound="[paging_click]" />
<sprite depth="4" name="urn" pos="114,-178" width="36" height="36" pivot="center" sprite="ui_game_symbol_skull" color="[white]" />
</window>
</append>
<!-- Лимит урн (#65, 23.10.2026; HarmonySrc/CinerariumLimitPanel.cs): блок справа от ячеек урны в «Содержимом» - всего, <!-- Лимит урн (#65, 23.10.2026; HarmonySrc/CinerariumLimitPanel.cs): блок справа от ячеек урны в «Содержимом» - всего,
доступно, до следующей. Позицию по числу ячеек ставит контроллер; вне урны блок скрыт. --> доступно, до следующей. Позицию по числу ячеек ставит контроллер; вне урны блок скрыт. -->
<append xpath="/windows/window[@name='windowBagStorage']"> <append xpath="/windows/window[@name='windowBagStorage']">
@@ -282,6 +314,19 @@
<label depth="7" name="next" pos="10,-50" width="404" height="22" font_size="20" color="200,200,205" /> <label depth="7" name="next" pos="10,-50" width="404" height="22" font_size="20" color="200,200,205" />
</rect> </rect>
</append> </append>
<!-- Чего не хватает ремонтнику (24.10.2026): ячейка с материалом и «!» под блоком лимита урн, только показ
(HarmonySrc/CinerariumRepairNeed.cs). -->
<append xpath="/windows/window[@name='windowBagStorage']">
<rect name="necroRepairNeed" pos="87,-132" width="420" height="75" depth="6" controller="NecromancerTome.NecroRepairNeed, NecromancerHarmony">
<sprite depth="6" pos="0,0" width="420" height="75" color="0,0,0,190" sprite="menu_empty2px" type="sliced" />
<sprite depth="7" pos="0,0" width="75" height="75" sprite="menu_empty3px" color="[black]" type="sliced" fillcenter="false" />
<sprite depth="7" pos="2,-2" width="71" height="71" sprite="menu_empty3px" color="200,40,40,255" type="sliced" fillcenter="false" />
<sprite depth="8" name="icon" pos="37,-37" width="64" height="64" atlas="ItemIconAtlas" pivot="center" foregroundlayer="true" />
<label depth="9" name="excl" pos="7,-2" width="24" height="38" font_size="38" color="235,40,40,255" text="!" effect="outline" foregroundlayer="true" />
<label depth="9" name="count" pos="4,-50" width="66" height="22" font_size="22" justify="right" color="[white]" effect="outline" foregroundlayer="true" />
<label depth="7" pos="85,-14" width="330" height="48" font_size="20" color="[white]" text_key="necroCinerariumRepairNeed" />
</rect>
</append>
<!-- Вкладка «Некромантия» (ui_display.xml): в рюкзаке и на верстаке 10 вкладок вместо 9 - шаг значков 43 -> 39, значки прежние (28). --> <!-- Вкладка «Некромантия» (ui_display.xml): в рюкзаке и на верстаке 10 вкладок вместо 9 - шаг значков 43 -> 39, значки прежние (28). -->
<set xpath="/windows/window[@name='windowCraftingList']//grid[@name='categories']/@cols">10</set> <set xpath="/windows/window[@name='windowCraftingList']//grid[@name='categories']/@cols">10</set>
<set xpath="/windows/window[@name='windowCraftingList']//grid[@name='categories']/@cell_width">39</set> <set xpath="/windows/window[@name='windowCraftingList']//grid[@name='categories']/@cell_width">39</set>
+1
View File
@@ -62,5 +62,6 @@
<!-- Фильтр урны-логиста - в окне хранилища игры (bagStorage), рядом с ячейкой «Содержимого» (windows.xml). --> <!-- Фильтр урны-логиста - в окне хранилища игры (bagStorage), рядом с ячейкой «Содержимого» (windows.xml). -->
<append xpath="/xui/window_group[@name='bagStorage']"> <append xpath="/xui/window_group[@name='bagStorage']">
<window name="necroUrnFilter" /> <window name="necroUrnFilter" />
<window name="necroUrnZone" />
</append> </append>
</xui> </xui>
+27 -10
View File
@@ -1913,16 +1913,35 @@
<property name="CustomIconTint" value="150, 205, 255"/> <property name="CustomIconTint" value="150, 205, 255"/>
</item> </item>
</append> </append>
<!-- Резная кость (#86, 28.10.2026): отдельный инструмент нитей для урн - делает то же, что нож хватом для нитей
(HarmonySrc/CinerariumThreads.cs, IsThreadTool), для тех, кто хват у ножа не нашёл. Наследует нож для нитей:
хват 68 + поза рук мода (нож в левой руке), действия ножа перехвачены нитями. В руке лежит ровно как нож:
модель - копия префаба ножа со своей сеткой (BlenderWork/necroCarvedBone, Unity NecroCarvedBoneSetup.cs,
бандл necroknife): бедренная кость длиной с нож, головка на месте клинка, железный колпачок, кольцо на месте
гарды, руны цвета нитей. Новый предмет - сейвов ножа не касается. -->
<append xpath="/items">
<item name="necroCarvedBone">
<property name="Extends" value="necroWpnBladeNecroKnifeThread"/>
<property name="CreativeMode" value="Player"/>
<property name="Group" value="Tools/Traps,Necromancy"/>
<property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necroknife?necroCarvedBonePrefab.prefab"/>
<property name="CustomIcon" value="necroCarvedBone"/>
<property name="CustomIconTint" value="255, 255, 255"/>
<property name="DescriptionKey" value="necroCarvedBoneDesc"/>
<property name="ShowQuality" value="false"/>
<property name="EconomicValue" value="0"/>
</item>
</append>
<!-- Магические ингредиенты (18.10.2026): магическая пыль, хранилище магии (колба-«батарейка» в латунных клеммах с <!-- Магические ингредиенты (18.10.2026): магическая пыль, хранилище магии (колба-«батарейка» в латунных клеммах с
маркировкой MAGIA KONSERVUJO / KAPACITO ✶ 30) и повреждённое хранилище. Пыль - из ступки (4 из повреждённого, маркировкой MAGIA KONSERVUJO / KAPACITO ✶ 30) и повреждённое хранилище. Пыль - из ступки (4 из повреждённого,
30 из целого); пыль - будущий ингредиент камня портала, целое хранилище - деталь батарей/портала, сами хранилища 30 из целого); пыль - будущий ингредиент камня портала, целое хранилище - деталь батарей/портала, сами хранилища
позже появятся в префабе. Модели в руке пока ванильный мешок; свои модели - _private/.../necroMagic/necroMagic.blend. --> позже появятся в префабе. Модели свои (26.10.2026, #40): necroMagic.blend -> бандл necromagic (Unity NecroMagicSetup.cs);
хват книги (21) - пыль горкой лежит на ладони (указание 26.10), колба лежит вдоль ладони. -->
<append xpath="/items"> <append xpath="/items">
<item name="necroMagicDust"> <item name="necroMagicDust">
<property name="DescriptionKey" value="necroMagicDustDesc"/> <property name="DescriptionKey" value="necroMagicDustDesc"/>
<property name="HoldType" value="45"/> <property name="HoldType" value="21"/>
<property name="Meshfile" value="@:Other/Items/Misc/sackPrefab.prefab"/> <property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necromagic?necroMagicDustPrefab.prefab"/>
<property name="DropMeshfile" value="@:Other/Items/Misc/sack_droppedPrefab.prefab"/>
<property name="Material" value="MresourceCrop"/> <property name="Material" value="MresourceCrop"/>
<property name="Stacknumber" value="500"/> <property name="Stacknumber" value="500"/>
<property name="EconomicValue" value="20"/> <property name="EconomicValue" value="20"/>
@@ -1930,9 +1949,8 @@
</item> </item>
<item name="necroManaVessel"> <item name="necroManaVessel">
<property name="DescriptionKey" value="necroManaVesselDesc"/> <property name="DescriptionKey" value="necroManaVesselDesc"/>
<property name="HoldType" value="45"/> <property name="HoldType" value="21"/>
<property name="Meshfile" value="@:Other/Items/Misc/sackPrefab.prefab"/> <property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necromagic?necroManaVesselPrefab.prefab"/>
<property name="DropMeshfile" value="@:Other/Items/Misc/sack_droppedPrefab.prefab"/>
<property name="Material" value="MresourceCrop"/> <property name="Material" value="MresourceCrop"/>
<property name="Stacknumber" value="10"/> <property name="Stacknumber" value="10"/>
<property name="EconomicValue" value="300"/> <property name="EconomicValue" value="300"/>
@@ -1940,9 +1958,8 @@
</item> </item>
<item name="necroManaVesselBroken"> <item name="necroManaVesselBroken">
<property name="DescriptionKey" value="necroManaVesselBrokenDesc"/> <property name="DescriptionKey" value="necroManaVesselBrokenDesc"/>
<property name="HoldType" value="45"/> <property name="HoldType" value="21"/>
<property name="Meshfile" value="@:Other/Items/Misc/sackPrefab.prefab"/> <property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necromagic?necroManaVesselBrokenPrefab.prefab"/>
<property name="DropMeshfile" value="@:Other/Items/Misc/sack_droppedPrefab.prefab"/>
<property name="Material" value="MresourceCrop"/> <property name="Material" value="MresourceCrop"/>
<property name="Stacknumber" value="10"/> <property name="Stacknumber" value="10"/>
<property name="EconomicValue" value="40"/> <property name="EconomicValue" value="40"/>
+1 -1
View File
@@ -183,7 +183,7 @@
<unlock_entry item="necroModKnifeGravesRepose" unlock_tier="4"/> <unlock_entry item="necroModKnifeGravesRepose" unlock_tier="4"/>
<!-- 22.10.2026: урны - одной иконкой с recipes= (как комплекты брони в ванили): в окне навыка у строки только 14 иконок, в группе было 17 - последние урны не показывались («Не все рецепты цинерария доступны»). Клик по иконке - все урны списком. --> <!-- 22.10.2026: урны - одной иконкой с recipes= (как комплекты брони в ванили): в окне навыка у строки только 14 иконок, в группе было 17 - последние урны не показывались («Не все рецепты цинерария доступны»). Клик по иконке - все урны списком. -->
<unlock_entry item="necroModKnifeDarkSense" unlock_tier="5"/> <unlock_entry item="necroModKnifeDarkSense" unlock_tier="5"/>
<unlock_entry item="necroCinerarium" unlock_tier="5" recipes="necroCinerarium,necroCinerariumLogist,necroCinerariumWorker,necroCinerariumRepair,necroCinerariumLoot,necroCinerariumProspector"/> <unlock_entry item="necroCinerarium" unlock_tier="5" recipes="necroCinerarium,necroCarvedBone,necroCinerariumLogist,necroCinerariumWorker,necroCinerariumRepair,necroCinerariumLoot,necroCinerariumProspector"/>
</display_entry> </display_entry>
<!-- Ступенчатая разблокировка внутри группы. ПЕРЕРАСПРЕДЕЛЕНО 2026-09-09: четыре из <!-- Ступенчатая разблокировка внутри группы. ПЕРЕРАСПРЕДЕЛЕНО 2026-09-09: четыре из
шести модов ножа (вода/еда/покой/чутьё) уехали отсюда в группу 1 - см. большой шести модов ножа (вода/еда/покой/чутьё) уехали отсюда в группу 1 - см. большой
+35 -92
View File
@@ -33,7 +33,7 @@
weapon - "он не является начальным оружием... больше про комфорт" - so the same weapon - "он не является начальным оружием... больше про комфорт" - so the same
circular-dependency concern doesn't apply, and the user wants it to actually cost a circular-dependency concern doesn't apply, and the user wants it to actually cost a
real chunk of ash rather than a token amount. --> real chunk of ash rather than a token amount. -->
<recipe name="thrownStonePortalBlue" count="1" tags="packMuleCrafting"> <recipe name="thrownStonePortalBlue" count="1" craft_time="31" tags="packMuleCrafting">
<ingredient name="resourceRockSmall" count="1"/> <ingredient name="resourceRockSmall" count="1"/>
<ingredient name="drinkJarPureMineralWater" count="3"/> <ingredient name="drinkJarPureMineralWater" count="3"/>
<ingredient name="resourceYuccaFibers" count="15"/> <ingredient name="resourceYuccaFibers" count="15"/>
@@ -90,7 +90,7 @@
their current HP per unit to make (NecromancerBloodPatch.cs) - this recipe shouldn't their current HP per unit to make (NecromancerBloodPatch.cs) - this recipe shouldn't
demand many. --> demand many. -->
<append xpath="/recipes"> <append xpath="/recipes">
<recipe name="necroHeresyPyramid" count="1" craft_area="workbench" tags="workbenchCrafting,necroNecromancyAdept"> <recipe name="necroHeresyPyramid" count="1" craft_area="workbench" craft_time="191" tags="workbenchCrafting,necroNecromancyAdept">
<ingredient name="resourceZombieAsh" count="1200"/> <ingredient name="resourceZombieAsh" count="1200"/>
<ingredient name="resourceRockSmall" count="300"/> <ingredient name="resourceRockSmall" count="300"/>
<ingredient name="resourceYuccaFibers" count="200"/> <ingredient name="resourceYuccaFibers" count="200"/>
@@ -124,15 +124,15 @@
personal crafting). workbenchCrafting is just the same UI-categorization tag vanilla's personal crafting). workbenchCrafting is just the same UI-categorization tag vanilla's
own workbench recipes carry alongside craft_area, not a separate gate. --> own workbench recipes carry alongside craft_area, not a separate gate. -->
<append xpath="/recipes"> <append xpath="/recipes">
<recipe name="bookSummonZombieDog" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyLvl1300"> <!-- 26.10.2026: окно рецепта показывает только 8 компонентов - девятый (прах) был не виден, книга «не делалась».
Сокращено до 7 (без монет и ткани). Время - вчетверо меньше расчётного игрой (50 гнилого мяса по 95 с = 80 мин). -->
<recipe name="bookSummonZombieDog" count="1" craft_area="workbench" craft_time="1210" tags="learnable,workbenchCrafting,necroNecromancyLvl1300">
<ingredient name="foodRottingFlesh" count="50"/> <ingredient name="foodRottingFlesh" count="50"/>
<ingredient name="medicalBloodBag" count="3"/> <ingredient name="medicalBloodBag" count="3"/>
<ingredient name="drinkJarBoiledWater" count="4"/> <ingredient name="drinkJarBoiledWater" count="4"/>
<ingredient name="resourceYuccaFibers" count="50"/> <ingredient name="resourceYuccaFibers" count="50"/>
<ingredient name="resourceCloth" count="3"/>
<ingredient name="resourceLeather" count="10"/> <ingredient name="resourceLeather" count="10"/>
<ingredient name="resourceBone" count="40"/> <ingredient name="resourceBone" count="40"/>
<ingredient name="casinoCoin" count="5"/>
<ingredient name="resourceZombieAsh" count="10"/> <ingredient name="resourceZombieAsh" count="10"/>
</recipe> </recipe>
</append> </append>
@@ -160,7 +160,7 @@
REDISTRIBUTED 2026-08-30, was necroNecromancyLvl200 before) - not specified by the user, REDISTRIBUTED 2026-08-30, was necroNecromancyLvl200 before) - not specified by the user,
guessed consistent with the other "advanced ritual" summon items. --> guessed consistent with the other "advanced ritual" summon items. -->
<append xpath="/recipes"> <append xpath="/recipes">
<recipe name="bookBanshee" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyNecromancer"> <recipe name="bookBanshee" count="1" craft_area="workbench" craft_time="241" tags="learnable,workbenchCrafting,necroNecromancyNecromancer">
<ingredient name="resourcePaper" count="15"/> <ingredient name="resourcePaper" count="15"/>
<ingredient name="resourceGlue" count="3"/> <ingredient name="resourceGlue" count="3"/>
<ingredient name="foodRottingFlesh" count="10"/> <ingredient name="foodRottingFlesh" count="10"/>
@@ -453,15 +453,10 @@
именно тогда (правило «порог по нужде, а не по силе»). Тег и unlock_level группы 2 в именно тогда (правило «порог по нужде, а не по силе»). Тег и unlock_level группы 2 в
progression.xml обязаны совпадать. --> progression.xml обязаны совпадать. -->
<append xpath="/recipes"> <append xpath="/recipes">
<!-- 22.10.2026 (#66): котёл кованый - в кузню («Он весь из себя кованный, так что будет логично»): железо вместо дерева <!-- 22.10.2026 (#66): котёл кованый («Он весь из себя кованный, так что будет логично»): железо вместо дерева
и юкки, кости и прах - ноги и содержимое; две версии кузни (плавильня / лом). --> и юкки, кости и прах - ноги и содержимое. 26.10: из кузни - на верстак, на ломе: в кузне делается только то, что
<recipe name="necroGeoOssuary" count="1" craft_area="forge" craft_time="60" material_based="true" tags="learnable,necroNecromancyJourneyman,use_smelter"> в неё вплавлено, а кости и прах в неё не кладутся (стрим 26.10). -->
<ingredient name="unit_iron" count="60"/> <recipe name="necroGeoOssuary" count="1" craft_area="workbench" craft_time="60" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
<ingredient name="resourceBone" count="400"/>
<ingredient name="resourceZombieAsh" count="200"/>
<ingredient name="resourceNecromancerBlood" count="1"/>
</recipe>
<recipe name="necroGeoOssuary" count="1" craft_area="forge" craft_time="60" tags="learnable,necroNecromancyJourneyman,replace_smelter">
<ingredient name="resourceScrapIron" count="60"/> <ingredient name="resourceScrapIron" count="60"/>
<ingredient name="resourceBone" count="400"/> <ingredient name="resourceBone" count="400"/>
<ingredient name="resourceZombieAsh" count="200"/> <ingredient name="resourceZombieAsh" count="200"/>
@@ -473,7 +468,7 @@
гео-оссуария: транспорт нужен, когда карта уже велика для пеших ходок. Состав - по гео-оссуария: транспорт нужен, когда карта уже велика для пеших ходок. Состав - по
указанию пользователя: кости, гнилое мясо, прах, кожа жертвы, два флакона крови. --> указанию пользователя: кости, гнилое мясо, прах, кожа жертвы, два флакона крови. -->
<append xpath="/recipes"> <append xpath="/recipes">
<recipe name="necroBoneSteedPlaceable" count="1" craft_time="120" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyJourneyman"> <recipe name="necroBoneSteedPlaceable" count="1" craft_time="30" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
<ingredient name="resourceBone" count="300"/> <ingredient name="resourceBone" count="300"/>
<ingredient name="foodRottingFlesh" count="30"/> <ingredient name="foodRottingFlesh" count="30"/>
<ingredient name="resourceZombieAsh" count="150"/> <ingredient name="resourceZombieAsh" count="150"/>
@@ -526,7 +521,7 @@
<ingredient name="resourceYuccaFibers" count="15"/> <ingredient name="resourceYuccaFibers" count="15"/>
<ingredient name="resourceZombieAsh" count="20"/> <ingredient name="resourceZombieAsh" count="20"/>
</recipe> </recipe>
<recipe name="necroHeresyPyramid" count="1" craft_area="workbench" tags="workbenchCrafting,necroNecromancyAdept"> <recipe name="necroHeresyPyramid" count="1" craft_area="workbench" craft_time="191" tags="workbenchCrafting,necroNecromancyAdept">
<ingredient name="resourceZombieAsh" count="1200"/> <ingredient name="resourceZombieAsh" count="1200"/>
<ingredient name="resourceRockSmall" count="300"/> <ingredient name="resourceRockSmall" count="300"/>
<ingredient name="resourceYuccaFibers" count="200"/> <ingredient name="resourceYuccaFibers" count="200"/>
@@ -560,21 +555,16 @@
<ingredient name="medicalBloodBag" count="4"/> <ingredient name="medicalBloodBag" count="4"/>
<ingredient name="resourceNecroBlackRoseJuice" count="1"/> <ingredient name="resourceNecroBlackRoseJuice" count="1"/>
</recipe> </recipe>
<!-- 22.10.2026 (#66): котёл кованый - в кузню («Он весь из себя кованный, так что будет логично»): железо вместо дерева <!-- 22.10.2026 (#66): котёл кованый («Он весь из себя кованный, так что будет логично»): железо вместо дерева
и юкки, кости и прах - ноги и содержимое; две версии кузни (плавильня / лом). --> и юкки, кости и прах - ноги и содержимое. 26.10: из кузни - на верстак, на ломе: в кузне делается только то, что
<recipe name="necroGeoOssuary" count="1" craft_area="forge" craft_time="60" material_based="true" tags="learnable,necroNecromancyJourneyman,use_smelter"> в неё вплавлено, а кости и прах в неё не кладутся (стрим 26.10). -->
<ingredient name="unit_iron" count="60"/> <recipe name="necroGeoOssuary" count="1" craft_area="workbench" craft_time="60" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
<ingredient name="resourceBone" count="400"/>
<ingredient name="resourceZombieAsh" count="200"/>
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
</recipe>
<recipe name="necroGeoOssuary" count="1" craft_area="forge" craft_time="60" tags="learnable,necroNecromancyJourneyman,replace_smelter">
<ingredient name="resourceScrapIron" count="60"/> <ingredient name="resourceScrapIron" count="60"/>
<ingredient name="resourceBone" count="400"/> <ingredient name="resourceBone" count="400"/>
<ingredient name="resourceZombieAsh" count="200"/> <ingredient name="resourceZombieAsh" count="200"/>
<ingredient name="resourceNecroBlackRoseJuice" count="1"/> <ingredient name="resourceNecroBlackRoseJuice" count="1"/>
</recipe> </recipe>
<recipe name="necroBoneSteedPlaceable" count="1" craft_time="120" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyJourneyman"> <recipe name="necroBoneSteedPlaceable" count="1" craft_time="30" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
<ingredient name="resourceBone" count="300"/> <ingredient name="resourceBone" count="300"/>
<ingredient name="foodRottingFlesh" count="30"/> <ingredient name="foodRottingFlesh" count="30"/>
<ingredient name="resourceZombieAsh" count="150"/> <ingredient name="resourceZombieAsh" count="150"/>
@@ -610,48 +600,31 @@
</recipe> </recipe>
<!-- Урна-логист (16.10: «Рецепт с набором для шитья») - набор для шитья; копия на соке чёрной розы. <!-- Урна-логист (16.10: «Рецепт с набором для шитья») - набор для шитья; копия на соке чёрной розы.
18.10: своя модель - латунное кадило («Сейчас кадило выглядит бронзовым. Учти это в рецепте, используя латунь и кузню»): 18.10: своя модель - латунное кадило («Сейчас кадило выглядит бронзовым. Учти это в рецепте, используя латунь и кузню»):
вместо глины латунь, плюс немного песка и угля («добавь в рецепт немного песка и угля») и воск - покрытие, чтобы латунь не тускнела («Это кстати будет шикарным поводом ставить пасеку»), делается в кузне. Как у кузнечных рецептов игры - две версии: с плавильней (use_smelter, unit_brass) вместо глины латунь, плюс немного песка и угля («добавь в рецепт немного песка и угля») и воск - покрытие, чтобы латунь не тускнела («Это кстати будет шикарным поводом ставить пасеку»), делается в кузне.
и без неё (replace_smelter, латунный лом). --> 26.10: на верстаке, на латунном ломе, без песка и угля - кузня не берёт кости и прах (стрим 26.10). -->
<recipe name="necroCinerariumLogist" count="1" craft_area="forge" craft_time="60" material_based="true" tags="learnable,necroNecromancyLvl300,use_smelter"> <recipe name="necroCinerariumLogist" count="1" craft_area="workbench" craft_time="60" tags="learnable,workbenchCrafting,necroNecromancyLvl300">
<ingredient name="unit_brass" count="50"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecromancerBlood" count="1"/>
<ingredient name="resourceSewingKit" count="1"/>
<ingredient name="resourceCrushedSand" count="10"/>
<ingredient name="resourceCoal" count="5"/>
<ingredient name="resourceBeeswax" count="2"/>
</recipe>
<recipe name="necroCinerariumLogist" count="1" craft_area="forge" craft_time="60" material_based="true" tags="learnable,necroNecromancyLvl300,use_smelter">
<ingredient name="unit_brass" count="50"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
<ingredient name="resourceSewingKit" count="1"/>
<ingredient name="resourceCrushedSand" count="10"/>
<ingredient name="resourceCoal" count="5"/>
<ingredient name="resourceBeeswax" count="2"/>
</recipe>
<recipe name="necroCinerariumLogist" count="1" craft_area="forge" craft_time="60" tags="learnable,necroNecromancyLvl300,replace_smelter">
<ingredient name="resourceScrapBrass" count="50"/> <ingredient name="resourceScrapBrass" count="50"/>
<ingredient name="resourceZombieAsh" count="100"/> <ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/> <ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecromancerBlood" count="1"/> <ingredient name="resourceNecromancerBlood" count="1"/>
<ingredient name="resourceSewingKit" count="1"/> <ingredient name="resourceSewingKit" count="1"/>
<ingredient name="resourceCrushedSand" count="10"/>
<ingredient name="resourceCoal" count="5"/>
<ingredient name="resourceBeeswax" count="2"/> <ingredient name="resourceBeeswax" count="2"/>
</recipe> </recipe>
<recipe name="necroCinerariumLogist" count="1" craft_area="forge" craft_time="60" tags="learnable,necroNecromancyLvl300,replace_smelter"> <recipe name="necroCinerariumLogist" count="1" craft_area="workbench" craft_time="60" tags="learnable,workbenchCrafting,necroNecromancyLvl300">
<ingredient name="resourceScrapBrass" count="50"/> <ingredient name="resourceScrapBrass" count="50"/>
<ingredient name="resourceZombieAsh" count="100"/> <ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/> <ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecroBlackRoseJuice" count="1"/> <ingredient name="resourceNecroBlackRoseJuice" count="1"/>
<ingredient name="resourceSewingKit" count="1"/> <ingredient name="resourceSewingKit" count="1"/>
<ingredient name="resourceCrushedSand" count="10"/>
<ingredient name="resourceCoal" count="5"/>
<ingredient name="resourceBeeswax" count="2"/> <ingredient name="resourceBeeswax" count="2"/>
</recipe> </recipe>
<!-- Резная кость (#86, 28.10.2026) - инструмент нитей: «Рецепт простой - одна кость и 10 железа» (кованое), «В рецепт
добавь щепотку праха». В рюкзаке, открывается вместе с урнами (progression.xml, та же строка). -->
<recipe name="necroCarvedBone" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
<ingredient name="resourceBone" count="1"/>
<ingredient name="resourceForgedIron" count="10"/>
<ingredient name="resourceZombieAsh" count="5"/>
</recipe>
<!-- Урна рабочего (18.10: «Молоток годится») - рецепт урны и молоток; копия на соке чёрной розы. --> <!-- Урна рабочего (18.10: «Молоток годится») - рецепт урны и молоток; копия на соке чёрной розы. -->
<recipe name="necroCinerariumWorker" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300"> <recipe name="necroCinerariumWorker" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
<ingredient name="resourceClayLump" count="30"/> <ingredient name="resourceClayLump" count="30"/>
@@ -685,24 +658,8 @@
<ingredient name="resourceScrapBrass" count="5"/> <ingredient name="resourceScrapBrass" count="5"/>
</recipe> </recipe>
<!-- Урна лутера (18.10) - железная банка с крышкой-кадилом, шов пропаян свинцом («в рецепте железо и немного свинца»): <!-- Урна лутера (18.10) - железная банка с крышкой-кадилом, шов пропаян свинцом («в рецепте железо и немного свинца»):
в кузне, железо 40 и свинец 5 вместо глины, отмычка; две версии кузни (плавильня / лом), на крови и на соке. --> железный и свинцовый лом вместо глины, отмычка; на крови и на соке. 26.10: из кузни - на верстак (кузня не берёт кости и прах). -->
<recipe name="necroCinerariumLoot" count="1" craft_area="forge" craft_time="60" material_based="true" tags="learnable,necroNecromancyLvl300,use_smelter"> <recipe name="necroCinerariumLoot" count="1" craft_area="workbench" craft_time="60" tags="learnable,workbenchCrafting,necroNecromancyLvl300">
<ingredient name="unit_iron" count="40"/>
<ingredient name="unit_lead" count="5"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecromancerBlood" count="1"/>
<ingredient name="resourceLockPick" count="1"/>
</recipe>
<recipe name="necroCinerariumLoot" count="1" craft_area="forge" craft_time="60" material_based="true" tags="learnable,necroNecromancyLvl300,use_smelter">
<ingredient name="unit_iron" count="40"/>
<ingredient name="unit_lead" count="5"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
<ingredient name="resourceLockPick" count="1"/>
</recipe>
<recipe name="necroCinerariumLoot" count="1" craft_area="forge" craft_time="60" tags="learnable,necroNecromancyLvl300,replace_smelter">
<ingredient name="resourceScrapIron" count="40"/> <ingredient name="resourceScrapIron" count="40"/>
<ingredient name="resourceScrapLead" count="5"/> <ingredient name="resourceScrapLead" count="5"/>
<ingredient name="resourceZombieAsh" count="100"/> <ingredient name="resourceZombieAsh" count="100"/>
@@ -710,7 +667,7 @@
<ingredient name="resourceNecromancerBlood" count="1"/> <ingredient name="resourceNecromancerBlood" count="1"/>
<ingredient name="resourceLockPick" count="1"/> <ingredient name="resourceLockPick" count="1"/>
</recipe> </recipe>
<recipe name="necroCinerariumLoot" count="1" craft_area="forge" craft_time="60" tags="learnable,necroNecromancyLvl300,replace_smelter"> <recipe name="necroCinerariumLoot" count="1" craft_area="workbench" craft_time="60" tags="learnable,workbenchCrafting,necroNecromancyLvl300">
<ingredient name="resourceScrapIron" count="40"/> <ingredient name="resourceScrapIron" count="40"/>
<ingredient name="resourceScrapLead" count="5"/> <ingredient name="resourceScrapLead" count="5"/>
<ingredient name="resourceZombieAsh" count="100"/> <ingredient name="resourceZombieAsh" count="100"/>
@@ -718,30 +675,16 @@
<ingredient name="resourceNecroBlackRoseJuice" count="1"/> <ingredient name="resourceNecroBlackRoseJuice" count="1"/>
<ingredient name="resourceLockPick" count="1"/> <ingredient name="resourceLockPick" count="1"/>
</recipe> </recipe>
<!-- Урна старателя (22.10, #64) - бронзовая катушка: в кузне латунь 40 вместо железа и свинца, железная кирка вместо отмычки; <!-- Урна старателя (22.10, #64) - бронзовая катушка: латунный лом 40 вместо железа и свинца, железная кирка вместо отмычки;
две версии кузни (плавильня / лом), на крови и на соке. Открывается вместе с остальными урнами. --> на крови и на соке. Открывается вместе с остальными урнами. 26.10: из кузни - на верстак (кузня не берёт кости и прах). -->
<recipe name="necroCinerariumProspector" count="1" craft_area="forge" craft_time="60" material_based="true" tags="learnable,necroNecromancyLvl300,use_smelter"> <recipe name="necroCinerariumProspector" count="1" craft_area="workbench" craft_time="60" tags="learnable,workbenchCrafting,necroNecromancyLvl300">
<ingredient name="unit_brass" count="40"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecromancerBlood" count="1"/>
<ingredient name="meleeToolPickT1IronPickaxe" count="1"/>
</recipe>
<recipe name="necroCinerariumProspector" count="1" craft_area="forge" craft_time="60" material_based="true" tags="learnable,necroNecromancyLvl300,use_smelter">
<ingredient name="unit_brass" count="40"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
<ingredient name="meleeToolPickT1IronPickaxe" count="1"/>
</recipe>
<recipe name="necroCinerariumProspector" count="1" craft_area="forge" craft_time="60" tags="learnable,necroNecromancyLvl300,replace_smelter">
<ingredient name="resourceScrapBrass" count="40"/> <ingredient name="resourceScrapBrass" count="40"/>
<ingredient name="resourceZombieAsh" count="100"/> <ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/> <ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecromancerBlood" count="1"/> <ingredient name="resourceNecromancerBlood" count="1"/>
<ingredient name="meleeToolPickT1IronPickaxe" count="1"/> <ingredient name="meleeToolPickT1IronPickaxe" count="1"/>
</recipe> </recipe>
<recipe name="necroCinerariumProspector" count="1" craft_area="forge" craft_time="60" tags="learnable,necroNecromancyLvl300,replace_smelter"> <recipe name="necroCinerariumProspector" count="1" craft_area="workbench" craft_time="60" tags="learnable,workbenchCrafting,necroNecromancyLvl300">
<ingredient name="resourceScrapBrass" count="40"/> <ingredient name="resourceScrapBrass" count="40"/>
<ingredient name="resourceZombieAsh" count="100"/> <ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/> <ingredient name="resourceBone" count="10"/>
+2 -2
View File
@@ -75,14 +75,14 @@ namespace NecromancerTome
} }
} }
/// <summary>Череп и нож (меч прилива) - модели предметов: в руке и на земле.</summary> /// <summary>Череп, нож (меч прилива) и резная кость (#86) - модели предметов: в руке и на земле.</summary>
[HarmonyPatch(typeof(ItemClass), nameof(ItemClass.CloneModel))] [HarmonyPatch(typeof(ItemClass), nameof(ItemClass.CloneModel))]
public static class Patch_ItemClass_CloneModel_BundleShaderFix public static class Patch_ItemClass_CloneModel_BundleShaderFix
{ {
public static void Postfix(ItemClass __instance, AssetManagement.InstanceHandle<GameObject> __result) public static void Postfix(ItemClass __instance, AssetManagement.InstanceHandle<GameObject> __result)
{ {
string n = __instance?.Name; string n = __instance?.Name;
if (n != "necroSkull" && n != SnakeMode.KnifeName) if (n != "necroSkull" && n != SnakeMode.KnifeName && n != CinerariumThreads.BoneName)
{ {
return; return;
} }
+74 -11
View File
@@ -264,8 +264,9 @@ namespace NecromancerTome
AppendVia(via, l); AppendVia(via, l);
} }
CinerariumWriteHelper.WriteInts(_bw, via.ToArray()); CinerariumWriteHelper.WriteInts(_bw, via.ToArray());
// v6: ремонтник/лутер - сторона зоны, идёт ли проход, где курсор обхода, кто нажал кнопку. // v6: ремонтник/лутер - сторона зоны, идёт ли проход, где курсор обхода, кто нажал кнопку; пятое (28.10, #84) -
CinerariumWriteHelper.WriteInts(_bw, new[] { ZoneSide, ZoneRunning ? 1 : 0, ZoneCursor, ZonePressedBy }); // выключенные квадраты зоны. Числа идут со счётчиком - старая DLL пятое просто пропустит, версия та же.
CinerariumWriteHelper.WriteInts(_bw, new[] { ZoneSide, ZoneRunning ? 1 : 0, ZoneCursor, ZonePressedBy, ZoneOff });
} }
static void AppendVia(List<int> _dst, List<Vector3i> _l) static void AppendVia(List<int> _dst, List<Vector3i> _l)
@@ -411,6 +412,7 @@ namespace NecromancerTome
ZoneRunning = false; ZoneRunning = false;
ZoneCursor = 0; ZoneCursor = 0;
ZonePressedBy = -1; ZonePressedBy = -1;
ZoneOff = 0;
if (version >= 6) if (version >= 6)
{ {
int n = _br.ReadInt32(); int n = _br.ReadInt32();
@@ -423,6 +425,7 @@ namespace NecromancerTome
ZoneRunning = n > 1 && v[1] != 0; ZoneRunning = n > 1 && v[1] != 0;
ZoneCursor = n > 2 ? v[2] : 0; ZoneCursor = n > 2 ? v[2] : 0;
ZonePressedBy = n > 3 ? v[3] : -1; ZonePressedBy = n > 3 ? v[3] : -1;
ZoneOff = n > 4 ? v[4] : 0;
} }
_threadsDirty = true; _threadsDirty = true;
if (version > SaveVersion) if (version > SaveVersion)
@@ -1099,19 +1102,11 @@ namespace NecromancerTome
} }
_bounds = new GameObject("NecroCinerariumBounds"); _bounds = new GameObject("NecroCinerariumBounds");
_bounds.transform.SetParent(t, false); _bounds.transform.SetParent(t, false);
LineRenderer lr = _bounds.AddComponent<LineRenderer>();
lr.useWorldSpace = false;
lr.widthMultiplier = 0.06f;
if (_boundsMat == null) if (_boundsMat == null)
{ {
_boundsMat = new Material(Shader.Find("Sprites/Default")) { name = "NecroCinerariumBounds" }; _boundsMat = new Material(Shader.Find("Sprites/Default")) { name = "NecroCinerariumBounds" };
} }
lr.sharedMaterial = _boundsMat; LineRenderer lr = BoundsLine(_bounds, BoundsOn);
Color col = new Color(0.55f, 0.9f, 0.75f, 0.85f);
lr.startColor = col;
lr.endColor = col;
lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
lr.receiveShadows = false;
// Углы по граням блоков: x и z от -2.5 до +2.5 от центра урны, y от низа блока под ней до верха +4. Большая зона // Углы по граням блоков: x и z от -2.5 до +2.5 от центра урны, y от низа блока под ней до верха +4. Большая зона
// (ремонтник, лутер) - сторона ZoneSideNow, по высоте весь столб мира. // (ремонтник, лутер) - сторона ZoneSideNow, по высоте весь столб мира.
@@ -1129,6 +1124,74 @@ namespace NecromancerTome
{ {
lr.SetPosition(i, t.InverseTransformPoint(path[i])); lr.SetPosition(i, t.InverseTransformPoint(path[i]));
} }
if (IsBigZone)
{
SpawnSectorLines(t, c, x0, z0, y0, y1);
}
}
static readonly Color BoundsOn = new Color(0.55f, 0.9f, 0.75f, 0.85f);
static readonly Color BoundsOff = new Color(0.95f, 0.3f, 0.25f, 0.85f);
/// <summary>Ломаная границ - дочерний объект _parent (уходит с ним в RemoveBounds), материал общий.</summary>
static LineRenderer BoundsLine(GameObject _parent, Color _col)
{
GameObject go = _parent;
if (go.GetComponent<LineRenderer>() != null)
{
go = new GameObject("NecroCinerariumBoundsLine");
go.transform.SetParent(_parent.transform, false);
}
LineRenderer lr = go.AddComponent<LineRenderer>();
lr.useWorldSpace = false;
lr.widthMultiplier = 0.06f;
lr.sharedMaterial = _boundsMat;
lr.startColor = _col;
lr.endColor = _col;
lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
lr.receiveShadows = false;
return lr;
}
/// <summary>Квадраты большой зоны (#84): на уровне пола урны - рамка каждого квадрата (включён - зелёная, выключен - красная
/// с крестом), чуть отступив внутрь, чтобы соседние не сливались; на границах квадратов - вертикали на всю высоту зоны.</summary>
void SpawnSectorLines(Transform _t, Vector3 _c, float _x0, float _z0, float _y0, float _y1)
{
int side = ZoneSideNow;
float y = _c.y + 0.05f, inset = 0.2f;
float Edge(float _from, int _k) => _from + BandStart(_k, side);
for (int sz = 0; sz < ZoneGrid; sz++)
{
for (int sx = 0; sx < ZoneGrid; sx++)
{
bool on = SectorOn(sx + sz * ZoneGrid);
float ax = Edge(_x0, sx) + inset, bx = Edge(_x0, sx + 1) - inset;
float az = Edge(_z0, sz) + inset, bz = Edge(_z0, sz + 1) - inset;
Vector3 p0 = new Vector3(ax, y, az), p1 = new Vector3(bx, y, az), p2 = new Vector3(bx, y, bz), p3 = new Vector3(ax, y, bz);
Vector3[] path = on ? new[] { p0, p1, p2, p3, p0 } : new[] { p0, p1, p2, p3, p0, p2, p1, p3 };
LineRenderer lr = BoundsLine(_bounds, on ? BoundsOn : BoundsOff);
lr.positionCount = path.Length;
for (int i = 0; i < path.Length; i++)
{
lr.SetPosition(i, _t.InverseTransformPoint(path[i]));
}
}
}
// Вертикали - во всех узлах сетки, кроме углов зоны (их уже рисует рамка).
for (int kz = 0; kz <= ZoneGrid; kz++)
{
for (int kx = 0; kx <= ZoneGrid; kx++)
{
if ((kx == 0 || kx == ZoneGrid) && (kz == 0 || kz == ZoneGrid))
{
continue;
}
LineRenderer lr = BoundsLine(_bounds, BoundsOn);
lr.positionCount = 2;
lr.SetPosition(0, _t.InverseTransformPoint(new Vector3(Edge(_x0, kx), _y0, Edge(_z0, kz))));
lr.SetPosition(1, _t.InverseTransformPoint(new Vector3(Edge(_x0, kx), _y1, Edge(_z0, kz))));
}
}
} }
public void RemoveBounds() public void RemoveBounds()
+107 -14
View File
@@ -119,6 +119,47 @@ namespace NecromancerTome
break; break;
} }
} }
// 2а. Кузня: вход сырья держать полным (24.10, пользователь: «Места не должны пустовать. Нужно чтобы заполнены были
// обе ячейки»). Раньше сырьё подкладывалось, только когда переплавленного не хватало даже на одну штуку, - кузня
// плавила остатки, и дух ставил цемент по одному. Подкладка - ДО изготовления (после него очередь до неё не
// доходила: переплавленного каждый шаг хватало на одну штуку), но только в пустую или наполовину опустевшую ячейку -
// кузня плавит непрерывно, и иначе подкладка отнимала бы все шаги.
if (hasSample)
{
string feedName = sample.itemValue.ItemClass?.GetItemName();
foreach (Station st in stations)
{
if (!st.Te.isModuleUsed[MaterialModule] || !OutputHasRoom(st.Te, sample) && !outRoom)
{
continue;
}
foreach (Recipe r in CraftingManager.GetRecipes(feedName))
{
if (r.IsScrap || !AreaFits(r, st.Areas) || owner != null && !r.IsUnlocked(owner))
{
continue;
}
foreach (ItemStack ing in r.ingredients)
{
string iname = ing?.itemValue?.ItemClass?.GetItemName();
if (iname == null || UnitSlot(st.Te, iname) < 0)
{
continue;
}
if (FeedMaterial(st, iname, src, _dry, out Vector3i? rawAt, true))
{
_from = rawAt;
if (!_dry)
{
Ignite(st.Te, src, false);
}
return st.Pos;
}
}
}
}
}
if (hasSample) if (hasSample)
{ {
string name = sample.itemValue.ItemClass?.GetItemName(); string name = sample.itemValue.ItemClass?.GetItemName();
@@ -133,13 +174,17 @@ namespace NecromancerTome
{ {
continue; // выход полон и ящики «куда» полны (или их нет) - ниже станция погаснет continue; // выход полон и ящики «куда» полны (или их нет) - ниже станция погаснет
} }
if (InQueue(st.Te, sample.itemValue.type))
{
continue; // партия этого предмета уже в очереди - следующая, когда она кончится
}
foreach (Recipe r in CraftingManager.GetRecipes(name)) foreach (Recipe r in CraftingManager.GetRecipes(name))
{ {
if (r.IsScrap || !AreaFits(r, st.Areas) || !ToolFits(r, st.Te) || owner != null && !r.IsUnlocked(owner)) if (r.IsScrap || !AreaFits(r, st.Areas) || !ToolFits(r, st.Te) || owner != null && !r.IsUnlocked(owner))
{ {
continue; continue;
} }
if (!Ingredients(r, st.Te, src, out Vector3i? srcAt, out string shortUnit)) if (!Ingredients(r, st.Te, src, out Vector3i? srcAt, out string shortUnit, out int batches))
{ {
// Кузня: не хватает переплавленного материала - положить сырьё во вход (плавит сама, с огнём). // Кузня: не хватает переплавленного материала - положить сырьё во вход (плавит сама, с огнём).
if (shortUnit != null && FeedMaterial(st, shortUnit, src, _dry, out Vector3i? rawAt)) if (shortUnit != null && FeedMaterial(st, shortUnit, src, _dry, out Vector3i? rawAt))
@@ -158,10 +203,10 @@ namespace NecromancerTome
{ {
return st.Pos; return st.Pos;
} }
TakeIngredients(r, st.Te, src); TakeIngredients(r, st.Te, src, batches);
Enqueue(st.Te, slot, r); Enqueue(st.Te, slot, r, batches);
Ignite(st.Te, src, false); Ignite(st.Te, src, false);
Debug.Log("[NecromancerTome] Cinerarium: рабочий поставил " + name + " на станцию " + st.Pos); Debug.Log("[NecromancerTome] Cinerarium: рабочий поставил " + name + " x" + batches + " на станцию " + st.Pos);
return st.Pos; return st.Pos;
} }
} }
@@ -289,6 +334,21 @@ namespace NecromancerTome
return -1; return -1;
} }
/// <summary>Есть ли в очереди станции рецепт этого предмета. 24.10, пользователь: партия - до стака, и «если бы их было
/// двое, то тут нужно занимать очередь на ячейку»: один предмет держит в очереди одну ячейку, прочие - другим урнам на
/// той же станции (иначе одна урна забила бы очередь стаками на часы вперёд).</summary>
static bool InQueue(TileEntityWorkstation _te, int _itemType)
{
foreach (RecipeQueueItem q in _te.Queue)
{
if (q != null && q.Recipe != null && q.Multiplier > 0 && q.Recipe.itemValueType == _itemType)
{
return true;
}
}
return false;
}
static bool OutputHasRoom(TileEntityWorkstation _te, ItemStack _sample) static bool OutputHasRoom(TileEntityWorkstation _te, ItemStack _sample)
{ {
foreach (ItemStack s in _te.Output) foreach (ItemStack s in _te.Output)
@@ -325,11 +385,17 @@ namespace NecromancerTome
} }
/// <summary>Хватает ли всего: переплавленное - в кузне, прочее - в ящиках «откуда». _srcAt - ящик, откуда первое; /// <summary>Хватает ли всего: переплавленное - в кузне, прочее - в ящиках «откуда». _srcAt - ящик, откуда первое;
/// _shortUnit - какого переплавленного не хватило (кузне можно подложить сырья).</summary> /// _shortUnit - какого переплавленного не хватило (кузне можно подложить сырья). _batches - сколько раз рецепт ставится
static bool Ingredients(Recipe _r, TileEntityWorkstation _te, List<(Vector3i p, TEFeatureStorage box)> _src, out Vector3i? _srcAt, out string _shortUnit) /// в очередь за одно дело (24.10, пользователь: «в бетономешалку кладётся по одному камушку... ожидается, что он будет
/// класть хотя бы по целому стаку, а если на стак не хватает, то сколько найдёт»): на сколько хватает каждого
/// ингредиента, но не больше стака ни одного ингредиента и не больше стака готового.</summary>
static bool Ingredients(Recipe _r, TileEntityWorkstation _te, List<(Vector3i p, TEFeatureStorage box)> _src, out Vector3i? _srcAt, out string _shortUnit, out int _batches)
{ {
_srcAt = null; _srcAt = null;
_shortUnit = null; _shortUnit = null;
ItemClass outClass = ItemClass.GetForId(_r.itemValueType);
_batches = outClass != null && _r.count > 0 ? Mathf.Max(1, outClass.MaxCount / _r.count) : 1;
_batches = Mathf.Min(_batches, short.MaxValue);
foreach (ItemStack ing in _r.ingredients) foreach (ItemStack ing in _r.ingredients)
{ {
if (ing == null || ing.IsEmpty() || ing.count <= 0) if (ing == null || ing.IsEmpty() || ing.count <= 0)
@@ -345,6 +411,7 @@ namespace NecromancerTome
_shortUnit = iname; _shortUnit = iname;
return false; return false;
} }
_batches = Mathf.Min(_batches, _te.Input[us].count / ing.count);
continue; continue;
} }
int have = 0; int have = 0;
@@ -367,11 +434,14 @@ namespace NecromancerTome
{ {
return false; return false;
} }
int stack = ing.itemValue.ItemClass != null ? ing.itemValue.ItemClass.MaxCount : ing.count;
_batches = Mathf.Min(_batches, Mathf.Max(1, stack / ing.count), have / ing.count);
} }
_batches = Mathf.Max(1, _batches);
return true; return true;
} }
static void TakeIngredients(Recipe _r, TileEntityWorkstation _te, List<(Vector3i p, TEFeatureStorage box)> _src) static void TakeIngredients(Recipe _r, TileEntityWorkstation _te, List<(Vector3i p, TEFeatureStorage box)> _src, int _batches)
{ {
ItemStack[] input = _te.Input; ItemStack[] input = _te.Input;
bool inputChanged = false; bool inputChanged = false;
@@ -384,11 +454,11 @@ namespace NecromancerTome
int us = UnitSlot(_te, ing.itemValue.ItemClass?.GetItemName() ?? ""); int us = UnitSlot(_te, ing.itemValue.ItemClass?.GetItemName() ?? "");
if (us >= 0) if (us >= 0)
{ {
input[us].count -= ing.count; input[us].count -= ing.count * _batches;
inputChanged = true; inputChanged = true;
continue; continue;
} }
int need = ing.count; int need = ing.count * _batches;
foreach ((Vector3i p, TEFeatureStorage box) s in _src) foreach ((Vector3i p, TEFeatureStorage box) s in _src)
{ {
ItemStack[] grid = s.box.ItemGrid.items; ItemStack[] grid = s.box.ItemGrid.items;
@@ -413,7 +483,7 @@ namespace NecromancerTome
} }
} }
static void Enqueue(TileEntityWorkstation _te, int _slot, Recipe _r) static void Enqueue(TileEntityWorkstation _te, int _slot, Recipe _r, int _batches)
{ {
RecipeQueueItem[] q = _te.Queue; RecipeQueueItem[] q = _te.Queue;
float t = _r.craftingTime > 0f ? _r.craftingTime : 1f; float t = _r.craftingTime > 0f ? _r.craftingTime : 1f;
@@ -421,7 +491,7 @@ namespace NecromancerTome
q[_slot] = new RecipeQueueItem q[_slot] = new RecipeQueueItem
{ {
Recipe = _r, Recipe = _r,
Multiplier = 1, Multiplier = (short)_batches,
CraftingTimeLeft = t, CraftingTimeLeft = t,
OneItemCraftTime = t, OneItemCraftTime = t,
IsCrafting = _slot == q.Length - 1, IsCrafting = _slot == q.Length - 1,
@@ -448,22 +518,27 @@ namespace NecromancerTome
} }
/// <summary>Сырьё для переплавки в unit_X: предмет из ящика «откуда», сделанный из этого материала, - во вход кузни.</summary> /// <summary>Сырьё для переплавки в unit_X: предмет из ящика «откуда», сделанный из этого материала, - во вход кузни.</summary>
static bool FeedMaterial(Station _st, string _unit, List<(Vector3i p, TEFeatureStorage box)> _src, bool _dry, out Vector3i? _at) /// <remarks>_halfEmpty - только в пустую ячейку или заполненную меньше чем наполовину (подкладка впрок, шаг 2а).</remarks>
static bool FeedMaterial(Station _st, string _unit, List<(Vector3i p, TEFeatureStorage box)> _src, bool _dry, out Vector3i? _at, bool _halfEmpty = false)
{ {
_at = null; _at = null;
string mat = _unit.Substring(5); string mat = _unit.Substring(5);
ItemStack[] input = _st.Te.Input; ItemStack[] input = _st.Te.Input;
// 23.10 («вместо камней вплавляет булыжник»): сначала сырьё, у которого нет рецепта (камни, лом), сделанное руками // 23.10 («вместо камней вплавляет булыжник»): сначала сырьё, у которого нет рецепта (камни, лом), сделанное руками
// (булыжник) - только если сырья нет. Раньше - первое подходящее по порядку ячеек. // (булыжник) - только если сырья нет. Раньше - первое подходящее по порядку ячеек.
// 24.10 («работник положил сланец в переплавку в печку»): горючее (нефтяной сланец - сырьё для бензина) не плавится
// вовсе. 24.10 («и снова засунул булыжник!!!»): плавится только сырьё - сделанное руками (булыжник) не плавится
// никогда. Прежняя проверка «нет рецепта» не отличала камни от булыжника: у камня тоже есть рецепты (разбор в лом,
// выплавка из переплавленного) - см. Handmade.
foreach ((Vector3i p, TEFeatureStorage box, int k) s in Ordered(_src, g => foreach ((Vector3i p, TEFeatureStorage box, int k) s in Ordered(_src, g =>
{ {
ItemClass c = g.itemValue.ItemClass; ItemClass c = g.itemValue.ItemClass;
if (c?.MadeOfMaterial?.ForgeCategory == null || !c.MadeOfMaterial.ForgeCategory.EqualsCaseInsensitive(mat) || if (c?.MadeOfMaterial?.ForgeCategory == null || !c.MadeOfMaterial.ForgeCategory.EqualsCaseInsensitive(mat) ||
c.GetItemName().StartsWith("unit_")) c.GetItemName().StartsWith("unit_") || ItemClass.GetFuelValue(g.itemValue) > 0 || Handmade(c))
{ {
return -1; return -1;
} }
return CraftingManager.GetRecipe(c.GetItemName()) == null ? 0 : 1; return 0;
})) }))
{ {
{ {
@@ -474,6 +549,10 @@ namespace NecromancerTome
{ {
bool empty = input[i] == null || input[i].IsEmpty(); bool empty = input[i] == null || input[i].IsEmpty();
int room = empty ? ic.MaxCount : input[i].itemValue.type == g.itemValue.type ? ic.MaxCount - input[i].count : 0; int room = empty ? ic.MaxCount : input[i].itemValue.type == g.itemValue.type ? ic.MaxCount - input[i].count : 0;
if (_halfEmpty && !empty && input[i].count * 2 > ic.MaxCount)
{
room = 0;
}
if (room <= 0) if (room <= 0)
{ {
continue; continue;
@@ -502,6 +581,20 @@ namespace NecromancerTome
return false; return false;
} }
/// <summary>Сделано руками: есть рецепт изготовления - не разбор (IsScrap, wildcard_forge_category) и не выплавка в кузне
/// из переплавленного (material_based). Камни и лом - сырьё (только такие рецепты), булыжник - сделан руками.</summary>
static bool Handmade(ItemClass _c)
{
foreach (Recipe r in CraftingManager.GetRecipes(_c.GetItemName()))
{
if (!r.IsScrap && !r.wildcardForgeCategory && !r.materialBasedRecipe)
{
return true;
}
}
return false;
}
static Vector3i? FuelSource(List<(Vector3i p, TEFeatureStorage box)> _src) static Vector3i? FuelSource(List<(Vector3i p, TEFeatureStorage box)> _src)
{ {
foreach ((Vector3i p, TEFeatureStorage box, int k) s in Ordered(_src, FuelRank)) foreach ((Vector3i p, TEFeatureStorage box, int k) s in Ordered(_src, FuelRank))
+65
View File
@@ -0,0 +1,65 @@
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Чего не хватает ремонтнику (24.10.2026, пользователь: «сделать в его урне блок текста, который показывает какой материал
/// нужен в данный момент? А то не угадать», «Пусть показывает в виде ячейки с материалом», «И восклицательный знак. Чтобы
/// было понятно что именно это требуется»). Ячейка дописана в окно игры windowBagStorage (Config/XUi_InGame/windows.xml)
/// под блоком лимита урн: иконка материала, сколько доложить в ящики с нитями, красный «!». Только показ - не ячейка
/// сумки, взять из неё нечего. Видна у ремонтника, пока хоть один блок ждёт материал (TEFeatureCinerarium.LackType/LackCount) - и когда он чинит
/// остальное.
/// </summary>
[Preserve]
public class XUiC_NecroRepairNeed : XUiController
{
XUiV_Sprite _icon;
XUiV_Label _count;
float _nextRefresh;
public override void Init()
{
base.Init();
_icon = GetChildById("icon")?.ViewComponent as XUiV_Sprite;
_count = GetChildById("count")?.ViewComponent as XUiV_Label;
}
public override void OnOpen()
{
base.OnOpen();
_nextRefresh = 0f;
Refresh();
}
public override void Update(float _dt)
{
base.Update(_dt);
if (Time.time >= _nextRefresh)
{
Refresh();
}
}
void Refresh()
{
_nextRefresh = Time.time + 0.5f;
TEFeatureCinerarium urn = CinerariumContents.Urn;
ItemClass ic = urn != null && urn.IsRepairer && urn.LackType > 0 ? ItemClass.GetForId(urn.LackType) : null;
ViewComponent.IsVisible = ic != null;
if (ic == null)
{
return;
}
if (_icon != null)
{
_icon.SpriteName = ic.GetIconName();
_icon.Color = ic.GetIconTint();
}
if (_count != null)
{
_count.Text = urn.LackCount.ToString();
}
}
}
}
+11 -4
View File
@@ -34,6 +34,10 @@ namespace NecromancerTome
{ {
public const string KnifeName = Patch_ItemClass_ExecuteAction_KnifePowerCost.KnifeName; public const string KnifeName = Patch_ItemClass_ExecuteAction_KnifePowerCost.KnifeName;
public const string ThreadKnifeName = "necroWpnBladeNecroKnifeThread"; public const string ThreadKnifeName = "necroWpnBladeNecroKnifeThread";
/// <summary>Резная кость (#86, 28.10.2026): отдельный инструмент нитей - делает то же, что нож хватом для нитей, хват
/// всегда для нитей, меню хватов по R у неё нет. Для тех, кто не нашёл хват у ножа (пользователь: «чтобы игроки, которые
/// не обнаружили нитяной хват у ножа, легко могли найти отдельный инструмент управления цинерариями»).</summary>
public const string BoneName = "necroCarvedBone";
/// <summary>Как у провода электрики: ItemActionConnectPower.maxWireLength = 15.</summary> /// <summary>Как у провода электрики: ItemActionConnectPower.maxWireLength = 15.</summary>
public const float MaxLength = 15f; public const float MaxLength = 15f;
public const int MaxFrom = 8; public const int MaxFrom = 8;
@@ -51,6 +55,9 @@ namespace NecromancerTome
return n == SkullDecorPlace.CenterBlock || n == SkullDecorPlace.BackBlock || n == "necroSkullDecor"; return n == SkullDecorPlace.CenterBlock || n == SkullDecorPlace.BackBlock || n == "necroSkullDecor";
} }
public static bool IsThreadKnife(ItemClass _i) => _i != null && _i.Name == ThreadKnifeName; public static bool IsThreadKnife(ItemClass _i) => _i != null && _i.Name == ThreadKnifeName;
/// <summary>Инструмент нитей в руке: нож хватом для нитей или резная кость.</summary>
public static bool IsThreadTool(ItemClass _i) => _i != null && IsThreadToolName(_i.Name);
public static bool IsThreadToolName(string _n) => _n == ThreadKnifeName || _n == BoneName;
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// Хват. // Хват.
@@ -439,7 +446,7 @@ namespace NecromancerTome
public static bool ToolInHand() public static bool ToolInHand()
{ {
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer(); EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
return pl != null && CinerariumThreads.IsThreadKnife(pl.inventory?.holdingItem); return pl != null && CinerariumThreads.IsThreadTool(pl.inventory?.holdingItem);
} }
void Update() void Update()
@@ -492,7 +499,7 @@ namespace NecromancerTome
void Update() void Update()
{ {
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer(); EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
if (pl == null || !CinerariumThreads.Start.HasValue || !CinerariumThreads.IsThreadKnife(pl.inventory?.holdingItem)) if (pl == null || !CinerariumThreads.Start.HasValue || !CinerariumThreads.IsThreadTool(pl.inventory?.holdingItem))
{ {
CinerariumThreads.Cancel(); CinerariumThreads.Cancel();
return; return;
@@ -517,13 +524,13 @@ namespace NecromancerTome
} }
} }
/// <summary>Нож для нитей не бьёт: ЛКМ и ПКМ - нити (на отпускание кнопки).</summary> /// <summary>Нож для нитей и резная кость не бьют: ЛКМ и ПКМ - нити (на отпускание кнопки).</summary>
[HarmonyPatch(typeof(ItemClass), nameof(ItemClass.ExecuteAction))] [HarmonyPatch(typeof(ItemClass), nameof(ItemClass.ExecuteAction))]
public static class Patch_ItemClass_ExecuteAction_ThreadKnife public static class Patch_ItemClass_ExecuteAction_ThreadKnife
{ {
public static bool Prefix(ItemClass __instance, int _actionIdx, ItemInventoryData _data, bool _bReleased) public static bool Prefix(ItemClass __instance, int _actionIdx, ItemInventoryData _data, bool _bReleased)
{ {
if (!CinerariumThreads.IsThreadKnife(__instance)) if (!CinerariumThreads.IsThreadTool(__instance))
{ {
return true; return true;
} }
+24 -1
View File
@@ -1,3 +1,4 @@
using System.Collections;
using UnityEngine; using UnityEngine;
namespace NecromancerTome namespace NecromancerTome
@@ -42,7 +43,7 @@ namespace NecromancerTome
_bag.SetSlot(1, _u.Sample.Clone()); _bag.SetSlot(1, _u.Sample.Clone());
} }
XUiC_BagStorageWindowGroup.Open(ui.xui, _p, _bag, LootContainer.GetLootContainer(LootTemplate), XUiC_BagStorageWindowGroup.Open(ui.xui, _p, _bag, LootContainer.GetLootContainer(LootTemplate),
Localization.Get("necroCinerariumTrunk"), Check, () => Localization.Get("necroCinerariumTrunk"), CheckLater, () =>
{ {
Check(); Check();
_urn = null; _urn = null;
@@ -52,6 +53,28 @@ namespace NecromancerTome
Debug.Log("[NecromancerTome] Cinerarium: «Содержимое» урны " + _u.ToWorldPos() + " открыто, череп " + (_u.HasSkull ? "есть" : "нет")); Debug.Log("[NecromancerTome] Cinerarium: «Содержимое» урны " + _u.ToWorldPos() + " открыто, череп " + (_u.HasSkull ? "есть" : "нет"));
} }
static bool _checkQueued;
/// <summary>Изменение в ячейках - проверка в следующем кадре. Игра зовёт onModified посреди своей перекладки: при Shift+клике
/// стопки черепов из рюкзака она сначала кладёт стопку в урну (тут срабатывал Check и возвращал лишние черепа в ту же ячейку
/// рюкзака), а потом пишет в исходную ячейку пустоту - и вернувшиеся черепа пропадали (стрим 26.10: «черепа исчезли из сумки»).</summary>
static void CheckLater()
{
if (_checkQueued)
{
return;
}
_checkQueued = true;
GameManager.Instance.StartCoroutine(CheckNextFrame());
}
static IEnumerator CheckNextFrame()
{
yield return null;
_checkQueued = false;
Check();
}
/// <summary>Оставить в ячейке один череп, остальное - игроку; отметить в урне. У рабочего - запомнить образец; череп во /// <summary>Оставить в ячейке один череп, остальное - игроку; отметить в урне. У рабочего - запомнить образец; череп во
/// второй ячейке - не образец, переложить в первую (или вернуть).</summary> /// второй ячейке - не образец, переложить в первую (или вернуть).</summary>
static void Check() static void Check()
+182 -19
View File
@@ -8,7 +8,8 @@ namespace NecromancerTome
/// Урны с большой зоной - ремонтник и лутер (решения 17.10 и 18.10, BACKLOG «Урна ремонтника», «Урна лутера»). /// Урны с большой зоной - ремонтник и лутер (решения 17.10 и 18.10, BACKLOG «Урна ремонтника», «Урна лутера»).
/// ///
/// Общее. Зона - квадрат со стороной блока притязания (LandClaimSize сервера, по умолчанию 41) или меньше (31/21/11, пункт /// Общее. Зона - квадрат со стороной блока притязания (LandClaimSize сервера, по умолчанию 41) или меньше (31/21/11, пункт
/// меню «Размер зоны»), по высоте - весь столб мира; зоны таких урн могут пересекаться. Работа - только по кнопке (17.10: «А /// меню «Размер зоны»), по высоте - весь столб мира; делится на 3x3 квадрата, любой можно выключить (#84, ZoneOff); зоны
/// таких урн могут пересекаться. Работа - только по кнопке (17.10: «А
/// если сделать рубильник или кнопку. Включил, ремонтник начал работу»): «Запустить» - один проход по зоне, конец прохода - /// если сделать рубильник или кнопку. Включил, ремонтник начал работу»): «Запустить» - один проход по зоне, конец прохода -
/// снова ожидание; до нажатия урна ничего не считает. Кнопки - на самой урне с четырёх сторон («как у дверного звонка, но с /// снова ожидание; до нажатия урна ничего не считает. Кнопки - на самой урне с четырёх сторон («как у дверного звонка, но с
/// черепом»), их цвет - состояние (CinerariumButtons): не горят - нет черепа; жёлтая ровно - ожидание; жёлтая мигает - не /// черепом»), их цвет - состояние (CinerariumButtons): не горят - нет черепа; жёлтая ровно - ожидание; жёлтая мигает - не
@@ -75,6 +76,9 @@ namespace NecromancerTome
public int ZonePressedBy = -1; public int ZonePressedBy = -1;
/// <summary>Не хватает материалов / ящик полон - кнопка мигает жёлтым.</summary> /// <summary>Не хватает материалов / ящик полон - кнопка мигает жёлтым.</summary>
public bool ZoneLacking; public bool ZoneLacking;
/// <summary>Ремонтник: какого материала и сколько не хватило последней проверке (ячейка в «Содержимом»,
/// XUiC_NecroRepairNeed). Не сохраняется.</summary>
public int LackType, LackCount;
public static float RepairSeconds = 2.5f; public static float RepairSeconds = 2.5f;
public static float LootSeconds = 1.5f; public static float LootSeconds = 1.5f;
@@ -86,6 +90,10 @@ namespace NecromancerTome
static readonly HashSet<Vector3i> Claimed = new HashSet<Vector3i>(); static readonly HashSet<Vector3i> Claimed = new HashSet<Vector3i>();
readonly List<Vector3i> _queue = new List<Vector3i>(); readonly List<Vector3i> _queue = new List<Vector3i>();
/// <summary>Ремонтник: блоки, на которые нет материалов (24.10, пользователь: «Если не может починить что-то для чего
/// нет ресурсов, пусть сначала чинит то, для чего они есть»). Не держат очередь обхода; к ним - когда больше нечего.</summary>
readonly List<Vector3i> _lacking = new List<Vector3i>();
const int LackingMax = 256;
float _zoneNext; float _zoneNext;
bool _zonePending; bool _zonePending;
float _zoneActAt; float _zoneActAt;
@@ -116,6 +124,88 @@ namespace NecromancerTome
} }
} }
/// <summary>Квадраты зоны (28.10, #84, пользователь: «разделить уже имеющуюся зону на 9 квадратов, с цинерарием посередине...
/// по умолчанию все 9 были выбраны, но можно было какой-нибудь отключить. Иначе он начинает ремонтировать близстоящие
/// здания»): сетка ZoneGrid x ZoneGrid, урна в среднем квадрате. Номер квадрата - столбец + строка * ZoneGrid, столбец 0 -
/// запад (меньший x), строка 0 - юг (меньший z). Бит в ZoneOff - квадрат выключен; 0 - все включены. Сохраняется (v6,
/// пятое число). Выключать - в «Содержимом» (XUiC_NecroUrnZone) или «necrourn x y z sectors».</summary>
public int ZoneOff;
public const int ZoneGrid = 3;
public const int ZoneSectors = ZoneGrid * ZoneGrid;
public bool SectorOn(int _s) => (ZoneOff & (1 << _s)) == 0;
/// <summary>Полоса сетки клетки _i (0..side-1, от западного/южного края зоны).</summary>
static int Band(int _i, int _side) => _i * ZoneGrid / _side;
/// <summary>Первая клетка полосы _k от края зоны; _k = ZoneGrid - сторона зоны.</summary>
public static int BandStart(int _k, int _side) => (_k * _side + ZoneGrid - 1) / ZoneGrid;
/// <summary>Работает ли урна в столбе мира (x, z): в зоне и квадрат включён.</summary>
public bool InZone(int _x, int _z)
{
int side = ZoneSideNow, half = side / 2;
Vector3i c = ToWorldPos();
int ix = _x - c.x + half, iz = _z - c.z + half;
if (ix < 0 || iz < 0 || ix >= side || iz >= side)
{
return false;
}
return SectorOn(Band(ix, side) + Band(iz, side) * ZoneGrid);
}
/// <summary>Включить/выключить квадрат. Блоки выключенного квадрата, уже взятые в очередь, отпускаются; линии перерисовать.</summary>
public void SetZoneOff(int _mask)
{
ZoneOff = _mask & ((1 << ZoneSectors) - 1);
DropOutside(_queue);
DropOutside(_lacking);
if (_zonePending && IsRepairer && !InZone(_zoneTarget.x, _zoneTarget.z))
{
_zonePending = false;
Claimed.Remove(_zoneTarget);
}
if (ZoneShown)
{
RemoveBounds();
SpawnBounds();
}
SetModified();
Debug.Log("[NecromancerTome] Cinerarium: урна " + ToWorldPos() + " - квадраты зоны " + SectorsText());
}
public void ToggleSector(int _s) => SetZoneOff(ZoneOff ^ (1 << _s));
/// <summary>Квадраты строками с севера: «+» включён, «-» выключен (для лога и консоли).</summary>
public string SectorsText()
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
for (int r = ZoneGrid - 1; r >= 0; r--)
{
for (int col = 0; col < ZoneGrid; col++)
{
sb.Append(SectorOn(col + r * ZoneGrid) ? '+' : '-');
}
if (r > 0)
{
sb.Append('/');
}
}
return sb.ToString();
}
void DropOutside(List<Vector3i> _l)
{
for (int i = _l.Count - 1; i >= 0; i--)
{
if (!InZone(_l[i].x, _l[i].z))
{
Claimed.Remove(_l[i]);
_l.RemoveAt(i);
}
}
}
public enum ButtonState public enum ButtonState
{ {
Off, Off,
@@ -172,7 +262,7 @@ namespace NecromancerTome
ZoneCursor = 0; ZoneCursor = 0;
ZoneLacking = false; ZoneLacking = false;
ZonePressedBy = _player != null ? _player.entityId : -1; ZonePressedBy = _player != null ? _player.entityId : -1;
_queue.Clear(); ReleaseClaims();
_lootDone.Clear(); _lootDone.Clear();
Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), "button4"); Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), "button4");
Debug.Log("[NecromancerTome] Cinerarium: " + (IsLooter ? "лутер" : "ремонтник") + " " + ToWorldPos() + " запущен, зона " + ZoneSideNow); Debug.Log("[NecromancerTome] Cinerarium: " + (IsLooter ? "лутер" : "ремонтник") + " " + ToWorldPos() + " запущен, зона " + ZoneSideNow);
@@ -203,11 +293,7 @@ namespace NecromancerTome
/// <summary>Остановить проход (кнопка, урна убрана): очередь и взятые блоки отпускаются.</summary> /// <summary>Остановить проход (кнопка, урна убрана): очередь и взятые блоки отпускаются.</summary>
public void ZoneStop(bool _log) public void ZoneStop(bool _log)
{ {
foreach (Vector3i p in _queue) ReleaseClaims();
{
Claimed.Remove(p);
}
_queue.Clear();
_zonePending = false; _zonePending = false;
if (ZoneRunning && _log) if (ZoneRunning && _log)
{ {
@@ -217,6 +303,23 @@ namespace NecromancerTome
ZoneLacking = false; ZoneLacking = false;
} }
/// <summary>Очередь и отложенные блоки - отпустить (Claimed) и забыть.</summary>
void ReleaseClaims()
{
foreach (Vector3i p in _queue)
{
Claimed.Remove(p);
}
foreach (Vector3i p in _lacking)
{
Claimed.Remove(p);
}
_queue.Clear();
_lacking.Clear();
LackType = 0;
LackCount = 0;
}
void ZoneWork(World _world) void ZoneWork(World _world)
{ {
SyncButtons(); SyncButtons();
@@ -297,6 +400,10 @@ namespace NecromancerTome
{ {
int x = c.x - half + ZoneCursor % side, z = c.z - half + ZoneCursor / side; int x = c.x - half + ZoneCursor % side, z = c.z - half + ZoneCursor / side;
ZoneCursor++; ZoneCursor++;
if (ZoneOff != 0 && !InZone(x, z))
{
continue; // квадрат выключен (#84)
}
if (!(_world.GetChunkFromWorldPos(x, 0, z) is Chunk ch)) if (!(_world.GetChunkFromWorldPos(x, 0, z) is Chunk ch))
{ {
continue; // не загружен - пропустить столб continue; // не загружен - пропустить столб
@@ -323,7 +430,7 @@ namespace NecromancerTome
} }
} }
} }
if (ZoneCursor >= total && _queue.Count == 0 && !_zonePending) if (ZoneCursor >= total && _queue.Count == 0 && _lacking.Count == 0 && !_zonePending)
{ {
Debug.Log("[NecromancerTome] Cinerarium: ремонтник " + c + " обошёл зону - ожидание"); Debug.Log("[NecromancerTome] Cinerarium: ремонтник " + c + " обошёл зону - ожидание");
ZoneStop(false); ZoneStop(false);
@@ -331,32 +438,78 @@ namespace NecromancerTome
} }
} }
/// <summary>Следующий блок: сначала из очереди обхода, на что есть материалы (на что нет - в _lacking), потом из _lacking,
/// если материал появился. Нечего чинить, кроме ждущих материала, - кнопка жёлтая. Ячейка «не хватает» в «Содержимом» -
/// первый блок из _lacking (LackType/LackCount), пока он есть.</summary>
void RepairPick(World _world, CinerariumGenieMotion _motion) void RepairPick(World _world, CinerariumGenieMotion _motion)
{ {
while (_queue.Count > 0) while (_queue.Count > 0)
{ {
Vector3i p = _queue[0]; Vector3i p = _queue[0];
_queue.RemoveAt(0);
BlockValue bv = _world.GetBlock(p); BlockValue bv = _world.GetBlock(p);
if (bv.isair || bv.damage <= 0 || bv.Block?.RepairItems == null) if (bv.isair || bv.damage <= 0 || bv.Block?.RepairItems == null)
{ {
_queue.RemoveAt(0);
Claimed.Remove(p); Claimed.Remove(p);
continue; // починили до нас или блок убран continue; // починили до нас или блок убран
} }
if (!HasMaterials(_world, bv, false)) if (!HasMaterials(_world, bv, false))
{ {
if (!ZoneLacking) if (_lacking.Count < LackingMax)
{ {
Debug.Log("[NecromancerTome] Cinerarium: ремонтнику " + ToWorldPos() + " не хватает материалов для " + bv.Block.GetBlockName() + " в " + p); _lacking.Add(p);
} }
ZoneLacking = true; else
return; // ждём: положат в ящик - продолжим с того же блока {
Claimed.Remove(p); // найдётся при следующем обходе
}
continue;
} }
ZoneLacking = false; Go(_world, _motion, p);
_zoneTarget = p;
FlyThenAct(_world, _motion, p);
return; return;
} }
int lackType = 0, lackCount = 0;
for (int i = 0; i < _lacking.Count; i++)
{
Vector3i p = _lacking[i];
BlockValue bv = _world.GetBlock(p);
if (bv.isair || bv.damage <= 0 || bv.Block?.RepairItems == null)
{
_lacking.RemoveAt(i--);
Claimed.Remove(p);
continue;
}
if (HasMaterials(_world, bv, false))
{
_lacking.RemoveAt(i);
Go(_world, _motion, p);
LackType = lackType;
LackCount = lackCount;
return;
}
if (lackType == 0)
{
lackType = LackType;
lackCount = LackCount;
}
}
LackType = lackType;
LackCount = lackCount;
bool stuck = _lacking.Count > 0;
if (stuck && !ZoneLacking)
{
ItemClass ic = ItemClass.GetForId(LackType);
Debug.Log("[NecromancerTome] Cinerarium: ремонтнику " + ToWorldPos() + " нечего чинить без материалов: ждут " + _lacking.Count +
" блок(ов), первым не хватает " + LackCount + " " + ic?.GetItemName());
}
ZoneLacking = stuck;
}
void Go(World _world, CinerariumGenieMotion _motion, Vector3i _p)
{
ZoneLacking = false;
_zoneTarget = _p;
FlyThenAct(_world, _motion, _p);
} }
void RepairNow(World _world, Vector3i _p) void RepairNow(World _world, Vector3i _p)
@@ -370,8 +523,7 @@ namespace NecromancerTome
} }
if (!HasMaterials(_world, bv, true)) if (!HasMaterials(_world, bv, true))
{ {
ZoneLacking = true; _lacking.Add(_p); // материал забрали, пока дух летел
_queue.Insert(0, _p);
Claimed.Add(_p); Claimed.Add(_p);
return; return;
} }
@@ -403,6 +555,8 @@ namespace NecromancerTome
} }
if (have < n) if (have < n)
{ {
LackType = iv.type;
LackCount = n - have;
return false; return false;
} }
} }
@@ -518,7 +672,16 @@ namespace NecromancerTome
foreach (Entity e in _ents) foreach (Entity e in _ents)
{ {
int rank = LootRank(e); int rank = LootRank(e);
if (rank < bestRank && !_lootDone.Contains(e.entityId) && !LockManager.Instance.IsLockedServer(e, 0)) if (rank >= bestRank)
{
continue;
}
Vector3i ep = World.worldToBlockPos(e.GetPosition());
if (ZoneOff != 0 && !InZone(ep.x, ep.z))
{
continue; // квадрат выключен (#84)
}
if (!_lootDone.Contains(e.entityId) && !LockManager.Instance.IsLockedServer(e, 0))
{ {
best = e; best = e;
bestRank = rank; bestRank = rank;
+79
View File
@@ -0,0 +1,79 @@
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Квадраты зоны ремонтника и лутера (28.10.2026, #84): сетка 3x3 в окне «Содержимое» - окно necroUrnZone в группе окон
/// игры bagStorage (Config/XUi_InGame), как фильтр логиста. Север сверху, в среднем квадрате - урна. Щелчок включает /
/// выключает квадрат (TEFeatureCinerarium.ToggleSector); выключенный - тёмный с красной рамкой. Видна только у урн с
/// большой зоной; у прочих урн и сумок транспорта и волка скрыта. s0..s8 в XML - номера квадратов (столбец + строка * 3,
/// строка 0 - юг), f0..f8 - их рамки.
/// </summary>
[Preserve]
public class XUiC_NecroUrnZone : XUiController
{
readonly XUiController[] _cells = new XUiController[TEFeatureCinerarium.ZoneSectors];
readonly XUiController[] _frames = new XUiController[TEFeatureCinerarium.ZoneSectors];
static readonly Color On = new Color(0.3f, 0.6f, 0.45f, 0.9f);
static readonly Color Off = new Color(0.12f, 0.12f, 0.12f, 0.9f);
static readonly Color FrameOn = new Color(0.55f, 0.9f, 0.75f, 1f);
static readonly Color FrameOff = new Color(0.95f, 0.3f, 0.25f, 1f);
public override void Init()
{
base.Init();
for (int i = 0; i < _cells.Length; i++)
{
int s = i;
_cells[i] = GetChildById("s" + i);
_frames[i] = GetChildById("f" + i);
if (_cells[i] != null)
{
_cells[i].OnPress += (_x, _m) => Toggle(s);
}
}
}
public override void OnOpen()
{
base.OnOpen();
Refresh();
}
void Toggle(int _s)
{
TEFeatureCinerarium urn = CinerariumContents.Urn;
if (urn == null)
{
return;
}
urn.ToggleSector(_s);
Refresh();
}
void Refresh()
{
TEFeatureCinerarium urn = CinerariumContents.Urn;
bool show = urn != null && urn.IsBigZone;
ViewComponent.IsVisible = show;
if (!show)
{
return;
}
for (int i = 0; i < _cells.Length; i++)
{
bool on = urn.SectorOn(i);
if (_cells[i]?.ViewComponent is XUiV_Sprite sp)
{
sp.Color = on ? On : Off;
sp.ToolTip = Localization.Get(on ? "necroZoneSectorOn" : "necroZoneSectorOff");
}
if (_frames[i]?.ViewComponent is XUiV_Sprite fr)
{
fr.Color = on ? FrameOn : FrameOff;
}
}
}
}
}
+73 -1
View File
@@ -68,6 +68,9 @@ namespace NecromancerTome
static GameObject _rig; static GameObject _rig;
static SDCSUtils.TransformCatalog _bones; static SDCSUtils.TransformCatalog _bones;
/// <summary>Модель оружия с пояса в правой руке (#37) и чья она (тип предмета; -1 - кулаки).</summary>
static AssetManagement.InstanceHandle<GameObject> _weapon;
static int _weaponType = -1;
static bool _layerWasShown; static bool _layerWasShown;
public static bool Active => _rig != null; public static bool Active => _rig != null;
@@ -279,6 +282,65 @@ namespace NecromancerTome
} }
} }
/// <summary>Оружие с пояса в руках (#37): модель предмета на кость RightWeapon, как у персонажа от первого лица
/// (ItemInventoryData.SeatModel - без сдвига), и хват WeaponHoldType этого предмета; null - кулаки (хват 0).
/// Ставится каждый кадр, пересобирается только при смене предмета.</summary>
public static void SetWeapon(ItemValue _iv)
{
int type = _iv != null && !_iv.IsEmpty() ? _iv.type : -1;
if (_rig == null || type == _weaponType)
{
return;
}
DestroyWeapon();
_weaponType = type;
Animator a = Anim;
ItemClass ic = type >= 0 ? _iv.ItemClass : null;
Transform hand = _rig.transform.FindInChilds("RightWeapon");
if (ic != null && hand != null)
{
_weapon = ic.CloneModel(_iv, hand, BlockShape.MeshPurpose.Hold);
GameObject go = _weapon.Instance;
if (go != null)
{
go.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
if (!ic.GetCorrectionScale().Equals(Vector3.zero))
{
go.transform.localScale = ic.GetCorrectionScale();
}
int layer = LayerMask.NameToLayer("HoldingItem");
foreach (Transform t in go.GetComponentsInChildren<Transform>(true))
{
t.gameObject.layer = layer;
}
foreach (Collider c in go.GetComponentsInChildren<Collider>(true))
{
c.enabled = false;
}
go.SetActive(!ic.HoldingItemHidden);
}
}
if (a != null)
{
SetParam(a, "WeaponHoldType", ic != null ? ic.HoldType.Value : 0f);
SetParam(a, "ItemHasChangedTrigger", 1f);
}
Debug.Log("[NecromancerTome] FpArms: в руке " + (ic != null ? ic.Name + " (хват " + ic.HoldType.Value + (_weapon.Instance != null ? "" : ", БЕЗ модели") + ")" : "ничего - кулаки") +
(hand == null && ic != null ? " - кость RightWeapon не найдена" : ""));
}
static void DestroyWeapon()
{
if (_weapon.Instance != null)
{
_weapon.Instance.transform.SetParent(null, false);
_weapon.Instance.SetActive(false);
_weapon.Destroy();
}
_weapon = default(AssetManagement.InstanceHandle<GameObject>);
_weaponType = -1;
}
/// <summary>Анимация удара рук вошла в состояние удара (AnimatorMeleeAttackState у наших рук): через RaycastTime - /// <summary>Анимация удара рук вошла в состояние удара (AnimatorMeleeAttackState у наших рук): через RaycastTime -
/// миг удара, как у персонажа (impactStart). Руки на это время уходят в нижнее положение (под прицел).</summary> /// миг удара, как у персонажа (impactStart). Руки на это время уходят в нижнее положение (под прицел).</summary>
public static void OnSwingEnter(float _raycastTime) public static void OnSwingEnter(float _raycastTime)
@@ -421,6 +483,7 @@ namespace NecromancerTome
{ {
_player?.ShowHoldingItemLayer(false); _player?.ShowHoldingItemLayer(false);
} }
DestroyWeapon();
SDCSUtils.UnloadViz(_rig); SDCSUtils.UnloadViz(_rig);
_rig = null; _rig = null;
_bones = null; _bones = null;
@@ -437,7 +500,7 @@ namespace NecromancerTome
public class ConsoleCmdNecroArms : ConsoleCmdAbstract public class ConsoleCmdNecroArms : ConsoleCmdAbstract
{ {
public override string[] getCommands() => new[] { "necroarms" }; public override string[] getCommands() => new[] { "necroarms" };
public override string getDescription() => "NecromancerTome: руки одержимого от первого лица - [pos x y z | parent cam|ufps | anim | mat | set <param> <value> | trigger <name> | tint r g b | albedo on|off | punch <trigger> | fov <deg>]"; public override string getDescription() => "NecromancerTome: руки одержимого от первого лица - [pos x y z | parent cam|ufps | anim | mat | set <param> <value> | trigger <name> | tint r g b | albedo on|off | punch <trigger> | fov <deg> | weapon [on|off]]";
void Out(string _s) void Out(string _s)
{ {
@@ -477,6 +540,15 @@ namespace NecromancerTome
{ {
FpArms.ArmsFov = fov; FpArms.ArmsFov = fov;
} }
else if (cmd == "weapon")
{
if (_params.Count >= 2)
{
ZombieWeapons.Enabled = _params[1] != "off";
}
Out(Possession.Active && Possession.Zombie != null ? ZombieWeapons.Dump(Possession.Zombie) : "оружие " + (ZombieWeapons.Enabled ? "вкл" : "ВЫКЛ") + " (не в одержимости)");
return;
}
else if (cmd == "anim") else if (cmd == "anim")
{ {
Out(FpArms.DumpAnim()); Out(FpArms.DumpAnim());
+3 -3
View File
@@ -29,10 +29,10 @@ namespace NecromancerTome
/// </summary> /// </summary>
public static class GameVersionGuard public static class GameVersionGuard
{ {
/// <summary>Под какую версию собран мод: Major и десяток Minor (3.3.x = 3 и 3; сборка 1.4.x была под 3.2).</summary> /// <summary>Под какую версию собран мод: Major и десяток Minor (3.4.x = 3 и 4; 1.5-1.6 были под 3.3, 1.4.x - под 3.2).</summary>
public const int SupportedMajor = 3; public const int SupportedMajor = 3;
public const int SupportedMinorTens = 3; public const int SupportedMinorTens = 4;
public const string SupportedText = "3.3"; public const string SupportedText = "3.4";
private static bool? supported; private static bool? supported;
+1 -1
View File
@@ -64,7 +64,7 @@ namespace NecromancerTome
{ {
// Только от первого лица (16.10: «В виде от третьего лица что-то страшное»): поза и перенос ножа - для рук первого // Только от первого лица (16.10: «В виде от третьего лица что-то страшное»): поза и перенос ножа - для рук первого
// лица; от третьего лица модель в руке - у тела, и нож улетал к невидимой руке первого лица. // лица; от третьего лица модель в руке - у тела, и нож улетал к невидимой руке первого лица.
if (_player != null && _player.inventory?.holdingItem?.Name == CinerariumThreads.ThreadKnifeName && if (_player != null && CinerariumThreads.IsThreadToolName(_player.inventory?.holdingItem?.Name) &&
(_player.emodel as EModelSDCS)?.IsFPV == true && !SnakeMode.Active && !MindPalace.Active && !Possession.Active && !_player.IsDead()) (_player.emodel as EModelSDCS)?.IsFPV == true && !SnakeMode.Active && !MindPalace.Active && !Possession.Active && !_player.IsDead())
{ {
if (_threadSince < 0f) if (_threadSince < 0f)
+81 -2
View File
@@ -17,8 +17,9 @@ namespace NecromancerTome
/// necroslot [N] - какая ячейка пояса в руке и что в ячейках; N - взять в руку ячейку N (23.10: проверка Archer's Eye без мыши) /// necroslot [N] - какая ячейка пояса в руке и что в ячейках; N - взять в руку ячейку N (23.10: проверка Archer's Eye без мыши)
/// necrofire press|release - ЛКМ предметом в руке: нажать / отпустить (лук: натянуть / выстрелить; 23.10, ошибка в Hit от стрелы) /// necrofire press|release - ЛКМ предметом в руке: нажать / отпустить (лук: натянуть / выстрелить; 23.10, ошибка в Hit от стрелы)
/// necrobox x y z list|clear|add предмет число - содержимое хранилища (ящик, сумка урны) /// necrobox x y z list|clear|add предмет число - содержимое хранилища (ящик, сумка урны)
/// necrourn x y z info|skull 0|1|from|to|out bx by bz|clear|sample предмет число|group все|нет - урна /// necrourn x y z info|skull 0|1|from|to|out bx by bz|clear|sample предмет число|group все|нет|sectors all|N|-маска - урна
/// necroclick 0|1 - отпущенная ЛКМ/ПКМ ножом для нитей по блоку под прицелом /// necroclick 0|1 - отпущенная ЛКМ/ПКМ ножом для нитей по блоку под прицелом
/// necropossess near [класс]|fp|hit|look id / x y z|slot N|belt N предмет [качество]|info|exit - одержимость без мыши (#37, 25.10)
/// </summary> /// </summary>
public static class NecroDev public static class NecroDev
{ {
@@ -265,6 +266,7 @@ namespace NecromancerTome
Out("necrocanplace: " + (TEFeatureCinerarium.PlacementRefusal(P(_params, 1), Block.GetBlockByName(_params[0], true)) ?? "можно")); Out("necrocanplace: " + (TEFeatureCinerarium.PlacementRefusal(P(_params, 1), Block.GetBlockByName(_params[0], true)) ?? "можно"));
break; break;
case "necrogod": God(pl, _params.Count == 0 || _params[0] != "off"); break; case "necrogod": God(pl, _params.Count == 0 || _params[0] != "off"); break;
case "necropossess": Possess(w, pl, _params); break;
case "necroclick": case "necroclick":
// necroclick 0|1 [x y z] - по блоку под прицелом или по указанной клетке (луч не всегда попадает в низкий череп). // necroclick 0|1 [x y z] - по блоку под прицелом или по указанной клетке (луч не всегда попадает в низкий череп).
if (_params.Count >= 4) if (_params.Count >= 4)
@@ -310,6 +312,64 @@ namespace NecromancerTome
/// <summary>Режим бога и невидимость для ИИ: пока ассистент проверяет, персонаж стоит - зомби тестового мира его не трогают /// <summary>Режим бога и невидимость для ИИ: пока ассистент проверяет, персонаж стоит - зомби тестового мира его не трогают
/// (18.10: «пока персонаж просто стоит, его скушают»; «Я обычно ставлю режим Бога»).</summary> /// (18.10: «пока персонаж просто стоит, его скушают»; «Я обычно ставлю режим Бога»).</summary>
/// <summary>necropossess: вселиться в ближайшего зомби (класс - начало имени), вид от первого лица, ЛКМ, ячейка и
/// предмет пояса, состояние, выход.</summary>
static void Possess(World _w, EntityPlayerLocal _pl, List<string> _p)
{
string cmd = _p.Count > 0 ? _p[0] : "info";
EntityAlive z = Possession.Zombie;
switch (cmd)
{
case "near":
{
string cls = _p.Count > 1 ? _p[1] : "";
EntityAlive best = null;
float bestD = 40f * 40f;
foreach (EntityAlive e in _w.EntityAlives)
{
if (!Possession.CanPossess(e) || !EntityClass.list.TryGetValue(e.entityClass, out EntityClass ec) ||
!ec.entityClassName.StartsWith(cls, System.StringComparison.OrdinalIgnoreCase))
{
continue;
}
float d = (e.position - _pl.position).sqrMagnitude;
if (d < bestD)
{
bestD = d;
best = e;
}
}
Out("necropossess: " + (best == null ? "зомби рядом нет" : best.EntityName + " (" + best.entityId + ") - " + (Possession.Enter(_pl, best) ? "вселился" : "не вышло")));
return;
}
case "fp": Possession.DevView = true; break;
case "hit": Possession.DevAttack = true; break;
case "look":
{
// look id - на существо (грудь), look x y z - на точку
Vector3 at = _p.Count >= 4 ? new Vector3(F(_p[1]), F(_p[2]), F(_p[3]))
: (_w.GetEntity(int.Parse(_p[1])) as EntityAlive)?.getChestPosition() ?? Vector3.zero;
Possession.DevLookAt(at);
break;
}
case "slot": Possession.BeltSlot = Mathf.Clamp(int.Parse(_p[1]) - 1, 0, Possession.BeltSlots - 1); break;
case "belt":
{
ItemValue iv = ItemClass.GetItem(_p[2]);
if (iv == null || iv.IsEmpty())
{
Out("necropossess: нет предмета " + _p[2]);
return;
}
iv = new ItemValue(iv.type, _p.Count > 3 ? int.Parse(_p[3]) : 1, _p.Count > 3 ? int.Parse(_p[3]) : 1);
Out("necropossess: " + (ZombieBag.DevSetBelt(int.Parse(_p[1]) - 1, new ItemStack(iv, 1)) ? "положено" : "сумки нет"));
break;
}
case "exit": Possession.Exit("necropossess"); Out("necropossess: выход"); return;
}
Out("necropossess: " + (z == null ? "не в одержимости" : z.EntityName + ", от первого лица " + Possession.FirstPerson + "\n" + ZombieWeapons.Dump(z)));
}
static void God(EntityPlayerLocal _pl, bool _on) static void God(EntityPlayerLocal _pl, bool _on)
{ {
_pl.IsGodMode.Value = _on; _pl.IsGodMode.Value = _on;
@@ -492,6 +552,16 @@ namespace NecromancerTome
case "run": u.ZoneCommand("zurn_run", GameManager.Instance.World.GetPrimaryPlayer()); break; case "run": u.ZoneCommand("zurn_run", GameManager.Instance.World.GetPrimaryPlayer()); break;
case "stop": u.ZoneCommand("zurn_stop", null); break; case "stop": u.ZoneCommand("zurn_stop", null); break;
case "size": u.ZoneSide = int.Parse(_p[4]); u.SetModified(); break; case "size": u.ZoneSide = int.Parse(_p[4]); u.SetModified(); break;
// necrourn x y z sectors all|N|-N - квадраты зоны (#84): all - все включить; N - переключить квадрат N (0..8, столбец +
// строка * 3, 0 - юго-запад); -N - маска выключенных числом.
case "sectors":
if (_p.Count > 4)
{
if (_p[4] == "all" || _p[4] == "все") u.SetZoneOff(0);
else if (_p[4].StartsWith("-")) u.SetZoneOff(int.Parse(_p[4].Substring(1)));
else u.ToggleSector(int.Parse(_p[4]));
}
break;
case "group": case "group":
u.GroupMask = _p[4] == "нет" || _p[4] == "none" ? 0u : CinerariumGroups.All; u.GroupMask = _p[4] == "нет" || _p[4] == "none" ? 0u : CinerariumGroups.All;
u.SetModified(); u.SetModified();
@@ -507,7 +577,7 @@ namespace NecromancerTome
} }
Out("necrourn: " + pos + " " + _w.GetBlock(pos).Block?.GetBlockName() + ", череп " + u.HasSkull + ", застрял " + u.Stuck + ", мигание " + blink + Out("necrourn: " + pos + " " + _w.GetBlock(pos).Block?.GetBlockName() + ", череп " + u.HasSkull + ", застрял " + u.Stuck + ", мигание " + blink +
", откуда " + u.ThreadFrom.Count + ", куда " + (u.HasThreadTo ? u.ThreadTo.ToString() : "нет") + ", от урны " + u.ThreadOut.Count + ", откуда " + u.ThreadFrom.Count + ", куда " + (u.HasThreadTo ? u.ThreadTo.ToString() : "нет") + ", от урны " + u.ThreadOut.Count +
", зона " + (u.IsBigZone ? u.ZoneSideNow + (u.ZoneRunning ? " идёт" : " ждёт") + (u.ZoneLacking ? " нехватка" : "") + " курсор " + u.ZoneCursor : "-") + ", образец " + (u.Sample == null || u.Sample.IsEmpty() ? "нет" : u.Sample.itemValue.ItemClass?.GetItemName() + "x" + u.Sample.count) + "; " + u.WorkState()); ", зона " + (u.IsBigZone ? u.ZoneSideNow + (u.ZoneRunning ? " идёт" : " ждёт") + (u.ZoneLacking ? " нехватка" : "") + " курсор " + u.ZoneCursor + " квадраты " + u.SectorsText() : "-") + ", образец " + (u.Sample == null || u.Sample.IsEmpty() ? "нет" : u.Sample.itemValue.ItemClass?.GetItemName() + "x" + u.Sample.count) + "; " + u.WorkState());
} }
} }
@@ -592,6 +662,15 @@ namespace NecromancerTome
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necroclick", _params); public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necroclick", _params);
} }
[Preserve]
public class ConsoleCmdNecroPossess : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necropossess" };
public override string getDescription() => "NecromancerTome (разработка): одержимость без мыши - near [класс] | fp | hit | slot N | belt N предмет [качество] | info | exit";
public override bool AllowedInMainMenu => false;
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necropossess", _params);
}
[Preserve] [Preserve]
public class ConsoleCmdNecroGod : ConsoleCmdAbstract public class ConsoleCmdNecroGod : ConsoleCmdAbstract
{ {
+107 -13
View File
@@ -55,6 +55,9 @@ namespace NecromancerTome
public static EntityAlive Zombie; public static EntityAlive Zombie;
/// <summary>Вселились в своего питомца (книга призыва), а не в чужого зомби.</summary> /// <summary>Вселились в своего питомца (книга призыва), а не в чужого зомби.</summary>
public static bool IsPet; public static bool IsPet;
/// <summary>Разведка (туман карты вокруг одержимого, карта открывается на нём): стервятник (08.10) и любой одержимый
/// зомби (#36, 25.10); прочие питомцы - нет.</summary>
public static bool Scouts => Active && (Flying || !IsPet);
/// <summary>Питомец-стервятник: летает (FlyTick из его updateTasks).</summary> /// <summary>Питомец-стервятник: летает (FlyTick из его updateTasks).</summary>
public static bool Flying; public static bool Flying;
/// <summary>Враг у тела (CheckBody) - для надписи в PossessionHud.</summary> /// <summary>Враг у тела (CheckBody) - для надписи в PossessionHud.</summary>
@@ -153,7 +156,13 @@ namespace NecromancerTome
public static bool FirstPerson; public static bool FirstPerson;
/// <summary>Пояс одержимого (13.10): 2 ячейки, пока пустые; выбор 1/2 или колесом, только от первого лица.</summary> /// <summary>Пояс одержимого (13.10): 2 ячейки, пока пустые; выбор 1/2 или колесом, только от первого лица.</summary>
public const int BeltSlots = 2; public const int BeltSlots = 2;
/// <summary>Чаще руки не успевают доиграть замах (ножи в игре - до 100+ ударов в минуту).</summary>
const float MinWeaponSwing = 0.45f;
public static int BeltSlot; public static int BeltSlot;
/// <summary>Нажатия от necropossess (проверка без мыши): ЛКМ и смена вида - срабатывают в ближайшем кадре.</summary>
public static bool DevAttack, DevView;
/// <summary>Ячейка и тип предмета в ней при прошлой проверке - подсказка о неумелом оружии один раз на смену.</summary>
static int _beltKey = -1;
public const float FpForward = 0.12f, FpSmooth = 12f; public const float FpForward = 0.12f, FpSmooth = 12f;
static Vector3 _fpEye; static Vector3 _fpEye;
static readonly List<Renderer> _hiddenRenderers = new List<Renderer>(); static readonly List<Renderer> _hiddenRenderers = new List<Renderer>();
@@ -218,6 +227,18 @@ namespace NecromancerTome
/// <summary>Игрок, который сейчас в одержимом (тело).</summary> /// <summary>Игрок, который сейчас в одержимом (тело).</summary>
public static EntityPlayerLocal Player => _player; public static EntityPlayerLocal Player => _player;
static float _yaw, _pitch; static float _yaw, _pitch;
/// <summary>necropossess look: взгляд одержимого на точку (проверка без мыши).</summary>
public static void DevLookAt(Vector3 _pos)
{
if (Zombie == null)
{
return;
}
Vector3 d = _pos - Zombie.getHeadPosition();
_yaw = Mathf.Atan2(d.x, d.z) * Mathf.Rad2Deg;
_pitch = Mathf.Atan2(d.y, new Vector2(d.x, d.z).magnitude) * Mathf.Rad2Deg;
}
/// <summary>Куда повёрнуто тело зомби. Шага вбок и назад у зомби нет (тест 03.10: «не работают A, D и S») - /// <summary>Куда повёрнуто тело зомби. Шага вбок и назад у зомби нет (тест 03.10: «не работают A, D и S») -
/// при движении тело разворачивается в сторону шага и идёт вперёд, без движения - смотрит туда же, куда камера.</summary> /// при движении тело разворачивается в сторону шага и идёт вперёд, без движения - смотрит туда же, куда камера.</summary>
static float _bodyYaw; static float _bodyYaw;
@@ -567,9 +588,9 @@ namespace NecromancerTome
Exit("кровавый прилив - дух выброшен в тело"); Exit("кровавый прилив - дух выброшен в тело");
return; return;
} }
if (Flying) if (Scouts)
{ {
RevealMap(world, z); // разведка - только стервятник (указание 08.10) RevealMap(world, z); // разведка - стервятник (указание 08.10) и одержимый зомби (#36)
} }
// Подсказка тела (тест 08.10: «стоял перед дверью - подсказка осталась в стервятнике»): её гасит // Подсказка тела (тест 08.10: «стоял перед дверью - подсказка осталась в стервятнике»): её гасит
// PlayerMoveController.Update, а он на время одержимости выключен. Прочие сообщения (голод и т.п.) - // PlayerMoveController.Update, а он на время одержимости выключен. Прочие сообщения (голод и т.п.) -
@@ -615,7 +636,8 @@ namespace NecromancerTome
move = new Vector2(input.Move.X, input.Move.Y); move = new Vector2(input.Move.X, input.Move.Y);
run = input.Run.IsPressed; run = input.Run.IsPressed;
jump = input.Jump.WasPressed; jump = input.Jump.WasPressed;
attack = input.Primary.WasPressed; attack = input.Primary.WasPressed || DevAttack;
DevAttack = false;
ranged = input.Secondary.WasPressed; ranged = input.Secondary.WasPressed;
rangedHeld = input.Secondary.IsPressed; rangedHeld = input.Secondary.IsPressed;
// E на клавиатуре - в PlayerActionsPermanent.Activate; у PlayerActionsLocal.Activate только // E на клавиатуре - в PlayerActionsPermanent.Activate; у PlayerActionsLocal.Activate только
@@ -630,8 +652,9 @@ namespace NecromancerTome
} }
// Только гуманоиды обычной комплекции (указание 13.10, FpArms.Allows) - у остальных ~/ё ничего не делает. // Только гуманоиды обычной комплекции (указание 13.10, FpArms.Allows) - у остальных ~/ё ничего не делает.
if (input != null && !gui && FpArms.Allows(z) && if (input != null && !gui && FpArms.Allows(z) &&
(input.CameraChange.WasPressed || input.QuickMenu.WasPressed || input.PermanentActions.QuickMenu.WasPressed)) (input.CameraChange.WasPressed || input.QuickMenu.WasPressed || input.PermanentActions.QuickMenu.WasPressed || DevView))
{ {
DevView = false;
FirstPerson = !FirstPerson; FirstPerson = !FirstPerson;
_fpEye = Vector3.zero; _fpEye = Vector3.zero;
if (FirstPerson) if (FirstPerson)
@@ -665,6 +688,21 @@ namespace NecromancerTome
Debug.Log("[NecromancerTome] Possession: пояс - ячейка " + (BeltSlot + 1)); Debug.Log("[NecromancerTome] Possession: пояс - ячейка " + (BeltSlot + 1));
} }
} }
if (FirstPerson && ZombieBag.Active)
{
// Оружие с пояса (#37): в руки - если зомби им владеет; сменился выбранный предмет - подсказка, если нет.
ItemStack sel = ZombieBag.Belt(BeltSlot);
int key = BeltSlot * 100000 + (sel != null ? sel.itemValue.type : -1);
if (key != _beltKey)
{
_beltKey = key;
if (sel != null)
{
ZombieWeapons.Hint(_player, z, sel.itemValue);
}
}
FpArms.SetWeapon(ZombieWeapons.Current(z));
}
if (input != null && !gui && input.ToggleCrouch.WasPressed) if (input != null && !gui && input.ToggleCrouch.WasPressed)
{ {
_crouchToggled = !_crouchToggled; _crouchToggled = !_crouchToggled;
@@ -845,7 +883,8 @@ namespace NecromancerTome
{ {
// Tab в зомби - окно его сумки, а не рюкзак персонажа (указание 13.10), как у волка. // Tab в зомби - окно его сумки, а не рюкзак персонажа (указание 13.10), как у волка.
GUIWindowManager zwm = LocalPlayerUI.GetUIForPlayer(_player)?.windowManager; GUIWindowManager zwm = LocalPlayerUI.GetUIForPlayer(_player)?.windowManager;
if (zwm != null && zwm.IsWindowOpen("windowpaging") && !ZombieBag.WindowOpen && !RatLoot.Viewing) // M открывает то же окно windowpaging (с картой) - карту не подменять (#36: M в зомби открывал сумку).
if (zwm != null && zwm.IsWindowOpen("windowpaging") && !zwm.IsWindowOpen("map") && !ZombieBag.WindowOpen && !RatLoot.Viewing)
{ {
zwm.CloseAllOpenModalWindows(); zwm.CloseAllOpenModalWindows();
zwm.Close("windowpaging"); zwm.Close("windowpaging");
@@ -862,7 +901,7 @@ namespace NecromancerTome
{ {
// Tab в волке - окно его сумки, а не рюкзак персонажа (тест 10.10: «в волке Tab - вижу сумку игрока»). // Tab в волке - окно его сумки, а не рюкзак персонажа (тест 10.10: «в волке Tab - вижу сумку игрока»).
GUIWindowManager wm = LocalPlayerUI.GetUIForPlayer(_player)?.windowManager; GUIWindowManager wm = LocalPlayerUI.GetUIForPlayer(_player)?.windowManager;
if (wm != null && wm.IsWindowOpen("windowpaging") && !WolfBag.WindowOpen && !RatLoot.Viewing) if (wm != null && wm.IsWindowOpen("windowpaging") && !wm.IsWindowOpen("map") && !WolfBag.WindowOpen && !RatLoot.Viewing)
{ {
wm.CloseAllOpenModalWindows(); wm.CloseAllOpenModalWindows();
wm.Close("windowpaging"); wm.Close("windowpaging");
@@ -905,7 +944,10 @@ namespace NecromancerTome
// до точки удара (FpArms -> FpImpact), а не по его собственному замаху. // до точки удара (FpArms -> FpImpact), а не по его собственному замаху.
PossessionPowers.BreakInvis(z, "удар"); PossessionPowers.BreakInvis(z, "удар");
FpArms.Punch(); FpArms.Punch();
_nextAttack = Time.time + Mathf.Max(FpArms.SwingTime, z.GetAttackTimeoutTicks() / 20f); ItemValue weapon = ZombieWeapons.Current(z);
// С оружием - темп оружия, как у персонажа (#37); кулаками - темп зомби.
_nextAttack = Time.time + (weapon != null ? Mathf.Max(MinWeaponSwing, ZombieWeapons.SwingTime(weapon))
: Mathf.Max(FpArms.SwingTime, z.GetAttackTimeoutTicks() / 20f));
} }
else if (attack && Time.time >= _nextAttack) else if (attack && Time.time >= _nextAttack)
{ {
@@ -2386,6 +2428,18 @@ namespace NecromancerTome
{ {
return; return;
} }
if (ZombieWeapons.Current(z) != null)
{
// Оружие с пояса (#37) - удар как у персонажа, луч из глаз (камера одержимости - в голове зомби). Камера - в
// координатах Unity, луч мира - с Origin (тест 25.10: без него «луч ничего не задел» вплотную).
Transform cam = _player?.playerCamera != null ? _player.playerCamera.transform : null;
Vector3 head = z.getHeadPosition();
if (!ZombieWeapons.Strike(z.world, z, cam != null ? new Ray(cam.position + Origin.position, cam.forward) : new Ray(head, (AimPoint() - head).normalized)))
{
Debug.Log("[NecromancerTome] ZombieWeapons: удар мимо");
}
return;
}
z.SetLookPosition(MeleeAim(z.world, z)); z.SetLookPosition(MeleeAim(z.world, z));
_meleeLookUntil = Time.time + 0.6f; _meleeLookUntil = Time.time + 0.6f;
if (!StartStrike(z)) if (!StartStrike(z))
@@ -2590,14 +2644,13 @@ namespace NecromancerTome
} }
} }
/// <summary>Карта в одержимом стервятнике открывается на нём, а не на теле (указание 08.10: разведка - /// <summary>Карта в одержимом стервятнике и зомби открывается на нём, а не на теле (указание 08.10, #36).</summary>
/// только у стервятника).</summary>
[HarmonyPatch(typeof(XUiC_MapArea), nameof(XUiC_MapArea.OnOpen))] [HarmonyPatch(typeof(XUiC_MapArea), nameof(XUiC_MapArea.OnOpen))]
public static class Patch_XUiC_MapArea_OnOpen_Possession public static class Patch_XUiC_MapArea_OnOpen_Possession
{ {
public static void Postfix(XUiC_MapArea __instance) public static void Postfix(XUiC_MapArea __instance)
{ {
if (Possession.Active && Possession.Flying && Possession.Zombie != null) if (Possession.Scouts && Possession.Zombie != null)
{ {
__instance.PositionMapAt(Possession.Zombie.GetPosition()); __instance.PositionMapAt(Possession.Zombie.GetPosition());
} }
@@ -2610,7 +2663,7 @@ namespace NecromancerTome
{ {
public static bool Prefix(XUiC_MapArea __instance) public static bool Prefix(XUiC_MapArea __instance)
{ {
if (!Possession.Active || !Possession.Flying || Possession.Zombie == null) if (!Possession.Scouts || Possession.Zombie == null)
{ {
return true; return true;
} }
@@ -2871,8 +2924,9 @@ namespace NecromancerTome
} }
} }
/// <summary>Пояс одержимого от первого лица (указание 13.10: «собственный пояс на 2 ячейки», пока пустой) - на месте /// <summary>Пояс одержимого от первого лица (указание 13.10: «собственный пояс на 2 ячейки») - на месте пояса персонажа,
/// пояса персонажа, внизу по центру; выбранная ячейка в рамке. Ячейки пустые - бьёт кулаками.</summary> /// внизу по центру; выбранная ячейка в рамке. В ячейке - иконка предмета из нижнего ряда сумки (#37), полоска износа;
/// чем зомби не владеет или что сломано - бледное (бьёт кулаками).</summary>
static void DrawBelt(float _scale) static void DrawBelt(float _scale)
{ {
float size = 64f * _scale, gap = 6f * _scale, frame = 3f * _scale; float size = 64f * _scale, gap = 6f * _scale, frame = 3f * _scale;
@@ -2890,11 +2944,51 @@ namespace NecromancerTome
GUI.color = Back; GUI.color = Back;
GUI.DrawTexture(r, Texture2D.whiteTexture); GUI.DrawTexture(r, Texture2D.whiteTexture);
GUI.color = prev; GUI.color = prev;
ItemStack st = ZombieBag.Belt(i);
if (st != null)
{
bool fits = ZombieWeapons.Enabled && ZombieWeapons.Check(Possession.Zombie, st.itemValue) == ZombieWeapons.Fit.Ok;
DrawIcon(new Rect(r.x + 6f * _scale, r.y + 6f * _scale, r.width - 12f * _scale, r.height - 12f * _scale), st.itemValue, fits ? 1f : 0.3f);
if (st.itemValue.MaxUseTimes > 0)
{
float left = 1f - Mathf.Clamp01(st.itemValue.UseTimes / st.itemValue.MaxUseTimes);
GUI.color = Color.Lerp(new Color(0.85f, 0.2f, 0.15f), new Color(0.35f, 0.8f, 0.3f), left);
GUI.DrawTexture(new Rect(r.x + 4f * _scale, r.yMax - 6f * _scale, (r.width - 8f * _scale) * left, 3f * _scale), Texture2D.whiteTexture);
GUI.color = prev;
}
if (st.count > 1)
{
GUI.Label(new Rect(r.x, r.yMax - 22f * _scale, r.width - 4f * _scale, 18f * _scale), st.count.ToString(), _style);
}
}
GUI.Label(new Rect(r.x + 4f * _scale, r.y + 2f * _scale, r.width, 18f * _scale), (i + 1).ToString(), _style); GUI.Label(new Rect(r.x + 4f * _scale, r.y + 2f * _scale, r.width, 18f * _scale), (i + 1).ToString(), _style);
} }
GUI.color = prev; GUI.color = prev;
} }
/// <summary>Иконка предмета из атласа ItemIconAtlas (как у ячеек игры) - кусок текстуры атласа по данным спрайта.</summary>
static void DrawIcon(Rect _r, ItemValue _iv, float _alpha)
{
ItemClass ic = _iv?.ItemClass;
string name = ic?.GetIconName();
INGUIAtlas atlas = string.IsNullOrEmpty(name) ? null : LocalPlayerUI.GetUIForPrimaryPlayer()?.xui?.GetAtlasByName("ItemIconAtlas", name);
UISpriteData sd = atlas?.GetSprite(name);
Texture tex = atlas?.texture;
if (sd == null || tex == null || tex.width == 0 || tex.height == 0)
{
return;
}
Rect uv = new Rect((float)sd.x / tex.width, 1f - (float)(sd.y + sd.height) / tex.height, (float)sd.width / tex.width, (float)sd.height / tex.height);
// Иконка не растягивается: вписать по большей стороне.
float k = Mathf.Min(_r.width / sd.width, _r.height / sd.height);
Rect fit = new Rect(_r.center.x - sd.width * k / 2f, _r.center.y - sd.height * k / 2f, sd.width * k, sd.height * k);
Color prev = GUI.color;
Color tint = ic.GetIconTint(_iv);
GUI.color = new Color(tint.r, tint.g, tint.b, tint.a * _alpha);
GUI.DrawTextureWithTexCoords(fit, tex, uv);
GUI.color = prev;
}
static void DrawBar(Rect _r, float _frac, Color _fill, string _text) static void DrawBar(Rect _r, float _frac, Color _fill, string _text)
{ {
Color prev = GUI.color; Color prev = GUI.color;
+42 -1
View File
@@ -487,7 +487,6 @@ namespace NecromancerTome
yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveFillAndSmart)); yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveFillAndSmart));
yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveFillStacks)); yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveFillStacks));
yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveSmart)); yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveSmart));
yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.ToggleLockMode));
} }
public static bool Prefix(XUiC_ContainerStandardControls __instance, System.Reflection.MethodBase __originalMethod) public static bool Prefix(XUiC_ContainerStandardControls __instance, System.Reflection.MethodBase __originalMethod)
@@ -504,6 +503,48 @@ namespace NecromancerTome
} }
} }
/// <summary>Замок ячеек при обыске не включается. В 3.4 ToggleLockMode нет: замок - свойство UserLockMode, кнопка -
/// XUiC_ButtonSelectable (её подсветку снимаем, иначе кнопка осталась бы «нажатой»).</summary>
[HarmonyPatch(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.UserLockMode), MethodType.Setter)]
public static class Patch_UserLockMode
{
public static bool Prefix(XUiC_ContainerStandardControls __instance, bool value)
{
if (!value || !Viewing)
{
return true;
}
if (__instance.btnToggleLockMode != null)
{
__instance.btnToggleLockMode.IsSelected = false;
}
return false;
}
}
/// <summary>3.4: предмет, брошенный на пустое место окна (не в ячейку), игра кладёт прямо в ящик, мешок или рюкзак
/// (dropItemToGrid) - мимо запертых ячеек. При обыске - отказ, предмет остаётся в руке.</summary>
[HarmonyPatch]
public static class Patch_DropItemToGrid
{
static System.Collections.Generic.IEnumerable<System.Reflection.MethodBase> TargetMethods()
{
yield return AccessTools.Method(typeof(XUiC_LootContainer), nameof(XUiC_LootContainer.dropItemToGrid));
yield return AccessTools.Method(typeof(XUiC_BagContainer), nameof(XUiC_BagContainer.dropItemToGrid));
yield return AccessTools.Method(typeof(XUiC_Backpack), nameof(XUiC_Backpack.dropItemToGrid));
}
public static bool Prefix()
{
if (!Viewing)
{
return true;
}
Deny();
return false;
}
}
/// <summary>Щелчок по запертой ячейке контейнера - отказ «рюкзак полон».</summary> /// <summary>Щелчок по запертой ячейке контейнера - отказ «рюкзак полон».</summary>
[HarmonyPatch(typeof(XUiC_ItemStack), nameof(XUiC_ItemStack.Update))] [HarmonyPatch(typeof(XUiC_ItemStack), nameof(XUiC_ItemStack.Update))]
public static class Patch_ItemStack_Update public static class Patch_ItemStack_Update
+1 -1
View File
@@ -27,7 +27,7 @@ namespace NecromancerTome
public static class SnakeControl public static class SnakeControl
{ {
public const float WalkSpeed = 6f; // велосипед, vehicles.xml velocityMax public const float WalkSpeed = 6f; // велосипед, vehicles.xml velocityMax
public const float RunSpeed = 16.5f; // мотоцикл турбо с нагнетателем public const float RunSpeed = 12.5f; // было 16.5 (мотоцикл турбо); 26.10: «Уменьши скорость змеи на шифте на четверть» - фризы подгрузки карты
public const float Gravity = 20f; public const float Gravity = 20f;
/// <summary>Прыжок на Пробел (указание 28.09: «иначе не выбраться из ям»). 10 м/с при g = 20 - /// <summary>Прыжок на Пробел (указание 28.09: «иначе не выбраться из ям»). 10 м/с при g = 20 -
/// подъём на 2,5 м: вместе с заползанием на блок хватает на стену в три блока.</summary> /// подъём на 2,5 м: вместе с заползанием на блок хватает на стену в три блока.</summary>
+2 -2
View File
@@ -33,7 +33,7 @@ namespace NecromancerTome
} }
foreach (EntityPlayer p in w.Players.list) foreach (EntityPlayer p in w.Players.list)
{ {
if (p == null || p.inventory?.holdingItem?.Name != CinerariumThreads.ThreadKnifeName || p.emodel == null || if (p == null || !CinerariumThreads.IsThreadToolName(p.inventory?.holdingItem?.Name) || p.emodel == null ||
p.emodel.IsFPV && !p.isEntityRemote) p.emodel.IsFPV && !p.isEntityRemote)
{ {
continue; continue;
@@ -74,7 +74,7 @@ namespace NecromancerTome
public static bool Applies(EntityAlive _e) public static bool Applies(EntityAlive _e)
{ {
return TpHold > 0 && _e != null && _e.inventory?.holdingItem?.Name == CinerariumThreads.ThreadKnifeName; return TpHold > 0 && _e != null && CinerariumThreads.IsThreadToolName(_e.inventory?.holdingItem?.Name);
} }
} }
+32
View File
@@ -0,0 +1,32 @@
using System;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Баг игры 3.4 (найден 24.10.2026: «словил ошибку при переключении ножа», #79). Inventory.SetStackAt
/// ДО записи в слот пояса зовёт новую аналитику LogToolBeltUsageEvent; та считает характеристики
/// предмета через EffectManager.GetValue без сущности (_entity = null), и у passive_effect со
/// значением из CVar (у кинжала некроманта value="@$necroKnifeDamageCVar") PassiveEffect.ModifyValue
/// падает на _ea.Buffs. Исключение вылетает раньше ItemGrid.SetItem - нож не меняется, слот не пишется.
///
/// Лечение: аналитика - только телеметрия игры, её исключение глотаем (Finalizer), SetStackAt
/// продолжает и пишет слот как обычно. Предупреждение в лог - один раз за запуск.
/// </summary>
[HarmonyPatch(typeof(Inventory), nameof(Inventory.LogToolBeltUsageEvent))]
public static class Patch_Inventory_LogToolBeltUsageEvent_Guard
{
static bool _warned;
public static Exception Finalizer(Exception __exception)
{
if (__exception != null && !_warned)
{
_warned = true;
Debug.LogWarning("[NecromancerTome] ToolBeltAnalyticsGuard: game analytics failed in Inventory.LogToolBeltUsageEvent, ignored: " + __exception.Message);
}
return null;
}
}
}
+18 -1
View File
@@ -690,7 +690,6 @@ namespace NecromancerTome
yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveFillAndSmart)); yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveFillAndSmart));
yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveFillStacks)); yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveFillStacks));
yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveSmart)); yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveSmart));
yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.ToggleLockMode));
} }
public static bool Prefix(XUiC_ContainerStandardControls __instance) public static bool Prefix(XUiC_ContainerStandardControls __instance)
@@ -699,6 +698,24 @@ namespace NecromancerTome
} }
} }
/// <summary>Замок ячеек у сумки волка не включается (в 3.4 вместо ToggleLockMode - свойство UserLockMode).</summary>
[HarmonyPatch(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.UserLockMode), MethodType.Setter)]
public static class Patch_ContainerStandardControls_UserLockMode_WolfBag
{
public static bool Prefix(XUiC_ContainerStandardControls __instance, bool value)
{
if (!value || !WolfBagControls.Blocked(__instance))
{
return true;
}
if (__instance.btnToggleLockMode != null)
{
__instance.btnToggleLockMode.IsSelected = false;
}
return false;
}
}
/// <summary>Кнопка замка видна по привязке {hasslotlocks} - у волка её нет.</summary> /// <summary>Кнопка замка видна по привязке {hasslotlocks} - у волка её нет.</summary>
[HarmonyPatch(typeof(XUiC_BagContainer), nameof(XUiC_BagContainer.GetBindingValueInternal))] [HarmonyPatch(typeof(XUiC_BagContainer), nameof(XUiC_BagContainer.GetBindingValueInternal))]
public static class Patch_BagContainer_Binding_WolfBag public static class Patch_BagContainer_Binding_WolfBag
+34 -2
View File
@@ -11,6 +11,8 @@ namespace NecromancerTome
/// - при выходе из одержимости зомби сбрасывает рюкзаком всё, что было в сумке (и на поясе - когда в поясе /// - при выходе из одержимости зомби сбрасывает рюкзаком всё, что было в сумке (и на поясе - когда в поясе
/// появятся предметы); /// появятся предметы);
/// - гибель одержимого - мешок на месте и метка на карте, как у сумки волка. /// - гибель одержимого - мешок на месте и метка на карте, как у сумки волка.
/// ПОЯС (#37, 25.10) - нижний ряд той же сумки: Possession.BeltSlots ячеек под верхним рядом, оружие в руке берётся
/// оттуда (ZombieWeapons). Хранение, выброс мешком и метка - общие с сумкой.
/// Лут (указание 13.10: «раз у них E свободно, подключай лутание») - как у волка: E на ящик или мешок - обыск с /// Лут (указание 13.10: «раз у них E свободно, подключай лутание») - как у волка: E на ящик или мешок - обыск с
/// переносом в сумку, E на предмет на земле - подобрать (WolfBag.Target - сумка зомби, пока она есть). /// переносом в сумку, E на предмет на земле - подобрать (WolfBag.Target - сумка зомби, пока она есть).
/// Только зомби из списка вида от первого лица (FpArms.Allows), не питомцы. Окно - то же, что у сумки волка /// Только зомби из списка вида от первого лица (FpArms.Allows), не питомцы. Окно - то же, что у сумки волка
@@ -28,12 +30,41 @@ namespace NecromancerTome
public const string WaypointName = "necroZombieBagWaypoint"; public const string WaypointName = "necroZombieBagWaypoint";
public static bool WindowOpen; public static bool WindowOpen;
static Bag _bag; static Bag _bag;
/// <summary>Ячеек в ряду: верхний ряд - сумка, нижний - пояс.</summary>
static int _cols;
static string FilePath => Path.Combine(GameIO.GetSaveGameDir(), FileName); static string FilePath => Path.Combine(GameIO.GetSaveGameDir(), FileName);
public static bool Active => _bag != null; public static bool Active => _bag != null;
public static Bag Current => _bag; public static Bag Current => _bag;
/// <summary>Ячейка пояса (0..BeltSlots-1) - нижний ряд сумки; нет сумки - null.</summary>
public static ItemStack Belt(int _i)
{
ItemStack[] items = _bag?.ItemGrid?.items;
int idx = _cols + _i;
return items != null && _i >= 0 && _i < Possession.BeltSlots && idx < items.Length && !items[idx].IsEmpty() ? items[idx] : null;
}
/// <summary>necropossess belt: положить предмет в ячейку пояса (проверка без мыши).</summary>
public static bool DevSetBelt(int _i, ItemStack _s)
{
if (_bag == null || _i < 0 || _i >= Possession.BeltSlots)
{
return false;
}
_bag.SetSlot(_cols + _i, _s);
Save();
return true;
}
/// <summary>Предмет пояса изменился на месте (износ) - окно и файл.</summary>
public static void BeltChanged()
{
_bag?.onBackpackChanged();
Save();
}
/// <summary>Записать после лута (WolfBag.SaveTarget).</summary> /// <summary>Записать после лута (WolfBag.SaveTarget).</summary>
public static void SaveNow() => Save(); public static void SaveNow() => Save();
@@ -64,8 +95,9 @@ namespace NecromancerTome
{ {
Debug.LogWarning("[NecromancerTome] ZombieBag: файл сумки не прочитан (" + e.Message + ") - оставлен как есть"); Debug.LogWarning("[NecromancerTome] ZombieBag: файл сумки не прочитан (" + e.Message + ") - оставлен как есть");
} }
_bag = new Bag(new Vector2i(SlotsFor(_z), 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null); _cols = Mathf.Max(SlotsFor(_z), Possession.BeltSlots);
Debug.Log("[NecromancerTome] ZombieBag: сумка " + _z.EntityName + " - ячеек " + _bag.SlotCount); _bag = new Bag(new Vector2i(_cols, 2), XUiC_ItemStack.StackLocationTypes.LootContainer, null);
Debug.Log("[NecromancerTome] ZombieBag: сумка " + _z.EntityName + " - ячеек " + _bag.SlotCount + ", из них пояс " + Possession.BeltSlots);
} }
/// <summary>Вселение кончилось: всё из сумки - мешком на месте зомби; погиб - ещё и метка на карте.</summary> /// <summary>Вселение кончилось: всё из сумки - мешком на месте зомби; погиб - ещё и метка на карте.</summary>
+339
View File
@@ -0,0 +1,339 @@
using System.Collections.Generic;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Оружие на поясе одержимого зомби (#37, решения пользователя 25.10). Пояс - две ячейки нижнего ряда сумки зомби
/// (ZombieBag.Belt), туда попадает только то, что зомби нашёл сам или что положили в сумку. Чем зомби владеет -
/// по его типу («зомби строитель сможет пользоваться простыми инструментами: топоры, молотки и т.п. Медсестра -
/// ножами»; список утверждён 25.10, карточка #37). Неумелое или сломанное в выбранной ячейке - кулаки и подсказка.
///
/// Только ближний бой (ItemActionDynamic - всё оружие и инструменты ближнего боя персонажа). Бензопила и бур в игре
/// стрелковые (Ranged, топливо), гвоздомёт - метатель: они придут вместе с огнестрелом (#80).
///
/// Удар - от первого лица, в миг удара рук (FpArms -> Possession.FpImpact): луч из глаз зомби на дальность оружия и
/// ItemActionAttack.Hit, как у персонажа (урон по существам и блокам, кровь, звук, отрубание, износ). Урон - самого
/// предмета с его модами, без перков и баффов зомби (holdingEntity = null): «урон от оружия».
/// </summary>
public static class ZombieWeapons
{
/// <summary>Выключатель (necroarms weapon on|off): выкл - все бьют кулаками, как до #37.</summary>
public static bool Enabled = true;
// Группы предметов: имена групп (не путать с концовками имён предметов, их тоже можно писать в умениях).
const string Knife = "Knife", SmallKnife = "SmallKnife", Club = "Club", Baton = "Baton", Knuckles = "Knuckles", Spear = "Spear",
Sledge = "Sledge", Axe = "Axe", Hammer = "Hammer", Pick = "Pick", Shovel = "Shovel", Wrench = "Wrench", Light = "Light";
static readonly HashSet<string> GroupNames = new HashSet<string>
{ Knife, SmallKnife, Club, Baton, Knuckles, Spear, Sledge, Axe, Hammer, Pick, Shovel, Wrench, Light };
/// <summary>Умения по началу имени класса зомби (как FpArms.Allows; Feral/Radiated/Charged/Infernal - как базовый).
/// Строка - группа или концовка имени предмета игры (StoneAxe подходит и к TazasStoneAxe).</summary>
static readonly Dictionary<string, string[]> Skills = new Dictionary<string, string[]>
{
{ "zombieUtilityWorker", new[] { Hammer, Pick, Shovel, Axe, Sledge, Wrench, "ImpactDriver", "Auger", "Nailgun" } },
{ "zombieLumberjack", new[] { Axe, "Chainsaw", Sledge } },
{ "zombieTomClark", new[] { Shovel, Axe, Spear } },
{ "zombieJanitor", new[] { Wrench, Shovel, Light, Club } },
{ "zombieNurse", new[] { Knife } },
{ "zombieLab", new[] { Knife, "StunBaton", "PlasmaBaton" } },
{ "zombieMaleHazmat", new[] { Shovel, Light, Wrench } },
{ "zombieSoldier", new[] { Knife, Baton, Spear } },
{ "zombieBiker", new[] { Knuckles, Club, "PipeBaton", Wrench } },
{ "zombieInmate", new[] { "BoneKnife", Knuckles, "PipeBaton" } },
{ "zombieYo", new[] { Knuckles, "BaseballBat", "PipeBaton" } },
{ "zombieSkateboarder", new[] { "BaseballBat", "PipeBaton" } },
{ "zombieBowler", new[] { Club, Sledge } },
{ "zombieBoe", new[] { Club, Knuckles, "StoneAxe", "StoneShovel" } },
{ "zombieJoe", new[] { Club, Knuckles, "StoneAxe", "StoneShovel" } },
{ "zombieMoe", new[] { Sledge, Club } },
{ "zombieSteve", new[] { Club, "StoneAxe", "StoneShovel" } },
{ "zombieBusinessMan", new[] { "BaseballBat", SmallKnife } },
{ "zombieBurnt", new[] { "Torch", Club } },
{ "zombieDarlene", new[] { Knife, "WoodenClub" } },
{ "zombieArlene", new[] { Knife, "BaseballBat" } },
{ "zombieMarlene", new[] { Knife, "BaseballBat" } },
{ "zombiePartyGirl", new[] { "CandyKnife", "CandyClub", "BaseballBat" } },
};
static readonly Dictionary<int, HashSet<string>> _groups = new Dictionary<int, HashSet<string>>();
static bool Tag(ItemClass _ic, string _tag) => _ic.ItemTags.Test_AnySet(FastTags<TagGroup.Global>.Parse(_tag));
/// <summary>Группы предмета: у предметов игры - по имени, у предметов других модов - по тегам (как у игры).</summary>
static HashSet<string> Groups(ItemClass _ic)
{
if (_groups.TryGetValue(_ic.Id, out HashSet<string> g))
{
return g;
}
g = new HashSet<string>();
string n = _ic.Name ?? "";
bool motor = Tag(_ic, "motorTool") || Tag(_ic, "nailgun");
if (n.StartsWith("meleeWpnBlade") || Tag(_ic, "knife") || Tag(_ic, "machete"))
{
g.Add(Knife);
if (!n.Contains("Machete") && !Tag(_ic, "machete"))
{
g.Add(SmallKnife);
}
}
if (n.StartsWith("meleeWpnClub") || Tag(_ic, "club"))
{
g.Add(Club);
}
if (n.StartsWith("meleeWpnBaton") || Tag(_ic, "batonJunkie") || Tag(_ic, "stunBaton"))
{
g.Add(Baton);
}
if (n.StartsWith("meleeWpnKnuckles") || Tag(_ic, "knuckleSkill"))
{
g.Add(Knuckles);
}
if (n.StartsWith("meleeWpnSpear") || Tag(_ic, "spearSkill"))
{
g.Add(Spear);
}
if (n.StartsWith("meleeWpnSledge") || Tag(_ic, "sledge"))
{
g.Add(Sledge);
}
if (!motor && (n.StartsWith("meleeToolAxe") || Tag(_ic, "axe")))
{
g.Add(Axe);
}
if (n.Contains("ClawHammer") || !motor && Tag(_ic, "repairTool") && !Tag(_ic, "axe"))
{
g.Add(Hammer);
}
if (n.StartsWith("meleeToolShovel") || Tag(_ic, "shovel"))
{
g.Add(Shovel);
}
else if (!motor && (n.StartsWith("meleeToolPick") || Tag(_ic, "miningTool") && !Tag(_ic, "axe")))
{
g.Add(Pick);
}
if (!n.Contains("ImpactDriver") && (n.StartsWith("meleeToolSalvage") || Tag(_ic, "salvageTool")))
{
g.Add(Wrench);
}
if (n.Contains("Flashlight") || n.Contains("Torch") || Tag(_ic, "heldTorch"))
{
g.Add(Light);
}
_groups[_ic.Id] = g;
return g;
}
static string[] SkillsOf(EntityAlive _z)
{
if (_z == null || !EntityClass.list.TryGetValue(_z.entityClass, out EntityClass cls) || cls?.entityClassName == null)
{
return null;
}
foreach (KeyValuePair<string, string[]> kv in Skills)
{
if (cls.entityClassName.StartsWith(kv.Key))
{
return kv.Value;
}
}
return null;
}
/// <summary>Зомби этого типа владеет предметом.</summary>
public static bool Knows(EntityAlive _z, ItemClass _ic)
{
string[] skills = SkillsOf(_z);
if (skills == null || _ic == null)
{
return false;
}
HashSet<string> g = Groups(_ic);
string n = _ic.Name ?? "";
foreach (string s in skills)
{
if (GroupNames.Contains(s) ? g.Contains(s) : n.EndsWith(s))
{
return true;
}
}
return false;
}
public enum Fit { Fists, Ok, Unknown, Broken }
/// <summary>Чем бьёт зомби с этим предметом в выбранной ячейке.</summary>
public static Fit Check(EntityAlive _z, ItemValue _iv)
{
if (_iv == null || _iv.IsEmpty() || _iv.ItemClass == null)
{
return Fit.Fists;
}
ItemClass ic = _iv.ItemClass;
if (!(ic.Actions != null && ic.Actions.Length > 0 && ic.Actions[0] is ItemActionDynamic) || !Knows(_z, ic))
{
return Fit.Unknown;
}
if (_iv.MaxUseTimes > 0 && _iv.UseTimes >= _iv.MaxUseTimes)
{
return Fit.Broken;
}
return Fit.Ok;
}
/// <summary>Оружие в руке одержимого сейчас: выбранная ячейка пояса, если зомби им владеет и оно цело; иначе null.</summary>
public static ItemValue Current(EntityAlive _z)
{
if (!Enabled || !ZombieBag.Active)
{
return null;
}
ItemStack s = ZombieBag.Belt(Possession.BeltSlot);
return s != null && Check(_z, s.itemValue) == Fit.Ok ? s.itemValue : null;
}
/// <summary>Пауза между ударами этим оружием - как у персонажа (удары в минуту).</summary>
public static float SwingTime(ItemValue _iv)
{
float apm = EffectManager.GetValue(PassiveEffects.AttacksPerMinute, _iv, 60f, null, null, _iv.ItemClass.ItemTags);
return apm > 1f ? 60f / apm : 1f;
}
/// <summary>Удар оружием в миг удара рук: луч из глаз зомби (камера одержимости) на дальность оружия. Мимо - false
/// (звука нет, как у персонажа - замах в воздух).</summary>
public static bool Strike(World _world, EntityAlive _z, Ray _ray)
{
ItemStack stack = ZombieBag.Belt(Possession.BeltSlot);
ItemValue iv = stack?.itemValue;
if (iv == null || Check(_z, iv) != Fit.Ok || !(iv.ItemClass.Actions[0] is ItemActionDynamic act))
{
return false;
}
FastTags<TagGroup.Global> tags = ItemActionAttack.PrimaryTag | iv.ItemClass.ItemTags;
float range = EffectManager.GetValue(PassiveEffects.MaxRange, iv, act.RangeDefault, null, null, tags);
float blockRange = EffectManager.GetValue(PassiveEffects.BlockRange, iv, range, null, null, tags);
float sphere = act.SphereRadius;
float dist = Mathf.Max(range, blockRange) + sphere;
_ray.origin -= sphere * _ray.direction;
// Свои коллайдеры зомби - мимо луча (как GetExecuteActionTarget у игры). Камера одержимости - в голове зомби:
// если луч всё же задел его самого (тест 25.10: все удары «мимо» вплотную), - продолжить луч за ним.
int layer = _z.GetModelLayer();
_z.SetModelLayer(2);
WorldRayHitInfo hit = null;
float passed = 0f;
try
{
for (int i = 0; i < 4 && hit == null; i++)
{
Ray ray = new Ray(_ray.origin + _ray.direction * passed, _ray.direction);
EntityAlive target = null;
bool any = Voxel.Raycast(_world, ray, dist - passed, -538767389, 128, sphere);
if (any)
{
target = ItemActionAttack.GetEntityFromHit(Voxel.voxelRayHitInfo) as EntityAlive;
}
if (target == null)
{
any = Voxel.Raycast(_world, ray, dist - passed, -538488845, 128, sphere);
target = any ? ItemActionAttack.GetEntityFromHit(Voxel.voxelRayHitInfo) as EntityAlive : null;
}
if (!any)
{
break;
}
if (target == _z)
{
passed += Mathf.Sqrt(Voxel.voxelRayHitInfo.hit.distanceSq) + 0.05f;
continue;
}
hit = Voxel.voxelRayHitInfo.Clone();
}
}
finally
{
_z.SetModelLayer(layer);
}
if (hit?.tag == null)
{
string phys = "ничего";
if (Physics.Raycast(_ray.origin - Origin.position, _ray.direction, out RaycastHit ph, 5f, -5, QueryTriggerInteraction.Ignore))
{
phys = ph.transform.name + " слой " + LayerMask.LayerToName(ph.transform.gameObject.layer) + " в " + ph.distance.ToString("F2");
}
Debug.Log("[NecromancerTome] ZombieWeapons: луч ничего не задел (дальность " + dist.ToString("F1") + (passed > 0f ? ", сквозь себя " + passed.ToString("F1") : "") +
"), луч " + _ray.origin.ToString("F2") + " -> " + _ray.direction.ToString("F2") + ", голова " + _z.getHeadPosition().ToString("F2") + ", физика: " + phys);
return false;
}
bool entity = hit.tag.StartsWith("E_");
float d = Vector3.Distance(_ray.origin, hit.hit.pos);
if (entity ? d > range + sphere : d > blockRange + sphere)
{
Debug.Log("[NecromancerTome] ZombieWeapons: далеко - " + hit.tag + " в " + d.ToString("F1") + " м, достаёт " + (entity ? range : blockRange).ToString("F1"));
return false;
}
if (entity && ItemActionAttack.GetEntityFromHit(hit) is EntityAlive e && (e == _z || e is EntityPlayer || SnakeControl.IsNecroEntity(e)))
{
Debug.Log("[NecromancerTome] ZombieWeapons: под ударом свой - " + e.EntityName);
return false; // хозяина и его нежить одержимый не бьёт - как кулаками (MeleeAim)
}
BlockValue bv = ItemActionAttack.GetBlockHit(_world, hit);
float blockDamage = entity || bv.isair ? 0f : act.GetDamageBlock(iv, bv, null, 0);
float entityDamage = act.GetDamageEntity(iv, null, 0);
string tagHit = hit.tag;
FastTags<TagGroup.Global> dTags = tags;
if (tagHit == "E_BP_Head")
{
dTags |= FastTags<TagGroup.Global>.Parse("head");
}
float dismember = EffectManager.GetValue(PassiveEffects.DismemberChance, iv, 0f, null, null, dTags);
ItemActionAttack.AttackHitInfo details = new ItemActionAttack.AttackHitInfo { WeaponTypeTag = ItemActionAttack.MeleeTag };
ItemActionAttack.Hit(hit, _z.entityId, act.DamageType, blockDamage, entityDamage, 1f, 1f, 0f, dismember,
iv.ItemClass.MadeOfMaterial.SurfaceCategory, new DamageMultiplier(), new List<string>(), details, 1, 0, 0f, null,
act.ToolBonuses, ItemActionAttack.EnumAttackMode.RealNoHarvesting, act.HitSoundOverrides, -1, iv);
// Износ - как у персонажа (ItemActionDynamic.hitTarget); сломалось - звук, дальше кулаки.
if (iv.MaxUseTimes > 0)
{
iv.UseTimes += EffectManager.GetValue(PassiveEffects.DegradationPerUse, iv, 1f, null, null, tags) * ItemAction.ItemDegradationModifier;
if (iv.UseTimes >= iv.MaxUseTimes)
{
Audio.Manager.BroadcastPlay(_z, "itembreak");
Debug.Log("[NecromancerTome] ZombieWeapons: " + iv.ItemClass.Name + " сломалось");
}
ZombieBag.BeltChanged();
}
Debug.Log("[NecromancerTome] ZombieWeapons: удар " + iv.ItemClass.Name + " по " + (entity ? tagHit : bv.Block?.GetBlockName()) +
", урон " + (entity ? entityDamage : blockDamage).ToString("F0") + (details.bKilled ? ", убит" : ""));
return true;
}
/// <summary>Подсказка при смене выбранного предмета: неумелое или сломанное - кулаки.</summary>
public static void Hint(EntityPlayerLocal _player, EntityAlive _z, ItemValue _iv)
{
Fit f = Enabled ? Check(_z, _iv) : Fit.Fists;
if (f == Fit.Unknown)
{
GameManager.ShowTooltip(_player, string.Format(Localization.Get("necroZombieWeaponUnknown"), _iv.ItemClass.GetLocalizedItemName()));
}
else if (f == Fit.Broken)
{
GameManager.ShowTooltip(_player, string.Format(Localization.Get("necroZombieWeaponBroken"), _iv.ItemClass.GetLocalizedItemName()));
}
}
/// <summary>necroarms weapon: что умеет текущий одержимый и что в его поясе.</summary>
public static string Dump(EntityAlive _z)
{
string[] sk = SkillsOf(_z);
string s = "оружие " + (Enabled ? "вкл" : "ВЫКЛ") + ", умения: " + (sk != null ? string.Join(", ", sk) : "нет");
for (int i = 0; i < Possession.BeltSlots; i++)
{
ItemStack st = ZombieBag.Belt(i);
s += "\n ячейка " + (i + 1) + (i == Possession.BeltSlot ? " (выбрана)" : "") + ": " +
(st == null || st.IsEmpty() ? "пусто" : st.itemValue.ItemClass.Name + " [" + string.Join(",", Groups(st.itemValue.ItemClass)) + "] - " + Check(_z, st.itemValue));
}
return s;
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 KiB

After

Width:  |  Height:  |  Size: 631 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB