Compare commits
@@ -210,9 +210,9 @@
|
||||
</append>
|
||||
<!-- Фильтр урны-логиста (16.10.2026, HarmonySrc/CinerariumFilterWindow.cs, CinerariumGroups.cs): значки групп предметов
|
||||
в окне «Содержимое» рядом с ячейкой черепа. Окно в группе bagStorage (xui.xml) - контроллер прячет его, если окно
|
||||
открыто не урной-логистом (сумки транспорта и волка). f0..f21 - рамки, g0..g21 - значки (иконки ставит контроллер). -->
|
||||
открыто не урной-логистом (сумки транспорта и волка). f0..f25 - рамки, g0..g25 - значки (иконки ставит контроллер). -->
|
||||
<append xpath="/windows">
|
||||
<window name="necroUrnFilter" width="348" height="279" depth="5" panel="Left" backgroundcolor="[black]" cursor_area="true" controller="NecromancerTome.NecroUrnFilter, NecromancerHarmony">
|
||||
<window name="necroUrnFilter" width="348" height="335" depth="5" panel="Left" backgroundcolor="[black]" cursor_area="true" controller="NecromancerTome.NecroUrnFilter, NecromancerHarmony">
|
||||
<rect name="header" height="43" depth="0">
|
||||
<sprite depth="1" name="headerbackground" sprite="ui_game_panel_header" width="348" height="43" />
|
||||
<sprite depth="3" name="windowIcon" style="icon32px" pivot="center" pos="21,-21" sprite="ui_game_symbol_sort" />
|
||||
@@ -262,6 +262,73 @@
|
||||
<sprite depth="3" name="g20" pos="120,-219" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f21" pos="174,-217" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g21" pos="176,-219" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f22" pos="230,-217" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g22" pos="232,-219" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f23" pos="286,-217" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g23" pos="288,-219" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f24" pos="6,-273" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g24" pos="8,-275" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f25" pos="62,-273" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<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>
|
||||
</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): блок справа от ячеек урны в «Содержимом» - всего,
|
||||
доступно, до следующей. Позицию по числу ячеек ставит контроллер; вне урны блок скрыт. -->
|
||||
<append xpath="/windows/window[@name='windowBagStorage']">
|
||||
<rect name="necroUrnLimit" pos="87,-49" width="420" height="75" depth="6" controller="NecromancerTome.NecroUrnLimit, NecromancerHarmony">
|
||||
<sprite depth="6" pos="0,0" width="420" height="75" color="0,0,0,190" sprite="menu_empty2px" type="sliced" />
|
||||
<label depth="7" name="placed" pos="10,-4" width="404" height="22" font_size="20" color="[white]" />
|
||||
<label depth="7" name="available" pos="10,-27" width="404" height="22" font_size="20" color="[white]" />
|
||||
<label depth="7" name="next" pos="10,-50" width="404" height="22" font_size="20" color="200,200,205" />
|
||||
</rect>
|
||||
</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). -->
|
||||
<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']/@pos">20,-21</set>
|
||||
</windows>
|
||||
|
||||
@@ -62,5 +62,6 @@
|
||||
<!-- Фильтр урны-логиста - в окне хранилища игры (bagStorage), рядом с ячейкой «Содержимого» (windows.xml). -->
|
||||
<append xpath="/xui/window_group[@name='bagStorage']">
|
||||
<window name="necroUrnFilter" />
|
||||
<window name="necroUrnZone" />
|
||||
</append>
|
||||
</xui>
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
it on top would recolor it unintentionally). TintColor above is unaffected - that's
|
||||
the 3D block MESH's tint, a completely separate thing from the 2D inventory icon. -->
|
||||
<property name="CustomIcon" value="pyramidOfSpirit" />
|
||||
<property name="Group" value="Decoration" />
|
||||
<property name="Group" value="Decoration,Necromancy" />
|
||||
<property name="FilterTags" value="MC_building,SC_decor" />
|
||||
<property name="Collide" value="movement,melee,bullet,arrow,rocket" />
|
||||
<property name="StabilitySupport" value="false" />
|
||||
@@ -201,6 +201,7 @@
|
||||
necroossuary, собрана из префаба коллектора росы: LODGroup, слои и коллайдер ванильные.
|
||||
Шейдер материалу ставит GeoOssuaryPatch (шейдер ванильного коллектора). -->
|
||||
<block name="necroGeoOssuary">
|
||||
<property name="Group" value="Basics,Food/Cooking,Building,advBuilding,Necromancy"/>
|
||||
<property name="Extends" value="cntDewCollector" />
|
||||
<property name="UnlockedBy" value="craftingNecroNecromancy" />
|
||||
<property name="DescriptionKey" value="necroGeoOssuaryDesc" />
|
||||
@@ -208,9 +209,13 @@
|
||||
<property name="ItemIconBackdrop" value="ui_game_symbol_mining" />
|
||||
<property name="ItemBackgroundColor" value="140,20,20,255" />
|
||||
<property name="FuelTypes" value="{blood,resourceBloodSphere,resourceBloodStone}" /><!-- камень - вечное топливо (GeoOssuaryPatch.removeFuel) -->
|
||||
<property name="OutputTypes" value="{os1,blood,1,0,1,resourceRockSmall,resourceRockSmall,1,1,6000,10000,collector_complete_item}{os2,blood,1,0,1,resourceRockSmall,resourceRockSmall,1,1,6000,10000,collector_complete_item}{os3,blood,1,0,1,resourceRockSmall,resourceRockSmall,1,1,6000,10000,collector_complete_item}" />
|
||||
<property name="Outputs" value="os1,os2,os3" />
|
||||
<property name="OutputGridHeight" value="1" />
|
||||
<!-- #66 (22.10.2026): хранилище - 2 ряда (6 ячеек, «Хранилище добычи - увеличить»); столбец берёт ресурс из образца
|
||||
над ним (GeoOssuary.ModSlotOf: «положить 3 разных образца, и тогда в ячейки под ними пойдут такие же ресурсы»);
|
||||
время цикла вдвое дольше - общая добыча прежняя. Видно одну ячейку топлива; в данных их по-прежнему 6
|
||||
(FuelGridHeight 2) - уменьшение обрезало бы сохранённое. -->
|
||||
<property name="OutputTypes" value="{os1,blood,1,0,1,resourceRockSmall,resourceRockSmall,1,1,12000,20000,collector_complete_item}{os2,blood,1,0,1,resourceRockSmall,resourceRockSmall,1,1,12000,20000,collector_complete_item}{os3,blood,1,0,1,resourceRockSmall,resourceRockSmall,1,1,12000,20000,collector_complete_item}{os4,blood,1,0,1,resourceRockSmall,resourceRockSmall,1,1,12000,20000,collector_complete_item}{os5,blood,1,0,1,resourceRockSmall,resourceRockSmall,1,1,12000,20000,collector_complete_item}{os6,blood,1,0,1,resourceRockSmall,resourceRockSmall,1,1,12000,20000,collector_complete_item}" />
|
||||
<property name="Outputs" value="os1,os2,os3,os4,os5,os6" />
|
||||
<property name="OutputGridHeight" value="2" />
|
||||
<property name="FuelGridHeight" value="2" />
|
||||
<property name="RequiredMods" value="resourceRockSmall,resourceRockSmall,resourceRockSmall" />
|
||||
<property name="RequiredModsOnly" value="true" />
|
||||
@@ -223,7 +228,9 @@
|
||||
<property name="FuelTitleSprite" value="ui_game_symbol_skull" />
|
||||
<property name="FuelTitleSpriteAtlas" value="UIAtlas" />
|
||||
<property name="FuelTypesSprites" value="BloodSphere,ItemIconAtlasGreyscale" />
|
||||
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necroossuary?necroOssuaryPrefab.prefab" />
|
||||
<!-- 22.10.2026 (#67): новая модель - клёпаный котёл на костяной треноге (necroOssuaryCauldronPrefab: body, lid, sphere,
|
||||
glow; Standard-шейдер, тег коллайдеру - DecorColliderTagPatch). Старый necroOssuaryPrefab убран из бандла 23.10. -->
|
||||
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necroossuary?necroOssuaryCauldronPrefab.prefab" />
|
||||
<property name="CustomIcon" value="necroGeoOssuary" />
|
||||
<property name="EconomicValue" value="0" />
|
||||
<property name="SellableToTrader" value="false" />
|
||||
@@ -331,6 +338,7 @@
|
||||
<property name="CanPickup" value="false"/>
|
||||
</block>
|
||||
<block name="necroChalkboardPlayer">
|
||||
<property name="Group" value="Decor/Miscellaneous,Necromancy"/>
|
||||
<!-- Меловая доска игрока (указание 10.10: «мне нравится использовать её во время игры, делая записи»).
|
||||
Та же модель и надпись, что у сюжетной necroChalkboard, но крафтится и пишется в обычной игре. Отдельный
|
||||
блок, чтобы сюжетная доска чертогов (на ней пишет мод, MindPalace.UpdateBoards) осталась неизменяемой.
|
||||
@@ -391,7 +399,7 @@
|
||||
<drop event="Destroy" name="plantedNecroBloodRose1" count="1"/>
|
||||
<property name="SortOrder1" value="B953"/>
|
||||
<property name="SortOrder2" value="0100"/>
|
||||
<property name="Group" value="Resources"/>
|
||||
<property name="Group" value="Resources,Necromancy"/>
|
||||
<property name="SoundPickup" value="seeds_generic1_grab"/>
|
||||
<property name="SoundPlace" value="seeds_generic1_place"/>
|
||||
</block>
|
||||
@@ -464,7 +472,7 @@
|
||||
<property name="OpenSound" value="open_workbench"/>
|
||||
<property name="CloseSound" value="close_workbench"/>
|
||||
<property name="TakeDelay" value="5"/>
|
||||
<property name="Group" value="Building,advBuilding"/>
|
||||
<property name="Group" value="Building,advBuilding,Necromancy"/>
|
||||
<property name="FilterTags" value="MC_playerBlocks,SC_decor"/>
|
||||
<drop event="Destroy" name="resourceRockSmall" count="10"/>
|
||||
</block>
|
||||
@@ -529,6 +537,7 @@
|
||||
включает TEFeatureCinerarium по черепу. Иконка - рендер той же модели (make_icon.py). -->
|
||||
<append xpath="/blocks">
|
||||
<block name="necroCinerarium">
|
||||
<property name="Group" value="Decor/Miscellaneous,Necromancy"/>
|
||||
<property name="CreativeMode" value="Player"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumDesc"/>
|
||||
<property name="UnlockedBy" value="craftingNecroNecromancy"/>
|
||||
@@ -568,19 +577,95 @@
|
||||
</block>
|
||||
<!-- Урна-логист (16.10.2026): «Я не могу ставить урны рядом. Может нам для логиста вообще сделать отдельные урны?»
|
||||
Без зоны (ставится вплотную к любым урнам), только нити и перевозка (CinerariumThreads.cs), лимит урн общий.
|
||||
Модель - та же; отличие - код (TEFeatureCinerarium.SyncGlow): зелёные руны и глазницы
|
||||
(Resources/necroCinerarium_e_logist.png), зеленоватая глазурь. Иконка - своя (зелёные руны, нити из горлышка).
|
||||
Своя модель логиста - позже, в Blender. -->
|
||||
18.10 - своя модель: латунное кадило с чеканкой (BlenderWork/necroCinerariumLogist, бандл necrocinerarium), два вида -
|
||||
на подставке по центру клетки (necroCinerariumLogist - блок уже в сейвах, остаётся видом по умолчанию) и на кронштейне
|
||||
у стены (necroCinerariumLogistWall, стена - задняя грань, как у черепа-декора). Вид - R с урной в руке, «Форма»
|
||||
(CinerariumLogistForm.cs). Дух в покое сидит в кадиле, вылетает из окон крышки только на перевозку, втрое меньше. -->
|
||||
<block name="necroCinerariumLogist">
|
||||
<property name="Extends" value="necroCinerarium"/>
|
||||
<property name="CreativeMode" value="Player"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumLogistDesc"/>
|
||||
<property name="CustomIcon" value="necroCinerariumLogist"/>
|
||||
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necrocinerarium?necroCinerariumLogistPrefab.prefab"/>
|
||||
<property name="Material" value="Mmetal"/>
|
||||
</block>
|
||||
<block name="necroCinerariumLogistSkull">
|
||||
<property name="Extends" value="necroCinerariumLogist"/>
|
||||
<property name="CreativeMode" value="None"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumLogistDesc"/>
|
||||
</block>
|
||||
<block name="necroCinerariumLogistWall">
|
||||
<property name="Extends" value="necroCinerariumLogist"/>
|
||||
<property name="CreativeMode" value="Player"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumLogistDesc"/>
|
||||
<property name="CustomIcon" value="necroCinerariumLogistWall"/>
|
||||
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necrocinerarium?necroCinerariumLogistWallPrefab.prefab"/>
|
||||
<property name="Place" value="TowardsPlacerInverted"/>
|
||||
</block>
|
||||
<block name="necroCinerariumLogistWallSkull">
|
||||
<property name="Extends" value="necroCinerariumLogistWall"/>
|
||||
<property name="CreativeMode" value="None"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumLogistDesc"/>
|
||||
</block>
|
||||
<!-- Урна рабочего (17-18.10.2026, BACKLOG «Урна рабочего»): чаша на трёх костях (BlenderWork/necroCinerariumWorker,
|
||||
necroCinerariumWorkerPrefab в бандле necrocinerarium). Изготавливает предмет из второй ячейки «Содержимого» (образец)
|
||||
на станциях по нитям, пока запас меньше стопки образца (CinerariumCraft.cs). Без зоны и вне лимита, как логист.
|
||||
Рецепт (18.10) - урна и молоток, открывается вместе с урнами; иконка - своя (18.10, рендер модели). -->
|
||||
<block name="necroCinerariumWorker">
|
||||
<property name="Extends" value="necroCinerarium"/>
|
||||
<property name="CreativeMode" value="Player"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumWorkerDesc"/>
|
||||
<property name="CustomIcon" value="necroCinerariumWorker"/>
|
||||
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necrocinerarium?necroCinerariumWorkerPrefab.prefab"/>
|
||||
</block>
|
||||
<block name="necroCinerariumWorkerSkull">
|
||||
<property name="Extends" value="necroCinerariumWorker"/>
|
||||
<property name="CreativeMode" value="None"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumWorkerDesc"/>
|
||||
</block>
|
||||
<!-- Урна ремонтника и урна лутера (18.10.2026, BACKLOG «Урна ремонтника», «Урна лутера»; CinerariumZone.cs): большая зона -
|
||||
сторона притязания или меньше, весь столб мира; работа - по кнопке «Запустить» (один проход), кнопки-звонки с черепом
|
||||
на модели показывают состояние. Ремонтник чинит повреждённые блоки материалами из ящиков по нитям; лутер собирает
|
||||
предметы на земле, мешки с зомби и красные трупы в ящик по нити. Без зоны 5x5 и вне лимита, как логист. -->
|
||||
<block name="necroCinerariumRepair">
|
||||
<property name="Extends" value="necroCinerarium"/>
|
||||
<property name="CreativeMode" value="Player"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumRepairDesc"/>
|
||||
<property name="CustomIcon" value="necroCinerariumRepair"/>
|
||||
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necrocinerarium?necroCinerariumRepairPrefab.prefab"/>
|
||||
</block>
|
||||
<block name="necroCinerariumRepairSkull">
|
||||
<property name="Extends" value="necroCinerariumRepair"/>
|
||||
<property name="CreativeMode" value="None"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumRepairDesc"/>
|
||||
</block>
|
||||
<block name="necroCinerariumLoot">
|
||||
<property name="Extends" value="necroCinerarium"/>
|
||||
<property name="CreativeMode" value="Player"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumLootDesc"/>
|
||||
<property name="CustomIcon" value="necroCinerariumLoot"/>
|
||||
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necrocinerarium?necroCinerariumLootPrefab.prefab"/>
|
||||
</block>
|
||||
<block name="necroCinerariumLootSkull">
|
||||
<property name="Extends" value="necroCinerariumLoot"/>
|
||||
<property name="CreativeMode" value="None"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumLootDesc"/>
|
||||
</block>
|
||||
<!-- Урна старателя (22.10.2026, #64; CinerariumProspector.cs): бронзовая катушка с золотой чеканкой на основе банки лутера
|
||||
(BlenderWork/necroCinerariumProspector, necroCinerariumProspectorPrefab в бандле necrocinerarium). В «Содержимом» череп
|
||||
и образец - что добывать; дух уходит под землю и раз в игровой час приносит добычу в ящик по нити. Без зоны и вне
|
||||
лимита, как логист. -->
|
||||
<block name="necroCinerariumProspector">
|
||||
<property name="Extends" value="necroCinerarium"/>
|
||||
<property name="CreativeMode" value="Player"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumProspectorDesc"/>
|
||||
<property name="CustomIcon" value="necroCinerariumProspector"/>
|
||||
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necrocinerarium?necroCinerariumProspectorPrefab.prefab"/>
|
||||
</block>
|
||||
<block name="necroCinerariumProspectorSkull">
|
||||
<property name="Extends" value="necroCinerariumProspector"/>
|
||||
<property name="CreativeMode" value="None"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumProspectorDesc"/>
|
||||
</block>
|
||||
</append>
|
||||
</config>
|
||||
|
||||
@@ -469,4 +469,9 @@
|
||||
<property name="Faction" value="none"/>
|
||||
</entity_class>
|
||||
</append>
|
||||
<!-- Живой кролик в мешке (22.10.2026, RabbitGrab.cs): «Я бы хватал кролика, но в отличие от курицы пусть он сразу
|
||||
оказывается в сумке». PickupItem - ванильный механизм захвата курицы (голой рукой, E); мешок кладёт в рюкзак код. -->
|
||||
<append xpath="/entity_classes/entity_class[@name='animalRabbit']">
|
||||
<property name="PickupItem" value="necroRabbitSack"/>
|
||||
</append>
|
||||
</config>
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
Gated to zombies via EntityTagCompare on "other" - killing a bear or a bird gives
|
||||
nothing, matching how every other on-hit effect in this mod is gated. -->
|
||||
<item_modifier name="necroModKnifeTearsOfTheDead" installable_tags="necroKnife" modifier_tags="necroKnifeWater" type="attachment">
|
||||
<property name="Group" value="Mods,Necromancy"/>
|
||||
<property name="Extends" value="modGeneralMaster" param1="CustomIcon"/>
|
||||
<property name="CustomIcon" value="TearsOfTheDead"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
@@ -92,6 +93,7 @@
|
||||
checks water and food independently and hands out whichever buffs apply, so stacking
|
||||
the two mods costs nothing extra and neither one cancels the other. -->
|
||||
<item_modifier name="necroModKnifeScavengersFeast" installable_tags="necroKnife" modifier_tags="necroKnifeFood" type="attachment">
|
||||
<property name="Group" value="Mods,Necromancy"/>
|
||||
<property name="Extends" value="modGeneralMaster" param1="CustomIcon"/>
|
||||
<property name="CustomIcon" value="ScavengersFeast"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
@@ -113,6 +115,7 @@
|
||||
onSelfAttackedOther, not onSelfKilledOther: the point is to slow a zombie that is
|
||||
still coming at you, so it has to land on the hit, not on the kill. -->
|
||||
<item_modifier name="necroModKnifeDeadMansGrip" installable_tags="necroKnife" modifier_tags="necroKnifeSlow" type="attachment">
|
||||
<property name="Group" value="Mods,Necromancy"/>
|
||||
<property name="Extends" value="modGeneralMaster" param1="CustomIcon"/>
|
||||
<property name="CustomIcon" value="DeadMansGrip"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
@@ -177,6 +180,7 @@
|
||||
CustomIcon спрайт называется как он сам), то есть по смыслу ровно та картинка. Тинт
|
||||
тот же фиолетовый, что у остальных трёх, до появления собственной графики. -->
|
||||
<item_modifier name="necroModKnifeGravesRepose" installable_tags="necroKnife" modifier_tags="necroKnifeThermal" type="attachment">
|
||||
<property name="Group" value="Mods,Necromancy"/>
|
||||
<property name="Extends" value="modGeneralMaster" param1="CustomIcon"/>
|
||||
<property name="CustomIcon" value="GravesRepose"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
@@ -265,6 +269,7 @@
|
||||
Иконка-заглушка - спрайт ванильного мода-репульсора электродубинки, тематически
|
||||
ближайшее, что есть готового. -->
|
||||
<item_modifier name="necroModKnifeDeadStorm" installable_tags="necroKnife" modifier_tags="necroKnifePower" type="attachment">
|
||||
<property name="Group" value="Mods,Necromancy"/>
|
||||
<property name="Extends" value="modGeneralMaster" param1="CustomIcon"/>
|
||||
<property name="CustomIcon" value="DeadStorm"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
@@ -321,6 +326,7 @@
|
||||
Бафф снимается и при смерти игрока: remove_on_death у него не выставлен, а по
|
||||
умолчанию это true - то есть залипнуть после респавна он не может. -->
|
||||
<item_modifier name="necroModKnifeDarkSense" installable_tags="necroKnife" modifier_tags="necroKnifeSense" type="attachment">
|
||||
<property name="Group" value="Mods,Necromancy"/>
|
||||
<property name="Extends" value="modGeneralMaster" param1="CustomIcon"/>
|
||||
<property name="CustomIcon" value="DarkSense"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
@@ -360,6 +366,7 @@
|
||||
type="attachment" - сферу можно вынуть обратно. -->
|
||||
<append xpath="/item_modifiers">
|
||||
<item_modifier name="resourceBloodSphere" installable_tags="necroBracelet" modifier_tags="necroBraceletSphere" type="attachment">
|
||||
<property name="Group" value="Mods,Necromancy"/>
|
||||
<!-- Extends на modGeneralMaster - та же база, что у шести модов ножа: даёт Group
|
||||
"Mods", звуки mod_grab/mod_place, Stacknumber 1 и CreativeMode None.
|
||||
param1="CustomIcon" исключает наследование родительского missingIcon.
|
||||
@@ -471,6 +478,7 @@
|
||||
-->
|
||||
<append xpath="/item_modifiers">
|
||||
<item_modifier name="resourceBloodStone" installable_tags="necroBracelet" modifier_tags="necroBraceletStone" type="attachment">
|
||||
<property name="Group" value="Mods,Necromancy"/>
|
||||
<!-- Та же база, что у сферы и у шести модов ножа: Group "Mods", звуки
|
||||
mod_grab/mod_place, Stacknumber 1, CreativeMode None. param1="CustomIcon"
|
||||
исключает наследование родительского missingIcon.
|
||||
|
||||
@@ -9,131 +9,6 @@
|
||||
dropped entirely. See progression.xml/entityclasses.xml: the skill now levels from
|
||||
zombie kills instead, which already worked. -->
|
||||
|
||||
<!-- Reusable tin cans: eating canned food leaves you an empty can, which works like the
|
||||
vanilla mason jar (drinkJarEmpty/drinkJarRiverWater/drinkJarBoiledWater - fill from
|
||||
water, drink, get the empty can back). Ported from AC-EnergyBow (__NoMods reference
|
||||
library), fixing one bug in it along the way: it extends the jar items but never
|
||||
overrides Meshfile, so its "tin can" is visually a mason jar in hand. We use the mesh
|
||||
from vanilla's own disabled-but-fully-built "drinkCanEmpty" item (items.xml, commented
|
||||
out under "*** Drinks") instead - a real can model, just never wired up by TFP.
|
||||
|
||||
BUG FIXED 2026-09-01 (canned water didn't quench thirst): the initial port only copied
|
||||
each item's own property/property-class nodes and dropped the effect_group blocks,
|
||||
on the wrong assumption that Extends would pull those in from drinkJarRiverWater/
|
||||
drinkJarBoiledWater - it doesn't (see the comment above: effect_group is parsed from
|
||||
each item's own XML node only, never inherited through Extends). So tinCanRiverWater/
|
||||
tinCanBoiledWater ran their Eat action (sound, delay, empty-can-back) but never applied
|
||||
$waterAmountAdd - the can "drank" and did nothing. Restored the effect_group from
|
||||
AC-EnergyBow verbatim (river water still carries dysentery risk/HP hit without
|
||||
buffWaterPurifier, same as a jar; boiled water is clean), fixing one more small bug in
|
||||
the source along the way: its dDysenteryRisk display_value said .16 while the actual
|
||||
roll it sets is 8 (i.e. .08) - tooltip now matches what the roll does. -->
|
||||
<append xpath="/items">
|
||||
<item name="tinCanEmpty">
|
||||
<property name="Extends" value="drinkJarEmpty"/>
|
||||
<property name="CustomIcon" value="tinCanEmpty"/>
|
||||
<property name="DescriptionKey" value="tinCanEmptyDesc"/>
|
||||
<!-- Своя модель (01.10.2026): ванильная консервная банка без отогнутой крышки - пустая
|
||||
открытая банка. Бандл necrocan; текстура пока ванильная cannedfood01 - до релиза
|
||||
перерисовать (гигиена ассетов). Банки с водой - свои модели с водой внутри. -->
|
||||
<property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necrocan?necroTinCanPrefab.prefab"/>
|
||||
<!-- HoldType 87 = хват пустой стеклянной банки (drinkJarEmpty), единственный с анимацией набора воды;
|
||||
консервный 14 её не имеет - банка набирала воду без движения рук (05.10.2026). -->
|
||||
<property name="HoldType" value="87"/>
|
||||
<property name="Material" value="Mmetal"/>
|
||||
<property class="Action1"> <!-- UseAction -->
|
||||
<property name="Class" value="CollectWater"/>
|
||||
<property name="Delay" value="3.4"/>
|
||||
<property name="Change_item_to" value="tinCanRiverWater"/>
|
||||
<property name="Sound_start" value="bucketfill_water"/>
|
||||
</property>
|
||||
</item>
|
||||
|
||||
<item name="tinCanRiverWater">
|
||||
<property name="Extends" value="drinkJarRiverWater"/>
|
||||
<property name="CustomIcon" value="tinCanRiverWater"/>
|
||||
<property name="DescriptionKey" value="tinCanRiverWaterDesc"/>
|
||||
<!-- Своя модель (01.10.2026): банка без крышки, внутри мутная бурая вода (бандл necrocan). -->
|
||||
<property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necrocan?necroTinCanMurkyPrefab.prefab"/>
|
||||
<!-- HoldType 3 = хват стеклянной банки (drinkJar*): сразу питьё, без фазы вскрытия, которая есть у консервного хвата 14 (05.10.2026). -->
|
||||
<property name="HoldType" value="3"/>
|
||||
<property name="Material" value="Mmetal"/>
|
||||
<property class="Action0">
|
||||
<property name="Class" value="Eat"/>
|
||||
<property name="Delay" value="4"/>
|
||||
<property name="Sound_start" value="player_drinking"/>
|
||||
<property name="Create_item" value="tinCanEmpty"/>
|
||||
<property name="Use_jar_refund" value="false"/>
|
||||
</property>
|
||||
|
||||
<effect_group tiered="false" name="Drink Tier 0">
|
||||
<requirement name="!HasBuff" buff="buffIsOnFire"/>
|
||||
|
||||
<triggered_effect trigger="onSelfPrimaryActionEnd" action="ModifyCVar" cvar="$waterAmountAdd" operation="add" value="10"/>
|
||||
<display_value name="dStaminaRegen" value=".15"/>
|
||||
<triggered_effect trigger="onSelfPrimaryActionEnd" action="AddBuff" buff="buffProcessConsumables"/>
|
||||
<triggered_effect trigger="onSelfPrimaryActionEnd" action="ModifyStats" stat="Health" operation="add" value="-5">
|
||||
<requirement name="!HasBuff" buff="buffWaterPurifier"/>
|
||||
</triggered_effect>
|
||||
<display_value name="foodHealthAmount" value="-5"/>
|
||||
<triggered_effect trigger="onSelfPrimaryActionEnd" action="ModifyCVar" cvar=".DiseaseRoll" operation="set" value="8"/>
|
||||
<display_value name="dDysenteryRisk" value=".08"/>
|
||||
<triggered_effect trigger="onSelfPrimaryActionEnd" action="ModifyCVar" cvar=".DiseaseRoll" operation="add" value="@$MetabolismResist"/>
|
||||
<triggered_effect trigger="onSelfPrimaryActionEnd" action="AddBuff" buff="buffDysenteryCatchDrink">
|
||||
<requirement name="RandomRoll" seed_type="Random" min_max="1,100" operation="LTE" value="@.DiseaseRoll"/>
|
||||
</triggered_effect>
|
||||
<triggered_effect trigger="onSelfPrimaryActionEnd" action="PlaySound" sound="player#vomit">
|
||||
<requirement name="!HasBuff" buff="buffWaterPurifier"/>
|
||||
</triggered_effect>
|
||||
</effect_group>
|
||||
|
||||
<effect_group tiered="false">
|
||||
<requirement name="HasBuff" buff="buffIsOnFire"/>
|
||||
<triggered_effect trigger="onSelfPrimaryActionEnd" action="AddBuff" buff="buffExtinguishFire"/>
|
||||
</effect_group>
|
||||
</item>
|
||||
|
||||
<item name="tinCanBoiledWater">
|
||||
<property name="Extends" value="drinkJarBoiledWater"/>
|
||||
<property name="CustomIcon" value="tinCanBoiledWater"/>
|
||||
<property name="DescriptionKey" value="tinCanBoiledWaterDesc"/>
|
||||
<!-- Своя модель (01.10.2026): банка без крышки, внутри чистая прозрачная голубоватая вода (бандл necrocan). -->
|
||||
<property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necrocan?necroTinCanCleanPrefab.prefab"/>
|
||||
<property name="HoldType" value="3"/> <!-- хват стеклянной банки, без вскрытия - см. tinCanRiverWater -->
|
||||
<property name="Material" value="Mmetal"/>
|
||||
<property class="Action0">
|
||||
<property name="Class" value="Eat"/>
|
||||
<property name="Delay" value="4"/>
|
||||
<property name="Sound_start" value="player_drinking"/>
|
||||
<property name="Create_item" value="tinCanEmpty"/>
|
||||
<property name="Use_jar_refund" value="false"/>
|
||||
</property>
|
||||
|
||||
<effect_group tiered="false" name="Drink Tier 0/1">
|
||||
<requirement name="!HasBuff" buff="buffIsOnFire"/>
|
||||
|
||||
<triggered_effect trigger="onSelfPrimaryActionEnd" action="ModifyCVar" cvar="$waterAmountAdd" operation="add" value="16"/>
|
||||
<display_value name="dStaminaRegen" value=".15"/>
|
||||
<triggered_effect trigger="onSelfPrimaryActionEnd" action="AddBuff" buff="buffProcessConsumables"/>
|
||||
</effect_group>
|
||||
|
||||
<effect_group tiered="false">
|
||||
<requirement name="HasBuff" buff="buffIsOnFire"/>
|
||||
<triggered_effect trigger="onSelfPrimaryActionEnd" action="AddBuff" buff="buffExtinguishFire"/>
|
||||
</effect_group>
|
||||
</item>
|
||||
</append>
|
||||
|
||||
<!-- Only foodCanBeef needs patching, even though Chicken/Lamb/Catfood should also leave a
|
||||
can behind: those three Extends="foodCanBeef" without their own Action0 element, so
|
||||
there's nothing for a foodCanChicken/foodCanLamb/foodCanCatfood xpath to match - but for
|
||||
the same reason, they'll all inherit foodCanBeef's patched Action0 (Create_item included)
|
||||
at load time anyway, same as how our zombie kill counter above only had to patch
|
||||
zombieTemplateMale once. -->
|
||||
<append xpath="/items/item[@name='foodCanBeef']/property[@class='Action0']">
|
||||
<property name="Create_item" value="tinCanEmpty"/>
|
||||
</append>
|
||||
|
||||
<!-- Письмо архимага (ванильная записка noteDuke01) прочитано -> открыта глава «Письмо архимага»
|
||||
в книге с оглавлением (HarmonySrc/NecroTome.cs). Ваниль сама ставит .dukeNoteRead, но cvar
|
||||
с точкой в сейв не пишется (EntityBuffs.Write пропускает key[0]=='.', декомпиляция 3.3) -
|
||||
@@ -203,7 +78,7 @@
|
||||
<property name="RadiusEntities" value="0"/>
|
||||
</property>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo,Necromancy"/>
|
||||
<property name="UsableUnderwater" value="false"/>
|
||||
<property name="SoundPickup" value="stone_grab"/>
|
||||
<property name="SoundPlace" value="stone_place"/>
|
||||
@@ -341,7 +216,7 @@
|
||||
<property name="RadiusEntities" value="0"/>
|
||||
</property>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo,Necromancy"/>
|
||||
<property name="UsableUnderwater" value="false"/>
|
||||
<property name="SoundPickup" value="schematics_grab"/>
|
||||
<property name="SoundPlace" value="schematics_place"/>
|
||||
@@ -441,7 +316,7 @@
|
||||
<property name="Weight" value="0"/>
|
||||
<property name="Stacknumber" value="10"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo,Necromancy"/>
|
||||
<property name="SoundPickup" value="stone_grab"/>
|
||||
<property name="SoundPlace" value="stone_place"/>
|
||||
<!-- Delay="1" is just a re-click debounce now, not a channel length - see the big
|
||||
@@ -476,7 +351,7 @@
|
||||
<property name="Weight" value="0"/>
|
||||
<property name="Stacknumber" value="10"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo,Necromancy"/>
|
||||
<property name="SoundPickup" value="stone_grab"/>
|
||||
<property name="SoundPlace" value="stone_place"/>
|
||||
<property class="Action0">
|
||||
@@ -557,7 +432,7 @@
|
||||
<property name="Weight" value="0"/>
|
||||
<property name="Stacknumber" value="10"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo,Necromancy"/>
|
||||
<property name="SoundPickup" value="schematics_grab"/>
|
||||
<property name="SoundPlace" value="schematics_place"/>
|
||||
<property class="Action0">
|
||||
@@ -794,7 +669,7 @@
|
||||
<property name="Weight" value="0"/>
|
||||
<property name="Stacknumber" value="10"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo,Necromancy"/>
|
||||
<property name="SoundPickup" value="schematics_grab"/>
|
||||
<property name="SoundPlace" value="schematics_place"/>
|
||||
<property class="Action0">
|
||||
@@ -853,7 +728,7 @@
|
||||
<property name="Weight" value="0"/>
|
||||
<property name="Stacknumber" value="10"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo,Necromancy"/>
|
||||
<property name="SoundPickup" value="schematics_grab"/>
|
||||
<property name="SoundPlace" value="schematics_place"/>
|
||||
<property class="Action0">
|
||||
@@ -914,7 +789,7 @@
|
||||
<property name="Weight" value="0"/>
|
||||
<property name="Stacknumber" value="10"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo,Necromancy"/>
|
||||
<property name="SoundPickup" value="schematics_grab"/>
|
||||
<property name="SoundPlace" value="schematics_place"/>
|
||||
<property class="Action0">
|
||||
@@ -971,7 +846,7 @@
|
||||
<property name="Weight" value="0"/>
|
||||
<property name="Stacknumber" value="10"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo,Necromancy"/>
|
||||
<property name="SoundPickup" value="schematics_grab"/>
|
||||
<property name="SoundPlace" value="schematics_place"/>
|
||||
<property class="Action0">
|
||||
@@ -1092,7 +967,7 @@
|
||||
<property name="Weight" value="0"/>
|
||||
<property name="Stacknumber" value="10"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo,Necromancy"/>
|
||||
<property name="SoundPickup" value="schematics_grab"/>
|
||||
<property name="SoundPlace" value="schematics_place"/>
|
||||
<property class="Action0">
|
||||
@@ -1170,7 +1045,7 @@
|
||||
<property name="Weight" value="0"/>
|
||||
<property name="Stacknumber" value="10"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo,Necromancy"/>
|
||||
<property name="SoundPickup" value="schematics_grab"/>
|
||||
<property name="SoundPlace" value="schematics_place"/>
|
||||
<property class="Action0">
|
||||
@@ -1218,6 +1093,7 @@
|
||||
the necromancer build over time, not an expensive unlock. Say if that should change. -->
|
||||
<append xpath="/items">
|
||||
<item name="necroWpnBladeNecroKnife">
|
||||
<property name="Group" value="Limited,Ammo/Weapons,Melee Weapons,Necromancy"/>
|
||||
<property name="Extends" value="meleeWpnBladeT0BoneKnife"/>
|
||||
<!-- Not for sale, per user request 2026-08-29 ("убери у всех предметов некроманта
|
||||
цену продажи") - overridden explicitly since this item never had its own
|
||||
@@ -1677,6 +1553,7 @@
|
||||
HarmonySrc/NecromancerBloodPatch.cs instead. -->
|
||||
<append xpath="/items">
|
||||
<item name="resourceNecromancerBlood">
|
||||
<property name="Group" value="Science,Medical,TCMedical,Necromancy"/>
|
||||
<property name="Extends" value="medicalBloodBag"/>
|
||||
<property name="DescriptionKey" value="resourceNecromancerBloodDesc"/>
|
||||
<!-- Своя рисованная иконка, 2026-08-30. CustomIcon задаётся явно даже при Extends. -->
|
||||
@@ -1883,7 +1760,7 @@
|
||||
<property name="Weight" value="0"/>
|
||||
<property name="Stacknumber" value="1"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo"/>
|
||||
<property name="Group" value="Ammo/Weapons,Ammo,Necromancy"/>
|
||||
<property class="Action0">
|
||||
<property name="Class" value="Eat"/>
|
||||
<property name="Delay" value="0.3"/>
|
||||
@@ -1944,6 +1821,7 @@
|
||||
01.10: «не книга призыва, а та же система, что у ванильного транспорта». -->
|
||||
<append xpath="/items">
|
||||
<item name="necroBoneSteedPlaceable">
|
||||
<property name="Group" value="Science,TCScience,Necromancy"/>
|
||||
<property name="Extends" value="vehicleMotorcyclePlaceable"/>
|
||||
<property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necrobonesteed?necroBoneSteedP.prefab"/>
|
||||
<property name="CustomIcon" value="necroBoneSteedPlaceable"/>
|
||||
@@ -2003,7 +1881,7 @@
|
||||
<property name="Stacknumber" value="50"/>
|
||||
<property name="EconomicValue" value="500"/>
|
||||
<property name="SellableToTrader" value="false"/>
|
||||
<property name="Group" value="Resources"/>
|
||||
<property name="Group" value="Resources,Necromancy"/>
|
||||
</item>
|
||||
</append>
|
||||
<!-- Сок чёрной розы (05.10.2026): банка, на ступке из 4 роз. Заменяет кровь некроманта в копиях
|
||||
@@ -2035,4 +1913,87 @@
|
||||
<property name="CustomIconTint" value="150, 205, 255"/>
|
||||
</item>
|
||||
</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): магическая пыль, хранилище магии (колба-«батарейка» в латунных клеммах с
|
||||
маркировкой MAGIA KONSERVUJO / KAPACITO ✶ 30) и повреждённое хранилище. Пыль - из ступки (4 из повреждённого,
|
||||
30 из целого); пыль - будущий ингредиент камня портала, целое хранилище - деталь батарей/портала, сами хранилища
|
||||
позже появятся в префабе. Модели свои (26.10.2026, #40): necroMagic.blend -> бандл necromagic (Unity NecroMagicSetup.cs);
|
||||
хват книги (21) - пыль горкой лежит на ладони (указание 26.10), колба лежит вдоль ладони. -->
|
||||
<append xpath="/items">
|
||||
<item name="necroMagicDust">
|
||||
<property name="DescriptionKey" value="necroMagicDustDesc"/>
|
||||
<property name="HoldType" value="21"/>
|
||||
<property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necromagic?necroMagicDustPrefab.prefab"/>
|
||||
<property name="Material" value="MresourceCrop"/>
|
||||
<property name="Stacknumber" value="500"/>
|
||||
<property name="EconomicValue" value="20"/>
|
||||
<property name="Group" value="Resources,Necromancy"/>
|
||||
</item>
|
||||
<item name="necroManaVessel">
|
||||
<property name="DescriptionKey" value="necroManaVesselDesc"/>
|
||||
<property name="HoldType" value="21"/>
|
||||
<property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necromagic?necroManaVesselPrefab.prefab"/>
|
||||
<property name="Material" value="MresourceCrop"/>
|
||||
<property name="Stacknumber" value="10"/>
|
||||
<property name="EconomicValue" value="300"/>
|
||||
<property name="Group" value="Resources"/>
|
||||
</item>
|
||||
<item name="necroManaVesselBroken">
|
||||
<property name="DescriptionKey" value="necroManaVesselBrokenDesc"/>
|
||||
<property name="HoldType" value="21"/>
|
||||
<property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necromagic?necroManaVesselBrokenPrefab.prefab"/>
|
||||
<property name="Material" value="MresourceCrop"/>
|
||||
<property name="Stacknumber" value="10"/>
|
||||
<property name="EconomicValue" value="40"/>
|
||||
<property name="Group" value="Resources"/>
|
||||
</item>
|
||||
</append>
|
||||
<!-- Живой кролик в мешке (22.10.2026, RabbitGrab.cs). Пойманный игроком кролик сразу в рюкзаке. ЛКМ с мешком в руке -
|
||||
выпустить (ванильный SpawnEntity, мешок тратит код). «Разобрать» в рюкзаке - мясо 10, кожа 2, кости 5 на кролика,
|
||||
как ножом (указание 22.10: «Кролика можно будет разобрать на кожу, кости и мясо», «по команде сломать»). Пункт
|
||||
«Разобрать» игра даёт по материалу Mleather (кожа) и весу не меньше кожи (5). Мешок и поза в руке - как у кожи.
|
||||
Иконка - ванильная блока «спящий кролик» (sleeperRabbitAnimal), по ссылке; имя предмета - просто «Кролик» (22.10). -->
|
||||
<append xpath="/items">
|
||||
<item name="necroRabbitSack">
|
||||
<property name="Tags" value="noMods"/>
|
||||
<property name="HoldType" value="45"/>
|
||||
<property name="Meshfile" value="@:Other/Items/Misc/sackPrefab.prefab"/>
|
||||
<property name="DropMeshfile" value="@:Other/Items/Misc/sack_droppedPrefab.prefab"/>
|
||||
<property name="Material" value="Mleather"/>
|
||||
<property name="CustomIcon" value="sleeperRabbitAnimal"/>
|
||||
<property name="DescriptionKey" value="necroRabbitSackDesc"/>
|
||||
<property name="Stacknumber" value="10"/>
|
||||
<property name="Weight" value="5"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
<property name="SellableToTrader" value="false"/>
|
||||
<property name="Group" value="Resources"/>
|
||||
<property name="SoundPickup" value="rabbitpain"/>
|
||||
<property class="Action0">
|
||||
<property name="Class" value="SpawnEntity"/>
|
||||
<property name="AnimType" value="4"/>
|
||||
<property name="AnimWait" value="0.3"/>
|
||||
<property name="SoundWarn" value="rabbitpain"/>
|
||||
<property name="Entity" value="animalRabbit"/>
|
||||
<property name="EntityOffset" value="0, -1, 1.2"/>
|
||||
</property>
|
||||
</item>
|
||||
</append>
|
||||
</config>
|
||||
|
||||
@@ -181,7 +181,9 @@
|
||||
пользуется, вода сидит на necroNecromancyLvl20. -->
|
||||
<unlock_entry item="necroModKnifeScavengersFeast,bookSummonRatSpirit" unlock_tier="3"/>
|
||||
<unlock_entry item="necroModKnifeGravesRepose" unlock_tier="4"/>
|
||||
<unlock_entry item="necroModKnifeDarkSense,necroCinerarium,necroCinerariumLogist" unlock_tier="5"/>
|
||||
<!-- 22.10.2026: урны - одной иконкой с recipes= (как комплекты брони в ванили): в окне навыка у строки только 14 иконок, в группе было 17 - последние урны не показывались («Не все рецепты цинерария доступны»). Клик по иконке - все урны списком. -->
|
||||
<unlock_entry item="necroModKnifeDarkSense" unlock_tier="5"/>
|
||||
<unlock_entry item="necroCinerarium" unlock_tier="5" recipes="necroCinerarium,necroCarvedBone,necroCinerariumLogist,necroCinerariumWorker,necroCinerariumRepair,necroCinerariumLoot,necroCinerariumProspector"/>
|
||||
</display_entry>
|
||||
<!-- Ступенчатая разблокировка внутри группы. ПЕРЕРАСПРЕДЕЛЕНО 2026-09-09: четыре из
|
||||
шести модов ножа (вода/еда/покой/чутьё) уехали отсюда в группу 1 - см. большой
|
||||
|
||||
@@ -1,31 +1,4 @@
|
||||
<config>
|
||||
<!-- Tin can water boiling - user request 2026-08-30 ("Вода в консервных банках должна
|
||||
кипятиться на костре даже если там нету кастрюли... так было сделано в другом моём моде
|
||||
'Энерголук'" = AC-EnergyBow, the same __NoMods reference the tin can items themselves were
|
||||
ported from - see items.xml's own comment on tinCanEmpty/tinCanRiverWater/
|
||||
tinCanBoiledWater).
|
||||
|
||||
Root cause this was missing at all: `Extends` copies an item's PROPERTIES, never its
|
||||
RECIPES - recipes are separate top-level <recipe> nodes matched by name, with no
|
||||
inheritance mechanism of their own (confirmed by grepping this mod's own recipes.xml -
|
||||
zero "tinCan" hits before this entry). tinCanRiverWater/tinCanBoiledWater extending
|
||||
drinkJarRiverWater/drinkJarBoiledWater therefore got NONE of the vanilla mason-jar-boiling
|
||||
recipe (`<recipe name="drinkJarBoiledWater" craft_area="campfire"
|
||||
craft_tool="toolCookingPot">`, Data/Config/recipes.xml) - boiling a tin can was never
|
||||
possible at all until this recipe existed, not merely pot-gated.
|
||||
|
||||
No `craft_tool="toolCookingPot"` here BY DESIGN, per the direct request - a metal can can
|
||||
sit right in a campfire's coals on its own, unlike a mason jar. Confirmed real vanilla
|
||||
precedent for a pot-less campfire recipe existing at all: `foodBakedPotato`/
|
||||
`foodCharredMeat` both use craft_area="campfire" with no craft_tool attribute at all - a
|
||||
campfire recipe is not implicitly pot-gated, craft_tool is an opt-in extra requirement,
|
||||
not something `craft_area="campfire"` implies on its own. -->
|
||||
<append xpath="/recipes">
|
||||
<recipe name="tinCanBoiledWater" count="1" craft_area="campfire">
|
||||
<ingredient name="tinCanRiverWater" count="1"/>
|
||||
</recipe>
|
||||
</append>
|
||||
|
||||
<!-- "Камень духов" (Spirit Stone): craftable from the start, no perk/skill gate -
|
||||
packMuleCrafting is a weight-while-crafting tag, not a gate (see vanilla
|
||||
campfire/candle recipes for the same pattern). Per BACKLOG.md item 1: 1 rock +
|
||||
@@ -60,7 +33,7 @@
|
||||
weapon - "он не является начальным оружием... больше про комфорт" - so the same
|
||||
circular-dependency concern doesn't apply, and the user wants it to actually cost a
|
||||
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="drinkJarPureMineralWater" count="3"/>
|
||||
<ingredient name="resourceYuccaFibers" count="15"/>
|
||||
@@ -117,7 +90,7 @@
|
||||
their current HP per unit to make (NecromancerBloodPatch.cs) - this recipe shouldn't
|
||||
demand many. -->
|
||||
<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="resourceRockSmall" count="300"/>
|
||||
<ingredient name="resourceYuccaFibers" count="200"/>
|
||||
@@ -151,15 +124,15 @@
|
||||
personal crafting). workbenchCrafting is just the same UI-categorization tag vanilla's
|
||||
own workbench recipes carry alongside craft_area, not a separate gate. -->
|
||||
<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="medicalBloodBag" count="3"/>
|
||||
<ingredient name="drinkJarBoiledWater" count="4"/>
|
||||
<ingredient name="resourceYuccaFibers" count="50"/>
|
||||
<ingredient name="resourceCloth" count="3"/>
|
||||
<ingredient name="resourceLeather" count="10"/>
|
||||
<ingredient name="resourceBone" count="40"/>
|
||||
<ingredient name="casinoCoin" count="5"/>
|
||||
<ingredient name="resourceZombieAsh" count="10"/>
|
||||
</recipe>
|
||||
</append>
|
||||
@@ -187,7 +160,7 @@
|
||||
REDISTRIBUTED 2026-08-30, was necroNecromancyLvl200 before) - not specified by the user,
|
||||
guessed consistent with the other "advanced ritual" summon items. -->
|
||||
<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="resourceGlue" count="3"/>
|
||||
<ingredient name="foodRottingFlesh" count="10"/>
|
||||
@@ -480,12 +453,14 @@
|
||||
именно тогда (правило «порог по нужде, а не по силе»). Тег и unlock_level группы 2 в
|
||||
progression.xml обязаны совпадать. -->
|
||||
<append xpath="/recipes">
|
||||
<recipe name="necroGeoOssuary" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
|
||||
<!-- 22.10.2026 (#66): котёл кованый («Он весь из себя кованный, так что будет логично»): железо вместо дерева
|
||||
и юкки, кости и прах - ноги и содержимое. 26.10: из кузни - на верстак, на ломе: в кузне делается только то, что
|
||||
в неё вплавлено, а кости и прах в неё не кладутся (стрим 26.10). -->
|
||||
<recipe name="necroGeoOssuary" count="1" craft_area="workbench" craft_time="60" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
|
||||
<ingredient name="resourceScrapIron" count="60"/>
|
||||
<ingredient name="resourceBone" count="400"/>
|
||||
<ingredient name="resourceZombieAsh" count="200"/>
|
||||
<ingredient name="resourceNecromancerBlood" count="1"/>
|
||||
<ingredient name="resourceWood" count="100"/>
|
||||
<ingredient name="resourceYuccaFibers" count="50"/>
|
||||
</recipe>
|
||||
</append>
|
||||
|
||||
@@ -493,7 +468,7 @@
|
||||
гео-оссуария: транспорт нужен, когда карта уже велика для пеших ходок. Состав - по
|
||||
указанию пользователя: кости, гнилое мясо, прах, кожа жертвы, два флакона крови. -->
|
||||
<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="foodRottingFlesh" count="30"/>
|
||||
<ingredient name="resourceZombieAsh" count="150"/>
|
||||
@@ -546,7 +521,7 @@
|
||||
<ingredient name="resourceYuccaFibers" count="15"/>
|
||||
<ingredient name="resourceZombieAsh" count="20"/>
|
||||
</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="resourceRockSmall" count="300"/>
|
||||
<ingredient name="resourceYuccaFibers" count="200"/>
|
||||
@@ -580,14 +555,16 @@
|
||||
<ingredient name="medicalBloodBag" count="4"/>
|
||||
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
|
||||
</recipe>
|
||||
<recipe name="necroGeoOssuary" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
|
||||
<!-- 22.10.2026 (#66): котёл кованый («Он весь из себя кованный, так что будет логично»): железо вместо дерева
|
||||
и юкки, кости и прах - ноги и содержимое. 26.10: из кузни - на верстак, на ломе: в кузне делается только то, что
|
||||
в неё вплавлено, а кости и прах в неё не кладутся (стрим 26.10). -->
|
||||
<recipe name="necroGeoOssuary" count="1" craft_area="workbench" craft_time="60" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
|
||||
<ingredient name="resourceScrapIron" count="60"/>
|
||||
<ingredient name="resourceBone" count="400"/>
|
||||
<ingredient name="resourceZombieAsh" count="200"/>
|
||||
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
|
||||
<ingredient name="resourceWood" count="100"/>
|
||||
<ingredient name="resourceYuccaFibers" count="50"/>
|
||||
</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="foodRottingFlesh" count="30"/>
|
||||
<ingredient name="resourceZombieAsh" count="150"/>
|
||||
@@ -621,20 +598,107 @@
|
||||
<ingredient name="resourceBone" count="10"/>
|
||||
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
|
||||
</recipe>
|
||||
<!-- Урна-логист (16.10: «Рецепт с набором для шитья») - рецепт урны и набор для шитья; копия на соке чёрной розы. -->
|
||||
<recipe name="necroCinerariumLogist" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceClayLump" count="30"/>
|
||||
<!-- Урна-логист (16.10: «Рецепт с набором для шитья») - набор для шитья; копия на соке чёрной розы.
|
||||
18.10: своя модель - латунное кадило («Сейчас кадило выглядит бронзовым. Учти это в рецепте, используя латунь и кузню»):
|
||||
вместо глины латунь, плюс немного песка и угля («добавь в рецепт немного песка и угля») и воск - покрытие, чтобы латунь не тускнела («Это кстати будет шикарным поводом ставить пасеку»), делается в кузне.
|
||||
26.10: на верстаке, на латунном ломе, без песка и угля - кузня не берёт кости и прах (стрим 26.10). -->
|
||||
<recipe name="necroCinerariumLogist" count="1" craft_area="workbench" craft_time="60" tags="learnable,workbenchCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceScrapBrass" count="50"/>
|
||||
<ingredient name="resourceZombieAsh" count="100"/>
|
||||
<ingredient name="resourceBone" count="10"/>
|
||||
<ingredient name="resourceNecromancerBlood" count="1"/>
|
||||
<ingredient name="resourceSewingKit" count="1"/>
|
||||
<ingredient name="resourceBeeswax" count="2"/>
|
||||
</recipe>
|
||||
<recipe name="necroCinerariumLogist" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceClayLump" count="30"/>
|
||||
<recipe name="necroCinerariumLogist" count="1" craft_area="workbench" craft_time="60" tags="learnable,workbenchCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceScrapBrass" count="50"/>
|
||||
<ingredient name="resourceZombieAsh" count="100"/>
|
||||
<ingredient name="resourceBone" count="10"/>
|
||||
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
|
||||
<ingredient name="resourceSewingKit" count="1"/>
|
||||
<ingredient name="resourceBeeswax" count="2"/>
|
||||
</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: «Молоток годится») - рецепт урны и молоток; копия на соке чёрной розы. -->
|
||||
<recipe name="necroCinerariumWorker" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceClayLump" count="30"/>
|
||||
<ingredient name="resourceZombieAsh" count="100"/>
|
||||
<ingredient name="resourceBone" count="10"/>
|
||||
<ingredient name="resourceNecromancerBlood" count="1"/>
|
||||
<ingredient name="meleeToolRepairT1ClawHammer" count="1"/>
|
||||
</recipe>
|
||||
<recipe name="necroCinerariumWorker" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceClayLump" count="30"/>
|
||||
<ingredient name="resourceZombieAsh" count="100"/>
|
||||
<ingredient name="resourceBone" count="10"/>
|
||||
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
|
||||
<ingredient name="meleeToolRepairT1ClawHammer" count="1"/>
|
||||
</recipe>
|
||||
<!-- Урна ремонтника (18.10) - рецепт урны (глина - фляга), ремкомплект и латунь 5 (розетки звонков); копия на соке чёрной розы. -->
|
||||
<recipe name="necroCinerariumRepair" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceClayLump" count="30"/>
|
||||
<ingredient name="resourceZombieAsh" count="100"/>
|
||||
<ingredient name="resourceBone" count="10"/>
|
||||
<ingredient name="resourceNecromancerBlood" count="1"/>
|
||||
<ingredient name="resourceRepairKit" count="1"/>
|
||||
<ingredient name="resourceScrapBrass" count="5"/>
|
||||
</recipe>
|
||||
<recipe name="necroCinerariumRepair" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceClayLump" count="30"/>
|
||||
<ingredient name="resourceZombieAsh" count="100"/>
|
||||
<ingredient name="resourceBone" count="10"/>
|
||||
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
|
||||
<ingredient name="resourceRepairKit" count="1"/>
|
||||
<ingredient name="resourceScrapBrass" count="5"/>
|
||||
</recipe>
|
||||
<!-- Урна лутера (18.10) - железная банка с крышкой-кадилом, шов пропаян свинцом («в рецепте железо и немного свинца»):
|
||||
железный и свинцовый лом вместо глины, отмычка; на крови и на соке. 26.10: из кузни - на верстак (кузня не берёт кости и прах). -->
|
||||
<recipe name="necroCinerariumLoot" count="1" craft_area="workbench" craft_time="60" tags="learnable,workbenchCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceScrapIron" count="40"/>
|
||||
<ingredient name="resourceScrapLead" 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="workbench" craft_time="60" tags="learnable,workbenchCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceScrapIron" count="40"/>
|
||||
<ingredient name="resourceScrapLead" count="5"/>
|
||||
<ingredient name="resourceZombieAsh" count="100"/>
|
||||
<ingredient name="resourceBone" count="10"/>
|
||||
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
|
||||
<ingredient name="resourceLockPick" count="1"/>
|
||||
</recipe>
|
||||
<!-- Урна старателя (22.10, #64) - бронзовая катушка: латунный лом 40 вместо железа и свинца, железная кирка вместо отмычки;
|
||||
на крови и на соке. Открывается вместе с остальными урнами. 26.10: из кузни - на верстак (кузня не берёт кости и прах). -->
|
||||
<recipe name="necroCinerariumProspector" count="1" craft_area="workbench" craft_time="60" tags="learnable,workbenchCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceScrapBrass" 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="workbench" craft_time="60" tags="learnable,workbenchCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceScrapBrass" count="40"/>
|
||||
<ingredient name="resourceZombieAsh" count="100"/>
|
||||
<ingredient name="resourceBone" count="10"/>
|
||||
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
|
||||
<ingredient name="meleeToolPickT1IronPickaxe" count="1"/>
|
||||
</recipe>
|
||||
</append>
|
||||
<!-- Магическая пыль в ступке (18.10.2026): повреждённое хранилище - 4, целое - 10 (22.10: было 30; «Из неповреждённого пусть будет 10»). -->
|
||||
<append xpath="/recipes">
|
||||
<recipe name="necroMagicDust" count="4" craft_time="10" craft_area="necroMortar">
|
||||
<ingredient name="necroManaVesselBroken" count="1"/>
|
||||
</recipe>
|
||||
<recipe name="necroMagicDust" count="10" craft_time="20" craft_area="necroMortar">
|
||||
<ingredient name="necroManaVessel" count="1"/>
|
||||
</recipe>
|
||||
</append>
|
||||
</config>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<config>
|
||||
<!-- Вкладка «Некромантия» в окне крафта (22.10.2026: «Иначе рецепты теряются. Их не видно ни так, ни в навыках»).
|
||||
Отбор - по Group предмета/блока: у всех предметов мода в Group есть Necromancy (правило: новый предмет - сразу туда же).
|
||||
В рюкзаке и на верстаке вкладок становится 10 - шаг значков сужен в XUi_InGame/windows.xml. -->
|
||||
<append xpath="/ui_display_info/crafting_category_display/crafting_category_list[@display_type='']">
|
||||
<crafting_category name="Necromancy" icon="ui_game_symbol_skull" display_name="lblCategoryNecromancy" />
|
||||
</append>
|
||||
<append xpath="/ui_display_info/crafting_category_display/crafting_category_list[@display_type='workbench']">
|
||||
<crafting_category name="Necromancy" icon="ui_game_symbol_skull" display_name="lblCategoryNecromancy" />
|
||||
</append>
|
||||
<append xpath="/ui_display_info/crafting_category_display/crafting_category_list[@display_type='forge']">
|
||||
<crafting_category name="Necromancy" icon="ui_game_symbol_skull" display_name="lblCategoryNecromancy" />
|
||||
</append>
|
||||
<append xpath="/ui_display_info/crafting_category_display/crafting_category_list[@display_type='chemistryStation']">
|
||||
<crafting_category name="Necromancy" icon="ui_game_symbol_skull" display_name="lblCategoryNecromancy" />
|
||||
</append>
|
||||
</config>
|
||||
@@ -75,14 +75,14 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Череп и нож (меч прилива) - модели предметов: в руке и на земле.</summary>
|
||||
/// <summary>Череп, нож (меч прилива) и резная кость (#86) - модели предметов: в руке и на земле.</summary>
|
||||
[HarmonyPatch(typeof(ItemClass), nameof(ItemClass.CloneModel))]
|
||||
public static class Patch_ItemClass_CloneModel_BundleShaderFix
|
||||
{
|
||||
public static void Postfix(ItemClass __instance, AssetManagement.InstanceHandle<GameObject> __result)
|
||||
{
|
||||
string n = __instance?.Name;
|
||||
if (n != "necroSkull" && n != SnakeMode.KnifeName)
|
||||
if (n != "necroSkull" && n != SnakeMode.KnifeName && n != CinerariumThreads.BoneName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,698 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Работа духа урны рабочего (18.10.2026, BACKLOG «Урна рабочего»): изготовить предмет из ячейки образца, пока его в запасе
|
||||
/// меньше, чем в стопке образца (23.10 - без счёта: пока хватает ресурсов и есть куда класть). Решения 17.10: нити ящик -> урна - откуда ингредиенты и топливо, урна -> станция - где
|
||||
/// изготавливать, урна -> ящик - куда готовое (нет такой нити - готовое остаётся в выходе станции, выход полон - станция
|
||||
/// гаснет); только прямые рецепты (цепочки - несколько урн); качество 1; топливо - костру, химстанции, кузне, и станция
|
||||
/// гасится, когда делать нечего (верстаку, ступке, бетономешалке топливо не нужно). Изготавливает сама станция игры: дух
|
||||
/// только кладёт рецепт в её очередь, забрав ингредиенты, - время, огонь и выход считает игра.
|
||||
/// Запас = такие же предметы в ящиках «куда», в выходах станций и в их очередях.
|
||||
/// Одно дело за шаг, как у прочих духов (CinerariumWork.Step), _dry - пробный шаг без изменений.
|
||||
/// </summary>
|
||||
public static class CinerariumCraft
|
||||
{
|
||||
const int FuelModule = 3, MaterialModule = 4;
|
||||
|
||||
sealed class Station
|
||||
{
|
||||
public Vector3i Pos;
|
||||
public TileEntityWorkstation Te;
|
||||
public string[] Areas;
|
||||
}
|
||||
|
||||
public static Vector3i? Step(World _world, TEFeatureCinerarium _urn, bool _dry, out bool _stuck, out Vector3i? _from)
|
||||
{
|
||||
_stuck = false;
|
||||
_from = null;
|
||||
ItemStack sample = _urn.Sample;
|
||||
List<(Vector3i p, TEFeatureStorage box)> src = new List<(Vector3i, TEFeatureStorage)>();
|
||||
foreach (Vector3i p in _urn.ThreadFrom)
|
||||
{
|
||||
TEFeatureStorage b = CinerariumWork.BoxAt(_world, p);
|
||||
if (b != null)
|
||||
{
|
||||
src.Add((p, b));
|
||||
}
|
||||
}
|
||||
List<Station> stations = new List<Station>();
|
||||
List<(Vector3i p, TEFeatureStorage box)> outs = new List<(Vector3i, TEFeatureStorage)>();
|
||||
foreach (Vector3i p in _urn.ThreadOut)
|
||||
{
|
||||
TileEntityWorkstation te = CinerariumThreads.StationAt(_world, p);
|
||||
if (te != null)
|
||||
{
|
||||
if (!te.IsUserAccessing())
|
||||
{
|
||||
stations.Add(new Station { Pos = p, Te = te, Areas = AreasOf(_world.GetBlock(p).Block) });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
TEFeatureStorage b = CinerariumWork.BoxAt(_world, p);
|
||||
if (b != null)
|
||||
{
|
||||
outs.Add((p, b));
|
||||
}
|
||||
}
|
||||
bool hasSample = sample != null && !sample.IsEmpty();
|
||||
|
||||
// 1. Готовое (только предмет образца - чужое изготовленное игроком не трогаем) - из выхода станции в ящики «куда».
|
||||
if (hasSample && outs.Count > 0)
|
||||
{
|
||||
foreach (Station st in stations)
|
||||
{
|
||||
ItemStack[] output = st.Te.Output;
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
ItemStack s = output[i];
|
||||
if (s == null || s.IsEmpty() || s.itemValue.type != sample.itemValue.type)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Vector3i? dst = null;
|
||||
foreach ((Vector3i p, TEFeatureStorage box) o in outs)
|
||||
{
|
||||
if (CinerariumWork.Fits(o.box, s))
|
||||
{
|
||||
dst = o.p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!dst.HasValue)
|
||||
{
|
||||
_stuck = true; // ящики «куда» полны
|
||||
continue;
|
||||
}
|
||||
_from = st.Pos;
|
||||
if (_dry)
|
||||
{
|
||||
return dst;
|
||||
}
|
||||
ItemStack moving = s.Clone();
|
||||
List<TEFeatureStorage> boxes = new List<TEFeatureStorage>();
|
||||
foreach ((Vector3i p, TEFeatureStorage box) o in outs)
|
||||
{
|
||||
boxes.Add(o.box);
|
||||
}
|
||||
CinerariumWork.Place(moving, boxes);
|
||||
output[i] = moving.count > 0 ? moving : ItemStack.Empty.Clone();
|
||||
st.Te.Output = output;
|
||||
Debug.Log("[NecromancerTome] Cinerarium: рабочий перенёс " + (s.count - moving.count) + " " + s.itemValue.ItemClass?.GetItemName() + " из станции " + st.Pos);
|
||||
return dst;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Изготовить. 23.10, пользователь: «Рабочий должен делать бесконечное количество по образцу. Я положил 1 цемент,
|
||||
// а его задача наделать цемента столько, сколько возможно с учётом текущего количества ресурсов» - образец задаёт что,
|
||||
// не сколько (было: пока запас меньше стопки образца). Стоп - нет ингредиентов или готовое класть некуда.
|
||||
EntityPlayer owner = OwnerPlayer(_world, _urn);
|
||||
bool outRoom = false;
|
||||
foreach ((Vector3i p, TEFeatureStorage box) o in outs)
|
||||
{
|
||||
if (CinerariumWork.Fits(o.box, sample))
|
||||
{
|
||||
outRoom = true;
|
||||
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)
|
||||
{
|
||||
string name = sample.itemValue.ItemClass?.GetItemName();
|
||||
foreach (Station st in stations)
|
||||
{
|
||||
int slot = FreeQueueSlot(st.Te);
|
||||
if (slot < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!OutputHasRoom(st.Te, sample) && !outRoom)
|
||||
{
|
||||
continue; // выход полон и ящики «куда» полны (или их нет) - ниже станция погаснет
|
||||
}
|
||||
if (InQueue(st.Te, sample.itemValue.type))
|
||||
{
|
||||
continue; // партия этого предмета уже в очереди - следующая, когда она кончится
|
||||
}
|
||||
foreach (Recipe r in CraftingManager.GetRecipes(name))
|
||||
{
|
||||
if (r.IsScrap || !AreaFits(r, st.Areas) || !ToolFits(r, st.Te) || owner != null && !r.IsUnlocked(owner))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
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))
|
||||
{
|
||||
_from = rawAt;
|
||||
if (!_dry)
|
||||
{
|
||||
Ignite(st.Te, src, false);
|
||||
}
|
||||
return st.Pos;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
_from = srcAt;
|
||||
if (_dry)
|
||||
{
|
||||
return st.Pos;
|
||||
}
|
||||
TakeIngredients(r, st.Te, src, batches);
|
||||
Enqueue(st.Te, slot, r, batches);
|
||||
Ignite(st.Te, src, false);
|
||||
Debug.Log("[NecromancerTome] Cinerarium: рабочий поставил " + name + " x" + batches + " на станцию " + st.Pos);
|
||||
return st.Pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Огонь: работа есть - топливо и огонь; нет - погасить (экономия топлива).
|
||||
foreach (Station st in stations)
|
||||
{
|
||||
if (!st.Te.isModuleUsed[FuelModule])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
bool busy = st.Te.hasRecipeInQueue() || RawInInput(st.Te);
|
||||
bool full = hasSample && !OutputHasRoom(st.Te, sample) && !outRoom;
|
||||
if (busy && !full)
|
||||
{
|
||||
if (st.Te.BurnTotalTimeLeft <= 0f)
|
||||
{
|
||||
Vector3i? fuelAt = FuelSource(src);
|
||||
if (fuelAt.HasValue)
|
||||
{
|
||||
_from = fuelAt;
|
||||
if (_dry)
|
||||
{
|
||||
return st.Pos;
|
||||
}
|
||||
Ignite(st.Te, src, true);
|
||||
return st.Pos;
|
||||
}
|
||||
}
|
||||
else if (!_dry && !st.Te.IsBurning)
|
||||
{
|
||||
st.Te.IsBurning = true;
|
||||
}
|
||||
}
|
||||
else if (st.Te.IsBurning)
|
||||
{
|
||||
// Погасить - тоже дело: дух летит к станции. Пробный шаг должен его найти - иначе настоящий шаг не
|
||||
// запускается и огонь горел до конца топлива (проверка 18.10: костёр, химстанция, кузня).
|
||||
if (_dry)
|
||||
{
|
||||
return st.Pos;
|
||||
}
|
||||
st.Te.IsBurning = false;
|
||||
Debug.Log("[NecromancerTome] Cinerarium: рабочий погасил станцию " + st.Pos + (full ? " - выход полон" : " - делать нечего"));
|
||||
if (full)
|
||||
{
|
||||
_stuck = true;
|
||||
}
|
||||
return st.Pos;
|
||||
}
|
||||
if (full)
|
||||
{
|
||||
_stuck = true;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Чьи рецепты изучены: хозяин урны, если он в игре; в одиночной - игрок. Нет никого - не проверяем.</summary>
|
||||
static EntityPlayer OwnerPlayer(World _world, TEFeatureCinerarium _urn)
|
||||
{
|
||||
PlatformUserIdentifierAbs id = _urn.Parent?.Owner;
|
||||
if (id != null)
|
||||
{
|
||||
PersistentPlayerData ppd = GameManager.Instance.persistentPlayers?.GetPlayerData(id);
|
||||
if (ppd != null && _world.GetEntity(ppd.EntityId) is EntityPlayer p)
|
||||
{
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return _world.GetPrimaryPlayer();
|
||||
}
|
||||
|
||||
/// <summary>Какие рецепты станция делает: Workstation.CraftingAreaRecipes или имя блока («player» - рецепты без станции).</summary>
|
||||
static string[] AreasOf(Block _b)
|
||||
{
|
||||
if (_b == null)
|
||||
{
|
||||
return new string[0];
|
||||
}
|
||||
string a = _b.Properties.Contains("Workstation", "CraftingAreaRecipes") ? _b.Properties.GetString("Workstation", "CraftingAreaRecipes") : _b.GetBlockName();
|
||||
return a.Replace(" ", "").Split(',');
|
||||
}
|
||||
|
||||
static bool AreaFits(Recipe _r, string[] _areas)
|
||||
{
|
||||
string area = string.IsNullOrEmpty(_r.craftingArea) ? "player" : _r.craftingArea;
|
||||
foreach (string a in _areas)
|
||||
{
|
||||
if (a.EqualsCaseInsensitive(area))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ToolFits(Recipe _r, TileEntityWorkstation _te)
|
||||
{
|
||||
if (_r.craftingToolType == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
foreach (ItemStack t in _te.Tools)
|
||||
{
|
||||
if (t != null && !t.IsEmpty() && t.itemValue.type == _r.craftingToolType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/// <summary>Свободная ячейка очереди: самая близкая к работающей (последней).</summary>
|
||||
static int FreeQueueSlot(TileEntityWorkstation _te)
|
||||
{
|
||||
RecipeQueueItem[] q = _te.Queue;
|
||||
for (int i = q.Length - 1; i >= 0; i--)
|
||||
{
|
||||
if (q[i] == null || q[i].Recipe == null || q[i].Multiplier <= 0)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
foreach (ItemStack s in _te.Output)
|
||||
{
|
||||
if (s == null || s.IsEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
int n = 1;
|
||||
if (s.itemValue.type == _sample.itemValue.type && s.CanStackPartly(ref n))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Индекс ячейки переплавленного материала кузни для unit_X (вход 3 + номер материала), или -1.</summary>
|
||||
static int UnitSlot(TileEntityWorkstation _te, string _itemName)
|
||||
{
|
||||
if (!_te.isModuleUsed[MaterialModule] || !_itemName.StartsWith("unit_"))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
string mat = _itemName.Substring(5);
|
||||
for (int j = 0; j < _te.materialNames.Length; j++)
|
||||
{
|
||||
if (_te.materialNames[j].EqualsCaseInsensitive(mat) && 3 + j < _te.Input.Length)
|
||||
{
|
||||
return 3 + j;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>Хватает ли всего: переплавленное - в кузне, прочее - в ящиках «откуда». _srcAt - ящик, откуда первое;
|
||||
/// _shortUnit - какого переплавленного не хватило (кузне можно подложить сырья). _batches - сколько раз рецепт ставится
|
||||
/// в очередь за одно дело (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;
|
||||
_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)
|
||||
{
|
||||
if (ing == null || ing.IsEmpty() || ing.count <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string iname = ing.itemValue.ItemClass?.GetItemName() ?? "";
|
||||
int us = UnitSlot(_te, iname);
|
||||
if (us >= 0)
|
||||
{
|
||||
if (_te.Input[us].count < ing.count)
|
||||
{
|
||||
_shortUnit = iname;
|
||||
return false;
|
||||
}
|
||||
_batches = Mathf.Min(_batches, _te.Input[us].count / ing.count);
|
||||
continue;
|
||||
}
|
||||
int have = 0;
|
||||
foreach ((Vector3i p, TEFeatureStorage box) s in _src)
|
||||
{
|
||||
ItemStack[] grid = s.box.ItemGrid.items;
|
||||
for (int k = 0; k < grid.Length; k++)
|
||||
{
|
||||
if (grid[k] != null && !grid[k].IsEmpty() && grid[k].itemValue.type == ing.itemValue.type && !CinerariumWork.Locked(s.box, k))
|
||||
{
|
||||
have += grid[k].count;
|
||||
if (!_srcAt.HasValue)
|
||||
{
|
||||
_srcAt = s.p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (have < ing.count)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
static void TakeIngredients(Recipe _r, TileEntityWorkstation _te, List<(Vector3i p, TEFeatureStorage box)> _src, int _batches)
|
||||
{
|
||||
ItemStack[] input = _te.Input;
|
||||
bool inputChanged = false;
|
||||
foreach (ItemStack ing in _r.ingredients)
|
||||
{
|
||||
if (ing == null || ing.IsEmpty() || ing.count <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int us = UnitSlot(_te, ing.itemValue.ItemClass?.GetItemName() ?? "");
|
||||
if (us >= 0)
|
||||
{
|
||||
input[us].count -= ing.count * _batches;
|
||||
inputChanged = true;
|
||||
continue;
|
||||
}
|
||||
int need = ing.count * _batches;
|
||||
foreach ((Vector3i p, TEFeatureStorage box) s in _src)
|
||||
{
|
||||
ItemStack[] grid = s.box.ItemGrid.items;
|
||||
for (int k = 0; k < grid.Length && need > 0; k++)
|
||||
{
|
||||
if (grid[k] != null && !grid[k].IsEmpty() && grid[k].itemValue.type == ing.itemValue.type && !CinerariumWork.Locked(s.box, k))
|
||||
{
|
||||
int take = Mathf.Min(need, grid[k].count);
|
||||
CinerariumWork.Take(s.box, k, take);
|
||||
need -= take;
|
||||
}
|
||||
}
|
||||
if (need <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inputChanged)
|
||||
{
|
||||
_te.Input = input;
|
||||
}
|
||||
}
|
||||
|
||||
static void Enqueue(TileEntityWorkstation _te, int _slot, Recipe _r, int _batches)
|
||||
{
|
||||
RecipeQueueItem[] q = _te.Queue;
|
||||
float t = _r.craftingTime > 0f ? _r.craftingTime : 1f;
|
||||
ItemClass outClass = ItemClass.GetForId(_r.itemValueType);
|
||||
q[_slot] = new RecipeQueueItem
|
||||
{
|
||||
Recipe = _r,
|
||||
Multiplier = (short)_batches,
|
||||
CraftingTimeLeft = t,
|
||||
OneItemCraftTime = t,
|
||||
IsCrafting = _slot == q.Length - 1,
|
||||
Quality = (byte)(outClass != null && outClass.HasQuality ? 1 : 0),
|
||||
StartingEntityId = -1
|
||||
};
|
||||
_te.Queue = q;
|
||||
}
|
||||
|
||||
static bool RawInInput(TileEntityWorkstation _te)
|
||||
{
|
||||
if (!_te.isModuleUsed[MaterialModule])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < 3 && i < _te.Input.Length; i++)
|
||||
{
|
||||
if (_te.Input[i] != null && !_te.Input[i].IsEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Сырьё для переплавки в unit_X: предмет из ящика «откуда», сделанный из этого материала, - во вход кузни.</summary>
|
||||
/// <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;
|
||||
string mat = _unit.Substring(5);
|
||||
ItemStack[] input = _st.Te.Input;
|
||||
// 23.10 («вместо камней вплавляет булыжник»): сначала сырьё, у которого нет рецепта (камни, лом), сделанное руками
|
||||
// (булыжник) - только если сырья нет. Раньше - первое подходящее по порядку ячеек.
|
||||
// 24.10 («работник положил сланец в переплавку в печку»): горючее (нефтяной сланец - сырьё для бензина) не плавится
|
||||
// вовсе. 24.10 («и снова засунул булыжник!!!»): плавится только сырьё - сделанное руками (булыжник) не плавится
|
||||
// никогда. Прежняя проверка «нет рецепта» не отличала камни от булыжника: у камня тоже есть рецепты (разбор в лом,
|
||||
// выплавка из переплавленного) - см. Handmade.
|
||||
foreach ((Vector3i p, TEFeatureStorage box, int k) s in Ordered(_src, g =>
|
||||
{
|
||||
ItemClass c = g.itemValue.ItemClass;
|
||||
if (c?.MadeOfMaterial?.ForgeCategory == null || !c.MadeOfMaterial.ForgeCategory.EqualsCaseInsensitive(mat) ||
|
||||
c.GetItemName().StartsWith("unit_") || ItemClass.GetFuelValue(g.itemValue) > 0 || Handmade(c))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}))
|
||||
{
|
||||
{
|
||||
int k = s.k;
|
||||
ItemStack g = s.box.ItemGrid.items[k];
|
||||
ItemClass ic = g.itemValue.ItemClass;
|
||||
for (int i = 0; i < 3 && i < input.Length; i++)
|
||||
{
|
||||
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;
|
||||
if (_halfEmpty && !empty && input[i].count * 2 > ic.MaxCount)
|
||||
{
|
||||
room = 0;
|
||||
}
|
||||
if (room <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
_at = s.p;
|
||||
if (_dry)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
int take = Mathf.Min(room, g.count);
|
||||
if (empty)
|
||||
{
|
||||
input[i] = new ItemStack(g.itemValue.Clone(), take);
|
||||
}
|
||||
else
|
||||
{
|
||||
input[i].count += take;
|
||||
}
|
||||
CinerariumWork.Take(s.box, k, take);
|
||||
_st.Te.Input = input;
|
||||
Debug.Log("[NecromancerTome] Cinerarium: рабочий положил в кузню " + _st.Pos + " " + take + " " + ic.GetItemName());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
foreach ((Vector3i p, TEFeatureStorage box, int k) s in Ordered(_src, FuelRank))
|
||||
{
|
||||
return s.p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Порядок топлива (23.10: «вместо камней жжёт в печке сланец»): древесина, потом уголь, потом прочее горючее
|
||||
/// (сланец, бензин - ценное сырьё, только если больше нечего). -1 - не горит.</summary>
|
||||
static int FuelRank(ItemStack _g)
|
||||
{
|
||||
if (ItemClass.GetFuelValue(_g.itemValue) <= 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
string n = _g.itemValue.ItemClass?.GetItemName() ?? "";
|
||||
return n.StartsWith("resourceWood") ? 0 : n.StartsWith("resourceCoal") ? 1 : 2;
|
||||
}
|
||||
|
||||
/// <summary>Незакреплённые непустые ячейки ящиков «откуда», у которых _rank >= 0, по возрастанию ранга (при равном -
|
||||
/// в прежнем порядке ящиков и ячеек).</summary>
|
||||
static List<(Vector3i p, TEFeatureStorage box, int k)> Ordered(List<(Vector3i p, TEFeatureStorage box)> _src, System.Func<ItemStack, int> _rank)
|
||||
{
|
||||
List<(Vector3i p, TEFeatureStorage box, int k, int r, int i)> all = new List<(Vector3i, TEFeatureStorage, int, int, int)>();
|
||||
foreach ((Vector3i p, TEFeatureStorage box) s in _src)
|
||||
{
|
||||
ItemStack[] grid = s.box.ItemGrid.items;
|
||||
for (int k = 0; k < grid.Length; k++)
|
||||
{
|
||||
if (grid[k] == null || grid[k].IsEmpty() || CinerariumWork.Locked(s.box, k))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int r = _rank(grid[k]);
|
||||
if (r >= 0)
|
||||
{
|
||||
all.Add((s.p, s.box, k, r, all.Count));
|
||||
}
|
||||
}
|
||||
}
|
||||
all.Sort((a, b) => a.r != b.r ? a.r.CompareTo(b.r) : a.i.CompareTo(b.i));
|
||||
List<(Vector3i p, TEFeatureStorage box, int k)> res = new List<(Vector3i, TEFeatureStorage, int)>(all.Count);
|
||||
foreach (var a in all)
|
||||
{
|
||||
res.Add((a.p, a.box, a.k));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <summary>Огонь станции с топливом: нет топлива - стопку горючего из ящиков «откуда» (_addFuel), затем зажечь.</summary>
|
||||
static void Ignite(TileEntityWorkstation _te, List<(Vector3i p, TEFeatureStorage box)> _src, bool _addFuel)
|
||||
{
|
||||
if (!_te.isModuleUsed[FuelModule])
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_addFuel || _te.BurnTotalTimeLeft <= 0f)
|
||||
{
|
||||
ItemStack[] fuel = _te.Fuel;
|
||||
foreach ((Vector3i p, TEFeatureStorage box, int k) s in Ordered(_src, FuelRank))
|
||||
{
|
||||
{
|
||||
int k = s.k;
|
||||
ItemStack g = s.box.ItemGrid.items[k];
|
||||
for (int i = 0; i < fuel.Length; i++)
|
||||
{
|
||||
bool empty = fuel[i] == null || fuel[i].IsEmpty();
|
||||
int max = g.itemValue.ItemClass.MaxCount;
|
||||
int room = empty ? max : fuel[i].itemValue.type == g.itemValue.type ? max - fuel[i].count : 0;
|
||||
if (room <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int take = Mathf.Min(room, g.count);
|
||||
if (empty)
|
||||
{
|
||||
fuel[i] = new ItemStack(g.itemValue.Clone(), take);
|
||||
}
|
||||
else
|
||||
{
|
||||
fuel[i].count += take;
|
||||
}
|
||||
string fuelName = g.itemValue.ItemClass?.GetItemName(); // до Take: опустевшая ячейка очищает стопку (было NRE, 18.10)
|
||||
CinerariumWork.Take(s.box, k, take);
|
||||
_te.Fuel = fuel;
|
||||
Debug.Log("[NecromancerTome] Cinerarium: рабочий подбросил топливо " + take + " " + fuelName);
|
||||
goto lit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lit:
|
||||
if (_te.BurnTotalTimeLeft > 0f && !_te.IsBurning)
|
||||
{
|
||||
_te.IsBurning = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ namespace NecromancerTome
|
||||
/// <summary>Высота корня модели над низом блока урны, м. 16.10 в Blender по модели урны: -0.15 («духа чуть приподними»).
|
||||
/// Подгоняется командой necrogenie.</summary>
|
||||
public static float OffsetY = -0.15f;
|
||||
/// <summary>У урны рабочего горлышко ниже (земля в чаше на 0.45 м против 0.53 у урны сборщика) - дух ниже на столько же.</summary>
|
||||
public static float WorkerDrop = -0.08f;
|
||||
/// <summary>Сдвиг вперёд в системе самого духа, м (16.10: «чуть вперёд, чтобы хвост не просвечивал сквозь урну сзади»
|
||||
/// - загнутый назад кончик хвоста; в Blender 0.06 - ни одной вершины за стенкой, взято 0.07). Поворачивается вместе
|
||||
/// с духом, поэтому хвост всегда в горлышке. Знак проверить в игре: necrogenie fwd.</summary>
|
||||
@@ -49,11 +51,11 @@ namespace NecromancerTome
|
||||
}
|
||||
return (pl.GetPosition() - Origin.position - _scenePos).sqrMagnitude > SleepDistance * SleepDistance;
|
||||
}
|
||||
/// <summary>Руки (16.10: «Т-поза бесит»): плечо от вертикали вбок и вперёд, локоть согнут вперёд, градусы.
|
||||
/// Модель в позе покоя, аниматора нет - кости ставятся один раз при появлении.</summary>
|
||||
public static float ArmOut = 12f;
|
||||
public static float ArmFwd = 15f;
|
||||
public static float Elbow = 35f;
|
||||
/// <summary>Руки в жесте: плечо от вертикали вбок, локоть согнут вперёд, градусы. Покой - «смирно» (18.10: «Всё Смирно и
|
||||
/// будет позой для бездельничающих некротехов») запечён в позу покоя модели (pose_attention.json при выгрузке FBX),
|
||||
/// аниматора нет - без жеста кости не трогаются.</summary>
|
||||
public static float ArmOut = 3f;
|
||||
public static float Elbow = 0f;
|
||||
|
||||
static GameObject _prefab;
|
||||
static bool _tried;
|
||||
@@ -72,7 +74,19 @@ namespace NecromancerTome
|
||||
return _prefab != null;
|
||||
}
|
||||
|
||||
public static GameObject Spawn(Transform _urnModel)
|
||||
/// <summary>Дух логиста (18.10: «Пусть дух вылетает когда нужно что-то нести. Но уменьшим его в три раза»): размер.</summary>
|
||||
public static float LogistScale = 1f / 3f;
|
||||
/// <summary>Дом духа логиста в осях модели урны - корень модели духа внутри кадила, середина тела на высоте окон крышки
|
||||
/// (по центру - окна на 0.63 м, у стены - 0.73 м, кадило в 0.22 м от стены, стена - -z).</summary>
|
||||
public static Vector3 LogistHome = new Vector3(0f, 0.25f, 0f);
|
||||
public static Vector3 LogistWallHome = new Vector3(0f, 0.35f, -0.22f);
|
||||
/// <summary>Окна крышки - на расстоянии LogistExit от оси кадила (радиус крышки 0.21 м), по осям модели.</summary>
|
||||
public static float LogistExit = 0.3f;
|
||||
/// <summary>Лутер: сдвиг духа вперёд (подобран в Blender 18.10).</summary>
|
||||
public static float LootForward = 0.08f;
|
||||
|
||||
/// <summary>_logist: 0 - обычный дух над горлышком, 1 - дух логиста в кадиле по центру, 2 - в кадиле у стены.</summary>
|
||||
public static GameObject Spawn(Transform _urnModel, float _drop, int _logist = 0)
|
||||
{
|
||||
if (_urnModel == null || !Load())
|
||||
{
|
||||
@@ -103,7 +117,15 @@ namespace NecromancerTome
|
||||
{
|
||||
Object.Destroy(c);
|
||||
}
|
||||
g.AddComponent<CinerariumGenieMotion>().Pose();
|
||||
CinerariumGenieMotion m = g.AddComponent<CinerariumGenieMotion>();
|
||||
m.Drop = _drop;
|
||||
if (_logist > 0)
|
||||
{
|
||||
m.Small = true;
|
||||
m.Wall = _logist == 2;
|
||||
g.transform.localScale = Vector3.one * LogistScale;
|
||||
}
|
||||
m.Pose();
|
||||
return g;
|
||||
}
|
||||
|
||||
@@ -149,8 +171,7 @@ namespace NecromancerTome
|
||||
m.EnableKeyword("_EMISSION");
|
||||
m.globalIlluminationFlags = MaterialGlobalIlluminationFlags.None;
|
||||
}
|
||||
m.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
|
||||
SetLook(m);
|
||||
SetLook(m); // смешивание, глубина и очередь - тоже там (Solid)
|
||||
_ghostMats[_src] = m;
|
||||
return m;
|
||||
}
|
||||
@@ -160,12 +181,33 @@ namespace NecromancerTome
|
||||
/// <summary>Своё свечение тела, доля от Tint.</summary>
|
||||
public static float Glow = 0.35f;
|
||||
|
||||
/// <summary>ВРЕМЕННО (17.10: «Временно убери им прозрачность. А то плохо видно» - пока подбирается поза рук): дух
|
||||
/// непрозрачный, обычная заливка с записью глубины. Перед релизом вернуть false (BACKLOG). Консоль: necrogenie solid 0|1.</summary>
|
||||
public static bool Solid = false; // 18.10: прозрачность вернули («Прозрачность верни сразу»)
|
||||
|
||||
static void SetLook(Material _m)
|
||||
{
|
||||
if (_m == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_m.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
|
||||
_m.SetInt("_DstBlend", (int)(Solid ? UnityEngine.Rendering.BlendMode.Zero : UnityEngine.Rendering.BlendMode.One));
|
||||
_m.SetInt("_ZWrite", Solid ? 1 : 0);
|
||||
_m.SetOverrideTag("RenderType", Solid ? "Opaque" : "Transparent");
|
||||
_m.renderQueue = (int)(Solid ? UnityEngine.Rendering.RenderQueue.Geometry : UnityEngine.Rendering.RenderQueue.Transparent);
|
||||
if (Solid)
|
||||
{
|
||||
if (_m.HasProperty("_Color"))
|
||||
{
|
||||
_m.color = new Color(Tint.r, Tint.g, Tint.b, 1f);
|
||||
}
|
||||
if (_m.HasProperty("_EmissionColor"))
|
||||
{
|
||||
_m.SetColor("_EmissionColor", new Color(Tint.r * Glow * 0.5f, Tint.g * Glow * 0.5f, Tint.b * Glow * 0.5f, 1f));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_m.HasProperty("_Color"))
|
||||
{
|
||||
_m.color = new Color(Tint.r * Alpha, Tint.g * Alpha, Tint.b * Alpha, 1f);
|
||||
@@ -230,16 +272,52 @@ namespace NecromancerTome
|
||||
return p.Key;
|
||||
}
|
||||
}
|
||||
foreach (KeyValuePair<Material, Material> p in _stuckMats)
|
||||
{
|
||||
if (p.Value == _ghost)
|
||||
{
|
||||
return p.Key;
|
||||
}
|
||||
}
|
||||
return _ghost;
|
||||
}
|
||||
|
||||
/// <summary>Шейдер материала пропал - дух рисуется пурпурным (23.10.2026, скриншот пользователя: после выхода из
|
||||
/// питомца в 218 блоках от базы все духи некротехнологов пурпурные; раньше - #70, дух закопанной урны).</summary>
|
||||
public static bool Broken(Material _m)
|
||||
{
|
||||
return _m != null && (_m.shader == null || !_m.shader.isSupported || _m.shader.name == "Hidden/InternalErrorShader");
|
||||
}
|
||||
|
||||
/// <summary>Сломанную копию - из кэша вон и собрать заново из исходного материала модели.</summary>
|
||||
public static Material Repair(Material _broken, bool _stuck)
|
||||
{
|
||||
Material src = Source(_broken);
|
||||
Debug.Log("[NecromancerTome] CinerariumGenie: шейдер духа сломан (" + (_broken.shader != null ? _broken.shader.name : "null") +
|
||||
", исходный материал '" + (src != null ? src.name : "null") + "' - шейдер " +
|
||||
(src != null && src.shader != null ? src.shader.name + (src.shader.isSupported ? "" : " НЕ поддерживается") : "null") +
|
||||
", Standard " + (Shader.Find("Standard") != null ? "есть" : "НЕ найден") + ") - материал пересоздан");
|
||||
if (src == null || src == _broken)
|
||||
{
|
||||
return _broken;
|
||||
}
|
||||
_ghostMats.Remove(src);
|
||||
_stuckMats.Remove(src);
|
||||
return _stuck ? Stuck(src) : Ghost(src);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Покачивание и поворот джинна над урной. Только вид - никакой логики.</summary>
|
||||
[Preserve]
|
||||
public class CinerariumGenieMotion : MonoBehaviour
|
||||
public partial class CinerariumGenieMotion : MonoBehaviour
|
||||
{
|
||||
float _yaw;
|
||||
float _phase;
|
||||
/// <summary>Своя поправка высоты урны (CinerariumGenie.WorkerDrop у рабочего).</summary>
|
||||
public float Drop;
|
||||
/// <summary>Свой сдвиг вперёд (NaN - общий CinerariumGenie.Forward). Лутер (18.10, банка с крышкой-кадилом): 0.08 - хвост
|
||||
/// по центру отверстия на макушке («Вот сейчас было идеально»).</summary>
|
||||
public float ForwardOverride = float.NaN;
|
||||
/// <summary>Полёт к месту работы и обратно (16.10: «возвращается задом», «делал действие не сразу, а хоть руками
|
||||
/// пошевелив»): туда - лицом к месту, над местом - жест руками (дело делается в его середине, см. Visit),
|
||||
/// обратно - развернувшись лицом к урне. Точка - в координатах модели урны; _t - время с начала полёта, -1 - дома.</summary>
|
||||
@@ -247,8 +325,14 @@ namespace NecromancerTome
|
||||
{
|
||||
public Vector3 Hover, Look; // в осях модели урны
|
||||
public float Fly;
|
||||
public bool Pass; // точка пути (окно кадила) - без жеста
|
||||
}
|
||||
|
||||
/// <summary>Дух логиста (18.10): втрое меньше, дома (в кадиле) не виден, вылетает и влетает через окно крышки.</summary>
|
||||
public bool Small;
|
||||
/// <summary>Кадило у стены: дом сдвинут к кронштейну, окно в стену (-z) не используется.</summary>
|
||||
public bool Wall;
|
||||
|
||||
readonly List<Stop> _stops = new List<Stop>();
|
||||
float _backFly;
|
||||
float _t = -1f;
|
||||
@@ -268,14 +352,15 @@ namespace NecromancerTome
|
||||
Quaternion[] _armRest;
|
||||
static bool _armLogged;
|
||||
|
||||
/// <summary>Опустить руки из позы покоя: направление плеча и предплечья задаётся в осях модели (вперёд - +Z),
|
||||
/// поворот - от текущего направления кости к нужному, так что оси самих костей значения не имеют.</summary>
|
||||
/// <summary>Покой - руки в позе модели («смирно»).</summary>
|
||||
public void Pose()
|
||||
{
|
||||
Pose(CinerariumGenie.ArmFwd, CinerariumGenie.Elbow, CinerariumGenie.Elbow);
|
||||
Pose(0f, CinerariumGenie.Elbow, CinerariumGenie.Elbow);
|
||||
}
|
||||
|
||||
void Pose(float _fwd, float _elbowL, float _elbowR)
|
||||
/// <summary>Жест долей _w от покоя: плечо вперёд на GestureFwd, направление плеча и предплечья задаётся в осях модели
|
||||
/// (вперёд - +Z), поворот - от текущего направления кости к нужному, так что оси самих костей значения не имеют.</summary>
|
||||
void Pose(float _w, float _elbowL, float _elbowR)
|
||||
{
|
||||
if (_arm == null)
|
||||
{
|
||||
@@ -311,6 +396,10 @@ namespace NecromancerTome
|
||||
_arm[i].localRotation = _armRest[i];
|
||||
}
|
||||
}
|
||||
if (_w <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (int side = 0; side < 2; side++)
|
||||
{
|
||||
Transform up = _arm[side * 3], fore = _arm[side * 3 + 1], hand = _arm[side * 3 + 2];
|
||||
@@ -319,11 +408,11 @@ namespace NecromancerTome
|
||||
continue;
|
||||
}
|
||||
float sign = Mathf.Sign(transform.InverseTransformPoint(up.position).x);
|
||||
float o = CinerariumGenie.ArmOut * Mathf.Deg2Rad, f = _fwd * Mathf.Deg2Rad;
|
||||
float o = CinerariumGenie.ArmOut * Mathf.Deg2Rad, f = GestureFwd * Mathf.Deg2Rad;
|
||||
Vector3 upDir = new Vector3(sign * Mathf.Sin(o), -Mathf.Cos(o) * Mathf.Cos(f), Mathf.Sin(f)).normalized;
|
||||
Vector3 foreDir = Vector3.RotateTowards(upDir, Vector3.forward, (side == 0 ? _elbowL : _elbowR) * Mathf.Deg2Rad, 0f);
|
||||
up.rotation = Quaternion.FromToRotation(fore.position - up.position, transform.TransformDirection(upDir)) * up.rotation;
|
||||
fore.rotation = Quaternion.FromToRotation(hand.position - fore.position, transform.TransformDirection(foreDir)) * fore.rotation;
|
||||
up.rotation = Quaternion.Slerp(Quaternion.identity, Quaternion.FromToRotation(fore.position - up.position, transform.TransformDirection(upDir)), _w) * up.rotation;
|
||||
fore.rotation = Quaternion.Slerp(Quaternion.identity, Quaternion.FromToRotation(hand.position - fore.position, transform.TransformDirection(foreDir)), _w) * fore.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,11 +432,23 @@ namespace NecromancerTome
|
||||
Vector3 home = Home();
|
||||
Vector3 prev = home;
|
||||
float delay = 0f;
|
||||
Vector3 exit = home;
|
||||
if (Small)
|
||||
{
|
||||
// Сначала - к окну крышки в сторону первого места (у стены окно в стену не годится), оттуда - к месту.
|
||||
exit = Window(home, transform.parent.InverseTransformPoint(_list[0].stand) - home);
|
||||
_stops.Add(new Stop { Hover = exit, Look = exit + (exit - home), Fly = 0.35f, Pass = true });
|
||||
delay += 0.35f;
|
||||
prev = exit;
|
||||
}
|
||||
foreach ((Vector3 stand, Vector3 look, bool beside) in _list)
|
||||
{
|
||||
// Высота корня духа над низом клетки места: обычный дух - над станцией 0.8 (сбоку 0.15) с поправкой урны;
|
||||
// маленький - над верхом клетки (1.05) или у её середины сбоку (0.4).
|
||||
float up = Small ? (beside ? 0.4f : 1.05f) : CinerariumGenie.OffsetY + Drop + (beside ? 0.15f : 0.8f);
|
||||
Stop st = new Stop
|
||||
{
|
||||
Hover = transform.parent.InverseTransformPoint(stand) + new Vector3(0f, CinerariumGenie.OffsetY + (beside ? 0.15f : 0.8f), 0f),
|
||||
Hover = transform.parent.InverseTransformPoint(stand) + new Vector3(0f, up, 0f),
|
||||
Look = transform.parent.InverseTransformPoint(look)
|
||||
};
|
||||
st.Fly = Mathf.Max(OutSeconds, Vector3.Distance(prev, st.Hover) / FlySpeed);
|
||||
@@ -355,14 +456,46 @@ namespace NecromancerTome
|
||||
prev = st.Hover;
|
||||
_stops.Add(st);
|
||||
}
|
||||
_backFly = Mathf.Max(BackSeconds, Vector3.Distance(prev, home) / FlySpeed);
|
||||
if (Small)
|
||||
{
|
||||
// Назад - тем же окном.
|
||||
_stops.Add(new Stop { Hover = exit, Look = home, Fly = Mathf.Max(BackSeconds, Vector3.Distance(prev, exit) / FlySpeed), Pass = true });
|
||||
prev = exit;
|
||||
}
|
||||
_backFly = Small ? 0.35f : Mathf.Max(BackSeconds, Vector3.Distance(prev, home) / FlySpeed);
|
||||
_t = 0f;
|
||||
return delay - WorkSeconds * 0.5f;
|
||||
}
|
||||
|
||||
/// <summary>Окно крышки кадила по направлению _dir (оси модели): одно из четырёх по осям x/z, у стены - не в стену.</summary>
|
||||
Vector3 Window(Vector3 _home, Vector3 _dir)
|
||||
{
|
||||
Vector3 best = Vector3.forward;
|
||||
float bestDot = float.MinValue;
|
||||
foreach (Vector3 w in new[] { Vector3.forward, Vector3.back, Vector3.left, Vector3.right })
|
||||
{
|
||||
if (Wall && w == Vector3.back)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
float d = Vector3.Dot(w, new Vector3(_dir.x, 0f, _dir.z));
|
||||
if (d > bestDot)
|
||||
{
|
||||
bestDot = d;
|
||||
best = w;
|
||||
}
|
||||
}
|
||||
return _home + best * CinerariumGenie.LogistExit;
|
||||
}
|
||||
|
||||
Vector3 Home()
|
||||
{
|
||||
return new Vector3(0f, CinerariumGenie.OffsetY, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward);
|
||||
if (Small)
|
||||
{
|
||||
return Wall ? CinerariumGenie.LogistWallHome : CinerariumGenie.LogistHome;
|
||||
}
|
||||
float fwd = float.IsNaN(ForwardOverride) ? CinerariumGenie.Forward : ForwardOverride;
|
||||
return new Vector3(0f, CinerariumGenie.OffsetY + Drop, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, fwd);
|
||||
}
|
||||
|
||||
/// <summary>Повернуться лицом по направлению _dir (горизонтально), не быстрее 300°/с.</summary>
|
||||
@@ -378,8 +511,8 @@ namespace NecromancerTome
|
||||
{
|
||||
if (_w > 0f)
|
||||
{
|
||||
float wave = Mathf.Sin(_t * Mathf.PI * 2f * 1.5f) * GestureWave * _w;
|
||||
Pose(Mathf.Lerp(CinerariumGenie.ArmFwd, GestureFwd, _w), CinerariumGenie.Elbow + 15f * _w + wave, CinerariumGenie.Elbow + 15f * _w - wave);
|
||||
float wave = Mathf.Sin(_t * Mathf.PI * 2f * 1.5f) * GestureWave;
|
||||
Pose(_w, CinerariumGenie.Elbow + 15f + wave, CinerariumGenie.Elbow + 15f - wave);
|
||||
_gesturing = true;
|
||||
}
|
||||
else if (_gesturing)
|
||||
@@ -438,30 +571,75 @@ namespace NecromancerTome
|
||||
float _nextSleepCheck;
|
||||
bool _asleep;
|
||||
bool _hiddenApplied;
|
||||
float _nextShaderCheck;
|
||||
Renderer[] _renderers;
|
||||
|
||||
/// <summary>Раз в 5 с: не сломан ли шейдер у материалов духа (CinerariumGenie.Broken) - починить на месте.</summary>
|
||||
void CheckShaders()
|
||||
{
|
||||
if (_renderers == null)
|
||||
{
|
||||
_renderers = GetComponentsInChildren<Renderer>(true);
|
||||
}
|
||||
foreach (Renderer r in _renderers)
|
||||
{
|
||||
if (r == null || r.name.Contains("eye"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Material[] mats = r.sharedMaterials;
|
||||
bool fixedAny = false;
|
||||
for (int i = 0; i < mats.Length; i++)
|
||||
{
|
||||
if (CinerariumGenie.Broken(mats[i]))
|
||||
{
|
||||
mats[i] = CinerariumGenie.Repair(mats[i], _stuck);
|
||||
fixedAny = true;
|
||||
}
|
||||
}
|
||||
if (fixedAny)
|
||||
{
|
||||
r.sharedMaterials = mats;
|
||||
_glowing.Clear(); // сохранённые светлые наборы могли быть со сломанными копиями
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (Time.time >= _nextShaderCheck)
|
||||
{
|
||||
_nextShaderCheck = Time.time + 5f;
|
||||
CheckShaders();
|
||||
}
|
||||
if (Time.time >= _nextSleepCheck)
|
||||
{
|
||||
_nextSleepCheck = Time.time + 0.5f;
|
||||
bool asleep = CinerariumGenie.PlayerFar(transform.position);
|
||||
if (asleep && !_asleep)
|
||||
// Уснул - игрок далеко или дух не среди ближайших (CinerariumLoad.AnimateNearest): замирает дома, работа идёт.
|
||||
bool asleep = CinerariumGenie.PlayerFar(transform.position) || !CinerariumLoad.Animated(this);
|
||||
if (asleep && !_asleep && !Digging) // старатель под землёй - не возвращать домой
|
||||
{
|
||||
_t = -1f; // полёт прерван - дело всё равно выполнится
|
||||
Gesture(0f);
|
||||
transform.localPosition = Home();
|
||||
}
|
||||
_asleep = asleep;
|
||||
if (_hiddenApplied != CinerariumGenie.Hidden)
|
||||
}
|
||||
// Виден ли: не скрыт командой; дух логиста - только в полёте (дома сидит в кадиле). Рендереры - только при смене
|
||||
// (16.10, проверка на утечки: раньше список рендереров собирался каждые 0.5 с - мусор для GC).
|
||||
bool show = !CinerariumGenie.Hidden && (!Small || _t >= 0f) && !DigHidden;
|
||||
if (_hiddenApplied != !show)
|
||||
{
|
||||
_hiddenApplied = !show;
|
||||
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
// Только при смене (16.10, проверка на утечки: раньше список рендереров собирался каждые 0.5 с - мусор для GC).
|
||||
_hiddenApplied = CinerariumGenie.Hidden;
|
||||
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
r.enabled = !_hiddenApplied;
|
||||
}
|
||||
r.enabled = show;
|
||||
}
|
||||
}
|
||||
if (DigUpdate(_asleep))
|
||||
{
|
||||
return; // старатель: спуск, под землёй, подъём (CinerariumProspector.cs)
|
||||
}
|
||||
if (_stuck || _asleep)
|
||||
{
|
||||
return; // замер как был
|
||||
@@ -484,7 +662,7 @@ namespace NecromancerTome
|
||||
return;
|
||||
}
|
||||
acc += st.Fly;
|
||||
if (_t < acc + WorkSeconds)
|
||||
if (!st.Pass && _t < acc + WorkSeconds)
|
||||
{
|
||||
transform.localPosition = st.Hover;
|
||||
Face(st.Look - st.Hover); // за работой - лицом к месту
|
||||
@@ -492,7 +670,10 @@ namespace NecromancerTome
|
||||
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
|
||||
return;
|
||||
}
|
||||
acc += WorkSeconds;
|
||||
if (!st.Pass)
|
||||
{
|
||||
acc += WorkSeconds;
|
||||
}
|
||||
from = st.Hover;
|
||||
}
|
||||
if (_t < acc + _backFly)
|
||||
@@ -506,6 +687,11 @@ namespace NecromancerTome
|
||||
_t = -1f;
|
||||
Gesture(0f);
|
||||
}
|
||||
if (Small)
|
||||
{
|
||||
transform.localPosition = home; // в кадиле, не виден
|
||||
return;
|
||||
}
|
||||
_yaw = yawTarget % 360f;
|
||||
float bob = CinerariumGenie.BobPeriod > 0.01f
|
||||
? Mathf.Sin((Time.time + _phase) * Mathf.PI * 2f / CinerariumGenie.BobPeriod) * CinerariumGenie.BobHeight
|
||||
@@ -544,7 +730,8 @@ namespace NecromancerTome
|
||||
"necrogenie alpha <0-1> - body brightness (the body adds light, it is not alpha-blended)\n" +
|
||||
"necrogenie glow <0-1> - body's own glow (visible at night)\n" +
|
||||
"necrogenie tint <r> <g> <b> - body colour\n" +
|
||||
"necrogenie arms <out> <fwd> <elbow> - arm pose, degrees: upper arm from vertical sideways and forward, elbow bend\n" +
|
||||
"necrogenie arms <out> [elbow] - gesture arms, degrees: upper arm from vertical sideways, elbow bend (rest pose 'attention' is the model's own)\n" +
|
||||
"necrogenie solid <0|1> - TEMPORARY: opaque body (1) to see the pose, or the ghostly glow (0)\n" +
|
||||
"necrogenie sleep <m> - spirits and threads freeze when the player is farther (work goes on)\n" +
|
||||
"necrogenie hide|show - hide all spirits (weak PCs, video), work goes on";
|
||||
}
|
||||
@@ -589,18 +776,14 @@ namespace NecromancerTome
|
||||
break;
|
||||
case "arms":
|
||||
CinerariumGenie.ArmOut = v;
|
||||
if (_params.Count >= 3 && float.TryParse(_params[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float af))
|
||||
{
|
||||
CinerariumGenie.ArmFwd = af;
|
||||
}
|
||||
if (_params.Count >= 4 && float.TryParse(_params[3], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float ae))
|
||||
if (_params.Count >= 3 && float.TryParse(_params[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float ae))
|
||||
{
|
||||
CinerariumGenie.Elbow = ae;
|
||||
}
|
||||
foreach (CinerariumGenieMotion m in CinerariumGenie.Live)
|
||||
{
|
||||
m.Pose();
|
||||
}
|
||||
break;
|
||||
case "solid":
|
||||
CinerariumGenie.Solid = v != 0f;
|
||||
CinerariumGenie.ApplyAlpha();
|
||||
break;
|
||||
case "tint":
|
||||
if (_params.Count >= 4 &&
|
||||
@@ -620,7 +803,7 @@ namespace NecromancerTome
|
||||
", bob " + CinerariumGenie.BobHeight.ToString("F2") + " / " + CinerariumGenie.BobPeriod.ToString("F1") + " s, alpha " +
|
||||
CinerariumGenie.Alpha.ToString("F2") + ", glow " + CinerariumGenie.Glow.ToString("F2") + ", tint " +
|
||||
CinerariumGenie.Tint.r.ToString("F2") + " " + CinerariumGenie.Tint.g.ToString("F2") + " " + CinerariumGenie.Tint.b.ToString("F2") + ", arms " + CinerariumGenie.ArmOut.ToString("F0") + " " +
|
||||
CinerariumGenie.ArmFwd.ToString("F0") + " " + CinerariumGenie.Elbow.ToString("F0") + ", sleep " + CinerariumGenie.SleepDistance.ToString("F0") + " m" + (CinerariumGenie.Hidden ? ", HIDDEN" : "") + "; spirits on screen: " + CinerariumGenie.Live.Count);
|
||||
CinerariumGenie.Elbow.ToString("F0") + ", sleep " + CinerariumGenie.SleepDistance.ToString("F0") + " m" + (CinerariumGenie.Hidden ? ", HIDDEN" : "") + "; spirits on screen: " + CinerariumGenie.Live.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,15 @@ namespace NecromancerTome
|
||||
{
|
||||
public const int Cooked = 0, Canned = 1, Raw = 2, Harvest = 3, Flowers = 4, Seeds = 5, Drinks = 6, Medicine = 7, Ore = 8,
|
||||
Ingots = 9, Parts = 10, Materials = 11, Chemicals = 12, Tools = 13, Weapons = 14, Ammo = 15, Armor = 16, Mods = 17,
|
||||
Books = 18, Blocks = 19, Necro = 20, Other = 21;
|
||||
public const int Count = 22;
|
||||
Books = 18, Blocks = 19, Necro = 20, Other = 21,
|
||||
// 17.10: «Корм для кур вынеси в отдельную группу» (был в «Сыром»).
|
||||
ChickenFeed = 22,
|
||||
// 23.10 (проверка всех предметов: куры и сложенный транспорт падали в «Прочее»; «Можно и добавить группы»).
|
||||
Livestock = 23, Vehicles = 24,
|
||||
// 23.10: «Заведи группу стройматериалов. Руда и камень отдельно. Сортировка должна быть удобной и с учётом
|
||||
// производства» - то, чем игра улучшает блоки и что для этого производят: булыжник, цемент, бетонная смесь.
|
||||
Building = 25;
|
||||
public const int Count = 26;
|
||||
public const uint All = (1u << Count) - 1;
|
||||
|
||||
/// <summary>Значок группы - иконка типичного предмета (ItemIconAtlas).</summary>
|
||||
@@ -26,11 +33,12 @@ namespace NecromancerTome
|
||||
{
|
||||
"foodMeatStew", "foodCanChili", "foodRawMeat", "foodCropCorn", "plantedChrysanthemum3Harvest", "plantedCorn1",
|
||||
"drinkJarBoiledWater", "medicalFirstAidKit", "resourceRockSmall", "resourceForgedSteel", "resourceMechanicalParts",
|
||||
"resourceWood", "resourceGunPowder", "meleeToolPickT1IronPickaxe", "gunHandgunT1Pistol", "ammo9mmBulletBall",
|
||||
"armorMinerHelmet", "modGunLaserSight", "bookArtOfMiningLuckyStrike", "cobblestoneMaster", "necroSkull", "oldCash"
|
||||
"resourceCloth", "resourceGunPowder", "meleeToolPickT1IronPickaxe", "gunHandgunT1Pistol", "ammo9mmBulletBall",
|
||||
"armorMinerHelmet", "modGunLaserSight", "bookArtOfMiningLuckyStrike", "cobblestoneMaster", "necroSkull", "oldCash",
|
||||
"resourceChickenFeed", "domesticatedChicken", "vehicleMinibikePlaceable", "resourceConcreteMix"
|
||||
};
|
||||
|
||||
/// <summary>Ключ названия группы в Localization.csv: necroGroup0..necroGroup21.</summary>
|
||||
/// <summary>Ключ названия группы в Localization.csv: necroGroup0..necroGroup25.</summary>
|
||||
public static string NameKey(int _g) => "necroGroup" + _g;
|
||||
|
||||
static readonly HashSet<string> OreNames = new HashSet<string>
|
||||
@@ -38,9 +46,14 @@ namespace NecromancerTome
|
||||
"resourceRockSmall", "resourceRockSmallBundle", "resourceScrapIron", "resourceScrapIronBundle", "resourceScrapLead",
|
||||
"resourceLeadBundle", "resourceCoal", "resourceCoalBundle", "resourcePotassiumNitratePowder",
|
||||
"resourcePotassiumNitratePowderBundle", "resourceOilShale", "resourceOilShaleBundle", "resourceClayLump",
|
||||
"resourceCrushedSand", "resourceCobblestones", "resourceGoldNugget", "resourceSilverNugget", "resourceRawDiamond",
|
||||
"resourceCrushedSand", "resourceGoldNugget", "resourceSilverNugget", "resourceRawDiamond",
|
||||
"resourceSnowBall"
|
||||
};
|
||||
/// <summary>Стройматериалы (23.10): древесина, булыжник, цемент, бетонная смесь - чем строят и улучшают блоки; сталь - в «Слитках».</summary>
|
||||
static readonly HashSet<string> BuildingNames = new HashSet<string>
|
||||
{
|
||||
"resourceCobblestones", "resourceCement", "resourceConcreteMix", "resourceWood", "resourceWoodBundle" // древесина - 23.10, пользователь
|
||||
};
|
||||
static readonly HashSet<string> IngotNames = new HashSet<string> { "resourceForgedIron", "resourceForgedSteel" };
|
||||
static readonly HashSet<string> ChemNames = new HashSet<string>
|
||||
{
|
||||
@@ -49,8 +62,8 @@ namespace NecromancerTome
|
||||
};
|
||||
static readonly HashSet<string> MaterialNames = new HashSet<string>
|
||||
{
|
||||
"resourceWood", "resourceWoodBundle", "resourceCloth", "resourceLeather", "resourceBone", "resourceFeather",
|
||||
"resourceYuccaFibers", "resourcePaper", "resourceCement", "resourceConcreteMix", "resourceBeeswax", "resourceCandleStick",
|
||||
"resourceCloth", "resourceLeather", "resourceBone", "resourceFeather",
|
||||
"resourceYuccaFibers", "resourcePaper", "resourceBeeswax", "resourceCandleStick",
|
||||
"resourceAnimalFat"
|
||||
};
|
||||
/// <summary>Детали, которые теги отнесли бы не туда (гвозди - патроны гвоздомёта).</summary>
|
||||
@@ -62,11 +75,12 @@ namespace NecromancerTome
|
||||
};
|
||||
static readonly HashSet<string> RawNames = new HashSet<string>
|
||||
{
|
||||
"foodRawMeat", "foodRawMeatBundle", "foodEgg", "foodHoney", "foodRottingFlesh", "resourceChickenFeed"
|
||||
"foodRawMeat", "foodRawMeatBundle", "foodEgg", "foodHoney", "foodRottingFlesh"
|
||||
};
|
||||
|
||||
/// <summary>Предметы и блоки самого мода - по его Config/items.xml и blocks.xml (у многих нет «necro» в имени: книги
|
||||
/// призыва, браслет, камни портала, прах зомби, кожа жертвы). Читается один раз.</summary>
|
||||
/// <summary>Предметы и блоки самого мода - по его Config/items.xml, blocks.xml и item_modifiers.xml (у многих нет «necro»
|
||||
/// в имени: книги призыва, браслет, камни портала, прах зомби, кожа жертвы; 23.10 - кровавая сфера и кровавый камень,
|
||||
/// они модификаторы, и без item_modifiers.xml уходили в «Модификаторы»). Читается один раз.</summary>
|
||||
static HashSet<string> _modNames;
|
||||
|
||||
static HashSet<string> ModNames()
|
||||
@@ -76,13 +90,13 @@ namespace NecromancerTome
|
||||
return _modNames;
|
||||
}
|
||||
_modNames = new HashSet<string>();
|
||||
foreach (string f in new[] { "items.xml", "blocks.xml" })
|
||||
foreach (string f in new[] { "items.xml", "blocks.xml", "item_modifiers.xml" })
|
||||
{
|
||||
try
|
||||
{
|
||||
XmlDocument doc = new XmlDocument();
|
||||
doc.Load(Path.Combine(ModEntry.Instance.Path, "Config", f));
|
||||
foreach (XmlNode n in doc.SelectNodes("//item[@name] | //block[@name]"))
|
||||
foreach (XmlNode n in doc.SelectNodes("//item[@name] | //block[@name] | //item_modifier[@name]"))
|
||||
{
|
||||
_modNames.Add(n.Attributes["name"].Value);
|
||||
}
|
||||
@@ -120,10 +134,34 @@ namespace NecromancerTome
|
||||
static int Classify(ItemClass _ic, ItemValue _iv)
|
||||
{
|
||||
string n = _ic.GetItemName() ?? "";
|
||||
// 23.10: семена - раньше «Некромантии»: кровавые семена (plantedNecroBloodRose1) уходили с грядок к логисту
|
||||
// «только некромантия», и сборщику нечего было сеять. Всё, что сажается на грядку, - «Семена».
|
||||
if (_ic.IsBlock() && new ItemValue(_ic.Id).ToBlockValue().Block is BlockPlantGrowing)
|
||||
{
|
||||
return Seeds;
|
||||
}
|
||||
// 23.10: живность (куры для курятника, кролик в мешке) и сложенный транспорт - свои группы.
|
||||
if (n.StartsWith("domesticated") || n == RabbitGrab.SackItem)
|
||||
{
|
||||
return Livestock;
|
||||
}
|
||||
if (n.StartsWith("vehicle") && n.EndsWith("Placeable"))
|
||||
{
|
||||
return Vehicles;
|
||||
}
|
||||
// 23.10: жестяные банки - предметы мода, но по смыслу это стеклянные банки игры (кипятить воду) - к ним, не в «Некромантию».
|
||||
if (n.StartsWith("tinCan"))
|
||||
{
|
||||
return Drinks;
|
||||
}
|
||||
if (n.IndexOf("necro", StringComparison.OrdinalIgnoreCase) >= 0 || ModNames().Contains(n))
|
||||
{
|
||||
return Necro;
|
||||
}
|
||||
if (n == "resourceChickenFeed")
|
||||
{
|
||||
return ChickenFeed;
|
||||
}
|
||||
if (_ic is ItemClassModifier)
|
||||
{
|
||||
return Mods;
|
||||
@@ -140,6 +178,10 @@ namespace NecromancerTome
|
||||
{
|
||||
return Books;
|
||||
}
|
||||
if (BuildingNames.Contains(n))
|
||||
{
|
||||
return Building;
|
||||
}
|
||||
if (OreNames.Contains(n))
|
||||
{
|
||||
return Ore;
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
using System.Collections.Generic;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Урны работают, пока игрок далеко (решение 23.10.2026, #78: «Харвестры работают только когда рядом» - «если игрок далеко
|
||||
/// можно выключить анимацию духов... и пусть духи продолжат работать. С остальными тоже. Если ящики переполнятся, то
|
||||
/// харвестры встанут»; досчёт по времени не годится - сборщик следит за ростом и сеет). Игра обновляет участки, за
|
||||
/// которыми кто-то наблюдает (ChunkManager: активный набор = все участки наблюдателей), - поэтому у каждой группы урн
|
||||
/// свой наблюдатель без видимой геометрии: участки не выгружаются, урны и грядки идут. Анимация вдали и так выключена
|
||||
/// (CinerariumLoad.AnimateNearest). Группа - клетка CellSize×CellSize блоков по реестру урн сейва (CinerariumRegistry),
|
||||
/// наблюдатель в её центре держит (ViewDim+2) участков во все стороны - клетку и рабочие зоны у её краёв.
|
||||
/// Нагрузку мерить по логу (Heap/RSS, Chunks); если тяжело - сначала оптимизация, запасной путь - урны стоят без игрока.
|
||||
/// Консоль: necrournkeep on|off|dim N.
|
||||
/// </summary>
|
||||
public static class CinerariumKeepAlive
|
||||
{
|
||||
public static bool Enabled = true;
|
||||
public const int CellSize = 64;
|
||||
/// <summary>Радиус наблюдателя в участках минус 2 (так считает ChunkManager): 1 - 7×7 участков.</summary>
|
||||
public static int ViewDim = 1;
|
||||
const float SyncSeconds = 10f;
|
||||
|
||||
static readonly Dictionary<Vector2i, ChunkManager.ChunkObserver> _observers = new Dictionary<Vector2i, ChunkManager.ChunkObserver>();
|
||||
static World _world;
|
||||
static float _nextSync;
|
||||
|
||||
public static int Count => _observers.Count;
|
||||
|
||||
public static void Tick()
|
||||
{
|
||||
if (Time.time < _nextSync)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_nextSync = Time.time + SyncSeconds;
|
||||
GameManager gm = GameManager.Instance;
|
||||
World w = gm?.World;
|
||||
if (w != _world)
|
||||
{
|
||||
_observers.Clear(); // мир сменился - прежние наблюдатели ушли вместе с ним
|
||||
_world = w;
|
||||
}
|
||||
if (w == null || !ConnectionManager.Instance.IsServer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Клетки с урнами и средняя высота урн в каждой.
|
||||
Dictionary<Vector2i, (int sum, int n)> cells = new Dictionary<Vector2i, (int, int)>();
|
||||
if (Enabled)
|
||||
{
|
||||
foreach (Vector3i p in CinerariumRegistry.All())
|
||||
{
|
||||
Vector2i c = new Vector2i(Mathf.FloorToInt(p.x / (float)CellSize), Mathf.FloorToInt(p.z / (float)CellSize));
|
||||
cells.TryGetValue(c, out (int sum, int n) v);
|
||||
cells[c] = (v.sum + p.y, v.n + 1);
|
||||
}
|
||||
}
|
||||
List<Vector2i> gone = null;
|
||||
foreach (KeyValuePair<Vector2i, ChunkManager.ChunkObserver> kv in _observers)
|
||||
{
|
||||
if (!cells.ContainsKey(kv.Key) || kv.Value.viewDim != ViewDim)
|
||||
{
|
||||
(gone ??= new List<Vector2i>()).Add(kv.Key);
|
||||
}
|
||||
}
|
||||
if (gone != null)
|
||||
{
|
||||
foreach (Vector2i c in gone)
|
||||
{
|
||||
gm.RemoveChunkObserver(_observers[c]);
|
||||
_observers.Remove(c);
|
||||
Debug.Log("[NecromancerTome] CinerariumKeepAlive: группа урн " + c + " - наблюдатель снят, всего " + _observers.Count);
|
||||
}
|
||||
}
|
||||
foreach (KeyValuePair<Vector2i, (int sum, int n)> kv in cells)
|
||||
{
|
||||
if (_observers.ContainsKey(kv.Key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Vector3 center = new Vector3((kv.Key.x + 0.5f) * CellSize, kv.Value.sum / (float)kv.Value.n, (kv.Key.y + 0.5f) * CellSize);
|
||||
_observers[kv.Key] = gm.AddChunkObserver(center, false, ViewDim, -1);
|
||||
Debug.Log("[NecromancerTome] CinerariumKeepAlive: группа урн " + kv.Key + " (" + kv.Value.n + " шт.) - наблюдатель в " + center +
|
||||
", всего " + _observers.Count);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Сразу пересобрать (команда).</summary>
|
||||
public static void SyncNow()
|
||||
{
|
||||
_nextSync = 0f;
|
||||
Tick();
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(World), nameof(World.OnUpdateTick))]
|
||||
public static class Patch_World_OnUpdateTick_CinerariumKeepAlive
|
||||
{
|
||||
public static void Postfix()
|
||||
{
|
||||
CinerariumKeepAlive.Tick();
|
||||
}
|
||||
}
|
||||
|
||||
public class ConsoleCmdNecroUrnKeep : ConsoleCmdAbstract
|
||||
{
|
||||
public override bool IsExecuteOnClient => false;
|
||||
|
||||
public override bool AllowedInMainMenu => false;
|
||||
|
||||
public override string[] getCommands()
|
||||
{
|
||||
return new string[] { "necrournkeep" };
|
||||
}
|
||||
|
||||
public override string getDescription()
|
||||
{
|
||||
return "Necromancer's Tome: keep urn areas loaded so urns work while you are away - switch.";
|
||||
}
|
||||
|
||||
public override string getHelp()
|
||||
{
|
||||
return "necrournkeep - status\n" +
|
||||
"necrournkeep on|off - off: urns stop when you are far (areas unload), as before\n" +
|
||||
"necrournkeep dim <n> - observer size: (n+2) chunks each way around a 64-block group (default 1)";
|
||||
}
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
SdtdConsole con = SingletonMonoBehaviour<SdtdConsole>.Instance;
|
||||
if (_params.Count >= 1 && (_params[0] == "on" || _params[0] == "off"))
|
||||
{
|
||||
CinerariumKeepAlive.Enabled = _params[0] == "on";
|
||||
}
|
||||
else if (_params.Count >= 2 && _params[0] == "dim" && int.TryParse(_params[1], out int d) && d >= 0 && d <= 6)
|
||||
{
|
||||
CinerariumKeepAlive.ViewDim = d;
|
||||
}
|
||||
CinerariumKeepAlive.SyncNow();
|
||||
con.Output("necrournkeep: " + (CinerariumKeepAlive.Enabled ? "on" : "off") + ", dim " + CinerariumKeepAlive.ViewDim +
|
||||
", observers " + CinerariumKeepAlive.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Лимит урн в «Содержимом» (#65; 20.10: «индикатор, сколько урн уже поставлено и сколько ещё можно»; 23.10, по скриншоту
|
||||
/// с цифрами в заголовке: «У нас есть куча пространства под черепом или справа от него... Всего цинерариев. Доступно
|
||||
/// цинерариев. И до разблокировки ещё одного нужно упокоить зомби столько-то»). Блок дописан в окно игры windowBagStorage
|
||||
/// (Config/XUi_InGame/windows.xml), справа от ячеек урны; в сумках волка и транспорта (то же окно) скрыт.
|
||||
/// </summary>
|
||||
[Preserve]
|
||||
public class XUiC_NecroUrnLimit : XUiController
|
||||
{
|
||||
XUiV_Label _placed, _available, _next;
|
||||
float _nextRefresh;
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
base.Init();
|
||||
_placed = GetChildById("placed")?.ViewComponent as XUiV_Label;
|
||||
_available = GetChildById("available")?.ViewComponent as XUiV_Label;
|
||||
_next = GetChildById("next")?.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 + 1f;
|
||||
TEFeatureCinerarium urn = CinerariumContents.Urn;
|
||||
int have = 0, limit = 0, next = 0;
|
||||
bool show = urn != null && TEFeatureCinerarium.LimitNumbers(out have, out limit, out int _, out next);
|
||||
ViewComponent.IsVisible = show;
|
||||
if (!show)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Справа от ячеек урны: у рабочего и старателя их две (череп и образец).
|
||||
int slots = urn.IsWorker || urn.IsProspector ? 2 : 1;
|
||||
ViewComponent.Position = new Vector2i(slots * 75 + 12, -49);
|
||||
if (_placed != null)
|
||||
{
|
||||
_placed.Text = string.Format(Localization.Get("necroCinerariumPlaced"), have);
|
||||
}
|
||||
if (_available != null)
|
||||
{
|
||||
_available.Text = string.Format(Localization.Get("necroCinerariumAvailable"), Mathf.Max(0, limit - have));
|
||||
}
|
||||
if (_next != null)
|
||||
{
|
||||
_next.Text = string.Format(Localization.Get("necroCinerariumNext"), next);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Меры нагрузки урн (решение 17.10: «общий бюджет шагов работы духов за кадр (очередь по кругу - нагрузка постоянна при
|
||||
/// любом числе урн); полная анимация только ближайших ~15-20 духов; замер 100 урн»).
|
||||
/// Бюджет: дел (пробный шаг + настоящий) в секунду - не больше StepsPerSecond на все урны сейва (23.10, было за кадр); урна, которой не досталось,
|
||||
/// пробует в следующем кадре. Урна делает дело раз в CinerariumWork.StepSeconds, поэтому очередь сама идёт по кругу.
|
||||
/// Анимация: двигаются (полёт, жесты, покачивание) только AnimateNearest ближайших к игроку духов, остальные замирают дома,
|
||||
/// как уснувшие (CinerariumGenieMotion); работа у всех идёт.
|
||||
/// Прилив (решение 17.10, «Прилив прячет некротехнологов»): ночью кровавой луны духи всех урн уходят в урны и работа стоит -
|
||||
/// признак берётся раз за кадр.
|
||||
/// </summary>
|
||||
public static class CinerariumLoad
|
||||
{
|
||||
/// <summary>Дел в секунду на все урны (23.10, было «3 за кадр»). Замер: урна получает тик раз в ~1.4 с, и тики всех урн
|
||||
/// приходят пачкой в одном кадре, в порядке участков от ближних к игроку. Лимит «3 за кадр» доставался трём ближним урнам,
|
||||
/// дальние не получали шага никогда («стою в 19 м - не работают, подхожу - работают»). Теперь бюджет копится между
|
||||
/// тиками (не больше StepsPerSecond за раз), и пачка обслуживает всех, у кого подошёл срок. Консоль: necrourn budget N.</summary>
|
||||
public static float StepsPerSecond = 30f;
|
||||
/// <summary>Запас бюджета, секунд (пачка тиков раз в ~1.4 с должна уместиться целиком).</summary>
|
||||
public const float BurstSeconds = 2f;
|
||||
static float _tokens;
|
||||
static float _lastRefill;
|
||||
/// <summary>Очередь (23.10, пользователь: «нужно иметь очередь урн»): урна, которой отказали, ждёт; пока очередь не пуста,
|
||||
/// бюджет сначала ей - новая урна получит шаг, только если после всех ждущих ещё останется. Иначе ближние урны (их участки
|
||||
/// идут первыми) снова забирали бы всё. Запись старше QueueForget секунд выбрасывается (урну сняли или участок выгружен).</summary>
|
||||
static readonly Dictionary<Vector3i, float> _queue = new Dictionary<Vector3i, float>();
|
||||
const float QueueForget = 10f;
|
||||
static readonly List<Vector3i> _stale = new List<Vector3i>();
|
||||
public static int AnimateNearest = 16;
|
||||
|
||||
static int _frame = -1;
|
||||
static bool _tide;
|
||||
|
||||
static void NewFrame()
|
||||
{
|
||||
if (Time.frameCount != _frame)
|
||||
{
|
||||
_frame = Time.frameCount;
|
||||
// По реальному времени с прошлого пополнения: NewFrame зовут только кадры с тиками урн (раз в ~1.4 с), и
|
||||
// Time.deltaTime (один кадр) давал полшага на пачку (замер 23.10: 279 шагов за 600 с при лимите 30/с).
|
||||
float now = Time.time;
|
||||
_tokens = Mathf.Min(StepsPerSecond * BurstSeconds, _tokens + StepsPerSecond * Mathf.Max(0f, now - _lastRefill));
|
||||
_lastRefill = now;
|
||||
_stale.Clear();
|
||||
foreach (KeyValuePair<Vector3i, float> kv in _queue)
|
||||
{
|
||||
if (now - kv.Value > QueueForget)
|
||||
{
|
||||
_stale.Add(kv.Key);
|
||||
}
|
||||
}
|
||||
foreach (Vector3i p in _stale)
|
||||
{
|
||||
_queue.Remove(p);
|
||||
}
|
||||
_tide = SnakeMode.IsBloodTide();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Взять одно дело из бюджета кадра. _who - урна (учёт для necrourn budget).</summary>
|
||||
public static bool TryTakeStep(Vector3i _who)
|
||||
{
|
||||
NewFrame();
|
||||
bool waiting = _queue.ContainsKey(_who);
|
||||
// Ждущая урна - если есть хоть один шаг; новая - только если хватит и всем ждущим.
|
||||
if (_tokens < 1f || (!waiting && _tokens < 1f + _queue.Count))
|
||||
{
|
||||
if (!waiting)
|
||||
{
|
||||
_queue[_who] = Time.time;
|
||||
}
|
||||
Count(_denied, _who);
|
||||
return false;
|
||||
}
|
||||
_tokens -= 1f;
|
||||
_queue.Remove(_who);
|
||||
Count(_granted, _who);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Учёт бюджета (23.10: сборщики 600 с не получили ни одного шага, логист рядом - получал): кому дали, кому отказали.
|
||||
static readonly Dictionary<Vector3i, int> _granted = new Dictionary<Vector3i, int>();
|
||||
static readonly Dictionary<Vector3i, int> _denied = new Dictionary<Vector3i, int>();
|
||||
static float _countSince;
|
||||
static int _frames0;
|
||||
|
||||
static void Count(Dictionary<Vector3i, int> _d, Vector3i _who)
|
||||
{
|
||||
_d.TryGetValue(_who, out int n);
|
||||
_d[_who] = n + 1;
|
||||
}
|
||||
|
||||
/// <summary>Сводка с прошлого вызова и сброс (команда necrourn budget).</summary>
|
||||
public static string Budget()
|
||||
{
|
||||
float sec = Time.time - _countSince;
|
||||
int frames = Time.frameCount - _frames0;
|
||||
string s = "за " + sec.ToString("0") + " с, кадров " + frames + ", лимит " + StepsPerSecond + "/с, в очереди " + _queue.Count + "; дано:";
|
||||
foreach (KeyValuePair<Vector3i, int> kv in _granted)
|
||||
{
|
||||
s += " " + kv.Key + "=" + kv.Value;
|
||||
}
|
||||
s += "; отказано:";
|
||||
foreach (KeyValuePair<Vector3i, int> kv in _denied)
|
||||
{
|
||||
s += " " + kv.Key + "=" + kv.Value;
|
||||
}
|
||||
_granted.Clear();
|
||||
_denied.Clear();
|
||||
_countSince = Time.time;
|
||||
_frames0 = Time.frameCount;
|
||||
return s;
|
||||
}
|
||||
|
||||
/// <summary>Идёт ли кровавый прилив (раз за кадр).</summary>
|
||||
public static bool Tide
|
||||
{
|
||||
get
|
||||
{
|
||||
NewFrame();
|
||||
return _tide;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Ближайшие духи.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
static float _nextRank;
|
||||
static readonly HashSet<CinerariumGenieMotion> _animated = new HashSet<CinerariumGenieMotion>();
|
||||
static readonly List<CinerariumGenieMotion> _sorted = new List<CinerariumGenieMotion>();
|
||||
|
||||
/// <summary>Двигается ли дух: среди AnimateNearest ближайших к игроку (пересчёт раз в 0.5 с, без выделений памяти).</summary>
|
||||
public static bool Animated(CinerariumGenieMotion _m)
|
||||
{
|
||||
if (Time.time >= _nextRank)
|
||||
{
|
||||
_nextRank = Time.time + 0.5f;
|
||||
Rank();
|
||||
}
|
||||
return _animated.Contains(_m);
|
||||
}
|
||||
|
||||
static void Rank()
|
||||
{
|
||||
_animated.Clear();
|
||||
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||||
if (pl == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Vector3 me = pl.GetPosition() - Origin.position;
|
||||
_sorted.Clear();
|
||||
_sorted.AddRange(CinerariumGenie.Live);
|
||||
if (_sorted.Count > AnimateNearest)
|
||||
{
|
||||
_sorted.Sort((a, b) => (a.transform.position - me).sqrMagnitude.CompareTo((b.transform.position - me).sqrMagnitude));
|
||||
}
|
||||
for (int i = 0; i < _sorted.Count && i < AnimateNearest; i++)
|
||||
{
|
||||
_animated.Add(_sorted[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>necrofps [секунд] - средний и худший кадр за время замера (по умолчанию 5 с), число духов и урн; ответ - строкой
|
||||
/// почтового ящика в лог. Для замера нагрузки урн (17.10: «замер 100 урн»).</summary>
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroFps : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrofps" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): средний и худший кадр за N секунд";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
float sec = _params.Count > 0 ? float.Parse(_params[0], System.Globalization.CultureInfo.InvariantCulture) : 5f;
|
||||
bool uncap = _params.Count > 1 && _params[1] == "uncap";
|
||||
SdtdConsole.Instance.Output("necrofps: замер " + sec + " с" + (uncap ? " без ограничения кадров" : ""));
|
||||
ThreadManager.StartCoroutine(Measure(sec, uncap));
|
||||
}
|
||||
|
||||
/// <summary>_uncap - на время замера снять вертикальную синхронизацию и предел кадров: при 60 fps по синхронизации
|
||||
/// запас не виден (18.10, замер 100 урн).</summary>
|
||||
static IEnumerator Measure(float _sec, bool _uncap)
|
||||
{
|
||||
int vs = QualitySettings.vSyncCount, fr = Application.targetFrameRate;
|
||||
if (_uncap)
|
||||
{
|
||||
QualitySettings.vSyncCount = 0;
|
||||
Application.targetFrameRate = -1;
|
||||
yield return new WaitForSecondsRealtime(0.5f);
|
||||
}
|
||||
float end = Time.realtimeSinceStartup + _sec;
|
||||
int frames = 0;
|
||||
float worst = 0f, sum = 0f;
|
||||
long gc0 = System.GC.CollectionCount(0);
|
||||
while (Time.realtimeSinceStartup < end)
|
||||
{
|
||||
yield return null;
|
||||
frames++;
|
||||
sum += Time.unscaledDeltaTime;
|
||||
worst = Mathf.Max(worst, Time.unscaledDeltaTime);
|
||||
}
|
||||
if (_uncap)
|
||||
{
|
||||
QualitySettings.vSyncCount = vs;
|
||||
Application.targetFrameRate = fr;
|
||||
}
|
||||
float avg = frames > 0 ? sum / frames * 1000f : 0f;
|
||||
Debug.Log("[NecromancerTome] mailbox: necrofps: кадров " + frames + ", средний " + avg.ToString("F1") + " мс (" + (avg > 0 ? 1000f / avg : 0f).ToString("F0") +
|
||||
" fps), худший " + (worst * 1000f).ToString("F1") + " мс, сборок мусора " + (System.GC.CollectionCount(0) - gc0) +
|
||||
", духов " + CinerariumGenie.Live.Count + ", урн в сейве " + CinerariumRegistry.All().Count);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>necrotide on|off|auto - прилив для проверок без ожидания ночи кровавой луны (SnakeMode.ForceTide).</summary>
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroTide : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrotide" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): прилив on|off|auto";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
string v = _params.Count > 0 ? _params[0] : "";
|
||||
SnakeMode.ForceTide = v == "on" ? true : v == "off" ? (bool?)false : null;
|
||||
SdtdConsole.Instance.Output("necrotide: " + (SnakeMode.ForceTide.HasValue ? (SnakeMode.ForceTide.Value ? "прилив" : "нет прилива") : "по часам") +
|
||||
", сейчас " + (SnakeMode.IsBloodTide() ? "прилив" : "не прилив"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.Collections.Generic;
|
||||
using GUI_2;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Урна логиста - латунное кадило (18.10.2026, BACKLOG «Урна логиста - своя модель: кадило»): два вида - на подставке по
|
||||
/// центру клетки (necroCinerariumLogist) и на кронштейне у стены (necroCinerariumLogistWall). Пользователь: «Должна быть
|
||||
/// возможность крепить её и к стене, и по центру какого-нибудь столба... как мы делали с черепом. Только череп у нас изначально
|
||||
/// не блок, а тут должно получиться попроще». Вид выбирается, как у черепа-декора: урна в руке, R - меню блока игры, к нему
|
||||
/// добавлен второй вид; выбор превращает стопку в ячейке пояса в другой блок (тот же счёт). Пустая урна и урна с черепом -
|
||||
/// каждая в свой вид того же рода.
|
||||
/// </summary>
|
||||
public static class CinerariumLogistForm
|
||||
{
|
||||
/// <summary>Номера наших пунктов в меню блока - вне занятых игрой (-2..10) и черепом (40..42).</summary>
|
||||
public const int MenuCenter = 43, MenuWall = 44;
|
||||
|
||||
static readonly Dictionary<string, (string center, string wall)> Pairs = new Dictionary<string, (string, string)>
|
||||
{
|
||||
{ TEFeatureCinerarium.LogistBlockName, (TEFeatureCinerarium.LogistBlockName, TEFeatureCinerarium.LogistWallBlockName) },
|
||||
{ TEFeatureCinerarium.LogistWallBlockName, (TEFeatureCinerarium.LogistBlockName, TEFeatureCinerarium.LogistWallBlockName) },
|
||||
{ TEFeatureCinerarium.LogistSkullBlockName, (TEFeatureCinerarium.LogistSkullBlockName, TEFeatureCinerarium.LogistWallSkullBlockName) },
|
||||
{ TEFeatureCinerarium.LogistWallSkullBlockName, (TEFeatureCinerarium.LogistSkullBlockName, TEFeatureCinerarium.LogistWallSkullBlockName) },
|
||||
};
|
||||
|
||||
public static bool IsLogistItem(ItemClass _item) => _item != null && Pairs.ContainsKey(_item.Name);
|
||||
|
||||
static void Convert(EntityPlayerLocal _pl, bool _wall)
|
||||
{
|
||||
ItemStack cur = _pl?.inventory?.holdingItemStack;
|
||||
if (cur == null || cur.IsEmpty() || !Pairs.TryGetValue(cur.itemValue.ItemClass.Name, out var pair))
|
||||
{
|
||||
return;
|
||||
}
|
||||
string name = _wall ? pair.wall : pair.center;
|
||||
if (cur.itemValue.ItemClass.Name == name)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ItemValue iv = ItemClass.GetItem(name);
|
||||
if (iv == null || iv.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
_pl.inventory.SetItem(_pl.inventory.holdingItemIdx, new ItemStack(iv, cur.count));
|
||||
}
|
||||
|
||||
public static void OnBlockMenu(XUiC_Radial _sender, int _commandIndex, XUiC_Radial.RadialContextAbs _context)
|
||||
{
|
||||
EntityPlayerLocal pl = _sender?.xui?.playerUI?.entityPlayer;
|
||||
switch (_commandIndex)
|
||||
{
|
||||
case MenuCenter: Convert(pl, false); return;
|
||||
case MenuWall: Convert(pl, true); return;
|
||||
}
|
||||
_sender.handleBlockShapeCommand(_sender, _commandIndex, _context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Меню блока с урной логиста в руке: к пунктам игры - второй вид (подставка / у стены).
|
||||
/// 23.10 (#77, «зажимаю R - только вращает»): у блока с одним простым поворотом игра в конце SetupBlockShapeData зовёт
|
||||
/// SetCommonData с одним пунктом - и сразу выполняет его (поворот) и закрывает меню; пункт, добавленный после (постфикс),
|
||||
/// попадал в уже закрытое меню. Поэтому наш пункт и наш обработчик ставятся в самом SetCommonData этого вызова - до
|
||||
/// подсчёта пунктов. Короткое нажатие R по-прежнему поворачивает (пункт «-1» уходит игре).</summary>
|
||||
[HarmonyPatch(typeof(XUiC_Radial), nameof(XUiC_Radial.SetupBlockShapeData))]
|
||||
public static class Patch_XUiC_Radial_SetupBlockShapeData_LogistForm
|
||||
{
|
||||
/// <summary>Идёт сборка меню блока с урной логиста в руке.</summary>
|
||||
public static bool Building;
|
||||
|
||||
public static void Prefix(XUiC_Radial __instance)
|
||||
{
|
||||
Building = CinerariumLogistForm.IsLogistItem(__instance.xui?.playerUI?.entityPlayer?.inventory?.holdingItem);
|
||||
}
|
||||
|
||||
public static void Postfix()
|
||||
{
|
||||
Building = false;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(XUiC_Radial), nameof(XUiC_Radial.SetCommonData))]
|
||||
public static class Patch_XUiC_Radial_SetCommonData_LogistForm
|
||||
{
|
||||
public static void Prefix(XUiC_Radial __instance, ref XUiC_Radial.CommandHandlerDelegate _commandHandlerFunc)
|
||||
{
|
||||
if (!Patch_XUiC_Radial_SetupBlockShapeData_LogistForm.Building)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Patch_XUiC_Radial_SetupBlockShapeData_LogistForm.Building = false;
|
||||
ItemClass held = __instance.xui.playerUI.entityPlayer.inventory.holdingItem;
|
||||
bool wall = held.Name == TEFeatureCinerarium.LogistWallBlockName || held.Name == TEFeatureCinerarium.LogistWallSkullBlockName;
|
||||
if (wall)
|
||||
{
|
||||
__instance.CreateRadialEntry(CinerariumLogistForm.MenuCenter, TEFeatureCinerarium.LogistBlockName, "ItemIconAtlas", "", Localization.Get("necroCinerariumLogistFormCenter"));
|
||||
}
|
||||
else
|
||||
{
|
||||
__instance.CreateRadialEntry(CinerariumLogistForm.MenuWall, TEFeatureCinerarium.LogistWallBlockName, "ItemIconAtlas", "", Localization.Get("necroCinerariumLogistFormWall"));
|
||||
}
|
||||
_commandHandlerFunc = CinerariumLogistForm.OnBlockMenu;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Мигание урны логиста, когда класть некуда (18.10: «При застревании должно мигать»): свечение модели
|
||||
/// (MaterialPropertyBlock, как у TEFeatureCinerarium.SyncGlow) пульсирует от тёмного до обычного. Выключено - обычное свечение.</summary>
|
||||
[Preserve]
|
||||
public class CinerariumBlink : MonoBehaviour
|
||||
{
|
||||
public List<Renderer> Renderers = new List<Renderer>();
|
||||
public static float Period = 0.8f;
|
||||
static readonly MaterialPropertyBlock _block = new MaterialPropertyBlock();
|
||||
static readonly int EmissionColorId = Shader.PropertyToID("_EmissionColor");
|
||||
|
||||
void Update()
|
||||
{
|
||||
// Резкий переход - мигание, а не дыхание: половину периода горит, половину почти погасла.
|
||||
float k = Mathf.Repeat(Time.time, Period) < Period * 0.5f ? 1f : 0.08f;
|
||||
Set(TEFeatureCinerarium.GlowColor * k);
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
Set(TEFeatureCinerarium.GlowColor);
|
||||
}
|
||||
|
||||
void Set(Color _c)
|
||||
{
|
||||
foreach (Renderer r in Renderers)
|
||||
{
|
||||
if (r == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
r.GetPropertyBlock(_block);
|
||||
_block.SetColor(EmissionColorId, _c);
|
||||
r.SetPropertyBlock(_block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,650 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Цинерарий-старатель (#64, решения 20.10 и 22.10.2026): дух добывает ископаемые. В «Содержимом» две ячейки, как у рабочего:
|
||||
/// череп и образец - что добывать (один из Ores). Нить к ящику (любая, как у лутера) - куда класть. Есть всё - дух отлетает на
|
||||
/// ближайшую свободную клетку рядом с урной и уходит на ProspectorDepth блоков вниз, растворяясь; раз в игровой час поднимается
|
||||
/// так же, кладёт добычу в ящик и уходит снова. Без зоны и вне лимита урн, как остальные некротехнологи.
|
||||
///
|
||||
/// Добыча (20.10: «цена - скоростью, а не ресурсом»): за игровые сутки камень, глина, песок, древесина - 5 стопок по 6000,
|
||||
/// железо, свинец, уголь, селитра, сланец - 3 стопки; ходка - сутки / 24, разброс ±20%. Находки (20.10: «шанс добыть алмаз,
|
||||
/// серебро и золото, как и в игре»; 22.10 - всегда, без книги «Удачный удар», латуни нет): ходка руды = столько блоков, сколько
|
||||
/// даёт её выход с блока в игре (железо 105, прочие 50), на каждый - шансы игры (алмаз 0.5%, серебро 1%, золото 0.7%).
|
||||
///
|
||||
/// Время - игровое (worldTime), чтобы добыча за сутки не зависела от их длины. Ходка засчитывается, только пока чанк урны
|
||||
/// загружен; время перескочило больше чем на час (сон, прилив, settime) - ходка не засчитана, отсчёт заново. Ящик полон или
|
||||
/// открыт игроком - дух ждёт дома с добычей (почернел, если полон) и докладывает, как появится место; новой ходки нет.
|
||||
/// Добыча в руках духа не сохраняется: урну подняли или чанк выгрузился - ходка пропала.
|
||||
/// </summary>
|
||||
public partial class TEFeatureCinerarium
|
||||
{
|
||||
public const string ProspectorBlockName = "necroCinerariumProspector";
|
||||
public const string ProspectorSkullBlockName = "necroCinerariumProspectorSkull";
|
||||
|
||||
public static bool IsProspectorBlock(Block _b)
|
||||
{
|
||||
string n = _b?.GetBlockName();
|
||||
return n == ProspectorBlockName || n == ProspectorSkullBlockName;
|
||||
}
|
||||
|
||||
bool _prospector;
|
||||
|
||||
public bool IsProspector
|
||||
{
|
||||
get
|
||||
{
|
||||
_ = IsLogist; // узнаёт тип блока
|
||||
return _prospector;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Что добывает старатель: предмет, добыча за игровые сутки, выход с блока руды в игре (0 - находок нет).</summary>
|
||||
public static readonly (string item, int perDay, int perBlock)[] Ores =
|
||||
{
|
||||
("resourceRockSmall", 30000, 0),
|
||||
("resourceClayLump", 30000, 0),
|
||||
("resourceCrushedSand", 30000, 0),
|
||||
("resourceWood", 30000, 0),
|
||||
("resourceScrapIron", 18000, 105),
|
||||
("resourceScrapLead", 18000, 50),
|
||||
("resourceCoal", 18000, 50),
|
||||
("resourcePotassiumNitratePowder", 18000, 50),
|
||||
("resourceOilShale", 18000, 50),
|
||||
};
|
||||
|
||||
/// <summary>Находки с блока руды - как в blocks.xml игры (тег artOfMiningLuckyStrikeHarvest).</summary>
|
||||
public static readonly (string item, float prob)[] Finds =
|
||||
{
|
||||
("resourceRawDiamond", 0.005f),
|
||||
("resourceSilverNugget", 0.01f),
|
||||
("resourceGoldNugget", 0.007f),
|
||||
};
|
||||
|
||||
/// <summary>Выключатель (консоль necroprospector off): духи остаются дома, ходок нет.</summary>
|
||||
public static bool ProspectorOn = true;
|
||||
/// <summary>Множитель добычи за ходку (консоль, для проверки баланса).</summary>
|
||||
public static float ProspectorYield = 1f;
|
||||
public static float ProspectorSpread = 0.2f;
|
||||
/// <summary>На сколько блоков дух уходит вниз.</summary>
|
||||
public static int ProspectorDepth = 5;
|
||||
/// <summary>Ходка - игровой час (1000 единиц worldTime).</summary>
|
||||
public const ulong TripTicks = 1000UL;
|
||||
/// <summary>necroprospector trip - номер «внеочередной ходки»: урна, ещё не видевшая его, ходит сразу.</summary>
|
||||
public static int ForceTrip;
|
||||
|
||||
public enum ProspectorStatus
|
||||
{
|
||||
NoSkull,
|
||||
Tide,
|
||||
Off,
|
||||
NoSample,
|
||||
NoChest,
|
||||
Full,
|
||||
Digging
|
||||
}
|
||||
|
||||
bool _under;
|
||||
ulong _tripAt;
|
||||
float _nextProspect;
|
||||
float _deliverAt;
|
||||
int _forceSeen;
|
||||
readonly List<ItemStack> _carry = new List<ItemStack>();
|
||||
|
||||
public static int OreIndex(ItemValue _iv)
|
||||
{
|
||||
string n = _iv?.ItemClass?.GetItemName();
|
||||
if (n == null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
for (int i = 0; i < Ores.Length; i++)
|
||||
{
|
||||
if (Ores[i].item == n)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static bool IsOreSample(ItemStack _s) => _s != null && !_s.IsEmpty() && OreIndex(_s.itemValue) >= 0;
|
||||
|
||||
/// <summary>Сколько игровых минут до конца ходки (для консоли), -1 - дух не под землёй.</summary>
|
||||
public int TripMinutesLeft(World _w)
|
||||
{
|
||||
if (!_under || _w == null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
return _tripAt > _w.worldTime ? (int)((_tripAt - _w.worldTime) * 60UL / TripTicks) : 0;
|
||||
}
|
||||
|
||||
public int CarryCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int n = 0;
|
||||
foreach (ItemStack s in _carry)
|
||||
{
|
||||
n += s.count;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
public ProspectorStatus ProspectorNow(World _world)
|
||||
{
|
||||
if (!HasSkull)
|
||||
{
|
||||
return ProspectorStatus.NoSkull;
|
||||
}
|
||||
if (CinerariumLoad.Tide)
|
||||
{
|
||||
return ProspectorStatus.Tide;
|
||||
}
|
||||
if (!ProspectorOn)
|
||||
{
|
||||
return ProspectorStatus.Off;
|
||||
}
|
||||
if (!IsOreSample(Sample))
|
||||
{
|
||||
return ProspectorStatus.NoSample;
|
||||
}
|
||||
if (!HasThreads)
|
||||
{
|
||||
return ProspectorStatus.NoChest;
|
||||
}
|
||||
return Stuck ? ProspectorStatus.Full : ProspectorStatus.Digging;
|
||||
}
|
||||
|
||||
string ProspectorStateText(World _world)
|
||||
{
|
||||
string key = ProspectorNow(_world) switch
|
||||
{
|
||||
ProspectorStatus.Tide => "necroZoneUrnTide",
|
||||
ProspectorStatus.NoSample => "necroProspectorNoSample",
|
||||
ProspectorStatus.NoChest => "necroProspectorNoChest",
|
||||
ProspectorStatus.Full => "necroZoneUrnFull",
|
||||
ProspectorStatus.Digging => "necroProspectorDigging",
|
||||
_ => null
|
||||
};
|
||||
return key != null ? ", " + Localization.Get(key) : "";
|
||||
}
|
||||
|
||||
/// <summary>Работа старателя (сервер, раз в секунду): донести добычу, уйти под землю, ходка по часам игры.</summary>
|
||||
void ProspectorWork(World _world)
|
||||
{
|
||||
if (Time.time < _nextProspect)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_nextProspect = Time.time + 1f;
|
||||
CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent<CinerariumGenieMotion>() : null;
|
||||
ulong now = _world.worldTime;
|
||||
if (_carry.Count > 0)
|
||||
{
|
||||
if (Time.time < _deliverAt)
|
||||
{
|
||||
return; // дух ещё поднимается
|
||||
}
|
||||
Deliver(_world);
|
||||
bool full = _carry.Count > 0;
|
||||
if (full != Stuck)
|
||||
{
|
||||
Stuck = full;
|
||||
Debug.Log("[NecromancerTome] Cinerarium: старатель " + ToWorldPos() + (full ? " - ящик полон, дух ждёт с добычей" : " - добыча в ящике"));
|
||||
}
|
||||
motion?.SetStuck(full);
|
||||
if (full)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
ProspectorStatus st = ProspectorNow(_world);
|
||||
if (st != ProspectorStatus.Digging)
|
||||
{
|
||||
if (_under)
|
||||
{
|
||||
_under = false;
|
||||
motion?.Rise();
|
||||
Debug.Log("[NecromancerTome] Cinerarium: дух старателя " + ToWorldPos() + " вернулся без добычи - " + st);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!_under)
|
||||
{
|
||||
_under = true;
|
||||
if (_tripAt <= now)
|
||||
{
|
||||
_tripAt = now + TripTicks;
|
||||
}
|
||||
_forceSeen = ForceTrip;
|
||||
motion?.Dive(DigStand(_world) - Origin.position, ProspectorDepth);
|
||||
Debug.Log("[NecromancerTome] Cinerarium: дух старателя " + ToWorldPos() + " ушёл под землю за " + Sample.itemValue.ItemClass?.GetItemName());
|
||||
return;
|
||||
}
|
||||
if (motion != null && !motion.Digging)
|
||||
{
|
||||
motion.Bury(DigStand(_world) - Origin.position, ProspectorDepth); // дух появился заново (чанк, прилив) - сразу под землёй
|
||||
}
|
||||
bool forced = _forceSeen != ForceTrip;
|
||||
if (now < _tripAt && !forced)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_forceSeen = ForceTrip;
|
||||
if (!forced && now > _tripAt + TripTicks)
|
||||
{
|
||||
_tripAt = now + TripTicks; // время перескочило - ходка не засчитана
|
||||
Debug.Log("[NecromancerTome] Cinerarium: у старателя " + ToWorldPos() + " время перескочило - ходка заново");
|
||||
return;
|
||||
}
|
||||
DigTrip(OreIndex(Sample.itemValue));
|
||||
_tripAt = forced ? now + TripTicks : _tripAt + TripTicks;
|
||||
_under = false; // поднимается; донесёт - уйдёт снова
|
||||
_deliverAt = Time.time + (motion != null ? motion.Rise() : 0f);
|
||||
}
|
||||
|
||||
/// <summary>Добыча одной ходки - в руки духа (_carry), стопками не больше игровых.</summary>
|
||||
void DigTrip(int _ore)
|
||||
{
|
||||
if (_ore < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
(string item, int perDay, int perBlock) o = Ores[_ore];
|
||||
float mean = o.perDay / 24f * ProspectorYield;
|
||||
int n = Mathf.Max(1, Mathf.RoundToInt(mean * Random.Range(1f - ProspectorSpread, 1f + ProspectorSpread)));
|
||||
Carry(o.item, n);
|
||||
string log = o.item + " x" + n;
|
||||
if (o.perBlock > 0)
|
||||
{
|
||||
int blocks = Mathf.Max(1, Mathf.RoundToInt(n / (float)o.perBlock));
|
||||
foreach ((string item, float prob) f in Finds)
|
||||
{
|
||||
int k = 0;
|
||||
for (int b = 0; b < blocks; b++)
|
||||
{
|
||||
if (Random.value < f.prob)
|
||||
{
|
||||
k++;
|
||||
}
|
||||
}
|
||||
if (k > 0)
|
||||
{
|
||||
Carry(f.item, k);
|
||||
log += ", " + f.item + " x" + k;
|
||||
}
|
||||
}
|
||||
log += " (блоков руды " + blocks + ")";
|
||||
}
|
||||
Debug.Log("[NecromancerTome] Cinerarium: старатель " + ToWorldPos() + " - ходка: " + log);
|
||||
}
|
||||
|
||||
void Carry(string _item, int _n)
|
||||
{
|
||||
ItemValue iv = ItemClass.GetItem(_item);
|
||||
if (iv == null || iv.type <= 0)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] Cinerarium: старатель - нет предмета " + _item);
|
||||
return;
|
||||
}
|
||||
int max = Mathf.Max(1, iv.ItemClass.Stacknumber.Value);
|
||||
while (_n > 0)
|
||||
{
|
||||
int k = Mathf.Min(_n, max);
|
||||
_carry.Add(new ItemStack(iv.Clone(), k));
|
||||
_n -= k;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Добычу - в ящики по нитям; что не влезло, остаётся у духа.</summary>
|
||||
void Deliver(World _world)
|
||||
{
|
||||
List<TEFeatureStorage> boxes = new List<TEFeatureStorage>();
|
||||
void Add(Vector3i _p)
|
||||
{
|
||||
TEFeatureStorage s = CinerariumWork.BoxAt(_world, _p);
|
||||
if (s != null && !boxes.Contains(s))
|
||||
{
|
||||
boxes.Add(s);
|
||||
}
|
||||
}
|
||||
if (HasThreadTo)
|
||||
{
|
||||
Add(ThreadTo);
|
||||
}
|
||||
foreach (Vector3i p in ThreadOut)
|
||||
{
|
||||
Add(p);
|
||||
}
|
||||
foreach (Vector3i p in ThreadFrom)
|
||||
{
|
||||
Add(p);
|
||||
}
|
||||
if (boxes.Count == 0)
|
||||
{
|
||||
return; // ящик открыт игроком или нитей нет - подождать
|
||||
}
|
||||
for (int i = _carry.Count - 1; i >= 0; i--)
|
||||
{
|
||||
ItemStack s = _carry[i];
|
||||
CinerariumWork.Place(s, boxes);
|
||||
if (s.count <= 0)
|
||||
{
|
||||
_carry.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Куда дух спускается (20.10: «отлетает от урны на ближайший свободный блок, опускается на 5 блоков вниз»): из
|
||||
/// восьми соседних клеток - первая свободная (воздух или растение), сначала по сторонам; нет - над урной. Низ клетки, мир.</summary>
|
||||
Vector3 DigStand(World _world)
|
||||
{
|
||||
Vector3i urn = ToWorldPos();
|
||||
Vector3i[] dirs =
|
||||
{
|
||||
Vector3i.forward, Vector3i.right, Vector3i.back, Vector3i.left,
|
||||
new Vector3i(1, 0, 1), new Vector3i(1, 0, -1), new Vector3i(-1, 0, -1), new Vector3i(-1, 0, 1)
|
||||
};
|
||||
foreach (Vector3i d in dirs)
|
||||
{
|
||||
BlockValue bv = _world.GetBlock(urn + d);
|
||||
if (bv.isair || bv.Block is BlockPlant)
|
||||
{
|
||||
return World.blockToTransformPos(urn + d);
|
||||
}
|
||||
}
|
||||
return World.blockToTransformPos(urn + Vector3i.up);
|
||||
}
|
||||
|
||||
/// <summary>Урну убрали или выгрузили - добыча в руках духа пропадает.</summary>
|
||||
void ProspectorReset()
|
||||
{
|
||||
if (_carry.Count > 0)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Cinerarium: старатель " + ToWorldPos() + " - недонесённая добыча пропала (" + CarryCount + ")");
|
||||
}
|
||||
_carry.Clear();
|
||||
_under = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Спуск духа старателя под землю и подъём (20.10: «опускается на 5 блоков вниз, постепенно становясь полностью
|
||||
/// прозрачным; обратно - так же»). Яркость - MaterialPropertyBlock на рендерерах духа (материалы общие не трогаются): дух
|
||||
/// светится сложением, поэтому «прозрачнее» = тусклее цвет и свечение; глаза гаснут на половине.</summary>
|
||||
public partial class CinerariumGenieMotion
|
||||
{
|
||||
/// <summary>0 - нет; 1 - летит к месту и спускается; 2 - под землёй (не виден); 3 - поднимается и летит домой.</summary>
|
||||
int _digMode;
|
||||
float _digT;
|
||||
Vector3 _digFrom, _digHover;
|
||||
float _digDepth;
|
||||
public static float SinkSeconds = 2.5f;
|
||||
List<Renderer> _fadeRenderers;
|
||||
static readonly MaterialPropertyBlock _fadeBlock = new MaterialPropertyBlock();
|
||||
static readonly int ColorId = Shader.PropertyToID("_Color");
|
||||
static readonly int EmissionId = Shader.PropertyToID("_EmissionColor");
|
||||
float _fade = 1f;
|
||||
|
||||
/// <summary>Под землёй или на пути туда/обратно.</summary>
|
||||
public bool Digging => _digMode != 0;
|
||||
|
||||
bool DigHidden => _digMode == 2;
|
||||
|
||||
/// <summary>Лететь к клетке _stand (низ клетки, координаты сцены) и уйти на _depth блоков вниз.</summary>
|
||||
public void Dive(Vector3 _stand, int _depth)
|
||||
{
|
||||
if (transform.parent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_t = -1f;
|
||||
Gesture(0f);
|
||||
_digFrom = transform.localPosition;
|
||||
_digHover = DigHover(_stand);
|
||||
_digDepth = _depth;
|
||||
_digMode = 1;
|
||||
_digT = 0f;
|
||||
}
|
||||
|
||||
/// <summary>Сразу под землёй (дух появился заново, а работа идёт).</summary>
|
||||
public void Bury(Vector3 _stand, int _depth)
|
||||
{
|
||||
if (transform.parent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_digHover = DigHover(_stand);
|
||||
_digDepth = _depth;
|
||||
_digMode = 2;
|
||||
transform.localPosition = _digHover + Vector3.down * _digDepth;
|
||||
}
|
||||
|
||||
/// <summary>Подняться и вернуться к урне; через сколько секунд будет дома.</summary>
|
||||
public float Rise()
|
||||
{
|
||||
if (_digMode == 0)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
_digMode = 3;
|
||||
_digT = 0f;
|
||||
return SinkSeconds + Mathf.Max(BackSeconds, Vector3.Distance(_digHover, Home()) / FlySpeed);
|
||||
}
|
||||
|
||||
Vector3 DigHover(Vector3 _stand)
|
||||
{
|
||||
return transform.parent.InverseTransformPoint(_stand) + new Vector3(0f, CinerariumGenie.OffsetY + Drop + 0.15f, 0f);
|
||||
}
|
||||
|
||||
/// <summary>Шаг спуска/подъёма; false - дух не копает, обычное поведение.</summary>
|
||||
bool DigUpdate(bool _asleep)
|
||||
{
|
||||
if (_digMode == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (_asleep)
|
||||
{
|
||||
// Уснул - без полёта: спуск сразу закончен, подъём - сразу дома.
|
||||
if (_digMode == 1)
|
||||
{
|
||||
_digMode = 2;
|
||||
}
|
||||
else if (_digMode == 3)
|
||||
{
|
||||
_digMode = 0;
|
||||
SetFade(1f);
|
||||
transform.localPosition = Home();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
_digT += Time.deltaTime;
|
||||
Vector3 bottom = _digHover + Vector3.down * _digDepth;
|
||||
float fly = Mathf.Max(OutSeconds, Vector3.Distance(_digFrom, _digHover) / FlySpeed);
|
||||
if (_digMode == 1)
|
||||
{
|
||||
if (_digT < fly)
|
||||
{
|
||||
transform.localPosition = Vector3.Lerp(_digFrom, _digHover, Mathf.SmoothStep(0f, 1f, _digT / fly));
|
||||
Face(_digHover - _digFrom);
|
||||
SetFade(1f);
|
||||
}
|
||||
else if (_digT < fly + SinkSeconds)
|
||||
{
|
||||
float s = (_digT - fly) / SinkSeconds;
|
||||
transform.localPosition = Vector3.Lerp(_digHover, bottom, Mathf.SmoothStep(0f, 1f, s));
|
||||
SetFade(1f - s);
|
||||
}
|
||||
else
|
||||
{
|
||||
_digMode = 2;
|
||||
transform.localPosition = bottom;
|
||||
}
|
||||
}
|
||||
else if (_digMode == 2)
|
||||
{
|
||||
transform.localPosition = bottom;
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3 home = Home();
|
||||
float back = Mathf.Max(BackSeconds, Vector3.Distance(_digHover, home) / FlySpeed);
|
||||
if (_digT < SinkSeconds)
|
||||
{
|
||||
float s = _digT / SinkSeconds;
|
||||
transform.localPosition = Vector3.Lerp(bottom, _digHover, Mathf.SmoothStep(0f, 1f, s));
|
||||
SetFade(s);
|
||||
}
|
||||
else if (_digT < SinkSeconds + back)
|
||||
{
|
||||
SetFade(1f);
|
||||
transform.localPosition = Vector3.Lerp(_digHover, home, Mathf.SmoothStep(0f, 1f, (_digT - SinkSeconds) / back));
|
||||
Face(home - _digHover);
|
||||
}
|
||||
else
|
||||
{
|
||||
_digMode = 0;
|
||||
SetFade(1f);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Яркость духа долей _k (1 - как обычно, свойства сняты).</summary>
|
||||
void SetFade(float _k)
|
||||
{
|
||||
_k = Mathf.Clamp01(_k);
|
||||
if (Mathf.Abs(_k - _fade) < 0.01f && (_k < 1f || _fade >= 1f))
|
||||
{
|
||||
return;
|
||||
}
|
||||
_fade = _k;
|
||||
if (_fadeRenderers == null)
|
||||
{
|
||||
_fadeRenderers = new List<Renderer>(GetComponentsInChildren<Renderer>(true));
|
||||
}
|
||||
foreach (Renderer r in _fadeRenderers)
|
||||
{
|
||||
if (r == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (r.name.Contains("eye"))
|
||||
{
|
||||
r.forceRenderingOff = _k < 0.5f;
|
||||
continue;
|
||||
}
|
||||
if (_k >= 1f)
|
||||
{
|
||||
r.SetPropertyBlock(null);
|
||||
continue;
|
||||
}
|
||||
Material m = r.sharedMaterial;
|
||||
if (m == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
r.GetPropertyBlock(_fadeBlock);
|
||||
if (m.HasProperty(ColorId))
|
||||
{
|
||||
_fadeBlock.SetColor(ColorId, m.GetColor(ColorId) * _k);
|
||||
}
|
||||
if (m.HasProperty(EmissionId))
|
||||
{
|
||||
_fadeBlock.SetColor(EmissionId, m.GetColor(EmissionId) * _k);
|
||||
}
|
||||
r.SetPropertyBlock(_fadeBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Консоль старателя: выключатель, внеочередная ходка, множитель добычи, состояние урн.</summary>
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroProspector : ConsoleCmdAbstract
|
||||
{
|
||||
public override bool IsExecuteOnClient => true;
|
||||
|
||||
public override bool AllowedInMainMenu => false;
|
||||
|
||||
public override string[] getCommands()
|
||||
{
|
||||
return new string[] { "necroprospector" };
|
||||
}
|
||||
|
||||
public override string getDescription()
|
||||
{
|
||||
return "Necromancer's Tome: Prospector Cinerarium - switch, extra trip, yield multiplier, status.";
|
||||
}
|
||||
|
||||
public override string getHelp()
|
||||
{
|
||||
return "necroprospector - status of every loaded prospector urn\n" +
|
||||
"necroprospector on|off - off: spirits stay home, no trips\n" +
|
||||
"necroprospector trip - every digging spirit comes back with a trip right now\n" +
|
||||
"necroprospector yield <x> - trip size multiplier (1 = as designed)\n" +
|
||||
"necroprospector spread <x> - random spread of a trip (0.2 = ±20%)\n" +
|
||||
"necroprospector depth <n> - how many blocks the spirit sinks";
|
||||
}
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
SdtdConsole con = SingletonMonoBehaviour<SdtdConsole>.Instance;
|
||||
World w = GameManager.Instance?.World;
|
||||
if (_params.Count >= 1)
|
||||
{
|
||||
string cmd = _params[0].ToLowerInvariant();
|
||||
float v = 0f;
|
||||
bool num = _params.Count >= 2 && float.TryParse(_params[1], NumberStyles.Float, CultureInfo.InvariantCulture, out v);
|
||||
switch (cmd)
|
||||
{
|
||||
case "on":
|
||||
case "off":
|
||||
TEFeatureCinerarium.ProspectorOn = cmd == "on";
|
||||
break;
|
||||
case "trip":
|
||||
TEFeatureCinerarium.ForceTrip++;
|
||||
break;
|
||||
case "yield" when num:
|
||||
TEFeatureCinerarium.ProspectorYield = Mathf.Max(0f, v);
|
||||
break;
|
||||
case "spread" when num:
|
||||
TEFeatureCinerarium.ProspectorSpread = Mathf.Clamp01(v);
|
||||
break;
|
||||
case "depth" when num:
|
||||
TEFeatureCinerarium.ProspectorDepth = Mathf.Clamp((int)v, 1, 30);
|
||||
break;
|
||||
default:
|
||||
con.Output("necroprospector: unknown '" + _params[0] + "' - see 'help necroprospector'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
con.Output("necroprospector: " + (TEFeatureCinerarium.ProspectorOn ? "on" : "OFF") + ", yield x" + TEFeatureCinerarium.ProspectorYield.ToString("F2", CultureInfo.InvariantCulture) +
|
||||
", spread " + TEFeatureCinerarium.ProspectorSpread.ToString("F2", CultureInfo.InvariantCulture) + ", depth " + TEFeatureCinerarium.ProspectorDepth);
|
||||
if (w == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (Vector3i p in CinerariumRegistry.All())
|
||||
{
|
||||
TEFeatureCinerarium u = CinerariumThreads.UrnAt(w, p);
|
||||
if (u == null || !u.IsProspector)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int left = u.TripMinutesLeft(w);
|
||||
con.Output(" " + p + ": " + u.ProspectorNow(w) + ", sample " + (u.Sample == null || u.Sample.IsEmpty() ? "-" : u.Sample.itemValue.ItemClass?.GetItemName()) +
|
||||
(left >= 0 ? ", trip in " + left + " game min" : "") + (u.CarryCount > 0 ? ", carrying " + u.CarryCount : ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,23 @@ namespace NecromancerTome
|
||||
/// (ItemValue.Clone со сменой типа). Как у стриппера: ПКМ - начать нить и закончить её (порядок = направление: ящик, потом
|
||||
/// урна - «откуда»; урна, потом ящик - «куда»), ЛКМ по урне или ящику - снять его нити.
|
||||
/// Первая версия - одиночная игра и хост (связи меняются на месте, клиентам сети - позже).
|
||||
///
|
||||
/// Урна рабочего (18.10): нити ящик -> урна - откуда брать ингредиенты и топливо, урна -> станция - где изготавливать,
|
||||
/// урна -> ящик - куда класть готовое; станций и ящиков сколько угодно (до MaxFrom в каждую сторону). Нить между станцией
|
||||
/// и урной рабочего - всегда «от урны», в каком бы порядке её ни тянули.
|
||||
///
|
||||
/// Череп-ретранслятор (18.10): череп-декор (блок necroSkullDecorCenter/Back) в середине нити. Начал нить у ящика или урны,
|
||||
/// ПКМ по черепу - нить проходит через него, дальше - следующий череп или конец; каждый пролёт - до MaxLength. Черепа
|
||||
/// хранятся у нити в урне (TEFeatureCinerarium.FromVia/ToVia/OutVia), ЛКМ по черепу - снять все нити через него.
|
||||
/// </summary>
|
||||
public static class CinerariumThreads
|
||||
{
|
||||
public const string KnifeName = Patch_ItemClass_ExecuteAction_KnifePowerCost.KnifeName;
|
||||
public const string ThreadKnifeName = "necroWpnBladeNecroKnifeThread";
|
||||
/// <summary>Резная кость (#86, 28.10.2026): отдельный инструмент нитей - делает то же, что нож хватом для нитей, хват
|
||||
/// всегда для нитей, меню хватов по R у неё нет. Для тех, кто не нашёл хват у ножа (пользователь: «чтобы игроки, которые
|
||||
/// не обнаружили нитяной хват у ножа, легко могли найти отдельный инструмент управления цинерариями»).</summary>
|
||||
public const string BoneName = "necroCarvedBone";
|
||||
/// <summary>Как у провода электрики: ItemActionConnectPower.maxWireLength = 15.</summary>
|
||||
public const float MaxLength = 15f;
|
||||
public const int MaxFrom = 8;
|
||||
@@ -35,7 +47,17 @@ namespace NecromancerTome
|
||||
public static readonly Color SparkColor = new Color(0.8f, 0.95f, 1f, 1f);
|
||||
|
||||
public static bool IsKnife(ItemClass _i) => _i != null && (_i.Name == KnifeName || _i.Name == ThreadKnifeName);
|
||||
|
||||
/// <summary>Череп-ретранслятор - череп-декор в мире (оба вида и старый блок-помощник).</summary>
|
||||
public static bool IsRelay(Block _b)
|
||||
{
|
||||
string n = _b?.GetBlockName();
|
||||
return n == SkullDecorPlace.CenterBlock || n == SkullDecorPlace.BackBlock || n == "necroSkullDecor";
|
||||
}
|
||||
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;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Хват.
|
||||
@@ -66,14 +88,21 @@ namespace NecromancerTome
|
||||
|
||||
static Vector3i? _start;
|
||||
static bool _startUrn;
|
||||
static bool _startStation;
|
||||
static GameObject _preview;
|
||||
/// <summary>Черепа, через которые уже прошла тянущаяся нить, по порядку от начала.</summary>
|
||||
static readonly List<Vector3i> _via = new List<Vector3i>();
|
||||
|
||||
public static Vector3i? Start => _start;
|
||||
public static bool StartIsUrn => _startUrn;
|
||||
/// <summary>Откуда сейчас тянется нить: последний череп или начало.</summary>
|
||||
public static Vector3i? LastPoint => _via.Count > 0 ? _via[_via.Count - 1] : _start;
|
||||
public static bool LastIsRelay => _via.Count > 0;
|
||||
|
||||
public static void Cancel()
|
||||
{
|
||||
_start = null;
|
||||
_via.Clear();
|
||||
if (_preview != null)
|
||||
{
|
||||
Object.Destroy(_preview);
|
||||
@@ -98,6 +127,16 @@ namespace NecromancerTome
|
||||
return comp.PlayerPlaced && comp.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage _);
|
||||
}
|
||||
|
||||
/// <summary>Станция игры (верстак, костёр, кузня, химстанция, бетономешалка, ступка) в клетке _p; _owner - главная клетка.</summary>
|
||||
public static TileEntityWorkstation StationAt(World _w, Vector3i _p, out Vector3i _owner)
|
||||
{
|
||||
BlockValue bv = _w.GetBlock(_p);
|
||||
_owner = bv.ischild ? _p + bv.parent : _p;
|
||||
return _w.GetTileEntity(_owner) as TileEntityWorkstation;
|
||||
}
|
||||
|
||||
public static TileEntityWorkstation StationAt(World _w, Vector3i _p) => StationAt(_w, _p, out Vector3i _);
|
||||
|
||||
public static TEFeatureCinerarium UrnAt(World _w, Vector3i _p)
|
||||
{
|
||||
return _w.GetTileEntity(_p) is TileEntityComposite c && c.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium u) ? u : null;
|
||||
@@ -112,7 +151,24 @@ namespace NecromancerTome
|
||||
{
|
||||
return;
|
||||
}
|
||||
bool ok = Resolve(w, hi.hit.blockPos, out Vector3i owner, out TEFeatureCinerarium urn);
|
||||
ClickAt(_pl, w, hi.hit.blockPos, _connect);
|
||||
}
|
||||
|
||||
/// <summary>Щелчок ножом для нитей по клетке _pos (луч уже разобран; команда necroclick - напрямую).</summary>
|
||||
public static void ClickAt(EntityPlayerLocal _pl, World w, Vector3i _pos, bool _connect)
|
||||
{
|
||||
if (IsRelay(w.GetBlock(_pos).Block))
|
||||
{
|
||||
return; // 23.10: черепа-ретрансляторы отменены - череп-декор нитью не задевается
|
||||
}
|
||||
bool ok = Resolve(w, _pos, out Vector3i owner, out TEFeatureCinerarium urn);
|
||||
bool station = false;
|
||||
if (!ok && StationAt(w, _pos, out Vector3i stOwner) != null)
|
||||
{
|
||||
ok = true;
|
||||
station = true;
|
||||
owner = stOwner;
|
||||
}
|
||||
if (!_connect)
|
||||
{
|
||||
if (_start.HasValue)
|
||||
@@ -135,13 +191,14 @@ namespace NecromancerTome
|
||||
}
|
||||
if (!_start.HasValue)
|
||||
{
|
||||
if (urn != null && !urn.IsLogist)
|
||||
if (urn != null && !urn.IsZoneless)
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedLogist"));
|
||||
return;
|
||||
}
|
||||
_start = owner;
|
||||
_startUrn = urn != null;
|
||||
_startStation = station;
|
||||
_preview = new GameObject("NecroThreadPreview");
|
||||
_preview.AddComponent<CinerariumThreadPreview>();
|
||||
_pl.PlayOneShot("wiretool_grab");
|
||||
@@ -150,24 +207,56 @@ namespace NecromancerTome
|
||||
}
|
||||
Vector3i s = _start.Value;
|
||||
bool sUrn = _startUrn;
|
||||
bool sStation = _startStation;
|
||||
Vector3i last = LastPoint.Value;
|
||||
// Черепа - по порядку от урны: начали у урны - как шли, начали у ящика или станции - в обратном порядке.
|
||||
List<Vector3i> via = new List<Vector3i>(_via);
|
||||
Cancel();
|
||||
if (s == owner)
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadCancel"));
|
||||
return;
|
||||
}
|
||||
if (!sUrn)
|
||||
{
|
||||
via.Reverse();
|
||||
}
|
||||
// Станция - только с урной рабочего, нить всегда «от урны».
|
||||
if (sStation || station)
|
||||
{
|
||||
TEFeatureCinerarium wu = sStation ? urn : UrnAt(w, s);
|
||||
Vector3i st = sStation ? s : owner;
|
||||
if (wu == null || !wu.IsWorker)
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedWorker"));
|
||||
return;
|
||||
}
|
||||
if (Vector3.Distance(last.ToVector3(), owner.ToVector3()) > MaxLength)
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTooFar"));
|
||||
return;
|
||||
}
|
||||
if (!wu.AddThreadOut(st, via))
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadLimit"));
|
||||
return;
|
||||
}
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadStation"));
|
||||
_pl.PlayOneShot("wiretool_place");
|
||||
return;
|
||||
}
|
||||
if (sUrn == (urn != null))
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedPair"));
|
||||
return;
|
||||
}
|
||||
TEFeatureCinerarium end = sUrn ? UrnAt(w, s) : urn;
|
||||
if (end == null || !end.IsLogist)
|
||||
if (end == null || !end.IsZoneless)
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedLogist"));
|
||||
return;
|
||||
}
|
||||
if (Vector3.Distance(s.ToVector3(), owner.ToVector3()) > MaxLength)
|
||||
if (Vector3.Distance(last.ToVector3(), owner.ToVector3()) > MaxLength)
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTooFar"));
|
||||
return;
|
||||
@@ -179,12 +268,25 @@ namespace NecromancerTome
|
||||
{
|
||||
return;
|
||||
}
|
||||
u.SetThreadTo(owner);
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTo"));
|
||||
if (u.IsWorker)
|
||||
{
|
||||
// У рабочего ящиков «куда» сколько угодно - туда готовое.
|
||||
if (!u.AddThreadOut(owner, via))
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadLimit"));
|
||||
return;
|
||||
}
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadOut"));
|
||||
}
|
||||
else
|
||||
{
|
||||
u.SetThreadTo(owner, via);
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTo"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!urn.AddThreadFrom(s))
|
||||
if (!urn.AddThreadFrom(s, via))
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadLimit"));
|
||||
return;
|
||||
@@ -194,6 +296,29 @@ namespace NecromancerTome
|
||||
_pl.PlayOneShot("wiretool_place");
|
||||
}
|
||||
|
||||
/// <summary>Нить урны _urn к _end через черепа _via (по порядку от урны) - пролётами в _pairs, огоньки бегут по направлению
|
||||
/// нити: _toUrn - от конца к урне (нить «откуда»), иначе от урны к концу.</summary>
|
||||
public static void AddPath(List<(Vector3 a, Vector3 b)> _pairs, Vector3i _urn, List<Vector3i> _via, Vector3i _end, bool _toUrn)
|
||||
{
|
||||
List<Vector3> pts = new List<Vector3> { UrnPoint(_urn) };
|
||||
if (_via != null)
|
||||
{
|
||||
foreach (Vector3i v in _via)
|
||||
{
|
||||
pts.Add(RelayPoint(v));
|
||||
}
|
||||
}
|
||||
Vector3i before = _via != null && _via.Count > 0 ? _via[_via.Count - 1] : _urn;
|
||||
pts.Add(BoxPoint(_end, before));
|
||||
for (int i = 0; i + 1 < pts.Count; i++)
|
||||
{
|
||||
_pairs.Add(_toUrn ? (pts[i + 1], pts[i]) : (pts[i], pts[i + 1]));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Точка нити у черепа - над черепом-декором (он лежит на дне клетки). Сцена (без Origin).</summary>
|
||||
public static Vector3 RelayPoint(Vector3i _skull) => World.blockToTransformPos(_skull) - Origin.position + Vector3.up * 0.3f;
|
||||
|
||||
/// <summary>Снять все нити урны или все нити, идущие к ящику (у всех урн сейва). Сколько снято.</summary>
|
||||
static int RemoveAll(World _w, Vector3i _owner, TEFeatureCinerarium _urn)
|
||||
{
|
||||
@@ -321,7 +446,7 @@ namespace NecromancerTome
|
||||
public static bool ToolInHand()
|
||||
{
|
||||
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||||
return pl != null && CinerariumThreads.IsThreadKnife(pl.inventory?.holdingItem);
|
||||
return pl != null && CinerariumThreads.IsThreadTool(pl.inventory?.holdingItem);
|
||||
}
|
||||
|
||||
void Update()
|
||||
@@ -374,7 +499,7 @@ namespace NecromancerTome
|
||||
void Update()
|
||||
{
|
||||
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();
|
||||
return;
|
||||
@@ -383,8 +508,9 @@ namespace NecromancerTome
|
||||
{
|
||||
return;
|
||||
}
|
||||
Vector3i s = CinerariumThreads.Start.Value;
|
||||
Vector3 a = World.blockToTransformPos(s) - Origin.position + Vector3.up * (CinerariumThreads.StartIsUrn ? 0.5f : 0.6f);
|
||||
Vector3i s = CinerariumThreads.LastPoint.Value;
|
||||
Vector3 a = CinerariumThreads.LastIsRelay ? CinerariumThreads.RelayPoint(s)
|
||||
: World.blockToTransformPos(s) - Origin.position + Vector3.up * (CinerariumThreads.StartIsUrn ? 0.5f : 0.6f);
|
||||
Transform cam = pl.playerCamera != null ? pl.playerCamera.transform : null;
|
||||
Vector3 b = cam != null ? cam.position + cam.forward * 0.6f + cam.right * 0.2f - cam.up * 0.25f : pl.GetPosition() - Origin.position + Vector3.up * 1.2f;
|
||||
bool far = Vector3.Distance(s.ToVector3(), pl.GetPosition()) > CinerariumThreads.MaxLength;
|
||||
@@ -398,13 +524,13 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Нож для нитей не бьёт: ЛКМ и ПКМ - нити (на отпускание кнопки).</summary>
|
||||
/// <summary>Нож для нитей и резная кость не бьют: ЛКМ и ПКМ - нити (на отпускание кнопки).</summary>
|
||||
[HarmonyPatch(typeof(ItemClass), nameof(ItemClass.ExecuteAction))]
|
||||
public static class Patch_ItemClass_ExecuteAction_ThreadKnife
|
||||
{
|
||||
public static bool Prefix(ItemClass __instance, int _actionIdx, ItemInventoryData _data, bool _bReleased)
|
||||
{
|
||||
if (!CinerariumThreads.IsThreadKnife(__instance))
|
||||
if (!CinerariumThreads.IsThreadTool(__instance))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
@@ -9,6 +10,8 @@ namespace NecromancerTome
|
||||
/// ячейкой коллектора не принимало череп ни перетаскиванием, ни Shift+кликом). Своё здесь одно: после каждого изменения
|
||||
/// в ячейке остаётся один череп, всё остальное (лишние черепа, чужие предметы) - назад в рюкзак, и урна узнаёт, есть ли
|
||||
/// в ней череп. Череп - обычный предмет (ItemClass): он уже в сейвах, менять ему класс на модификацию нельзя.
|
||||
/// У урны рабочего (18.10) ячеек две: вторая - образец (любой предмет, стопка целиком - сколько держать в запасе).
|
||||
/// У старателя (22.10) вторая ячейка - что добывать: один предмет из TEFeatureCinerarium.Ores, лишнее и чужое - назад.
|
||||
/// </summary>
|
||||
public static class CinerariumContents
|
||||
{
|
||||
@@ -30,13 +33,17 @@ namespace NecromancerTome
|
||||
}
|
||||
_urn = _u;
|
||||
_player = _p;
|
||||
_bag = new Bag(new Vector2i(1, 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null);
|
||||
_bag = new Bag(new Vector2i(_u.IsWorker || _u.IsProspector ? 2 : 1, 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null);
|
||||
if (_u.HasSkull && skull != null && skull.type > 0)
|
||||
{
|
||||
_bag.SetSlot(0, new ItemStack(skull, 1));
|
||||
}
|
||||
if ((_u.IsWorker || _u.IsProspector) && _u.Sample != null && !_u.Sample.IsEmpty())
|
||||
{
|
||||
_bag.SetSlot(1, _u.Sample.Clone());
|
||||
}
|
||||
XUiC_BagStorageWindowGroup.Open(ui.xui, _p, _bag, LootContainer.GetLootContainer(LootTemplate),
|
||||
Localization.Get("necroCinerariumTrunk"), Check, () =>
|
||||
Localization.Get("necroCinerariumTrunk"), CheckLater, () =>
|
||||
{
|
||||
Check();
|
||||
_urn = null;
|
||||
@@ -46,13 +53,72 @@ namespace NecromancerTome
|
||||
Debug.Log("[NecromancerTome] Cinerarium: «Содержимое» урны " + _u.ToWorldPos() + " открыто, череп " + (_u.HasSkull ? "есть" : "нет"));
|
||||
}
|
||||
|
||||
/// <summary>Оставить в ячейке один череп, остальное - игроку; отметить в урне.</summary>
|
||||
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>
|
||||
static void Check()
|
||||
{
|
||||
if (_bag == null || _urn == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if ((_urn.IsWorker || _urn.IsProspector) && _bag.ItemGrid.items.Length > 1)
|
||||
{
|
||||
ItemStack smp = _bag.ItemGrid.items[1];
|
||||
ItemStack first = _bag.ItemGrid.items[0];
|
||||
if (smp != null && !smp.IsEmpty() && smp.itemValue.ItemClass?.GetItemName() == TEFeatureCinerarium.SkullItemName &&
|
||||
(first == null || first.IsEmpty()))
|
||||
{
|
||||
_bag.SetSlot(0, smp.Clone());
|
||||
_bag.SetSlot(1, ItemStack.Empty.Clone());
|
||||
smp = _bag.ItemGrid.items[1];
|
||||
}
|
||||
if (_urn.IsProspector && smp != null && !smp.IsEmpty())
|
||||
{
|
||||
// Старатель: образец - один предмет из списка добычи; чужое целиком, лишние штуки - назад.
|
||||
if (!TEFeatureCinerarium.IsOreSample(smp))
|
||||
{
|
||||
GiveBack(smp.Clone());
|
||||
_bag.SetSlot(1, ItemStack.Empty.Clone());
|
||||
if (_player != null)
|
||||
{
|
||||
GameManager.ShowTooltip(_player, Localization.Get("necroProspectorBadSample"));
|
||||
}
|
||||
}
|
||||
else if (smp.count > 1)
|
||||
{
|
||||
ItemStack back = smp.Clone();
|
||||
back.count = smp.count - 1;
|
||||
GiveBack(back);
|
||||
ItemStack one = smp.Clone();
|
||||
one.count = 1;
|
||||
_bag.SetSlot(1, one);
|
||||
}
|
||||
smp = _bag.ItemGrid.items[1];
|
||||
}
|
||||
_urn.SetSample(smp != null && !smp.IsEmpty() ? smp.Clone() : ItemStack.Empty.Clone());
|
||||
}
|
||||
ItemStack s = _bag.ItemGrid.items[0];
|
||||
bool has = false;
|
||||
if (s != null && !s.IsEmpty())
|
||||
|
||||
@@ -61,15 +61,40 @@ namespace NecromancerTome
|
||||
|
||||
/// <summary>У перевозки по нити - ящик «откуда» последнего шага (дух летит сначала к нему), иначе null.</summary>
|
||||
public static Vector3i? LastFrom;
|
||||
/// <summary>Что выбрал последний шаг (для necrourn probe, 23.10: «стою в 19 м - не работают»).</summary>
|
||||
public static string LastWhat;
|
||||
|
||||
/// <summary>Пробный шаг урны без изменений: что в зоне и что дух выбрал бы сейчас (команда necrourn x y z probe).</summary>
|
||||
public static string Probe(World _world, TEFeatureCinerarium _urn)
|
||||
{
|
||||
string zone = "";
|
||||
if (!_urn.IsLogist && !_urn.IsWorker)
|
||||
{
|
||||
Zone z = Scan(_world, _urn);
|
||||
zone = "коллекторов " + z.Collectors.Count + ", станций " + z.Stations.Count + ", ящиков " + z.Boxes.Count +
|
||||
", сумка " + (z.Bag != null) + ", спелых " + z.Ripe.Count + (z.Ripe.Count > 0 ? " (" + z.Ripe[0] + ")" : "") +
|
||||
", пустых клеток " + z.Empty.Count + "; ";
|
||||
}
|
||||
Vector3i? at = Step(_world, _urn, out bool stuck, true);
|
||||
return zone + "шаг: " + (at.HasValue ? LastWhat + " в " + at.Value : "нечего делать") + (stuck ? ", класть некуда" : "");
|
||||
}
|
||||
|
||||
static Vector3i? DoStep(World _world, TEFeatureCinerarium _urn, out bool _stuck)
|
||||
{
|
||||
_stuck = false;
|
||||
LastFrom = null;
|
||||
LastWhat = "?";
|
||||
// Урна с нитями - логист (16.10: «занимается только перетаскиванием из ящиков в ящики»).
|
||||
if (_urn.IsLogist)
|
||||
{
|
||||
return Ferry(_world, _urn);
|
||||
return Ferry(_world, _urn, out _stuck);
|
||||
}
|
||||
// Урна рабочего - изготовление по образцу на станциях по нитям (CinerariumCraft.cs).
|
||||
if (_urn.IsWorker)
|
||||
{
|
||||
Vector3i? at = CinerariumCraft.Step(_world, _urn, _dry, out _stuck, out Vector3i? from);
|
||||
LastFrom = from;
|
||||
return at;
|
||||
}
|
||||
Zone z = Scan(_world, _urn);
|
||||
// 1. Расходники.
|
||||
@@ -81,6 +106,7 @@ namespace NecromancerTome
|
||||
{
|
||||
c.SetModified();
|
||||
}
|
||||
LastWhat = "расходники коллектору";
|
||||
return z.Where[c];
|
||||
}
|
||||
}
|
||||
@@ -96,6 +122,7 @@ namespace NecromancerTome
|
||||
c.Items = items;
|
||||
c.SetModified();
|
||||
}
|
||||
LastWhat = "готовое из коллектора";
|
||||
return z.Where[c];
|
||||
}
|
||||
}
|
||||
@@ -108,6 +135,7 @@ namespace NecromancerTome
|
||||
{
|
||||
w.Output = output;
|
||||
}
|
||||
LastWhat = "готовое со станции";
|
||||
return z.Where[w];
|
||||
}
|
||||
}
|
||||
@@ -116,6 +144,7 @@ namespace NecromancerTome
|
||||
{
|
||||
if (Harvest(_world, p, z, ref blocked))
|
||||
{
|
||||
LastWhat = "сбор урожая";
|
||||
return p;
|
||||
}
|
||||
}
|
||||
@@ -124,6 +153,7 @@ namespace NecromancerTome
|
||||
Vector3i? planted = PlantRandom(_world, z);
|
||||
if (planted.HasValue)
|
||||
{
|
||||
LastWhat = "посадка";
|
||||
return planted;
|
||||
}
|
||||
}
|
||||
@@ -132,10 +162,11 @@ namespace NecromancerTome
|
||||
}
|
||||
|
||||
/// <summary>Перевозка по нитям: одна стопка из ящика «откуда» в ящик «куда». Только предметы включённых в урне групп
|
||||
/// (фильтр логиста, CinerariumGroups; 16.10 заменил прежнее правило «по образцу»); «куда» полон - ждём (дух не чернеет). Открытые у игрока ящики и закреплённые
|
||||
/// ячейки не трогаются. Возвращает ящик «куда», LastFrom - откуда взято.</summary>
|
||||
static Vector3i? Ferry(World _world, TEFeatureCinerarium _urn)
|
||||
/// (фильтр логиста, CinerariumGroups; 16.10 заменил прежнее правило «по образцу»); «куда» полон - ждём, _stuck - урна
|
||||
/// мигает (18.10). Открытые у игрока ящики и закреплённые ячейки не трогаются. Возвращает ящик «куда», LastFrom - откуда взято.</summary>
|
||||
static Vector3i? Ferry(World _world, TEFeatureCinerarium _urn, out bool _stuck)
|
||||
{
|
||||
_stuck = false;
|
||||
if (!_urn.HasThreadTo)
|
||||
{
|
||||
return null;
|
||||
@@ -159,16 +190,23 @@ namespace NecromancerTome
|
||||
for (int k = 0; k < src.ItemGrid.Length; k++)
|
||||
{
|
||||
ItemStack s = src.ItemGrid[k];
|
||||
if (s == null || s.IsEmpty() || Locked(src, k) || !_urn.GroupOn(CinerariumGroups.Of(s.itemValue)) || !Fits(dst, s))
|
||||
if (s == null || s.IsEmpty() || Locked(src, k) || !_urn.GroupOn(CinerariumGroups.Of(s.itemValue)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!Fits(dst, s))
|
||||
{
|
||||
_stuck = true; // есть что везти, но некуда
|
||||
continue;
|
||||
}
|
||||
LastFrom = f;
|
||||
_stuck = false; // везти есть куда - не застрял
|
||||
if (_dry)
|
||||
{
|
||||
return _urn.ThreadTo;
|
||||
}
|
||||
ItemStack moving = s.Clone();
|
||||
string what = s.itemValue.ItemClass?.GetItemName(); // до Take: забранная целиком стопка становится пустой
|
||||
Place(moving, new[] { dst });
|
||||
int moved = s.count - moving.count;
|
||||
if (moved <= 0)
|
||||
@@ -177,14 +215,14 @@ namespace NecromancerTome
|
||||
}
|
||||
Take(src, k, moved);
|
||||
dst.SetModified();
|
||||
Debug.Log("[NecromancerTome] Cinerarium: дух перенёс " + moved + " " + s.itemValue.ItemClass?.GetItemName() + " из " + f + " в " + _urn.ThreadTo);
|
||||
Debug.Log("[NecromancerTome] Cinerarium: дух перенёс " + moved + " " + what + " из " + f + " в " + _urn.ThreadTo);
|
||||
return _urn.ThreadTo;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static TEFeatureStorage BoxAt(World _world, Vector3i _p)
|
||||
internal static TEFeatureStorage BoxAt(World _world, Vector3i _p)
|
||||
{
|
||||
return _world.GetTileEntity(_p) is TileEntityComposite c && !c.IsUserAccessing() &&
|
||||
!c.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium _) &&
|
||||
@@ -285,7 +323,7 @@ namespace NecromancerTome
|
||||
return _z.Bag != null && Fits(_z.Bag, _s);
|
||||
}
|
||||
|
||||
static bool Fits(TEFeatureStorage _t, ItemStack _s)
|
||||
internal static bool Fits(TEFeatureStorage _t, ItemStack _s)
|
||||
{
|
||||
for (int i = 0; i < _t.ItemGrid.Length; i++)
|
||||
{
|
||||
@@ -519,7 +557,7 @@ namespace NecromancerTome
|
||||
return false;
|
||||
}
|
||||
|
||||
static void Place(ItemStack _s, IList<TEFeatureStorage> _dst)
|
||||
internal static void Place(ItemStack _s, IList<TEFeatureStorage> _dst)
|
||||
{
|
||||
for (int i = 0; i < _dst.Count && _s.count > 0; i++)
|
||||
{
|
||||
@@ -548,13 +586,13 @@ namespace NecromancerTome
|
||||
}
|
||||
|
||||
/// <summary>Ячейка, закреплённая игроком (замок в окне ящика), - не трогать.</summary>
|
||||
static bool Locked(TEFeatureStorage _src, int _slot)
|
||||
internal static bool Locked(TEFeatureStorage _src, int _slot)
|
||||
{
|
||||
PackedBoolArray locks = _src.ItemGrid.SlotLocks;
|
||||
return locks != null && _slot < locks.Length && locks[_slot];
|
||||
}
|
||||
|
||||
static void Take(TEFeatureStorage _src, int _slot, int _count)
|
||||
internal static void Take(TEFeatureStorage _src, int _slot, int _count)
|
||||
{
|
||||
ItemStack s = _src.ItemGrid[_slot].Clone();
|
||||
s.count -= _count;
|
||||
|
||||
@@ -0,0 +1,879 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Урны с большой зоной - ремонтник и лутер (решения 17.10 и 18.10, BACKLOG «Урна ремонтника», «Урна лутера»).
|
||||
///
|
||||
/// Общее. Зона - квадрат со стороной блока притязания (LandClaimSize сервера, по умолчанию 41) или меньше (31/21/11, пункт
|
||||
/// меню «Размер зоны»), по высоте - весь столб мира; делится на 3x3 квадрата, любой можно выключить (#84, ZoneOff); зоны
|
||||
/// таких урн могут пересекаться. Работа - только по кнопке (17.10: «А
|
||||
/// если сделать рубильник или кнопку. Включил, ремонтник начал работу»): «Запустить» - один проход по зоне, конец прохода -
|
||||
/// снова ожидание; до нажатия урна ничего не считает. Кнопки - на самой урне с четырёх сторон («как у дверного звонка, но с
|
||||
/// черепом»), их цвет - состояние (CinerariumButtons): не горят - нет черепа; жёлтая ровно - ожидание; жёлтая мигает - не
|
||||
/// хватает материалов (ремонтник) или ящик полон (лутер); зелёная - работа; красная - прилив (работа стоит, дух в урне).
|
||||
///
|
||||
/// Ремонтник. Обход - по столбам зоны, порциями из общего бюджета клеток на кадр (CellsPerFrame на все урны); в столбе - от
|
||||
/// низа до высоты чанка, земля мира (форма terrain) и воздух пропускаются сразу. Найденный повреждённый блок с RepairItems
|
||||
/// встаёт в очередь (не длиннее QueueMax - обход ждёт), блок берёт тот, кто нашёл первым (Claimed). Ремонт - неспешно: одно
|
||||
/// действие в RepairSeconds, дух летит к блоку и машет руками; материалы - RepairItems блока, доля по повреждению (как молотком),
|
||||
/// из ящиков с нитями к урне. Не хватает - кнопка мигает, урна ждёт (положил в ящик - продолжит сама). Разрушенное не
|
||||
/// восстанавливает - только повреждённое (17.10: «Восстанавливаю порушенное, затем включаю ремонтника и ухожу по делам»).
|
||||
///
|
||||
/// Лутер. За действие - одна цель в зоне: предмет на земле, затем мешок с зомби, затем красный труп (тем же путём, что игрок:
|
||||
/// LootBagOpened + NecroSkull.Roll, стадия добычи - того, кто нажал кнопку); добыча - в ящик с любой нитью к урне. Ящики
|
||||
/// зданий, аэродроп и рюкзаки погибших игроков не трогает (18.10 «Принимаю всё»). Целей нет - проход окончен.
|
||||
/// </summary>
|
||||
public partial class TEFeatureCinerarium
|
||||
{
|
||||
public const string RepairBlockName = "necroCinerariumRepair";
|
||||
public const string RepairSkullBlockName = "necroCinerariumRepairSkull";
|
||||
public const string LootBlockName = "necroCinerariumLoot";
|
||||
public const string LootSkullBlockName = "necroCinerariumLootSkull";
|
||||
|
||||
public static bool IsRepairBlock(Block _b)
|
||||
{
|
||||
string n = _b?.GetBlockName();
|
||||
return n == RepairBlockName || n == RepairSkullBlockName;
|
||||
}
|
||||
|
||||
public static bool IsLootBlock(Block _b)
|
||||
{
|
||||
string n = _b?.GetBlockName();
|
||||
return n == LootBlockName || n == LootSkullBlockName;
|
||||
}
|
||||
|
||||
bool _repair, _loot;
|
||||
|
||||
public bool IsRepairer
|
||||
{
|
||||
get
|
||||
{
|
||||
_ = IsLogist;
|
||||
return _repair;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLooter
|
||||
{
|
||||
get
|
||||
{
|
||||
_ = IsLogist;
|
||||
return _loot;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsBigZone => IsRepairer || IsLooter;
|
||||
|
||||
/// <summary>Сторона зоны, выбранная в меню (0 - по притязанию). Сохраняется (v6).</summary>
|
||||
public int ZoneSide;
|
||||
public bool ZoneRunning;
|
||||
/// <summary>Обход ремонтника: номер следующего столба зоны (строка за строкой).</summary>
|
||||
public int ZoneCursor;
|
||||
/// <summary>Кто нажал «Запустить» - стадия добычи лутера.</summary>
|
||||
public int ZonePressedBy = -1;
|
||||
/// <summary>Не хватает материалов / ящик полон - кнопка мигает жёлтым.</summary>
|
||||
public bool ZoneLacking;
|
||||
/// <summary>Ремонтник: какого материала и сколько не хватило последней проверке (ячейка в «Содержимом»,
|
||||
/// XUiC_NecroRepairNeed). Не сохраняется.</summary>
|
||||
public int LackType, LackCount;
|
||||
|
||||
public static float RepairSeconds = 2.5f;
|
||||
public static float LootSeconds = 1.5f;
|
||||
public static int CellsPerFrame = 4000;
|
||||
public static int QueueMax = 32;
|
||||
|
||||
static int _cellsFrame = -1, _cellsUsed;
|
||||
/// <summary>Блоки, взятые ремонтниками в очередь (зоны пересекаются - чинит взявший первым).</summary>
|
||||
static readonly HashSet<Vector3i> Claimed = new HashSet<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;
|
||||
bool _zonePending;
|
||||
float _zoneActAt;
|
||||
Vector3i _zoneTarget;
|
||||
int _zoneTargetEntity = -1;
|
||||
|
||||
/// <summary>Стороны зоны: притязание сервера и меньшие нечётные (31, 21, 11), не больше притязания.</summary>
|
||||
public static List<int> ZoneSides()
|
||||
{
|
||||
int claim = Mathf.Max(11, GamePrefs.GetInt(EnumGamePrefs.LandClaimSize));
|
||||
List<int> l = new List<int> { claim };
|
||||
foreach (int s in new[] { 31, 21, 11 })
|
||||
{
|
||||
if (s < claim)
|
||||
{
|
||||
l.Add(s);
|
||||
}
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
public int ZoneSideNow
|
||||
{
|
||||
get
|
||||
{
|
||||
List<int> l = ZoneSides();
|
||||
return ZoneSide > 0 && l.Contains(ZoneSide) ? ZoneSide : l[0];
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
{
|
||||
Off,
|
||||
Wait,
|
||||
Lacking,
|
||||
Busy,
|
||||
Tide
|
||||
}
|
||||
|
||||
public ButtonState Buttons
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!HasSkull)
|
||||
{
|
||||
return ButtonState.Off;
|
||||
}
|
||||
if (CinerariumLoad.Tide)
|
||||
{
|
||||
return ButtonState.Tide;
|
||||
}
|
||||
if (!ZoneRunning)
|
||||
{
|
||||
return ButtonState.Wait;
|
||||
}
|
||||
return ZoneLacking ? ButtonState.Lacking : ButtonState.Busy;
|
||||
}
|
||||
}
|
||||
|
||||
string ZoneStateText()
|
||||
{
|
||||
string key = Buttons switch
|
||||
{
|
||||
ButtonState.Tide => "necroZoneUrnTide",
|
||||
ButtonState.Lacking => IsLooter ? "necroZoneUrnFull" : "necroZoneUrnNoMaterials",
|
||||
ButtonState.Busy => "necroZoneUrnBusy",
|
||||
ButtonState.Wait => "necroZoneUrnWait",
|
||||
_ => null
|
||||
};
|
||||
string side = ZoneSideNow.ToString();
|
||||
return (key != null ? ", " + Localization.Get(key) : "") + ", " + string.Format(Localization.Get("necroZoneUrnSize"), side);
|
||||
}
|
||||
|
||||
/// <summary>Пункты меню на E: запустить, остановить, размер зоны (по кругу).</summary>
|
||||
public void ZoneCommand(string _cmd, EntityPlayerLocal _player)
|
||||
{
|
||||
if (_cmd.EndsWith("zurn_run"))
|
||||
{
|
||||
if (!HasSkull)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ZoneRunning = true;
|
||||
ZoneCursor = 0;
|
||||
ZoneLacking = false;
|
||||
ZonePressedBy = _player != null ? _player.entityId : -1;
|
||||
ReleaseClaims();
|
||||
_lootDone.Clear();
|
||||
Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), "button4");
|
||||
Debug.Log("[NecromancerTome] Cinerarium: " + (IsLooter ? "лутер" : "ремонтник") + " " + ToWorldPos() + " запущен, зона " + ZoneSideNow);
|
||||
}
|
||||
else if (_cmd.EndsWith("zurn_stop"))
|
||||
{
|
||||
ZoneStop(true);
|
||||
Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), "button4");
|
||||
}
|
||||
else if (_cmd.EndsWith("zurn_size"))
|
||||
{
|
||||
List<int> l = ZoneSides();
|
||||
int i = l.IndexOf(ZoneSideNow);
|
||||
ZoneSide = l[(i + 1) % l.Count];
|
||||
if (_player != null)
|
||||
{
|
||||
GameManager.ShowTooltip(_player, string.Format(Localization.Get("necroZoneUrnSize"), ZoneSide.ToString()));
|
||||
}
|
||||
if (ZoneShown)
|
||||
{
|
||||
RemoveBounds();
|
||||
SpawnBounds();
|
||||
}
|
||||
}
|
||||
SetModified();
|
||||
}
|
||||
|
||||
/// <summary>Остановить проход (кнопка, урна убрана): очередь и взятые блоки отпускаются.</summary>
|
||||
public void ZoneStop(bool _log)
|
||||
{
|
||||
ReleaseClaims();
|
||||
_zonePending = false;
|
||||
if (ZoneRunning && _log)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Cinerarium: " + (IsLooter ? "лутер" : "ремонтник") + " " + ToWorldPos() + " остановлен");
|
||||
}
|
||||
ZoneRunning = 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)
|
||||
{
|
||||
SyncButtons();
|
||||
if (!ZoneRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent<CinerariumGenieMotion>() : null;
|
||||
if (_zonePending)
|
||||
{
|
||||
if (Time.time < _zoneActAt)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_zonePending = false;
|
||||
if (IsRepairer)
|
||||
{
|
||||
RepairNow(_world, _zoneTarget);
|
||||
}
|
||||
else
|
||||
{
|
||||
LootNow(_world, _zoneTargetEntity);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (IsRepairer)
|
||||
{
|
||||
ScanColumns(_world);
|
||||
}
|
||||
if (Time.time < _zoneNext || !CinerariumLoad.TryTakeStep(ToWorldPos()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
_zoneNext = Time.time + (IsRepairer ? RepairSeconds : LootSeconds);
|
||||
if (IsRepairer)
|
||||
{
|
||||
RepairPick(_world, motion);
|
||||
}
|
||||
else
|
||||
{
|
||||
LootPick(_world, motion);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Слетать к месту и сделать дело по прилёте (как дух сборщика) или сразу, если духа нет.</summary>
|
||||
void FlyThenAct(World _world, CinerariumGenieMotion _motion, Vector3i _at)
|
||||
{
|
||||
_zonePending = true;
|
||||
_zoneActAt = Time.time;
|
||||
if (_motion != null)
|
||||
{
|
||||
List<(Vector3, Vector3, bool)> stops = new List<(Vector3, Vector3, bool)>
|
||||
{
|
||||
(Stand(_world, _at, out bool beside) - Origin.position, World.blockToTransformPos(_at) - Origin.position, beside)
|
||||
};
|
||||
_zoneActAt = Time.time + _motion.Visit(stops);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Ремонтник.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
void ScanColumns(World _world)
|
||||
{
|
||||
if (_queue.Count >= QueueMax)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Time.frameCount != _cellsFrame)
|
||||
{
|
||||
_cellsFrame = Time.frameCount;
|
||||
_cellsUsed = 0;
|
||||
}
|
||||
int side = ZoneSideNow, half = side / 2, total = side * side;
|
||||
Vector3i c = ToWorldPos();
|
||||
while (ZoneCursor < total && _cellsUsed < CellsPerFrame && _queue.Count < QueueMax)
|
||||
{
|
||||
int x = c.x - half + ZoneCursor % side, z = c.z - half + ZoneCursor / side;
|
||||
ZoneCursor++;
|
||||
if (ZoneOff != 0 && !InZone(x, z))
|
||||
{
|
||||
continue; // квадрат выключен (#84)
|
||||
}
|
||||
if (!(_world.GetChunkFromWorldPos(x, 0, z) is Chunk ch))
|
||||
{
|
||||
continue; // не загружен - пропустить столб
|
||||
}
|
||||
int lx = World.toBlockXZ(x), lz = World.toBlockXZ(z);
|
||||
int top = Mathf.Min(254, ch.GetHeight(lx, lz) + 1);
|
||||
_cellsUsed += top;
|
||||
for (int y = 1; y <= top; y++)
|
||||
{
|
||||
BlockValue bv = ch.GetBlock(lx, y, lz);
|
||||
if (bv.isair || bv.ischild || bv.damage <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Block b = bv.Block;
|
||||
if (b == null || b.shape.IsTerrain() || b.RepairItems == null || b.RepairItems.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Vector3i p = new Vector3i(x, y, z);
|
||||
if (Claimed.Add(p))
|
||||
{
|
||||
_queue.Add(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ZoneCursor >= total && _queue.Count == 0 && _lacking.Count == 0 && !_zonePending)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Cinerarium: ремонтник " + c + " обошёл зону - ожидание");
|
||||
ZoneStop(false);
|
||||
SetModified();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Следующий блок: сначала из очереди обхода, на что есть материалы (на что нет - в _lacking), потом из _lacking,
|
||||
/// если материал появился. Нечего чинить, кроме ждущих материала, - кнопка жёлтая. Ячейка «не хватает» в «Содержимом» -
|
||||
/// первый блок из _lacking (LackType/LackCount), пока он есть.</summary>
|
||||
void RepairPick(World _world, CinerariumGenieMotion _motion)
|
||||
{
|
||||
while (_queue.Count > 0)
|
||||
{
|
||||
Vector3i p = _queue[0];
|
||||
_queue.RemoveAt(0);
|
||||
BlockValue bv = _world.GetBlock(p);
|
||||
if (bv.isair || bv.damage <= 0 || bv.Block?.RepairItems == null)
|
||||
{
|
||||
Claimed.Remove(p);
|
||||
continue; // починили до нас или блок убран
|
||||
}
|
||||
if (!HasMaterials(_world, bv, false))
|
||||
{
|
||||
if (_lacking.Count < LackingMax)
|
||||
{
|
||||
_lacking.Add(p);
|
||||
}
|
||||
else
|
||||
{
|
||||
Claimed.Remove(p); // найдётся при следующем обходе
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Go(_world, _motion, p);
|
||||
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)
|
||||
{
|
||||
_queue.Remove(_p);
|
||||
Claimed.Remove(_p);
|
||||
BlockValue bv = _world.GetBlock(_p);
|
||||
if (bv.isair || bv.damage <= 0 || bv.Block?.RepairItems == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!HasMaterials(_world, bv, true))
|
||||
{
|
||||
_lacking.Add(_p); // материал забрали, пока дух летел
|
||||
Claimed.Add(_p);
|
||||
return;
|
||||
}
|
||||
int was = bv.damage;
|
||||
bv.damage = 0;
|
||||
_world.SetBlockRPC(_p, bv);
|
||||
Debug.Log("[NecromancerTome] Cinerarium: ремонтник " + ToWorldPos() + " починил " + bv.Block.GetBlockName() + " в " + _p + " (было повреждение " + was + ")");
|
||||
}
|
||||
|
||||
/// <summary>Хватает ли в ящиках с нитями к урне материалов на ремонт (RepairItems блока, доля по повреждению, не меньше
|
||||
/// одного); _take - сразу и забрать.</summary>
|
||||
bool HasMaterials(World _world, BlockValue _bv, bool _take)
|
||||
{
|
||||
Block b = _bv.Block;
|
||||
float share = Mathf.Clamp01(_bv.damage / (float)Mathf.Max(1, b.MaxDamage));
|
||||
List<TEFeatureStorage> boxes = ThreadBoxes(_world);
|
||||
foreach (Block.SItemNameCount need in b.RepairItems)
|
||||
{
|
||||
ItemValue iv = ItemClass.GetItem(need.ItemName);
|
||||
if (iv == null || iv.type == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int n = Mathf.Max(1, Mathf.CeilToInt(need.Count * share));
|
||||
int have = 0;
|
||||
foreach (TEFeatureStorage s in boxes)
|
||||
{
|
||||
have += Count(s, iv);
|
||||
}
|
||||
if (have < n)
|
||||
{
|
||||
LackType = iv.type;
|
||||
LackCount = n - have;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!_take)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
foreach (Block.SItemNameCount need in b.RepairItems)
|
||||
{
|
||||
ItemValue iv = ItemClass.GetItem(need.ItemName);
|
||||
if (iv == null || iv.type == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int n = Mathf.Max(1, Mathf.CeilToInt(need.Count * share));
|
||||
foreach (TEFeatureStorage s in boxes)
|
||||
{
|
||||
n -= TakeFrom(s, iv, n);
|
||||
if (n <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int Count(TEFeatureStorage _s, ItemValue _iv)
|
||||
{
|
||||
int n = 0;
|
||||
for (int i = 0; i < _s.ItemGrid.Length; i++)
|
||||
{
|
||||
ItemStack st = _s.ItemGrid[i];
|
||||
if (st != null && !st.IsEmpty() && st.itemValue.type == _iv.type && !CinerariumWork.Locked(_s, i))
|
||||
{
|
||||
n += st.count;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static int TakeFrom(TEFeatureStorage _s, ItemValue _iv, int _n)
|
||||
{
|
||||
int took = 0;
|
||||
for (int i = 0; i < _s.ItemGrid.Length && took < _n; i++)
|
||||
{
|
||||
ItemStack st = _s.ItemGrid[i];
|
||||
if (st == null || st.IsEmpty() || st.itemValue.type != _iv.type || CinerariumWork.Locked(_s, i))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int k = Mathf.Min(st.count, _n - took);
|
||||
CinerariumWork.Take(_s, i, k);
|
||||
took += k;
|
||||
}
|
||||
return took;
|
||||
}
|
||||
|
||||
/// <summary>Ящики урны по нитям: у ремонтника - «откуда» (материалы), у лутера - любая нить (куда класть).</summary>
|
||||
List<TEFeatureStorage> ThreadBoxes(World _world)
|
||||
{
|
||||
List<TEFeatureStorage> l = new List<TEFeatureStorage>();
|
||||
void Add(Vector3i _p)
|
||||
{
|
||||
TEFeatureStorage s = CinerariumWork.BoxAt(_world, _p);
|
||||
if (s != null && !l.Contains(s))
|
||||
{
|
||||
l.Add(s);
|
||||
}
|
||||
}
|
||||
foreach (Vector3i p in ThreadFrom)
|
||||
{
|
||||
Add(p);
|
||||
}
|
||||
if (IsLooter)
|
||||
{
|
||||
if (HasThreadTo)
|
||||
{
|
||||
Add(ThreadTo);
|
||||
}
|
||||
foreach (Vector3i p in ThreadOut)
|
||||
{
|
||||
Add(p);
|
||||
}
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Лутер.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
static readonly List<Entity> _ents = new List<Entity>();
|
||||
/// <summary>Цели, уже обысканные за этот проход (пустой мешок, нечего взять) - второй раз не трогать.</summary>
|
||||
readonly HashSet<int> _lootDone = new HashSet<int>();
|
||||
|
||||
/// <summary>Цель лутера в зоне: предмет на земле, потом мешок с зомби, потом красный труп. Ничего - проход окончен.</summary>
|
||||
void LootPick(World _world, CinerariumGenieMotion _motion)
|
||||
{
|
||||
List<TEFeatureStorage> boxes = ThreadBoxes(_world);
|
||||
if (boxes.Count == 0)
|
||||
{
|
||||
ZoneLacking = true; // некуда класть - как полный ящик
|
||||
return;
|
||||
}
|
||||
int side = ZoneSideNow;
|
||||
Vector3 c = World.blockToTransformPos(ToWorldPos());
|
||||
Bounds bb = new Bounds(new Vector3(c.x, 128f, c.z), new Vector3(side, 256f, side));
|
||||
Entity best = null;
|
||||
int bestRank = int.MaxValue;
|
||||
_ents.Clear();
|
||||
_world.GetEntitiesInBounds(typeof(Entity), bb, _ents);
|
||||
foreach (Entity e in _ents)
|
||||
{
|
||||
int rank = LootRank(e);
|
||||
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;
|
||||
bestRank = rank;
|
||||
}
|
||||
}
|
||||
_ents.Clear();
|
||||
if (best == null)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Cinerarium: лутер " + ToWorldPos() + " собрал всё в зоне - ожидание");
|
||||
ZoneStop(false);
|
||||
SetModified();
|
||||
return;
|
||||
}
|
||||
ZoneLacking = false;
|
||||
_zoneTargetEntity = best.entityId;
|
||||
FlyThenAct(_world, _motion, World.worldToBlockPos(best.GetPosition()));
|
||||
}
|
||||
|
||||
/// <summary>Что собирать и в каком порядке (меньше - раньше); int.MaxValue - не трогать. Рюкзаки игроков (EntityBackpack) и
|
||||
/// аэродроп (EntitySupplyCrate) - не трогать.</summary>
|
||||
static int LootRank(Entity _e)
|
||||
{
|
||||
if (_e == null || _e.IsDespawned)
|
||||
{
|
||||
return int.MaxValue;
|
||||
}
|
||||
if (_e is EntityBackpack || _e is EntitySupplyCrate)
|
||||
{
|
||||
return int.MaxValue;
|
||||
}
|
||||
if (_e is EntityLootContainer lc)
|
||||
{
|
||||
return lc.bag != null ? 1 : int.MaxValue;
|
||||
}
|
||||
if (_e is EntityItem it && !(_e is EntityContainerAbs))
|
||||
{
|
||||
return it.itemStack != null && !it.itemStack.IsEmpty() ? 0 : int.MaxValue;
|
||||
}
|
||||
if (_e is EntityAlive a && a.IsDead() && CorpseLoot.IsSearchable(a) && a.bag != null)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
return int.MaxValue;
|
||||
}
|
||||
|
||||
void LootNow(World _world, int _id)
|
||||
{
|
||||
Entity e = _world.GetEntity(_id);
|
||||
if (e == null || LootRank(e) == int.MaxValue || LockManager.Instance.IsLockedServer(e, 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<TEFeatureStorage> boxes = ThreadBoxes(_world);
|
||||
if (e is EntityItem it && !(e is EntityContainerAbs))
|
||||
{
|
||||
ItemStack s = it.itemStack.Clone();
|
||||
int before = s.count;
|
||||
CinerariumWork.Place(s, boxes);
|
||||
if (s.count == before)
|
||||
{
|
||||
ZoneLacking = true;
|
||||
return;
|
||||
}
|
||||
if (s.count <= 0)
|
||||
{
|
||||
_world.RemoveEntity(e.entityId, EnumRemoveEntityReason.Despawned);
|
||||
}
|
||||
else
|
||||
{
|
||||
it.itemStack = s;
|
||||
}
|
||||
Debug.Log("[NecromancerTome] Cinerarium: лутер " + ToWorldPos() + " подобрал " + s.itemValue.ItemClass?.GetItemName() + " x" + (before - s.count));
|
||||
return;
|
||||
}
|
||||
Bag bag = e is EntityAlive a ? a.bag : (e as EntityLootContainer)?.bag;
|
||||
if (bag == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!bag.Touched)
|
||||
{
|
||||
// Как игрок: первое открытие наполняет сумку по списку добычи (стадия - нажавшего кнопку); у красного трупа - и
|
||||
// находка черепа (CorpseLootPatch: OnLockRequestServer).
|
||||
int who = ZonePressedBy >= 0 ? ZonePressedBy : (_world.GetPrimaryPlayer()?.entityId ?? -1);
|
||||
GameManager.Instance.lootManager.LootBagOpened(bag, e, who);
|
||||
if (e is EntityAlive corpse)
|
||||
{
|
||||
NecroSkull.Roll(corpse, bag);
|
||||
}
|
||||
}
|
||||
bool moved = false, left = false;
|
||||
ItemStackGrid slots = bag.ItemGrid;
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
{
|
||||
if (slots[i] == null || slots[i].IsEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ItemStack s = slots[i].Clone();
|
||||
int before = s.count;
|
||||
CinerariumWork.Place(s, boxes);
|
||||
if (s.count != before)
|
||||
{
|
||||
moved = true;
|
||||
}
|
||||
if (s.count > 0)
|
||||
{
|
||||
left = true;
|
||||
}
|
||||
bag.SetSlot(i, s.count > 0 ? s : ItemStack.Empty.Clone());
|
||||
}
|
||||
ZoneLacking = left; // не всё влезло - ящик полон, кнопка мигает
|
||||
if (!left)
|
||||
{
|
||||
_lootDone.Add(e.entityId); // опустошён (или был пуст) - больше не цель; пустой убирает игра
|
||||
}
|
||||
Debug.Log("[NecromancerTome] Cinerarium: лутер " + ToWorldPos() + " обыскал " + e.GetType().Name + " " + e.entityId + (left ? " - не всё влезло" : ""));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Кнопки на модели.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
void SyncButtons()
|
||||
{
|
||||
Transform t = ModelTransform();
|
||||
if (t == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CinerariumButtons b = t.GetComponent<CinerariumButtons>();
|
||||
if (b == null)
|
||||
{
|
||||
b = t.gameObject.AddComponent<CinerariumButtons>();
|
||||
}
|
||||
b.State = Buttons;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Кнопки-звонки с черепом на урне ремонтника/лутера: рендереры модели с «button» в имени. Цвет свечения - по
|
||||
/// состоянию урны (TEFeatureCinerarium.Buttons), жёлтая мигает - нехватка.</summary>
|
||||
[Preserve]
|
||||
public class CinerariumButtons : MonoBehaviour
|
||||
{
|
||||
public TEFeatureCinerarium.ButtonState State;
|
||||
List<Renderer> _buttons;
|
||||
static readonly MaterialPropertyBlock _block = new MaterialPropertyBlock();
|
||||
static readonly int EmissionColorId = Shader.PropertyToID("_EmissionColor");
|
||||
public static Color Yellow = new Color(2.2f, 1.6f, 0.15f);
|
||||
public static Color Green = new Color(0.3f, 2.2f, 0.4f);
|
||||
public static Color Red = new Color(2.4f, 0.15f, 0.1f);
|
||||
Color _last = new Color(-1f, 0f, 0f);
|
||||
|
||||
public static bool IsButton(Renderer _r) => _r != null && _r.name.ToLowerInvariant().Contains("button");
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (_buttons == null)
|
||||
{
|
||||
_buttons = new List<Renderer>();
|
||||
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
if (IsButton(r))
|
||||
{
|
||||
_buttons.Add(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
Color c;
|
||||
switch (State)
|
||||
{
|
||||
case TEFeatureCinerarium.ButtonState.Wait: c = Yellow; break;
|
||||
case TEFeatureCinerarium.ButtonState.Lacking: c = Mathf.Repeat(Time.time, 0.8f) < 0.4f ? Yellow : Color.black; break;
|
||||
case TEFeatureCinerarium.ButtonState.Busy: c = Green; break;
|
||||
case TEFeatureCinerarium.ButtonState.Tide: c = Red; break;
|
||||
default: c = Color.black; break;
|
||||
}
|
||||
if (c == _last)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_last = c;
|
||||
foreach (Renderer r in _buttons)
|
||||
{
|
||||
if (r == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
r.GetPropertyBlock(_block);
|
||||
_block.SetColor(EmissionColorId, c);
|
||||
r.SetPropertyBlock(_block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ namespace NecromancerTome
|
||||
|
||||
static bool Ours(string _name)
|
||||
{
|
||||
return _name == "necroBookMindPrefab" || _name == "necroKnifeDecorPrefab" || _name == "necroMortarPrefab" || _name.StartsWith("necroBloodRose") || _name.StartsWith("necroPoster") || _name == "necroBookDoorPrefab" || _name.StartsWith("necroSkullDecor") || _name == "necroCinerariumPrefab";
|
||||
return _name == "necroBookMindPrefab" || _name == "necroKnifeDecorPrefab" || _name == "necroMortarPrefab" || _name.StartsWith("necroBloodRose") || _name.StartsWith("necroPoster") || _name == "necroBookDoorPrefab" || _name.StartsWith("necroSkullDecor") || _name == "necroCinerariumPrefab" || _name == "necroCinerariumWorkerPrefab" || _name.StartsWith("necroCinerariumLogist") || _name == "necroCinerariumRepairPrefab" || _name == "necroCinerariumLootPrefab" || _name == "necroCinerariumProspectorPrefab" || _name == "necroOssuaryCauldronPrefab";
|
||||
}
|
||||
|
||||
public static void Postfix(Transform __result)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Стрела: NullReferenceException в ItemAction.GetDismemberChance (23.10.2026). Вызывается из ProjectileMoveScript.checkCollision
|
||||
/// аргументом к ItemActionAttack.Hit - поэтому в первых логах исключение выглядело как ошибка «внутри Hit» (метод был встроен).
|
||||
/// Пустое звено цепочки actionData.invData.holdingEntity.inventory / invData.item у стрелы, запомнившей данные лука при выстреле.
|
||||
/// Исключение вылетало раньше, чем стрела сменит состояние, и она «попадала» снова каждый шаг физики - пачки по 6-19 ошибок.
|
||||
/// Здесь: при исключении шанс отстрела конечности 0 (стрела долетает как обычно), в лог - какое звено пустое (не больше
|
||||
/// MaxLogs раз за запуск). Причина звена - выяснить по этим строкам.
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(ItemAction), nameof(ItemAction.GetDismemberChance))]
|
||||
public static class Patch_ItemAction_GetDismemberChance_Guard
|
||||
{
|
||||
const int MaxLogs = 5;
|
||||
static int _logged;
|
||||
|
||||
public static Exception Finalizer(Exception __exception, ItemActionData _actionData, WorldRayHitInfo hitInfo, ref float __result)
|
||||
{
|
||||
if (__exception == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
__result = 0f;
|
||||
if (_logged < MaxLogs)
|
||||
{
|
||||
_logged++;
|
||||
try
|
||||
{
|
||||
ItemInventoryData inv = _actionData?.invData;
|
||||
EntityAlive holder = inv?.holdingEntity;
|
||||
Debug.Log("[NecromancerTome] DismemberGuard: " + __exception.GetType().Name + " в GetDismemberChance - actionData " +
|
||||
(_actionData == null ? "NULL" : _actionData.GetType().Name) +
|
||||
", invData " + (inv == null ? "NULL" : "slot " + inv.slotIdx) +
|
||||
", item " + (inv?.item == null ? "NULL" : inv.item.Name) +
|
||||
", holdingEntity " + (holder == null ? "NULL" : holder.GetType().Name + " " + holder.entityId + (holder.IsDead() ? " dead" : "")) +
|
||||
", inventory " + (holder == null ? "-" : holder.inventory == null ? "NULL" : "в руке " + holder.inventory.holdingItem?.Name) +
|
||||
", tag " + (hitInfo?.tag ?? "NULL") + " - шанс отстрела 0");
|
||||
}
|
||||
catch (Exception e2)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] DismemberGuard: не удалось описать - " + e2.Message);
|
||||
}
|
||||
}
|
||||
return null; // проглотить: стрела долетает и попадает как обычно
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,9 @@ namespace NecromancerTome
|
||||
|
||||
static GameObject _rig;
|
||||
static SDCSUtils.TransformCatalog _bones;
|
||||
/// <summary>Модель оружия с пояса в правой руке (#37) и чья она (тип предмета; -1 - кулаки).</summary>
|
||||
static AssetManagement.InstanceHandle<GameObject> _weapon;
|
||||
static int _weaponType = -1;
|
||||
static bool _layerWasShown;
|
||||
|
||||
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 -
|
||||
/// миг удара, как у персонажа (impactStart). Руки на это время уходят в нижнее положение (под прицел).</summary>
|
||||
public static void OnSwingEnter(float _raycastTime)
|
||||
@@ -421,6 +483,7 @@ namespace NecromancerTome
|
||||
{
|
||||
_player?.ShowHoldingItemLayer(false);
|
||||
}
|
||||
DestroyWeapon();
|
||||
SDCSUtils.UnloadViz(_rig);
|
||||
_rig = null;
|
||||
_bones = null;
|
||||
@@ -437,7 +500,7 @@ namespace NecromancerTome
|
||||
public class ConsoleCmdNecroArms : ConsoleCmdAbstract
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -477,6 +540,15 @@ namespace NecromancerTome
|
||||
{
|
||||
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")
|
||||
{
|
||||
Out(FpArms.DumpAnim());
|
||||
|
||||
@@ -29,10 +29,10 @@ namespace NecromancerTome
|
||||
/// </summary>
|
||||
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 SupportedMinorTens = 3;
|
||||
public const string SupportedText = "3.3";
|
||||
public const int SupportedMinorTens = 4;
|
||||
public const string SupportedText = "3.4";
|
||||
|
||||
private static bool? supported;
|
||||
|
||||
|
||||
@@ -53,15 +53,25 @@ namespace NecromancerTome
|
||||
|
||||
public static bool Is(TileEntityCollector te) => te != null && Is(te.blockValue.Block);
|
||||
|
||||
/// <summary>os1 -> 0, os2 -> 1, os3 -> 2; иначе -1.</summary>
|
||||
/// <summary>Ячейка образца для выхода: хранилище 2 ряда по 3, столбец - образец над ним (os1 и os4 - образец 1, os2 и
|
||||
/// os5 - 2, os3 и os6 - 3). 22.10 сначала решили «ячейка под образец - одна», но спрятать две лишние не вышло с первого
|
||||
/// раза, и пользователь: «Пусть можно будет положить 3 разных образца, и тогда в ячейки под ними пойдут такие же ресурсы.
|
||||
/// Примерно так оссуарий и работал». Не наш выход - -1.</summary>
|
||||
public static int ModSlotOf(BlockCollector.OutputType outputType)
|
||||
{
|
||||
string name = outputType?.Name;
|
||||
if (name == null || name.Length != 3 || !name.StartsWith("os"))
|
||||
return -1;
|
||||
return name[2] - '1';
|
||||
return (name[2] - '1') % 3;
|
||||
}
|
||||
|
||||
/// <summary>Видимых в окне ячеек образца и топлива (#66, 22.10: «Ячейка под образец - одна; ячейка под кровавую
|
||||
/// сферу (или камень) - одна»). В данных их по-прежнему 3 и 6: длины пишутся в сейв, а при загрузке игра обрезает
|
||||
/// лишнее по конфигу - уменьшение молча выбросило бы содержимое. Лишние прячутся в окне, их старое содержимое
|
||||
/// переносит в хранилище Patch_TileEntityCollector_UpdateTick_GeoOssuaryMigrate.</summary>
|
||||
public const int VisibleModSlots = 3; // все три (см. ModSlotOf); прятать нечего, перенос образцов не нужен
|
||||
public const int VisibleFuelSlots = 1;
|
||||
|
||||
/// <summary>Ресурс в ячейке модификации, если он из списка; иначе null.</summary>
|
||||
public static ItemClass ResourceIn(TileEntityCollector te, int modSlot)
|
||||
{
|
||||
@@ -82,8 +92,11 @@ namespace NecromancerTome
|
||||
public static void Postfix(XUiC_DewCollectorModGrid __instance, TileEntityCollector te)
|
||||
{
|
||||
bool ossuary = GeoOssuary.Is(te);
|
||||
foreach (XUiC_ItemStack slot in __instance.itemControllers)
|
||||
for (int i = 0; i < __instance.itemControllers.Length; i++)
|
||||
{
|
||||
XUiC_ItemStack slot = __instance.itemControllers[i];
|
||||
// окно у всех коллекторов общее - чужим возвращаем все ячейки
|
||||
slot.ViewComponent.IsVisible = !ossuary || i < GeoOssuary.VisibleModSlots;
|
||||
if (!(slot is XUiC_RequiredItemStack required))
|
||||
continue;
|
||||
required.OverrideStackCount = ossuary ? 1 : -1;
|
||||
@@ -96,6 +109,70 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Лишние ячейки держать скрытыми каждый кадр, пока окно открыто (22.10: «почему-то ячеек под образец 3, а мы
|
||||
/// планировали сделать 1»): сетка топлива сама выставляет видимость по высоте в своём Update (updateGridHeight), ячейки
|
||||
/// образца тоже показывались снова после SetTileEntity. Сеттер IsVisible при том же значении ничего не делает.</summary>
|
||||
[HarmonyPatch(typeof(XUiC_ItemStackGrid), nameof(XUiC_ItemStackGrid.Update))]
|
||||
public static class Patch_XUiC_ItemStackGrid_Update_GeoOssuary
|
||||
{
|
||||
public static void Postfix(XUiC_ItemStackGrid __instance)
|
||||
{
|
||||
if (__instance is XUiC_DewCollectorModGrid mods)
|
||||
{
|
||||
if (!GeoOssuary.Is(mods.tileEntity))
|
||||
return;
|
||||
for (int i = GeoOssuary.VisibleModSlots; i < mods.itemControllers.Length; i++)
|
||||
mods.itemControllers[i].ViewComponent.IsVisible = false;
|
||||
}
|
||||
else if (__instance is XUiC_CollectorFuelGrid fuel && fuel.fuelBehavior == XUiC_CollectorFuelGrid.FuelBehavior.Fuel &&
|
||||
GeoOssuary.Is(fuel.tileEntity))
|
||||
{
|
||||
GeoOssuaryFuelGrid.Apply(fuel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Сетка топлива меняет видимость после базового Update (updateGridHeight) - прятать и после него.</summary>
|
||||
[HarmonyPatch(typeof(XUiC_VariableHeightGrid), nameof(XUiC_VariableHeightGrid.Update))]
|
||||
public static class Patch_XUiC_VariableHeightGrid_Update_GeoOssuary
|
||||
{
|
||||
public static void Postfix(XUiC_VariableHeightGrid __instance)
|
||||
{
|
||||
if (__instance is XUiC_CollectorFuelGrid fuel && fuel.fuelBehavior == XUiC_CollectorFuelGrid.FuelBehavior.Fuel &&
|
||||
GeoOssuary.Is(fuel.tileEntity))
|
||||
GeoOssuaryFuelGrid.Apply(fuel);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Заголовок над ячейками образцов (22.10: «Переименуй»): у коллекторов игры - «Инструменты» (xuiTools), у котла -
|
||||
/// «Образцы». Окно общее, поэтому чужим возвращаем родное.</summary>
|
||||
[HarmonyPatch(typeof(XUiC_DewCollectorModGrid), nameof(XUiC_DewCollectorModGrid.OnOpen))]
|
||||
public static class Patch_XUiC_DewCollectorModGrid_OnOpen_GeoOssuaryHeader
|
||||
{
|
||||
public static void Postfix(XUiC_DewCollectorModGrid __instance)
|
||||
{
|
||||
XUiController window = __instance;
|
||||
while (window.Parent != null && !(window.ViewComponent is XUiV_Window))
|
||||
window = window.Parent;
|
||||
XUiV_Label header = FindHeader(window);
|
||||
if (header != null)
|
||||
header.Text = Localization.Get(GeoOssuary.Is(__instance.tileEntity) ? "necroGeoOssuarySamples" : "xuiTools");
|
||||
}
|
||||
|
||||
static XUiV_Label FindHeader(XUiController _c)
|
||||
{
|
||||
if (_c.ViewComponent is XUiV_Label l && (l.Text == Localization.Get("xuiTools") || l.Text == Localization.Get("necroGeoOssuarySamples")))
|
||||
return l;
|
||||
foreach (XUiController child in _c.Children)
|
||||
{
|
||||
XUiV_Label found = FindHeader(child);
|
||||
if (found != null)
|
||||
return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(TileEntityCollector), nameof(TileEntityCollector.isDisabled))]
|
||||
public static class Patch_TileEntityCollector_isDisabled_GeoOssuary
|
||||
{
|
||||
@@ -143,7 +220,7 @@ namespace NecromancerTome
|
||||
// Пирамида отрицания (01.10) собрана из копии префаба оссуария - те же имена объектов,
|
||||
// тот же запасной шейдер, поэтому лечится тем же постфиксом.
|
||||
static readonly System.Collections.Generic.HashSet<string> models =
|
||||
new System.Collections.Generic.HashSet<string> { "necroOssuaryPrefab", "necroWardPrefab" };
|
||||
new System.Collections.Generic.HashSet<string> { "necroWardPrefab" };
|
||||
static readonly System.Collections.Generic.HashSet<string> done = new System.Collections.Generic.HashSet<string>();
|
||||
|
||||
public static void Postfix(Transform __result)
|
||||
@@ -251,4 +328,108 @@ namespace NecromancerTome
|
||||
__result = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Одна ячейка топлива в окне (GeoOssuary.VisibleFuelSlots): остальные ячейки сетки и их подсказки-картинки
|
||||
/// спрятаны. Сетка общая для всех коллекторов (и для катализатора) - чужим всё показываем обратно.</summary>
|
||||
public static class GeoOssuaryFuelGrid
|
||||
{
|
||||
public static void Apply(XUiC_CollectorFuelGrid _grid)
|
||||
{
|
||||
if (_grid?.tileEntity == null || _grid.fuelBehavior != XUiC_CollectorFuelGrid.FuelBehavior.Fuel)
|
||||
return;
|
||||
// 23.10: окно ещё слушает урну, а блока коллектора под ней уже нет (сняли / поставили заново) - FuelGridLength
|
||||
// игры тогда падает NullReference.
|
||||
if (!(_grid.tileEntity.blockValue.Block is BlockCollector))
|
||||
return;
|
||||
bool ossuary = GeoOssuary.Is(_grid.tileEntity);
|
||||
int n = _grid.tileEntity.FuelGridLength;
|
||||
for (int i = 0; i < _grid.itemControllers.Length; i++)
|
||||
{
|
||||
bool show = i < n && (!ossuary || i < GeoOssuary.VisibleFuelSlots);
|
||||
if (_grid.itemControllers[i].ViewComponent.IsVisible != show && (ossuary || i < n))
|
||||
_grid.itemControllers[i].ViewComponent.IsVisible = show;
|
||||
if (_grid.previewSprites != null && i < _grid.previewSprites.Length && _grid.previewSprites[i] != null && ossuary && i >= GeoOssuary.VisibleFuelSlots)
|
||||
_grid.previewSprites[i].IsVisible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(XUiC_CollectorFuelGrid), nameof(XUiC_CollectorFuelGrid.SetTileEntity))]
|
||||
public static class Patch_XUiC_CollectorFuelGrid_SetTileEntity_GeoOssuary
|
||||
{
|
||||
public static void Postfix(XUiC_CollectorFuelGrid __instance) => GeoOssuaryFuelGrid.Apply(__instance);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(XUiC_CollectorFuelGrid), nameof(XUiC_CollectorFuelGrid.OnTileEntityChanged))]
|
||||
public static class Patch_XUiC_CollectorFuelGrid_OnTileEntityChanged_GeoOssuary
|
||||
{
|
||||
public static void Postfix(XUiC_CollectorFuelGrid __instance) => GeoOssuaryFuelGrid.Apply(__instance);
|
||||
}
|
||||
|
||||
/// <summary>Shift-клик топливом: у оссуария - только в видимую ячейку (ваниль раскладывает по всей сетке).</summary>
|
||||
[HarmonyPatch(typeof(XUiC_CollectorFuelGrid), nameof(XUiC_CollectorFuelGrid.TryAddFuel))]
|
||||
public static class Patch_XUiC_CollectorFuelGrid_TryAddFuel_GeoOssuary
|
||||
{
|
||||
public static bool Prefix(XUiC_CollectorFuelGrid __instance, ItemClass newItemClass, ItemStack newItemStack, ref bool __result)
|
||||
{
|
||||
if (__instance.fuelBehavior != XUiC_CollectorFuelGrid.FuelBehavior.Fuel || !GeoOssuary.Is(__instance.tileEntity))
|
||||
return true;
|
||||
if (System.Array.IndexOf(__instance.tileEntity.GetFuelTypes(), newItemClass.Name) >= 0)
|
||||
{
|
||||
XUiC_ItemStack slot = __instance.itemControllers[0];
|
||||
if (slot.ItemStack.IsEmpty())
|
||||
{
|
||||
slot.ItemStack = newItemStack.Clone();
|
||||
newItemStack.count = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
int n = slot.ItemStack.StackTransferCount(newItemStack);
|
||||
slot.ItemStack.count += n;
|
||||
newItemStack.count -= n;
|
||||
}
|
||||
__instance.UpdateBackend(__instance.getUISlots());
|
||||
}
|
||||
__result = newItemStack.count == 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Переход на котёл (#66, 22.10: «у уже поставленных оссуариев при первой загрузке новой версии всё лишнее из
|
||||
/// старых ячеек образцов и топлива переложить в новое хранилище - ничего не теряется»). Сервер на каждом тике смотрит
|
||||
/// спрятанные ячейки (образцы 2-3, топливо 2-6) и перекладывает их содержимое в свободные ячейки хранилища; нет места -
|
||||
/// ждёт, пока игрок заберёт добычу.</summary>
|
||||
[HarmonyPatch(typeof(TileEntityCollector), nameof(TileEntityCollector.UpdateTick))]
|
||||
public static class Patch_TileEntityCollector_UpdateTick_GeoOssuaryMigrate
|
||||
{
|
||||
public static void Postfix(TileEntityCollector __instance)
|
||||
{
|
||||
if (!GeoOssuary.Is(__instance) || !SingletonMonoBehaviour<ConnectionManager>.Instance.IsServer)
|
||||
return;
|
||||
bool moved = Move(__instance, __instance.ModSlots, GeoOssuary.VisibleModSlots, "образца");
|
||||
moved |= Move(__instance, __instance.FuelSlots, GeoOssuary.VisibleFuelSlots, "топлива");
|
||||
if (moved)
|
||||
__instance.SetModified();
|
||||
}
|
||||
|
||||
static bool Move(TileEntityCollector _te, ItemStack[] _from, int _keep, string _what)
|
||||
{
|
||||
bool moved = false;
|
||||
ItemStack[] store = _te.Items;
|
||||
for (int i = _keep; i < _from.Length; i++)
|
||||
{
|
||||
if (_from[i] == null || _from[i].IsEmpty())
|
||||
continue;
|
||||
int free = System.Array.FindIndex(store, s => s == null || s.IsEmpty());
|
||||
if (free < 0)
|
||||
return moved;
|
||||
store[free] = _from[i].Clone();
|
||||
_from[i] = ItemStack.Empty.Clone();
|
||||
moved = true;
|
||||
Debug.Log("[NecromancerTome] GeoOssuary: " + store[free].itemValue.ItemClass?.Name + " x" + store[free].count +
|
||||
" из ячейки " + _what + " " + (i + 1) + " -> хранилище " + (free + 1) + " в " + _te.ToWorldPos());
|
||||
}
|
||||
return moved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +319,9 @@ namespace NecromancerTome
|
||||
{
|
||||
if (_entity is EntityTrader)
|
||||
{
|
||||
return true;
|
||||
// Только торговцы самой игры (класс EntityTrader). 16.10 (Nexus, Crozekanov): NPC мода NPCCore построены на
|
||||
// наследнике EntityTrader и тоже становились призраками - «практически невидимыми».
|
||||
return _entity.GetType() == typeof(EntityTrader);
|
||||
}
|
||||
EntityClass entityClass = EntityClass.list[_entity.entityClass];
|
||||
if (entityClass == null || entityClass.entityClassName == null)
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace NecromancerTome
|
||||
{
|
||||
// Только от первого лица (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())
|
||||
{
|
||||
if (_threadSince < 0f)
|
||||
|
||||
@@ -0,0 +1,763 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Команды разработки для проверок без мыши (18.10.2026: «И управление персонажем тоже, чтобы ты мог его повернуть как надо,
|
||||
/// довести куда надо»). Набираются через почтовый ящик (NecroMailbox), ответы - в лог. Перемещение - teleport игры
|
||||
/// (tp x y z), время - settime, предметы - giveself; здесь - то, чего у игры нет.
|
||||
/// necrolook yaw [pitch] | necrolook at x y z - повернуть взгляд (градусы; север 0, восток 90; pitch вверх +, вниз -)
|
||||
/// necroaim - позиция игрока и блок под прицелом
|
||||
/// necroplace блок x y z [поворот 0-3] - поставить блок (как игрок: OnBlockAdded, TE)
|
||||
/// necrohold предмет [число] - положить в текущую ячейку пояса и взять в руку
|
||||
/// necroslot [N] - какая ячейка пояса в руке и что в ячейках; N - взять в руку ячейку N (23.10: проверка Archer's Eye без мыши)
|
||||
/// necrofire press|release - ЛКМ предметом в руке: нажать / отпустить (лук: натянуть / выстрелить; 23.10, ошибка в Hit от стрелы)
|
||||
/// necrobox x y z list|clear|add предмет число - содержимое хранилища (ящик, сумка урны)
|
||||
/// necrourn x y z info|skull 0|1|from|to|out bx by bz|clear|sample предмет число|group все|нет|sectors all|N|-маска - урна
|
||||
/// necroclick 0|1 - отпущенная ЛКМ/ПКМ ножом для нитей по блоку под прицелом
|
||||
/// necropossess near [класс]|fp|hit|look id / x y z|slot N|belt N предмет [качество]|info|exit - одержимость без мыши (#37, 25.10)
|
||||
/// </summary>
|
||||
public static class NecroDev
|
||||
{
|
||||
static void Out(string _s) => SdtdConsole.Instance.Output(_s);
|
||||
|
||||
static float F(string _s) => float.Parse(_s, CultureInfo.InvariantCulture);
|
||||
|
||||
static Vector3i P(List<string> _p, int _i) => new Vector3i(int.Parse(_p[_i]), int.Parse(_p[_i + 1]), int.Parse(_p[_i + 2]));
|
||||
|
||||
/// <summary>Общий вход всех команд: игрок есть - выполнить, ошибка - строкой в ответ.</summary>
|
||||
public static void Run(string _cmd, List<string> _params)
|
||||
{
|
||||
World w = GameManager.Instance.World;
|
||||
EntityPlayerLocal pl = w?.GetPrimaryPlayer();
|
||||
if (pl == null)
|
||||
{
|
||||
Out(_cmd + ": нет игрока");
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
switch (_cmd)
|
||||
{
|
||||
case "necrolook": Look(pl, _params); break;
|
||||
case "necroaim": Aim(w, pl); break;
|
||||
case "necroplace": PlaceBlock(w, _params); break;
|
||||
case "necrohold": Hold(pl, _params); break;
|
||||
case "necrofire":
|
||||
{
|
||||
bool rel = _params.Count > 0 && _params[0] == "release";
|
||||
pl.inventory.Execute(0, rel, pl.playerInput);
|
||||
Out("necrofire: " + (rel ? "отпущено" : "нажато") + ", в руке " + pl.inventory.holdingItem?.Name + ", заряжено " + pl.inventory.holdingItemItemValue.Meta);
|
||||
break;
|
||||
}
|
||||
case "necroslot":
|
||||
{
|
||||
if (_params.Count > 0)
|
||||
{
|
||||
pl.inventory.SetHoldingItemIdx(int.Parse(_params[0]));
|
||||
}
|
||||
string belt = "";
|
||||
for (int i = 0; i < pl.inventory.SlotCount; i++)
|
||||
{
|
||||
ItemStack st = pl.inventory.GetItem(i);
|
||||
belt += " " + i + ":" + (st == null || st.IsEmpty() ? "-" : st.itemValue.ItemClass.GetItemName());
|
||||
}
|
||||
Out("necroslot: в руке " + pl.inventory.holdingItemIdx + ";" + belt);
|
||||
break;
|
||||
}
|
||||
case "necrobox": Box(w, _params); break;
|
||||
case "necrourn": Urn(w, _params); break;
|
||||
case "necrodamage":
|
||||
{
|
||||
// necrodamage x y z урон - повредить блок (проверка ремонтника)
|
||||
Vector3i dp = P(_params, 0);
|
||||
BlockValue dbv = w.GetBlock(dp);
|
||||
dbv.damage = Mathf.Min(dbv.Block.MaxDamage - 1, dbv.damage + int.Parse(_params[3]));
|
||||
w.SetBlockRPC(dp, dbv);
|
||||
Out("necrodamage: " + dbv.Block.GetBlockName() + " в " + dp + " повреждение " + dbv.damage + "/" + dbv.Block.MaxDamage);
|
||||
break;
|
||||
}
|
||||
case "necrodrop":
|
||||
{
|
||||
// necrodrop предмет число x y z - бросить на землю (проверка лутера)
|
||||
ItemValue div = ItemClass.GetItem(_params[0], true);
|
||||
GameManager.Instance.ItemDropServer(new ItemStack(div, int.Parse(_params[1])), P(_params, 2).ToVector3() + new Vector3(0.5f, 0.5f, 0.5f), Vector3.zero);
|
||||
Out("necrodrop: " + _params[0] + " x" + _params[1]);
|
||||
break;
|
||||
}
|
||||
case "necrobag":
|
||||
{
|
||||
// necrobag x y z - мешок с зомби (EntityLootContainer со списком cntDropBag), нетронутый
|
||||
Entity bag = EntityFactory.CreateEntity(EntityClass.FromString("DroppedLootContainer"), P(_params, 0).ToVector3() + new Vector3(0.5f, 0.3f, 0.5f));
|
||||
bag.lootList = "cntDropBag";
|
||||
LootContainer lc = LootContainer.GetLootContainer(bag.lootList, false);
|
||||
if (lc != null)
|
||||
{
|
||||
bag.bag = new Bag(lc.size, XUiC_ItemStack.StackLocationTypes.LootContainer, bag);
|
||||
}
|
||||
w.SpawnEntityInWorld(bag);
|
||||
Out("necrobag: " + bag.entityId);
|
||||
break;
|
||||
}
|
||||
case "necrocorpse":
|
||||
{
|
||||
// necrocorpse x y z [класс] - зомби, убитый и помеченный красным трупом (как VictimPatch)
|
||||
string cls = _params.Count > 3 ? _params[3] : "zombieArlene";
|
||||
EntityAlive z = EntityFactory.CreateEntity(EntityClass.FromString(cls), P(_params, 0).ToVector3() + new Vector3(0.5f, 0.1f, 0.5f)) as EntityAlive;
|
||||
w.SpawnEntityInWorld(z);
|
||||
ThreadManager.StartCoroutine(KillAndMark(z));
|
||||
Out("necrocorpse: " + z.entityId + " " + cls);
|
||||
break;
|
||||
}
|
||||
case "necroclear":
|
||||
{
|
||||
// necroclear x0 z0 x1 z1 [y0 y1] - убрать только растения (трава, кусты) в прямоугольнике; землю и постройки
|
||||
// не трогает (18.10: «necroplace air» по ровной высоте срезал рельеф - яма в test8).
|
||||
int x0 = int.Parse(_params[0]), z0 = int.Parse(_params[1]), x1 = int.Parse(_params[2]), z1 = int.Parse(_params[3]);
|
||||
int y0 = _params.Count > 5 ? int.Parse(_params[4]) : 1, y1 = _params.Count > 5 ? int.Parse(_params[5]) : 250;
|
||||
int n = 0;
|
||||
for (int x = Mathf.Min(x0, x1); x <= Mathf.Max(x0, x1); x++)
|
||||
{
|
||||
for (int z = Mathf.Min(z0, z1); z <= Mathf.Max(z0, z1); z++)
|
||||
{
|
||||
for (int y = y0; y <= y1; y++)
|
||||
{
|
||||
BlockValue cb = w.GetBlock(x, y, z);
|
||||
if (!cb.isair && cb.Block != null && cb.Block.IsPlant())
|
||||
{
|
||||
w.SetBlockRPC(new Vector3i(x, y, z), BlockValue.Air);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Out("necroclear: растений убрано " + n);
|
||||
break;
|
||||
}
|
||||
case "necrostation":
|
||||
{
|
||||
// necrostation x y z - станция текстом: огонь, топливо, вход, очередь, выход (проверка урны рабочего)
|
||||
TileEntityWorkstation ws = CinerariumThreads.StationAt(w, P(_params, 0));
|
||||
if (ws == null)
|
||||
{
|
||||
Out("necrostation: нет станции");
|
||||
break;
|
||||
}
|
||||
string Stacks(ItemStack[] _a)
|
||||
{
|
||||
StringBuilder s = new StringBuilder();
|
||||
if (_a != null)
|
||||
{
|
||||
foreach (ItemStack st in _a)
|
||||
{
|
||||
if (st != null && !st.IsEmpty())
|
||||
{
|
||||
s.Append(' ').Append(st.itemValue.ItemClass?.GetItemName()).Append('x').Append(st.count);
|
||||
}
|
||||
}
|
||||
}
|
||||
return s.Length > 0 ? s.ToString() : " -";
|
||||
}
|
||||
int q = 0;
|
||||
if (ws.Queue != null)
|
||||
{
|
||||
foreach (RecipeQueueItem r in ws.Queue)
|
||||
{
|
||||
if (r != null && r.Recipe != null && r.Multiplier > 0)
|
||||
{
|
||||
q += r.Multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
Out("necrostation: огонь " + (ws.IsBurning ? "горит" : "погашен") + ", топливо" + Stacks(ws.Fuel) + ", вход" + Stacks(ws.Input) +
|
||||
", очередь " + q + ", выход" + Stacks(ws.Output));
|
||||
break;
|
||||
}
|
||||
case "necrofill":
|
||||
{
|
||||
// necrofill x0 z0 x1 z1 y [блок] [соседей] - засыпать ямы рельефа на уровне y: пустую клетку, у которой рядом по
|
||||
// уровню не меньше двух клеток земли, - землёй с полной плотностью; повторять, пока засыпается.
|
||||
// Ровную землю рядом не поднимает (у неё на этом уровне соседи - воздух). 18.10: ямы в test8.
|
||||
int x0 = int.Parse(_params[0]), z0 = int.Parse(_params[1]), x1 = int.Parse(_params[2]), z1 = int.Parse(_params[3]);
|
||||
int fy = int.Parse(_params[4]);
|
||||
Block fb = Block.GetBlockByName(_params.Count > 5 ? _params[5] : "terrForestGround", true);
|
||||
int total = 0;
|
||||
for (int pass = 0; pass < 20; pass++)
|
||||
{
|
||||
List<Vector3i> fill = new List<Vector3i>();
|
||||
for (int x = Mathf.Min(x0, x1); x <= Mathf.Max(x0, x1); x++)
|
||||
{
|
||||
for (int z = Mathf.Min(z0, z1); z <= Mathf.Max(z0, z1); z++)
|
||||
{
|
||||
Vector3i c = new Vector3i(x, fy, z);
|
||||
if (!w.GetBlock(c).isair || w.GetBlock(c - Vector3i.up).isair)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int n = 0;
|
||||
foreach (Vector3i d in new[] { Vector3i.forward, Vector3i.back, Vector3i.left, Vector3i.right })
|
||||
{
|
||||
BlockValue nb = w.GetBlock(c + d);
|
||||
if (!nb.isair && nb.Block != null && nb.Block.shape.IsTerrain())
|
||||
{
|
||||
n++;
|
||||
}
|
||||
}
|
||||
if (n >= (_params.Count > 6 ? int.Parse(_params[6]) : 2)) // седьмой параметр - сколько соседей-земли нужно (широкие ямы - 1)
|
||||
{
|
||||
fill.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fill.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
foreach (Vector3i c in fill)
|
||||
{
|
||||
w.SetBlockRPC(c, fb.ToBlockValue(), MarchingCubes.DensityTerrain);
|
||||
}
|
||||
total += fill.Count;
|
||||
}
|
||||
Out("necrofill: засыпано клеток " + total);
|
||||
break;
|
||||
}
|
||||
case "necrosmooth":
|
||||
{
|
||||
// necrosmooth x0 z0 x1 z1 y [плотность] - край насыпи скатом: клеткам земли на уровне y, у которых сбоку воздух,
|
||||
// - неполная плотность (по умолчанию -30; полная земля -128), игра рисует их наклоном.
|
||||
int x0 = int.Parse(_params[0]), z0 = int.Parse(_params[1]), x1 = int.Parse(_params[2]), z1 = int.Parse(_params[3]);
|
||||
int sy = int.Parse(_params[4]);
|
||||
sbyte dens = (sbyte)(_params.Count > 5 ? int.Parse(_params[5]) : -30);
|
||||
List<(Vector3i, BlockValue)> edge = new List<(Vector3i, BlockValue)>();
|
||||
for (int x = Mathf.Min(x0, x1); x <= Mathf.Max(x0, x1); x++)
|
||||
{
|
||||
for (int z = Mathf.Min(z0, z1); z <= Mathf.Max(z0, z1); z++)
|
||||
{
|
||||
Vector3i c = new Vector3i(x, sy, z);
|
||||
BlockValue bv = w.GetBlock(c);
|
||||
if (bv.isair || bv.Block == null || !bv.Block.shape.IsTerrain() || !w.GetBlock(c + Vector3i.up).isair)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach (Vector3i d in new[] { Vector3i.forward, Vector3i.back, Vector3i.left, Vector3i.right })
|
||||
{
|
||||
if (w.GetBlock(c + d).isair)
|
||||
{
|
||||
edge.Add((c, bv));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ((Vector3i c, BlockValue bv) in edge)
|
||||
{
|
||||
w.SetBlockRPC(c, bv, dens);
|
||||
}
|
||||
Out("necrosmooth: краевых клеток " + edge.Count);
|
||||
break;
|
||||
}
|
||||
case "necrocanplace":
|
||||
// necrocanplace блок x y z - подсказка, почему урну сюда нельзя (или «можно»)
|
||||
Out("necrocanplace: " + (TEFeatureCinerarium.PlacementRefusal(P(_params, 1), Block.GetBlockByName(_params[0], true)) ?? "можно"));
|
||||
break;
|
||||
case "necrogod": God(pl, _params.Count == 0 || _params[0] != "off"); break;
|
||||
case "necropossess": Possess(w, pl, _params); break;
|
||||
case "necroclick":
|
||||
// necroclick 0|1 [x y z] - по блоку под прицелом или по указанной клетке (луч не всегда попадает в низкий череп).
|
||||
if (_params.Count >= 4)
|
||||
{
|
||||
CinerariumThreads.ClickAt(pl, w, P(_params, 1), _params[0] == "1");
|
||||
}
|
||||
else
|
||||
{
|
||||
CinerariumThreads.Click(pl, _params.Count > 0 && _params[0] == "1");
|
||||
}
|
||||
Out("necroclick: ok");
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Out(_cmd + ": ошибка - " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
static void Look(EntityPlayerLocal _pl, List<string> _p)
|
||||
{
|
||||
float yaw, pitch = 0f;
|
||||
if (_p.Count >= 4 && _p[0] == "at")
|
||||
{
|
||||
Vector3 eye = _pl.getHeadPosition();
|
||||
Vector3 t = new Vector3(F(_p[1]) + 0.5f, F(_p[2]) + 0.5f, F(_p[3]) + 0.5f);
|
||||
Vector3 d = t - eye;
|
||||
yaw = Mathf.Atan2(d.x, d.z) * Mathf.Rad2Deg;
|
||||
pitch = Mathf.Atan2(d.y, new Vector2(d.x, d.z).magnitude) * Mathf.Rad2Deg; // в игре + вверх
|
||||
}
|
||||
else
|
||||
{
|
||||
yaw = F(_p[0]);
|
||||
if (_p.Count > 1)
|
||||
{
|
||||
pitch = F(_p[1]);
|
||||
}
|
||||
}
|
||||
_pl.TeleportToPosition(_pl.position, false, new Vector3(pitch, yaw, 0f));
|
||||
Out("necrolook: yaw " + yaw.ToString("F0") + ", pitch " + pitch.ToString("F0"));
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
_pl.IsGodMode.Value = _on;
|
||||
_pl.SetIgnoredByAI(_on);
|
||||
Out("necrogod: " + (_on ? "бог, ИИ не видит" : "обычный"));
|
||||
}
|
||||
|
||||
static System.Collections.IEnumerator KillAndMark(EntityAlive _z)
|
||||
{
|
||||
yield return new WaitForSeconds(1f);
|
||||
_z.DamageEntity(new DamageSource(EnumDamageSource.Internal, EnumDamageTypes.Suicide), 99999, false);
|
||||
yield return new WaitForSeconds(1f);
|
||||
if (_z.IsDead())
|
||||
{
|
||||
CorpseLoot.MarkCorpse(_z);
|
||||
}
|
||||
UnityEngine.Debug.Log("[NecromancerTome] mailbox: necrocorpse: " + _z.entityId + (_z.IsDead() ? " убит и помечен" : " НЕ умер"));
|
||||
}
|
||||
|
||||
static void Aim(World _w, EntityPlayerLocal _pl)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder("necroaim: игрок ").Append(_pl.position.ToString("F1")).Append(", взгляд ").Append(_pl.rotation.ToString("F0"));
|
||||
WorldRayHitInfo hi = _pl.HitInfo;
|
||||
if (hi != null && hi.bHitValid)
|
||||
{
|
||||
BlockValue bv = _w.GetBlock(hi.hit.blockPos);
|
||||
sb.Append(", под прицелом ").Append(hi.hit.blockPos).Append(' ').Append(bv.Block?.GetBlockName()).Append(" rot ").Append(bv.rotation)
|
||||
.Append(", дальность ").Append(Mathf.Sqrt(hi.hit.distanceSq).ToString("F1"));
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(", под прицелом ничего");
|
||||
}
|
||||
Out(sb.ToString());
|
||||
}
|
||||
|
||||
static void PlaceBlock(World _w, List<string> _p)
|
||||
{
|
||||
Block b = Block.GetBlockByName(_p[0], true);
|
||||
if (b == null)
|
||||
{
|
||||
Out("necroplace: нет блока " + _p[0]);
|
||||
return;
|
||||
}
|
||||
BlockValue bv = b.ToBlockValue();
|
||||
if (_p.Count > 4)
|
||||
{
|
||||
bv.rotation = (byte)int.Parse(_p[4]);
|
||||
}
|
||||
Vector3i pos = P(_p, 1);
|
||||
_w.SetBlockRPC(pos, bv);
|
||||
// Как поставленный игроком (хозяин - мы): ящики без хозяина нити и духи не считают ящиками игрока.
|
||||
if (_w.GetTileEntity(pos) is TileEntityComposite comp && comp.Owner == null)
|
||||
{
|
||||
comp.SetOwner(Platform.PlatformManager.InternalLocalUserIdentifier);
|
||||
}
|
||||
Out("necroplace: " + b.GetBlockName() + " в " + pos + " rot " + bv.rotation);
|
||||
}
|
||||
|
||||
static void Hold(EntityPlayerLocal _pl, List<string> _p)
|
||||
{
|
||||
ItemValue iv = ItemClass.GetItem(_p[0], true);
|
||||
if (iv == null || iv.IsEmpty())
|
||||
{
|
||||
Out("necrohold: нет предмета " + _p[0]);
|
||||
return;
|
||||
}
|
||||
int n = _p.Count > 1 ? int.Parse(_p[1]) : 1;
|
||||
if (iv.ItemClass.HasQuality)
|
||||
{
|
||||
iv = new ItemValue(iv.type, 1, 1, true);
|
||||
}
|
||||
_pl.inventory.SetItem(_pl.inventory.holdingItemIdx, new ItemStack(iv, n));
|
||||
Out("necrohold: " + iv.ItemClass.GetItemName() + " x" + n + " в ячейке " + _pl.inventory.holdingItemIdx);
|
||||
}
|
||||
|
||||
static TEFeatureStorage StorageAt(World _w, Vector3i _p)
|
||||
{
|
||||
BlockValue bv = _w.GetBlock(_p);
|
||||
Vector3i owner = bv.ischild ? _p + bv.parent : _p;
|
||||
return _w.GetTileEntity(owner) is TileEntityComposite c && c.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage s) ? s : null;
|
||||
}
|
||||
|
||||
static void Box(World _w, List<string> _p)
|
||||
{
|
||||
TEFeatureStorage s = StorageAt(_w, P(_p, 0));
|
||||
if (s == null)
|
||||
{
|
||||
Out("necrobox: нет хранилища в " + P(_p, 0));
|
||||
return;
|
||||
}
|
||||
string op = _p.Count > 3 ? _p[3] : "list";
|
||||
if (op == "fill")
|
||||
{
|
||||
// Все пустые ячейки - полной стопкой предмета (ящик «полон» по-честному).
|
||||
ItemValue iv = ItemClass.GetItem(_p[4], true);
|
||||
for (int i = 0; i < s.ItemGrid.Length; i++)
|
||||
{
|
||||
if (s.ItemGrid[i] == null || s.ItemGrid[i].IsEmpty())
|
||||
{
|
||||
s.UpdateSlot(i, new ItemStack(iv.Clone(), iv.ItemClass.Stacknumber.Value));
|
||||
}
|
||||
}
|
||||
s.SetModified();
|
||||
}
|
||||
else if (op == "clear")
|
||||
{
|
||||
for (int i = 0; i < s.ItemGrid.Length; i++)
|
||||
{
|
||||
s.UpdateSlot(i, ItemStack.Empty.Clone());
|
||||
}
|
||||
s.SetModified();
|
||||
}
|
||||
else if (op == "add")
|
||||
{
|
||||
ItemValue iv = ItemClass.GetItem(_p[4], true);
|
||||
ItemStack st = new ItemStack(iv, int.Parse(_p[5]));
|
||||
CinerariumWork.Place(st, new[] { s });
|
||||
s.SetModified();
|
||||
if (st.count > 0)
|
||||
{
|
||||
Out("necrobox: не влезло " + st.count);
|
||||
}
|
||||
}
|
||||
StringBuilder sb = new StringBuilder("necrobox: ").Append(P(_p, 0)).Append(':');
|
||||
for (int i = 0; i < s.ItemGrid.Length; i++)
|
||||
{
|
||||
ItemStack st = s.ItemGrid[i];
|
||||
if (st != null && !st.IsEmpty())
|
||||
{
|
||||
sb.Append(' ').Append(st.itemValue.ItemClass?.GetItemName()).Append('x').Append(st.count);
|
||||
}
|
||||
}
|
||||
Out(sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>Черепа на пути нити: тройки координат после конца нити, по порядку от урны.</summary>
|
||||
static List<Vector3i> Via(List<string> _p, int _i)
|
||||
{
|
||||
List<Vector3i> l = new List<Vector3i>();
|
||||
for (int k = _i; k + 2 < _p.Count; k += 3)
|
||||
{
|
||||
l.Add(P(_p, k));
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
static void Urn(World _w, List<string> _p)
|
||||
{
|
||||
if (_p.Count > 0 && _p[0] == "budget")
|
||||
{
|
||||
// necrourn budget [N] - кому из урн дали шаг и кому отказали с прошлого вызова; N - дел в секунду на все урны (30)
|
||||
if (_p.Count > 1)
|
||||
{
|
||||
CinerariumLoad.StepsPerSecond = float.Parse(_p[1], System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
Out("necrourn budget: " + CinerariumLoad.Budget());
|
||||
return;
|
||||
}
|
||||
Vector3i pos = P(_p, 0);
|
||||
TEFeatureCinerarium u = CinerariumThreads.UrnAt(_w, pos);
|
||||
if (u == null)
|
||||
{
|
||||
Out("necrourn: нет урны в " + pos);
|
||||
return;
|
||||
}
|
||||
string op = _p.Count > 3 ? _p[3] : "info";
|
||||
switch (op)
|
||||
{
|
||||
case "skull": u.SetSkull(_p[4] == "1"); break;
|
||||
case "from": u.AddThreadFrom(P(_p, 4), Via(_p, 7)); break;
|
||||
case "to": u.SetThreadTo(P(_p, 4), Via(_p, 7)); break;
|
||||
case "out": u.AddThreadOut(P(_p, 4), Via(_p, 7)); break;
|
||||
case "clear": u.ClearThreads(); break;
|
||||
case "sample":
|
||||
u.SetSample(_p.Count > 5 ? new ItemStack(ItemClass.GetItem(_p[4], true), int.Parse(_p[5])) : ItemStack.Empty.Clone());
|
||||
break;
|
||||
case "contents": CinerariumContents.Open(GameManager.Instance.World.GetPrimaryPlayer(), u); break;
|
||||
case "probe": Out("necrourn: " + pos + " " + CinerariumWork.Probe(GameManager.Instance.World, u)); return;
|
||||
case "run": u.ZoneCommand("zurn_run", GameManager.Instance.World.GetPrimaryPlayer()); break;
|
||||
case "stop": u.ZoneCommand("zurn_stop", null); 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":
|
||||
u.GroupMask = _p[4] == "нет" || _p[4] == "none" ? 0u : CinerariumGroups.All;
|
||||
u.SetModified();
|
||||
break;
|
||||
}
|
||||
string blink = "нет";
|
||||
Chunk ch = _w.GetChunkFromWorldPos(pos) as Chunk;
|
||||
BlockEntityData be = ch?.GetBlockEntity(pos);
|
||||
if (be != null && be.bHasTransform)
|
||||
{
|
||||
CinerariumBlink cb = be.transform.GetComponent<CinerariumBlink>();
|
||||
blink = cb == null ? "нет" : (cb.enabled ? "мигает" : "выкл") + " (" + cb.Renderers.Count + " рендер.)";
|
||||
}
|
||||
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.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());
|
||||
}
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroLook : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrolook" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): повернуть взгляд: yaw [pitch] | at x y z";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrolook", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroAim : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necroaim" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): позиция игрока и блок под прицелом";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necroaim", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroPlace : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necroplace" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): поставить блок: имя x y z [поворот]";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necroplace", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroHold : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrohold" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): предмет в руку: имя [число]";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrohold", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroFire : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrofire" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): ЛКМ предметом в руке: press|release";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrofire", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroSlot : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necroslot" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): ячейка пояса в руке: [N]";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necroslot", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroBox : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrobox" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): хранилище: x y z list|clear|add предмет число";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrobox", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroUrn : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrourn" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): урна: x y z info|skull|from|to|out|clear|sample|group";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrourn", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroClick : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necroclick" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): нож для нитей: 0 - ЛКМ, 1 - ПКМ по блоку под прицелом";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
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]
|
||||
public class ConsoleCmdNecroGod : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrogod" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): режим бога и невидимость для ИИ [off]";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrogod", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroDamage : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrodamage" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): повредить блок x y z урон";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrodamage", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroDrop : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrodrop" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): бросить предмет: имя число x y z";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrodrop", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroBag : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrobag" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): мешок с зомби x y z";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrobag", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroCorpse : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrocorpse" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): красный труп x y z [класс]";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrocorpse", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroClear : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necroclear" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): убрать растения x0 z0 x1 z1 [y0 y1]";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necroclear", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroStation : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrostation" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): станция текстом x y z";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrostation", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroFill : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrofill" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): засыпать ямы x0 z0 x1 z1 y [блок]";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrofill", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroSmooth : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrosmooth" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): край насыпи скатом x0 z0 x1 z1 y [плотность]";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrosmooth", _params);
|
||||
}
|
||||
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroCanPlace : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrocanplace" };
|
||||
public override string getDescription() => "NecromancerTome (разработка): почему урну нельзя поставить: блок x y z";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrocanplace", _params);
|
||||
}
|
||||
}
|
||||
@@ -83,9 +83,88 @@ namespace NecromancerTome
|
||||
string dir = Path.Combine(NecroMailbox.Dir, "shots");
|
||||
Directory.CreateDirectory(dir);
|
||||
string name = _params.Count > 0 ? _params[0] : System.DateTime.Now.ToString("HHmmss");
|
||||
// necroshot имя N интервал - серия из N кадров (имя1..имяN) через интервал секунд (18.10: мигание, полёт духа).
|
||||
if (_params.Count >= 3 && int.TryParse(_params[1], out int n) &&
|
||||
float.TryParse(_params[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float dt))
|
||||
{
|
||||
ThreadManager.StartCoroutine(Burst(dir, name, n, dt));
|
||||
SdtdConsole.Instance.Output("серия: " + n + " кадров через " + dt + " с");
|
||||
return;
|
||||
}
|
||||
string path = Path.Combine(dir, name + ".png");
|
||||
ScreenCapture.CaptureScreenshot(path);
|
||||
SdtdConsole.Instance.Output("снимок: " + path);
|
||||
}
|
||||
|
||||
static System.Collections.IEnumerator Burst(string _dir, string _name, int _n, float _dt)
|
||||
{
|
||||
for (int i = 1; i <= _n; i++)
|
||||
{
|
||||
ScreenCapture.CaptureScreenshot(Path.Combine(_dir, _name + i + ".png"));
|
||||
yield return new WaitForSecondsRealtime(_dt);
|
||||
}
|
||||
Debug.Log("[NecromancerTome] mailbox: серия " + _name + " готова");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>necrospawn - нажать «Появиться» в окне появления (18.10: ассистент сам запускает игру с сейвом
|
||||
/// -LoadSaveGame=true и входит в мир без мыши). То же, что кнопка: XUiC_SpawnSelectionWindow.SpawnButtonPressed.</summary>
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroSpawn : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrospawn" };
|
||||
public override string getDescription() => "NecromancerTome: нажать «Появиться» (разработка, почтовый ящик)";
|
||||
public override bool AllowedInMainMenu => false;
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
foreach (LocalPlayerUI ui in LocalPlayerUI.PlayerUIs)
|
||||
{
|
||||
XUiC_SpawnSelectionWindow w = ui?.xui?.GetWindowByType<XUiC_SpawnSelectionWindow>();
|
||||
if (w != null && w.IsOpen)
|
||||
{
|
||||
w.SpawnButtonPressed(SpawnMethod.Invalid);
|
||||
SdtdConsole.Instance.Output("necrospawn: нажато «Появиться»");
|
||||
return;
|
||||
}
|
||||
}
|
||||
SdtdConsole.Instance.Output("necrospawn: окно появления не открыто");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>necroexit [quit] - выйти из мира с сохранением, как «Выход» в меню паузы (GameManager.Disconnect); с quit -
|
||||
/// затем закрыть игру. Нужно перед сборкой бандлов и заменой DLL (игра держит файлы).</summary>
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroExit : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necroexit" };
|
||||
public override string getDescription() => "NecromancerTome: выйти из мира с сохранением [quit - и закрыть игру] (разработка)";
|
||||
public override bool AllowedInMainMenu => true;
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
bool quit = _params.Count > 0 && _params[0] == "quit";
|
||||
SdtdConsole.Instance.Output("necroexit: выход" + (quit ? " и закрытие игры" : " в меню"));
|
||||
ThreadManager.StartCoroutine(ExitCo(quit));
|
||||
}
|
||||
|
||||
static System.Collections.IEnumerator ExitCo(bool _quit)
|
||||
{
|
||||
if (GameManager.Instance.World != null)
|
||||
{
|
||||
GameManager.Instance.Disconnect();
|
||||
while (GameManager.Instance.World != null)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
Debug.Log("[NecromancerTome] necroexit: мир сохранён и закрыт");
|
||||
}
|
||||
if (_quit)
|
||||
{
|
||||
yield return new WaitForSecondsRealtime(2f);
|
||||
Debug.Log("[NecromancerTome] necroexit: закрываю игру");
|
||||
Application.Quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,17 @@ namespace NecromancerTome
|
||||
ImageTop = true,
|
||||
Unlocked = _p => GameUtils.WorldTimeToDays(_p.world.worldTime) >= TideChapterDay
|
||||
},
|
||||
// Урны некротехнологов (18.10, после всех урн - «нужно будет сделать иллюстрацию для книги некроманта и рисунки»):
|
||||
// лист Fig. VIII - пять урн эскизом с ремёслами на эсперанто (BlenderWork/make_urn_sheet.py). Открывается вместе с
|
||||
// рецептом урны.
|
||||
new Chapter
|
||||
{
|
||||
TitleKey = "necroTomeUrnsTitle",
|
||||
PageKeys = new[] { "necroTomeUrnsP1", "necroTomeUrnsP2", "necroTomeUrnsP3" },
|
||||
Image = "@modfolder(NecromancerTome):Textures/necroTomeUrns.png",
|
||||
ImageTop = true,
|
||||
Unlocked = _p => RecipeUnlocked(_p, "necroCinerarium")
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>С какого дня видна глава о кровавом приливе. По замыслу - 5 (тогда же надпись на доске
|
||||
@@ -265,6 +276,16 @@ namespace NecromancerTome
|
||||
if (_next != null) _next.Enabled = _tocPage < pages - 1;
|
||||
}
|
||||
|
||||
/// <summary>Открыть главу по ключу заголовка (команда necrotome <ключ> [страница] - проверка без мыши).</summary>
|
||||
public void ShowChapterKey(string _titleKey, int _page)
|
||||
{
|
||||
int i = NecroTomeBook.Chapters.FindIndex(c => c.TitleKey == _titleKey);
|
||||
if (i >= 0)
|
||||
{
|
||||
ShowChapter(i, _page);
|
||||
}
|
||||
}
|
||||
|
||||
void ShowChapter(int _c, int _p)
|
||||
{
|
||||
if (_c < 0 || _c >= NecroTomeBook.Chapters.Count)
|
||||
@@ -414,6 +435,11 @@ namespace NecromancerTome
|
||||
if (player != null)
|
||||
{
|
||||
NecroTomeBook.Open(player);
|
||||
if (_params.Count > 0)
|
||||
{
|
||||
XUiC_NecroTome w = LocalPlayerUI.GetUIForPlayer(player)?.xui?.GetWindowByType<XUiC_NecroTome>();
|
||||
w?.ShowChapterKey(_params[0], _params.Count > 1 ? int.Parse(_params[1]) : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,715 @@
|
||||
using System.Collections.Generic;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Новый оссуарий-котёл (#67, 22.10.2026): свечение модели и отладочная команда necroossuary.
|
||||
/// Свечение - руны на котле, знаки на крышке, прах и кости внутри (карта _e, цвет - EmissionColor через MaterialPropertyBlock,
|
||||
/// общий материал не трогается) и свет glow внутри котла (дочерний объект префаба, выключен) - пока оссуарий исправно работает
|
||||
/// (22.10: «Когда оссуарий исправно работает, он должен подсвечиваться изнутри. Чтобы сразу было видно если что-то не так»;
|
||||
/// «Пусть руны тоже загораются»). Сфера в держателе (sphere) видна, пока в топливе есть сфера или камень. Следит компонент
|
||||
/// OssuaryVisual на модели - поэтому работает и у клиентов сервера. Выключатель - necroossuary off (модель как в префабе).
|
||||
/// </summary>
|
||||
public static class OssuaryGlow
|
||||
{
|
||||
public static bool Enabled = true;
|
||||
/// <summary>Дикий дух-огонёк (#69): летает, пока оссуарий работает. Выключатель - necroossuary wisp off.</summary>
|
||||
public static bool WispEnabled = true;
|
||||
/// <summary>necroossuary wisp log - писать в лог вылет и влёт (чтобы успеть снять).</summary>
|
||||
public static bool WispLog;
|
||||
/// <summary>necroossuary wisp trace - трассер: линии обоих путей огонька (вылет и влёт) у каждого котла (22.10: «А не проще
|
||||
/// для отладки нарисовать трассер, по которому будет лететь огонёк?»).</summary>
|
||||
public static bool WispTrace;
|
||||
public const string WispPath = "#@modfolder(NecromancerTome):Resources/necrowisp?necroWispPrefab.prefab";
|
||||
static GameObject _wispPrefab;
|
||||
static bool _wispTried;
|
||||
|
||||
public static GameObject WispPrefab()
|
||||
{
|
||||
if (!_wispTried)
|
||||
{
|
||||
_wispTried = true;
|
||||
_wispPrefab = DataLoader.LoadAsset<GameObject>(WispPath);
|
||||
Debug.Log("[NecromancerTome] OssuaryGlow: огонёк " + (_wispPrefab != null ? "загружен" : "НЕ загружен: " + WispPath));
|
||||
}
|
||||
return _wispPrefab;
|
||||
}
|
||||
|
||||
/// <summary>Цвет свечения (HDR). 22.10: было (2, 0.24, 0.12) - «Руны нарисованы красным. Это не выглядит как свечение»:
|
||||
/// днём смотрелось краской. Теперь ярче и теплее, как раскалённый уголь, с пульсацией. Подбор - necroossuary color / pulse.</summary>
|
||||
public static Color GlowColor = new Color(6f, 1.4f, 0.35f, 1f);
|
||||
/// <summary>Размах пульсации свечения (доля яркости) и её скорость (радиан в секунду).</summary>
|
||||
public static float GlowPulse = 0.3f, GlowPulseSpeed = 1.6f;
|
||||
static readonly MaterialPropertyBlock _block = new MaterialPropertyBlock();
|
||||
static readonly int EmissionColorId = Shader.PropertyToID("_EmissionColor");
|
||||
|
||||
public static Transform Model(World _w, Vector3i _pos)
|
||||
{
|
||||
Chunk chunk = _w.GetChunkFromWorldPos(_pos) as Chunk;
|
||||
BlockEntityData be = chunk?.GetBlockEntity(_pos);
|
||||
return be != null && be.bHasTransform ? be.transform : null;
|
||||
}
|
||||
|
||||
/// <summary>true - модель найдена и переключена.</summary>
|
||||
public static bool Set(Transform _model, bool _on, bool _sphere = true)
|
||||
{
|
||||
if (_model == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
foreach (string part in new[] { "body", "lid" })
|
||||
{
|
||||
Transform t = _model.Find(part);
|
||||
Renderer r = t != null ? t.GetComponent<Renderer>() : null;
|
||||
if (r == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
r.GetPropertyBlock(_block);
|
||||
_block.SetColor(EmissionColorId, _on ? GlowColor : Color.black);
|
||||
r.SetPropertyBlock(_block);
|
||||
}
|
||||
Transform glow = _model.Find("glow");
|
||||
if (glow != null)
|
||||
{
|
||||
glow.gameObject.SetActive(_on);
|
||||
}
|
||||
Transform sphere = _model.Find("sphere");
|
||||
if (sphere != null)
|
||||
{
|
||||
sphere.gameObject.SetActive(_sphere);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Пульсация: свечение рун и нутра - GlowColor с множителем _k (каждый кадр, пока светится).</summary>
|
||||
public static void Pulse(Transform _model, float _k)
|
||||
{
|
||||
foreach (string part in new[] { "body", "lid" })
|
||||
{
|
||||
Transform t = _model.Find(part);
|
||||
Renderer r = t != null ? t.GetComponent<Renderer>() : null;
|
||||
if (r == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
r.GetPropertyBlock(_block);
|
||||
_block.SetColor(EmissionColorId, GlowColor * _k);
|
||||
r.SetPropertyBlock(_block);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Оссуарии (главные блоки) в кубе вокруг точки.</summary>
|
||||
public static List<Vector3i> Find(World _w, Vector3i _c, int _r, int _ry)
|
||||
{
|
||||
var res = new List<Vector3i>();
|
||||
for (int x = _c.x - _r; x <= _c.x + _r; x++)
|
||||
{
|
||||
for (int z = _c.z - _r; z <= _c.z + _r; z++)
|
||||
{
|
||||
for (int y = Mathf.Max(1, _c.y - _ry); y <= Mathf.Min(254, _c.y + _ry); y++)
|
||||
{
|
||||
var p = new Vector3i(x, y, z);
|
||||
BlockValue bv = _w.GetBlock(p);
|
||||
if (!bv.ischild && GeoOssuary.Is(bv.Block))
|
||||
{
|
||||
res.Add(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>На модели оссуария: два раза в секунду смотрит на коллектор и переключает сферу и свечение, только при смене.
|
||||
/// anyEnabled - ванильная проверка «есть рабочий выход»: учитывает топливо, заполненный выход, воду и пустые ячейки
|
||||
/// модификаций (GeoOssuaryPatch.isDisabled).</summary>
|
||||
public class OssuaryVisual : MonoBehaviour
|
||||
{
|
||||
public Vector3i Pos;
|
||||
float _next;
|
||||
int _state = -1;
|
||||
readonly float _phase = Random.Range(0f, 6.28f); // соседние котлы пульсируют не в такт
|
||||
|
||||
void Update()
|
||||
{
|
||||
bool lit = _state > 0 && (_state & 1) != 0 && OssuaryGlow.Enabled; // -1 - ещё не смотрели
|
||||
_wisp.Tick(transform, lit && OssuaryGlow.WispEnabled);
|
||||
if (lit && OssuaryGlow.GlowPulse > 0f)
|
||||
{
|
||||
OssuaryGlow.Pulse(transform, 1f + OssuaryGlow.GlowPulse * Mathf.Sin(Time.time * OssuaryGlow.GlowPulseSpeed + _phase));
|
||||
}
|
||||
if (Time.time < _next)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_next = Time.time + 0.5f;
|
||||
World w = GameManager.Instance?.World;
|
||||
if (w == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int st; // бит 1 - светится, бит 2 - сфера видна; выключатель - как в префабе (2)
|
||||
if (OssuaryGlow.Enabled)
|
||||
{
|
||||
st = 0;
|
||||
if (w.GetTileEntity(Pos) is TileEntityCollector te)
|
||||
{
|
||||
bool fuel = false;
|
||||
foreach (ItemStack s in te.FuelSlots)
|
||||
{
|
||||
if (s != null && !s.IsEmpty())
|
||||
{
|
||||
fuel = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
st = (fuel ? 2 : 0) + (fuel && te.anyEnabled() ? 1 : 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
st = 2;
|
||||
}
|
||||
if (st == _state)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_state = st;
|
||||
OssuaryGlow.Set(transform, (st & 1) != 0, (st & 2) != 0);
|
||||
}
|
||||
|
||||
readonly OssuaryWisp _wisp = new OssuaryWisp();
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
_wisp.Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Дикий дух оссуария (#69; 20.10: «Хватит одного огонька. Пусть светится сам. Производительность нам важна»):
|
||||
/// чистая анимация, без сущностей (22.10: «периодически из окошка в котле вылетает дух и уходит вниз, увеличивая прозрачность
|
||||
/// до полной, а через какое-то время с другой стороны снизу вылетает полностью прозрачный дух, плавно уменьшая прозрачность,
|
||||
/// и влетает в противоположное окошко»). Один огонёк на котёл (бандл necrowisp), только пока оссуарий исправно работает
|
||||
/// и игрок ближе WispViewRange. На добычу не влияет.</summary>
|
||||
public class OssuaryWisp
|
||||
{
|
||||
public const float WispViewRange = 60f;
|
||||
/// <summary>Окошки крышки (модель #67): шесть, через 60°, со сдвигом WindowAngle от оси X модели (рамки окошек в сетке -
|
||||
/// на 30°, 90°...; на 0°, 60°... - металлические полосы: 22.10 «Светлячок заходит в полосы металла, и игнорирует окошки»).
|
||||
/// По высоте - на WindowUp от низа крышки, от оси - на WindowR (доли размеров крышки; в модели окошко 0.46-0.56 м
|
||||
/// при низе крышки 0.382, радиус 0.37 при полуширине 0.418).</summary>
|
||||
const float WindowAngle = 30f;
|
||||
const float WindowUp = 0.13f / 0.836f, WindowR = 0.37f / 0.418f;
|
||||
/// <summary>Подъём и спуск - вертикально снаружи, на Clear дальше самого широкого места котла (22.10: «Огонёк летит сквозь
|
||||
/// котёл. А надо чтобы поднимался почти на уровень окошка, и по дуге летел прямо через его центр»).</summary>
|
||||
const float Clear = 0.25f;
|
||||
const float BelowWindow = 0.1f; // вертикаль кончается на столько ниже окошка, м; дальше дуга
|
||||
const float Under = 0.35f; // насколько под землю, м
|
||||
const float FlyTime = 2.4f;
|
||||
static readonly int TintId = Shader.PropertyToID("_TintColor");
|
||||
static readonly int ColorId = Shader.PropertyToID("_Color");
|
||||
static readonly MaterialPropertyBlock _mpb = new MaterialPropertyBlock();
|
||||
GameObject _go;
|
||||
Renderer[] _rs;
|
||||
Color[] _base;
|
||||
TrailRenderer _trail;
|
||||
int _phase = -1; // -1 дома, 0 вылет и уход в землю, 1 под землёй, 2 подъём и влёт
|
||||
float _t0, _until;
|
||||
LineRenderer _traceOut, _traceIn;
|
||||
Vector3[] _pathOut, _pathIn; // вылет: нутро -> окошко -> дуга -> вниз в землю; влёт - обратный порядок с другой стороны
|
||||
|
||||
static float Ease(float _k) => _k * _k * (3f - 2f * _k);
|
||||
|
||||
public void Tick(Transform _model, bool _work)
|
||||
{
|
||||
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||||
bool near = pl != null && (pl.position - Origin.position - _model.position).sqrMagnitude < WispViewRange * WispViewRange;
|
||||
float now = Time.time;
|
||||
if (!_work || !near)
|
||||
{
|
||||
if (_phase >= 0)
|
||||
{
|
||||
Hide();
|
||||
_phase = -1;
|
||||
}
|
||||
_until = Mathf.Max(_until, now + Random.Range(1f, 4f));
|
||||
return;
|
||||
}
|
||||
if (_go == null && !Spawn(_model))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!OssuaryGlow.WispTrace && _traceOut != null && _traceOut.enabled)
|
||||
{
|
||||
_traceOut.enabled = _traceIn.enabled = false;
|
||||
}
|
||||
else if (OssuaryGlow.WispTrace && _pathOut == null)
|
||||
{
|
||||
Places(_model);
|
||||
}
|
||||
float k = Mathf.Clamp01((now - _t0) / Mathf.Max(0.01f, _until - _t0));
|
||||
switch (_phase)
|
||||
{
|
||||
case -1:
|
||||
if (now < _until || !Places(_model))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Show(_pathOut[0]);
|
||||
if (OssuaryGlow.WispLog)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] OssuaryWisp: вылет " + _pathOut[1]);
|
||||
}
|
||||
Next(0, FlyTime);
|
||||
return;
|
||||
case 0: // из окошка наружу и вниз, в землю; тает
|
||||
Fly(_pathOut, k);
|
||||
Alpha(1f - Ease(Mathf.InverseLerp(0.45f, 1f, k)));
|
||||
if (k >= 1f)
|
||||
{
|
||||
Hide();
|
||||
Next(1, Random.Range(12f, 20f));
|
||||
}
|
||||
return;
|
||||
case 1: // под землёй
|
||||
if (now >= _until)
|
||||
{
|
||||
Alpha(0f);
|
||||
Show(_pathIn[0]);
|
||||
if (OssuaryGlow.WispLog)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] OssuaryWisp: влёт " + _pathIn[_pathIn.Length - 2]);
|
||||
}
|
||||
Next(2, FlyTime);
|
||||
}
|
||||
return;
|
||||
case 2: // с другой стороны снизу вверх, проявляется, влетает в противоположное окошко
|
||||
Fly(_pathIn, k);
|
||||
Alpha(Ease(Mathf.InverseLerp(0f, 0.55f, k)));
|
||||
if (k >= 1f)
|
||||
{
|
||||
Hide();
|
||||
_phase = -1;
|
||||
_until = now + Random.Range(3f, 6f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Next(int _p, float _dur)
|
||||
{
|
||||
_phase = _p;
|
||||
_t0 = Time.time;
|
||||
_until = _t0 + _dur;
|
||||
}
|
||||
|
||||
/// <summary>Гладкая кривая (Catmull-Rom) точно через точки пути; скорость по пути ровная (время - по длинам отрезков).</summary>
|
||||
void Fly(Vector3[] _path, float _k)
|
||||
{
|
||||
Vector3 p = At(_path, Ease(_k));
|
||||
p.y += 0.015f * Mathf.Sin(Time.time * 7f); // лёгкое дрожание огонька
|
||||
_go.transform.position = p;
|
||||
}
|
||||
|
||||
/// <summary>Точка пути на доле длины _s (0..1).</summary>
|
||||
static Vector3 At(Vector3[] _path, float _s01)
|
||||
{
|
||||
float total = 0f;
|
||||
for (int i = 1; i < _path.Length; i++)
|
||||
{
|
||||
total += Vector3.Distance(_path[i - 1], _path[i]);
|
||||
}
|
||||
float s = _s01 * total;
|
||||
int seg = 0;
|
||||
float len = 0f;
|
||||
for (; seg < _path.Length - 2; seg++)
|
||||
{
|
||||
len = Vector3.Distance(_path[seg], _path[seg + 1]);
|
||||
if (s <= len)
|
||||
{
|
||||
break;
|
||||
}
|
||||
s -= len;
|
||||
}
|
||||
len = Vector3.Distance(_path[seg], _path[seg + 1]);
|
||||
float u = len > 0.0001f ? Mathf.Clamp01(s / len) : 1f;
|
||||
Vector3 p0 = _path[Mathf.Max(0, seg - 1)], p1 = _path[seg], p2 = _path[seg + 1], p3 = _path[Mathf.Min(_path.Length - 1, seg + 2)];
|
||||
return 0.5f * (2f * p1 + (p2 - p0) * u + (2f * p0 - 5f * p1 + 4f * p2 - p3) * u * u + (3f * p1 - p0 - 3f * p2 + p3) * u * u * u);
|
||||
}
|
||||
|
||||
/// <summary>Окошко вылета - случайное из шести, влёта - напротив. Путь: нутро, центр окошка, точка дуги снаружи на высоте
|
||||
/// окошка, вертикаль снаружи котла чуть ниже окошка, та же вертикаль под землёй (высота земли - лучом).</summary>
|
||||
bool Places(Transform _model)
|
||||
{
|
||||
Transform lid = _model.Find("lid");
|
||||
Transform body = _model.Find("body");
|
||||
Renderer lr = lid != null ? lid.GetComponent<Renderer>() : null;
|
||||
Renderer br = body != null ? body.GetComponent<Renderer>() : null;
|
||||
if (lr == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Bounds b = lr.bounds;
|
||||
float half = (b.size.x + b.size.z) * 0.25f;
|
||||
float bodyHalf = br != null ? (br.bounds.size.x + br.bounds.size.z) * 0.25f : half * 1.1f;
|
||||
float winY = b.min.y + WindowUp * half * 2f;
|
||||
Vector3 axis = new Vector3(b.center.x, winY, b.center.z);
|
||||
int w = Random.Range(0, 6);
|
||||
Vector3 dOut = lid.rotation * Quaternion.Euler(0f, WindowAngle + w * 60f, 0f) * Vector3.right;
|
||||
dOut.y = 0f;
|
||||
dOut.Normalize();
|
||||
_pathOut = Path(axis, dOut, half, Mathf.Max(bodyHalf, half) + Clear);
|
||||
_pathIn = Path(axis, -dOut, half, Mathf.Max(bodyHalf, half) + Clear);
|
||||
System.Array.Reverse(_pathIn);
|
||||
Trace(_model);
|
||||
return true;
|
||||
}
|
||||
|
||||
Vector3[] Path(Vector3 _axis, Vector3 _dir, float _half, float _rOut)
|
||||
{
|
||||
float rWin = _half * WindowR;
|
||||
Vector3 low = _axis + _dir * _rOut + Vector3.down * BelowWindow;
|
||||
Vector3 under = Ground(_axis + _dir * _rOut) + Vector3.down * Under;
|
||||
return new[]
|
||||
{
|
||||
_axis + _dir * (rWin * 0.45f), // внутри котла
|
||||
_axis + _dir * rWin, // центр окошка
|
||||
_axis + _dir * (rWin + (_rOut - rWin) * 0.55f), // дуга: снаружи на высоте окошка
|
||||
low, // вертикаль снаружи, чуть ниже окошка
|
||||
new Vector3(low.x, Mathf.Lerp(low.y, under.y, 0.5f), low.z),
|
||||
under // в земле
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Трассер: путь вылета и путь влёта линиями, точками той же кривой, по которой летит огонёк.</summary>
|
||||
void Trace(Transform _model)
|
||||
{
|
||||
if (!OssuaryGlow.WispTrace || _trail == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_traceOut == null)
|
||||
{
|
||||
_traceOut = Line(_model, "NecroWispTraceOut", new Color(1f, 0.6f, 0.1f));
|
||||
_traceIn = Line(_model, "NecroWispTraceIn", new Color(0.2f, 0.8f, 1f));
|
||||
}
|
||||
Fill(_traceOut, _pathOut);
|
||||
Fill(_traceIn, _pathIn);
|
||||
}
|
||||
|
||||
LineRenderer Line(Transform _model, string _name, Color _c)
|
||||
{
|
||||
var g = new GameObject(_name);
|
||||
g.transform.SetParent(_model, false);
|
||||
LineRenderer l = g.AddComponent<LineRenderer>();
|
||||
l.sharedMaterial = _trail.sharedMaterial;
|
||||
l.useWorldSpace = true;
|
||||
l.widthMultiplier = 0.015f;
|
||||
l.startColor = l.endColor = _c;
|
||||
l.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
l.receiveShadows = false;
|
||||
return l;
|
||||
}
|
||||
|
||||
void Fill(LineRenderer _l, Vector3[] _path)
|
||||
{
|
||||
const int n = 60;
|
||||
var pts = new Vector3[n + 1];
|
||||
for (int i = 0; i <= n; i++)
|
||||
{
|
||||
pts[i] = At(_path, (float)i / n);
|
||||
}
|
||||
_l.positionCount = pts.Length;
|
||||
_l.SetPositions(pts);
|
||||
_l.enabled = true;
|
||||
}
|
||||
|
||||
Vector3 Ground(Vector3 _p)
|
||||
{
|
||||
if (Physics.Raycast(_p + Vector3.up * 1.5f, Vector3.down, out RaycastHit hit, 8f, SnakeControl.WallMask, QueryTriggerInteraction.Ignore))
|
||||
{
|
||||
_p.y = hit.point.y;
|
||||
}
|
||||
else
|
||||
{
|
||||
_p.y -= 1f;
|
||||
}
|
||||
return _p;
|
||||
}
|
||||
|
||||
bool Spawn(Transform _model)
|
||||
{
|
||||
GameObject prefab = OssuaryGlow.WispPrefab();
|
||||
if (prefab == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_go = Object.Instantiate(prefab, _model, false);
|
||||
_go.name = "NecroOssuaryWisp";
|
||||
_trail = _go.GetComponent<TrailRenderer>();
|
||||
_rs = _go.GetComponentsInChildren<Renderer>(true);
|
||||
_base = new Color[_rs.Length];
|
||||
for (int i = 0; i < _rs.Length; i++)
|
||||
{
|
||||
Material m = _rs[i].sharedMaterial;
|
||||
_base[i] = m == null ? Color.white : m.HasProperty(TintId) ? m.GetColor(TintId) : m.HasProperty(ColorId) ? m.GetColor(ColorId) : Color.white;
|
||||
}
|
||||
_go.SetActive(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Прозрачность - цветом материала через MaterialPropertyBlock (шейдер аддитивный: тусклее = прозрачнее),
|
||||
/// общий материал не трогается.</summary>
|
||||
void Alpha(float _a)
|
||||
{
|
||||
for (int i = 0; i < _rs.Length; i++)
|
||||
{
|
||||
Material m = _rs[i].sharedMaterial;
|
||||
if (m == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Color c = _base[i] * _a;
|
||||
_rs[i].GetPropertyBlock(_mpb);
|
||||
if (m.HasProperty(TintId))
|
||||
{
|
||||
_mpb.SetColor(TintId, c);
|
||||
}
|
||||
if (m.HasProperty(ColorId))
|
||||
{
|
||||
_mpb.SetColor(ColorId, c);
|
||||
}
|
||||
_rs[i].SetPropertyBlock(_mpb);
|
||||
}
|
||||
}
|
||||
|
||||
void Show(Vector3 _at)
|
||||
{
|
||||
_go.transform.position = _at;
|
||||
if (_trail != null)
|
||||
{
|
||||
_trail.Clear();
|
||||
}
|
||||
_go.SetActive(true);
|
||||
if (_phase != 1)
|
||||
{
|
||||
Alpha(1f);
|
||||
}
|
||||
}
|
||||
|
||||
void Hide()
|
||||
{
|
||||
if (_go != null)
|
||||
{
|
||||
_go.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
if (_go != null)
|
||||
{
|
||||
Object.Destroy(_go);
|
||||
_go = null;
|
||||
}
|
||||
foreach (LineRenderer l in new[] { _traceOut, _traceIn })
|
||||
{
|
||||
if (l != null)
|
||||
{
|
||||
Object.Destroy(l.gameObject);
|
||||
}
|
||||
}
|
||||
_traceOut = _traceIn = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Модель оссуария появилась в мире - повесить на неё OssuaryVisual.</summary>
|
||||
[HarmonyPatch(typeof(BlockCollector), nameof(BlockCollector.OnBlockEntityTransformBeforeActivated))]
|
||||
public static class Patch_BlockCollector_OnBlockEntityTransformBeforeActivated_OssuaryVisual
|
||||
{
|
||||
public static void Postfix(Vector3i _blockPos, BlockValue _blockValue, BlockEntityData _ebcd)
|
||||
{
|
||||
if (!GeoOssuary.Is(_blockValue.Block) || _ebcd?.transform == null || _ebcd.transform.Find("body") == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
OssuaryVisual v = _ebcd.transform.GetComponent<OssuaryVisual>() ?? _ebcd.transform.gameObject.AddComponent<OssuaryVisual>();
|
||||
v.Pos = _blockPos;
|
||||
}
|
||||
}
|
||||
|
||||
public class ConsoleCmdNecroOssuary : ConsoleCmdAbstract
|
||||
{
|
||||
public override bool IsExecuteOnClient => true;
|
||||
|
||||
public override bool AllowedInMainMenu => false;
|
||||
|
||||
public override string[] getCommands()
|
||||
{
|
||||
return new string[] { "necroossuary" };
|
||||
}
|
||||
|
||||
public override string getDescription()
|
||||
{
|
||||
return "Necromancer's Tome: cauldron ossuary - find, remove, sphere/glow switch.";
|
||||
}
|
||||
|
||||
public override string getHelp()
|
||||
{
|
||||
return "necroossuary find [r] - ossuaries around the player (r blocks, default 30)\n" +
|
||||
"necroossuary remove x y z - remove the ossuary at x y z (only if it is an ossuary)\n" +
|
||||
"necroossuary on|off - off: sphere and glow no longer follow the ossuary state\n" +
|
||||
"necroossuary wisp on|off - wild spirit wisp flying while the ossuary works\n" +
|
||||
"necroossuary wisp trace|log - dev: draw the wisp paths / log take-off and return\n" +
|
||||
"necroossuary open x y z - dev: open the ossuary window as with E\n" +
|
||||
"necroossuary info x y z - slots of the ossuary (samples, fuel, storage) and whether it works\n" +
|
||||
"necroossuary put x y z mod|fuel <slot 1..> <item|none> [count] - dev: put an item into a slot (none - empty it)\n" +
|
||||
"necroossuary color r g b - glow color (HDR, e.g. 6 1.4 0.35)\n" +
|
||||
"necroossuary pulse a [speed] - glow pulse amplitude (0 - steady) and speed";
|
||||
}
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
SdtdConsole con = SingletonMonoBehaviour<SdtdConsole>.Instance;
|
||||
World w = GameManager.Instance?.World;
|
||||
EntityPlayerLocal pl = w?.GetPrimaryPlayer();
|
||||
if (w == null || pl == null || _params.Count == 0)
|
||||
{
|
||||
con.Output(getHelp());
|
||||
return;
|
||||
}
|
||||
Vector3i me = World.worldToBlockPos(pl.position);
|
||||
switch (_params[0])
|
||||
{
|
||||
case "find":
|
||||
{
|
||||
int r = _params.Count > 1 ? int.Parse(_params[1]) : 30;
|
||||
List<Vector3i> found = OssuaryGlow.Find(w, me, r, 20);
|
||||
con.Output("necroossuary: найдено " + found.Count + (found.Count > 0 ? ": " + string.Join("; ", found) : ""));
|
||||
break;
|
||||
}
|
||||
case "remove":
|
||||
{
|
||||
var p = new Vector3i(int.Parse(_params[1]), int.Parse(_params[2]), int.Parse(_params[3]));
|
||||
BlockValue bv = w.GetBlock(p);
|
||||
if (!GeoOssuary.Is(bv.Block))
|
||||
{
|
||||
con.Output("necroossuary: в " + p + " не оссуарий (" + bv.Block?.GetBlockName() + ") - не трогаю");
|
||||
break;
|
||||
}
|
||||
if (bv.ischild)
|
||||
{
|
||||
p = p + bv.parent;
|
||||
}
|
||||
w.SetBlockRPC(p, BlockValue.Air);
|
||||
con.Output("necroossuary: убран оссуарий в " + p);
|
||||
break;
|
||||
}
|
||||
case "open":
|
||||
{
|
||||
var p = new Vector3i(int.Parse(_params[1]), int.Parse(_params[2]), int.Parse(_params[3]));
|
||||
BlockValue bv = w.GetBlock(p);
|
||||
if (bv.ischild)
|
||||
{
|
||||
p = p + bv.parent;
|
||||
bv = w.GetBlock(p);
|
||||
}
|
||||
con.Output("necroossuary: открыть " + p + " - " + (GeoOssuary.Is(bv.Block) && bv.Block.OnBlockActivated(w, p, bv, pl)));
|
||||
break;
|
||||
}
|
||||
case "info":
|
||||
case "put":
|
||||
{
|
||||
var p = new Vector3i(int.Parse(_params[1]), int.Parse(_params[2]), int.Parse(_params[3]));
|
||||
BlockValue bv = w.GetBlock(p);
|
||||
if (bv.ischild)
|
||||
{
|
||||
p = p + bv.parent;
|
||||
}
|
||||
if (!(w.GetTileEntity(p) is TileEntityCollector te) || !GeoOssuary.Is(te))
|
||||
{
|
||||
con.Output("necroossuary: в " + p + " не оссуарий");
|
||||
break;
|
||||
}
|
||||
if (_params[0] == "put")
|
||||
{
|
||||
ItemStack[] slots = _params[4] == "fuel" ? te.FuelSlots : te.ModSlots;
|
||||
int i = int.Parse(_params[5]) - 1;
|
||||
bool none = _params[6] == "none";
|
||||
ItemValue iv = none ? ItemValue.None : ItemClass.GetItem(_params[6]);
|
||||
if (i < 0 || i >= slots.Length || iv == null || (!none && iv.IsEmpty()))
|
||||
{
|
||||
con.Output("necroossuary: нет ячейки " + (i + 1) + " или предмета " + _params[6]);
|
||||
break;
|
||||
}
|
||||
slots[i] = none ? ItemStack.Empty.Clone() : new ItemStack(iv, _params.Count > 7 ? int.Parse(_params[7]) : 1);
|
||||
te.SetModified();
|
||||
}
|
||||
string S(ItemStack[] _a)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
for (int i = 0; i < _a.Length; i++)
|
||||
{
|
||||
parts.Add(_a[i] == null || _a[i].IsEmpty() ? "-" : _a[i].itemValue.ItemClass?.Name + " x" + _a[i].count);
|
||||
}
|
||||
return string.Join(", ", parts);
|
||||
}
|
||||
con.Output("necroossuary " + p + ": образцы [" + S(te.ModSlots) + "]; топливо [" + S(te.FuelSlots) + "]; хранилище [" +
|
||||
S(te.Items) + "]; работает " + te.anyEnabled());
|
||||
break;
|
||||
}
|
||||
case "color":
|
||||
if (_params.Count > 3)
|
||||
{
|
||||
OssuaryGlow.GlowColor = new Color(float.Parse(_params[1], System.Globalization.CultureInfo.InvariantCulture),
|
||||
float.Parse(_params[2], System.Globalization.CultureInfo.InvariantCulture),
|
||||
float.Parse(_params[3], System.Globalization.CultureInfo.InvariantCulture), 1f);
|
||||
}
|
||||
con.Output("necroossuary: цвет свечения " + OssuaryGlow.GlowColor);
|
||||
break;
|
||||
case "pulse":
|
||||
if (_params.Count > 1)
|
||||
{
|
||||
OssuaryGlow.GlowPulse = float.Parse(_params[1], System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
if (_params.Count > 2)
|
||||
{
|
||||
OssuaryGlow.GlowPulseSpeed = float.Parse(_params[2], System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
con.Output("necroossuary: пульсация " + OssuaryGlow.GlowPulse + ", скорость " + OssuaryGlow.GlowPulseSpeed);
|
||||
break;
|
||||
case "wisp":
|
||||
if (_params.Count > 1 && _params[1] == "trace")
|
||||
{
|
||||
OssuaryGlow.WispTrace = !OssuaryGlow.WispTrace;
|
||||
con.Output("necroossuary: трассер огонька " + (OssuaryGlow.WispTrace ? "включён (оранжевый - вылет, голубой - влёт)" : "выключен"));
|
||||
break;
|
||||
}
|
||||
if (_params.Count > 1 && _params[1] == "log")
|
||||
{
|
||||
OssuaryGlow.WispLog = !OssuaryGlow.WispLog;
|
||||
con.Output("necroossuary: лог огонька " + (OssuaryGlow.WispLog ? "включён" : "выключен"));
|
||||
break;
|
||||
}
|
||||
if (_params.Count > 1)
|
||||
{
|
||||
OssuaryGlow.WispEnabled = _params[1] == "on";
|
||||
}
|
||||
con.Output("necroossuary: огонёк " + (OssuaryGlow.WispEnabled ? "летает" : "выключен"));
|
||||
break;
|
||||
case "on":
|
||||
case "off":
|
||||
OssuaryGlow.Enabled = _params[0] == "on";
|
||||
con.Output("necroossuary: сфера и свечение по работе " + (OssuaryGlow.Enabled ? "включены" : "выключены (модель как в префабе)"));
|
||||
break;
|
||||
default:
|
||||
con.Output(getHelp());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Дальний питомец после одержимости (указание 23.10.2026, #35: «Если питомец далеко от хозяина и хозяин вышел из режима
|
||||
/// одержимости, то питомец должен сразу быть отозван. Скажем, если больше 100 блоков, то отзовём, если меньше, то
|
||||
/// возвращается своим ходом»). Отзыв - тот же, что книгой (RecallPet: подсказка «… отозван», книга назад, если призыв её
|
||||
/// тратит). Далеко ушедший питомец иначе остаётся в выгруженном участке, и PetFollowPatch снимает с него владение.
|
||||
/// </summary>
|
||||
public static class PetFarRecall
|
||||
{
|
||||
/// <summary>Порог в блоках; 0 - выключено (питомец всегда идёт своим ходом, как раньше).</summary>
|
||||
public static float Distance = 100f;
|
||||
|
||||
/// <summary>true - питомец отозван (дальше ничего с ним не делать).</summary>
|
||||
public static bool TryRecall(EntityPlayer _owner, EntityAlive _pet)
|
||||
{
|
||||
if (Distance <= 0f || _owner == null || _pet == null || _pet.IsDead())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
float d = Vector3.Distance(_owner.position, _pet.position);
|
||||
string cls = EntityClass.list.TryGetValue(_pet.entityClass, out EntityClass ec) ? ec.entityClassName : null;
|
||||
if (d <= Distance || cls == null ||
|
||||
!Patch_ItemActionSpawnEntity_Spawn_PetLimit.LimitedPets.TryGetValue(cls, out Patch_ItemActionSpawnEntity_Spawn_PetLimit.PetInfo info) ||
|
||||
info.RecalledKey == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Debug.Log("[NecromancerTome] PetFarRecall: " + cls + " " + _pet.entityId + " в " + Mathf.RoundToInt(d) + " блоках от хозяина - отозван");
|
||||
Patch_ItemActionSpawnEntity_Spawn_PetLimit.RecallPet(_owner, _pet.entityId, info.RecalledKey, info.ConsumesBook ? info.SummonItemName : null);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class ConsoleCmdNecroPetFar : ConsoleCmdAbstract
|
||||
{
|
||||
public override bool IsExecuteOnClient => true;
|
||||
|
||||
public override bool AllowedInMainMenu => false;
|
||||
|
||||
public override string[] getCommands()
|
||||
{
|
||||
return new string[] { "necropetfar" };
|
||||
}
|
||||
|
||||
public override string getDescription()
|
||||
{
|
||||
return "Necromancer's Tome: recall a far pet when leaving possession - distance.";
|
||||
}
|
||||
|
||||
public override string getHelp()
|
||||
{
|
||||
return "necropetfar - status\n" +
|
||||
"necropetfar <blocks> - recall the pet if it is farther than this from you\n" +
|
||||
"necropetfar off - never recall, the pet always walks back";
|
||||
}
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
SdtdConsole con = SingletonMonoBehaviour<SdtdConsole>.Instance;
|
||||
if (_params.Count >= 1)
|
||||
{
|
||||
if (_params[0] == "off")
|
||||
{
|
||||
PetFarRecall.Distance = 0f;
|
||||
}
|
||||
else if (float.TryParse(_params[0], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float v) && v >= 0f)
|
||||
{
|
||||
PetFarRecall.Distance = v;
|
||||
}
|
||||
}
|
||||
con.Output("necropetfar: " + (PetFarRecall.Distance > 0f ? "recall farther than " + PetFarRecall.Distance + " blocks" : "off - the pet always walks back"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,9 @@ namespace NecromancerTome
|
||||
public static EntityAlive Zombie;
|
||||
/// <summary>Вселились в своего питомца (книга призыва), а не в чужого зомби.</summary>
|
||||
public static bool IsPet;
|
||||
/// <summary>Разведка (туман карты вокруг одержимого, карта открывается на нём): стервятник (08.10) и любой одержимый
|
||||
/// зомби (#36, 25.10); прочие питомцы - нет.</summary>
|
||||
public static bool Scouts => Active && (Flying || !IsPet);
|
||||
/// <summary>Питомец-стервятник: летает (FlyTick из его updateTasks).</summary>
|
||||
public static bool Flying;
|
||||
/// <summary>Враг у тела (CheckBody) - для надписи в PossessionHud.</summary>
|
||||
@@ -106,8 +109,12 @@ namespace NecromancerTome
|
||||
/// угол из PlaceCamera затирался).</summary>
|
||||
public static float ZoomFov = -1f;
|
||||
/// <summary>Зазор между когтями стервятника и спиной ноши, м; минус - когти впиваются в спину
|
||||
/// (тест 08.10: «курица висит под стервятником, а не в его когтях»).</summary>
|
||||
public const float CarryGap = -0.06f;
|
||||
/// (тест 08.10: «курица висит под стервятником, а не в его когтях»). 22.10: -0.06 -> -0.15 («поднимай курицу и кролика
|
||||
/// ещё выше. Иначе они не достают до лап»). Подбор без перезапуска - necrocarry gap.</summary>
|
||||
public static float CarryGap = -0.15f;
|
||||
/// <summary>С ношей Shift не разгоняет (22.10: «он не может разгоняться по шифту, ибо перегружен»). Выключатель -
|
||||
/// necrocarry run on|off.</summary>
|
||||
public static bool CarryNoRun = true;
|
||||
/// <summary>Цель рывка под прицелом сейчас (каждый кадр) - обычный прицел игры краснеет (AimTint).</summary>
|
||||
public static EntityAlive FlyAim;
|
||||
static float _flyDashUntil;
|
||||
@@ -149,7 +156,13 @@ namespace NecromancerTome
|
||||
public static bool FirstPerson;
|
||||
/// <summary>Пояс одержимого (13.10): 2 ячейки, пока пустые; выбор 1/2 или колесом, только от первого лица.</summary>
|
||||
public const int BeltSlots = 2;
|
||||
/// <summary>Чаще руки не успевают доиграть замах (ножи в игре - до 100+ ударов в минуту).</summary>
|
||||
const float MinWeaponSwing = 0.45f;
|
||||
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;
|
||||
static Vector3 _fpEye;
|
||||
static readonly List<Renderer> _hiddenRenderers = new List<Renderer>();
|
||||
@@ -214,6 +227,18 @@ namespace NecromancerTome
|
||||
/// <summary>Игрок, который сейчас в одержимом (тело).</summary>
|
||||
public static EntityPlayerLocal Player => _player;
|
||||
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>
|
||||
static float _bodyYaw;
|
||||
@@ -445,10 +470,12 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
// Питомец просто отпущен: его ИИ (следование, охрана, поводок) сам вернёт его к хозяину.
|
||||
// Далеко ушедший - сразу отозван (23.10, #35: PetFarRecall).
|
||||
if (pet)
|
||||
{
|
||||
z.SetMoveForwardWithModifiers(0f, 0f, 0f, false);
|
||||
z.SetAttackTarget(null, 0);
|
||||
PetFarRecall.TryRecall(player, z);
|
||||
return;
|
||||
}
|
||||
// Сам зомби в ужасе бежит от тела игрока (решение 03.10: «пугается и убегает как при отрицании»).
|
||||
@@ -561,9 +588,9 @@ namespace NecromancerTome
|
||||
Exit("кровавый прилив - дух выброшен в тело");
|
||||
return;
|
||||
}
|
||||
if (Flying)
|
||||
if (Scouts)
|
||||
{
|
||||
RevealMap(world, z); // разведка - только стервятник (указание 08.10)
|
||||
RevealMap(world, z); // разведка - стервятник (указание 08.10) и одержимый зомби (#36)
|
||||
}
|
||||
// Подсказка тела (тест 08.10: «стоял перед дверью - подсказка осталась в стервятнике»): её гасит
|
||||
// PlayerMoveController.Update, а он на время одержимости выключен. Прочие сообщения (голод и т.п.) -
|
||||
@@ -609,7 +636,8 @@ namespace NecromancerTome
|
||||
move = new Vector2(input.Move.X, input.Move.Y);
|
||||
run = input.Run.IsPressed;
|
||||
jump = input.Jump.WasPressed;
|
||||
attack = input.Primary.WasPressed;
|
||||
attack = input.Primary.WasPressed || DevAttack;
|
||||
DevAttack = false;
|
||||
ranged = input.Secondary.WasPressed;
|
||||
rangedHeld = input.Secondary.IsPressed;
|
||||
// E на клавиатуре - в PlayerActionsPermanent.Activate; у PlayerActionsLocal.Activate только
|
||||
@@ -624,8 +652,9 @@ namespace NecromancerTome
|
||||
}
|
||||
// Только гуманоиды обычной комплекции (указание 13.10, FpArms.Allows) - у остальных ~/ё ничего не делает.
|
||||
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;
|
||||
_fpEye = Vector3.zero;
|
||||
if (FirstPerson)
|
||||
@@ -659,6 +688,21 @@ namespace NecromancerTome
|
||||
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)
|
||||
{
|
||||
_crouchToggled = !_crouchToggled;
|
||||
@@ -839,7 +883,8 @@ namespace NecromancerTome
|
||||
{
|
||||
// Tab в зомби - окно его сумки, а не рюкзак персонажа (указание 13.10), как у волка.
|
||||
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.Close("windowpaging");
|
||||
@@ -856,7 +901,7 @@ namespace NecromancerTome
|
||||
{
|
||||
// Tab в волке - окно его сумки, а не рюкзак персонажа (тест 10.10: «в волке Tab - вижу сумку игрока»).
|
||||
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.Close("windowpaging");
|
||||
@@ -899,7 +944,10 @@ namespace NecromancerTome
|
||||
// до точки удара (FpArms -> FpImpact), а не по его собственному замаху.
|
||||
PossessionPowers.BreakInvis(z, "удар");
|
||||
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)
|
||||
{
|
||||
@@ -961,7 +1009,8 @@ namespace NecromancerTome
|
||||
dir += Vector3.down;
|
||||
}
|
||||
bool moving = dir.sqrMagnitude > 0.01f;
|
||||
float accel = _flyRun ? FlyAccelRun * _v.moveSpeedAggro : FlyAccel * _v.moveSpeed;
|
||||
bool run = _flyRun && !(CarryNoRun && _carry != null); // с ношей - перегружен, без разгона
|
||||
float accel = run ? FlyAccelRun * _v.moveSpeedAggro : FlyAccel * _v.moveSpeed;
|
||||
if (moving)
|
||||
{
|
||||
_v.motion = _v.motion * 0.9f + dir.normalized * (accel * 0.1f);
|
||||
@@ -976,7 +1025,7 @@ namespace NecromancerTome
|
||||
_v.SetRotation(new Vector3(0f, _bodyYaw, 0f));
|
||||
if (_v.emodel != null && _v.emodel.avatarController != null)
|
||||
{
|
||||
_v.glidingCurrentPercent = Mathf.MoveTowards(_v.glidingCurrentPercent, _flyRun && moving ? 1f : 0f, 0.06f);
|
||||
_v.glidingCurrentPercent = Mathf.MoveTowards(_v.glidingCurrentPercent, run && moving ? 1f : 0f, 0.06f);
|
||||
_v.emodel.avatarController.UpdateFloat("Gliding", _v.glidingCurrentPercent);
|
||||
}
|
||||
// «Вниз» на земле посадке не мешает: Ctrl - переключатель, и с ним стервятник не сел бы никогда.
|
||||
@@ -2379,6 +2428,18 @@ namespace NecromancerTome
|
||||
{
|
||||
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));
|
||||
_meleeLookUntil = Time.time + 0.6f;
|
||||
if (!StartStrike(z))
|
||||
@@ -2583,14 +2644,13 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Карта в одержимом стервятнике открывается на нём, а не на теле (указание 08.10: разведка -
|
||||
/// только у стервятника).</summary>
|
||||
/// <summary>Карта в одержимом стервятнике и зомби открывается на нём, а не на теле (указание 08.10, #36).</summary>
|
||||
[HarmonyPatch(typeof(XUiC_MapArea), nameof(XUiC_MapArea.OnOpen))]
|
||||
public static class Patch_XUiC_MapArea_OnOpen_Possession
|
||||
{
|
||||
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());
|
||||
}
|
||||
@@ -2603,7 +2663,7 @@ namespace NecromancerTome
|
||||
{
|
||||
public static bool Prefix(XUiC_MapArea __instance)
|
||||
{
|
||||
if (!Possession.Active || !Possession.Flying || Possession.Zombie == null)
|
||||
if (!Possession.Scouts || Possession.Zombie == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -2864,8 +2924,9 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Пояс одержимого от первого лица (указание 13.10: «собственный пояс на 2 ячейки», пока пустой) - на месте
|
||||
/// пояса персонажа, внизу по центру; выбранная ячейка в рамке. Ячейки пустые - бьёт кулаками.</summary>
|
||||
/// <summary>Пояс одержимого от первого лица (указание 13.10: «собственный пояс на 2 ячейки») - на месте пояса персонажа,
|
||||
/// внизу по центру; выбранная ячейка в рамке. В ячейке - иконка предмета из нижнего ряда сумки (#37), полоска износа;
|
||||
/// чем зомби не владеет или что сломано - бледное (бьёт кулаками).</summary>
|
||||
static void DrawBelt(float _scale)
|
||||
{
|
||||
float size = 64f * _scale, gap = 6f * _scale, frame = 3f * _scale;
|
||||
@@ -2883,11 +2944,51 @@ namespace NecromancerTome
|
||||
GUI.color = Back;
|
||||
GUI.DrawTexture(r, Texture2D.whiteTexture);
|
||||
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.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)
|
||||
{
|
||||
Color prev = GUI.color;
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
using System.Collections.Generic;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Живой кролик в мешке (указание 22.10.2026: «Я бы хватал кролика, но в отличие от курицы пусть он сразу
|
||||
/// оказывается в сумке»; предмет - «Живой кролик в мешке»). Ваниль даёт схватить любое существо, у которого в
|
||||
/// описании есть PickupItem (EntityAlive: команда grab голой рукой, предмет кладётся в слот руки, существо
|
||||
/// уходит из мира через Collect) - так ловят курицу. Кролику PickupItem дан в entityclasses.xml, а здесь его
|
||||
/// мешок кладётся сразу в рюкзак (нет места - на пояс, иначе под ноги).
|
||||
///
|
||||
/// Выпустить - ЛКМ с мешком в руке (ItemActionSpawnEntity, Entity=animalRabbit): ваниль предмет не тратит, тратим
|
||||
/// здесь. Разделать - «Разобрать» в рюкзаке. Стервятник кролика по-прежнему несёт под лапами (Possession.Carryable). Выключатель - necrorabbit off:
|
||||
/// тогда захват идёт ванильно, мешок - в руку, как курица.
|
||||
/// </summary>
|
||||
public static class RabbitGrab
|
||||
{
|
||||
public const string SackItem = "necroRabbitSack";
|
||||
public static bool Enabled = true;
|
||||
|
||||
public static bool IsSackAnimal(EntityAlive _e) => _e != null && _e.EntityClass != null && _e.EntityClass.PickupItem == SackItem;
|
||||
|
||||
public static bool IsSack(ItemValue _iv) => _iv != null && _iv.ItemClass != null && _iv.ItemClass.GetItemName() == SackItem;
|
||||
}
|
||||
|
||||
/// <summary>Захват кролика - мешок сразу в рюкзак.</summary>
|
||||
[HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.OnEntityActivated))]
|
||||
public static class Patch_EntityAlive_OnEntityActivated_RabbitGrab
|
||||
{
|
||||
public static bool Prefix(EntityAlive __instance, EntityActivationCommand _command, EntityPlayerLocal _playerFocusing)
|
||||
{
|
||||
if (!RabbitGrab.Enabled || _command.commandId != "grab" || _playerFocusing == null || !RabbitGrab.IsSackAnimal(__instance))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
ItemClass ic = ItemClass.GetItemClass(RabbitGrab.SackItem);
|
||||
if (ic == null)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] RabbitGrab: нет предмета " + RabbitGrab.SackItem + " - захват ванильный");
|
||||
return true;
|
||||
}
|
||||
ItemStack sack = new ItemStack(new ItemValue(ic.Id), 1);
|
||||
string where = "рюкзак";
|
||||
if (_playerFocusing.bag == null || !_playerFocusing.bag.AddItem(sack))
|
||||
{
|
||||
where = "пояс";
|
||||
if (_playerFocusing.inventory == null || !_playerFocusing.inventory.AddItem(sack))
|
||||
{
|
||||
where = "под ноги";
|
||||
_playerFocusing.world.gameManager.ItemDropServer(sack, _playerFocusing.GetPosition() + Vector3.up * 0.5f, Vector3.zero);
|
||||
}
|
||||
}
|
||||
_playerFocusing.MinEventContext.Other = __instance;
|
||||
_playerFocusing.FireEvent(MinEventTypes.onSelfPickupOther);
|
||||
__instance.MinEventContext.Other = _playerFocusing;
|
||||
__instance.FireEvent(MinEventTypes.onSelfPickedUpByOther);
|
||||
Audio.Manager.PlayInsidePlayerHead(ic.Properties.GetString("SoundPickup"));
|
||||
__instance.Collect(_playerFocusing.entityId);
|
||||
Debug.Log("[NecromancerTome] RabbitGrab: кролик " + __instance.entityId + " пойман, мешок - " + where);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Подсказка над кроликом - своя (ваниль пишет «схватить курицу» для любого существа с PickupItem).</summary>
|
||||
[HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.GetActivationText))]
|
||||
public static class Patch_EntityAlive_GetActivationText_RabbitGrab
|
||||
{
|
||||
public static void Postfix(EntityAlive __instance, ref string __result)
|
||||
{
|
||||
if (RabbitGrab.Enabled && RabbitGrab.IsSackAnimal(__instance))
|
||||
{
|
||||
__result = Localization.Get("necroRabbitGrab");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Выпуск из мешка тратит мешок (ванильный SpawnEntity предмет не трогает).</summary>
|
||||
[HarmonyPatch(typeof(ItemActionSpawnEntity), "Spawn", new System.Type[] { typeof(ItemActionData) })]
|
||||
public static class Patch_ItemActionSpawnEntity_Spawn_RabbitSack
|
||||
{
|
||||
public static void Postfix(ItemActionData _actionData)
|
||||
{
|
||||
ItemInventoryData inv = _actionData?.invData;
|
||||
if (inv?.holdingEntity == null || !RabbitGrab.IsSack(inv.itemValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Inventory pack = inv.holdingEntity.inventory;
|
||||
ItemStack held = pack.GetItem(inv.slotIdx);
|
||||
if (held == null || held.IsEmpty() || !RabbitGrab.IsSack(held.itemValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
held = held.Clone();
|
||||
held.count--;
|
||||
pack.SetItem(inv.slotIdx, held.count > 0 ? held : ItemStack.Empty.Clone());
|
||||
Debug.Log("[NecromancerTome] RabbitGrab: кролик выпущен из мешка, осталось " + Mathf.Max(0, held.count));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Разделать кролика - «Разобрать» в рюкзаке (указание 22.10: «Кролика можно будет разобрать на кожу, кости и
|
||||
/// мясо»; «Разделывать будем по команде сломать»). Пункт ваниль показывает сама: мешок из кожи (Mleather, forge_category
|
||||
/// leather), вес не меньше кожи. Но ванильная разборка даёт один материал через очередь крафта - у мешка она наша:
|
||||
/// вся стопка сразу, на каждого кролика добыча разделки ножом (entityclasses.xml animalRabbit, butcherHarvest).</summary>
|
||||
[HarmonyPatch(typeof(ItemActionEntryScrap), nameof(ItemActionEntryScrap.OnActivated))]
|
||||
public static class Patch_ItemActionEntryScrap_OnActivated_RabbitSack
|
||||
{
|
||||
static readonly string[] Yield = { "foodRawMeat", "resourceLeather", "resourceBone" };
|
||||
static readonly int[] YieldCount = { 10, 2, 5 };
|
||||
|
||||
public static bool Prefix(ItemActionEntryScrap __instance)
|
||||
{
|
||||
if (!(__instance.ItemController is IItemStackController isc))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
ItemStack st = isc.ItemStack;
|
||||
if (st == null || st.IsEmpty() || !RabbitGrab.IsSack(st.itemValue))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
EntityPlayerLocal player = __instance.ItemController.xui.playerUI.entityPlayer;
|
||||
XUiM_PlayerInventory pack = __instance.ItemController.xui.PlayerInventory;
|
||||
int n = st.count;
|
||||
isc.ItemStack = ItemStack.Empty.Clone();
|
||||
for (int i = 0; i < Yield.Length; i++)
|
||||
{
|
||||
ItemClass ic = ItemClass.GetItemClass(Yield[i]);
|
||||
if (ic == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ItemStack s = new ItemStack(new ItemValue(ic.Id), YieldCount[i] * n);
|
||||
if (!pack.AddItem(s))
|
||||
{
|
||||
GameManager.Instance.ItemDropServer(s, player.GetPosition(), new Vector3(0.5f, 0f, 0.5f));
|
||||
}
|
||||
}
|
||||
isc.WindowGroup?.Controller?.SetAllChildrenDirty();
|
||||
Audio.Manager.PlayInsidePlayerHead("rabbitdeath");
|
||||
Debug.Log("[NecromancerTome] RabbitGrab: разобрано мешков с кроликами - " + n);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public class ConsoleCmdNecroRabbit : ConsoleCmdAbstract
|
||||
{
|
||||
public override bool IsExecuteOnClient => true;
|
||||
|
||||
public override bool AllowedInMainMenu => false;
|
||||
|
||||
public override string[] getCommands()
|
||||
{
|
||||
return new string[] { "necrorabbit" };
|
||||
}
|
||||
|
||||
public override string getDescription()
|
||||
{
|
||||
return "Necromancer's Tome: grabbed rabbit goes straight to the backpack - switch.";
|
||||
}
|
||||
|
||||
public override string getHelp()
|
||||
{
|
||||
return "necrorabbit - status\n" +
|
||||
"necrorabbit on|off - off: vanilla grab, the sack goes to the hand like a chicken";
|
||||
}
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
SdtdConsole con = SingletonMonoBehaviour<SdtdConsole>.Instance;
|
||||
if (_params.Count >= 1 && (_params[0] == "on" || _params[0] == "off"))
|
||||
{
|
||||
RabbitGrab.Enabled = _params[0] == "on";
|
||||
}
|
||||
con.Output("necrorabbit: " + (RabbitGrab.Enabled ? "on - rabbit sack goes to the backpack" : "off - vanilla grab"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.MoveFillStacks));
|
||||
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)
|
||||
@@ -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>
|
||||
[HarmonyPatch(typeof(XUiC_ItemStack), nameof(XUiC_ItemStack.Update))]
|
||||
public static class Patch_ItemStack_Update
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Collections.Generic;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Кузнечные рецепты в двух версиях (плавильня - use_smelter, лом - replace_smelter) игра разводит по настройке мира
|
||||
/// только в списке верстака (FilterRecipesByWorkstation) и в поиске по имени (FilterRecipesByName). Список по предмету
|
||||
/// (клик по иконке в окне навыка), по id и по ингредиенту показывал обе версии - 22.10.2026 на стриме рецепт оссуария
|
||||
/// «задвоился» (4 строки вместо 2: кровь/сок × плавильня/лом), у кузнечных урн так же. Здесь тот же фильтр, что у игры.
|
||||
/// Выключатель - necrosmelterfilter off.
|
||||
/// </summary>
|
||||
public static class RecipeSmelterFilter
|
||||
{
|
||||
public static bool Enabled = true;
|
||||
|
||||
public static void Apply(List<Recipe> _list)
|
||||
{
|
||||
if (!Enabled || _list == null)
|
||||
return;
|
||||
_list.RemoveAll(r => r != null && r.craftingArea == "forge" &&
|
||||
((XUiM_Recipes.DisableSmelter && r.tags.Test_AnySet(XUiM_Recipes.useSmelterTag)) ||
|
||||
(!XUiM_Recipes.DisableSmelter && r.tags.Test_AnySet(XUiM_Recipes.replaceSmelterTag))));
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(XUiM_Recipes), nameof(XUiM_Recipes.FilterRecipesByItem))]
|
||||
public static class Patch_XUiM_Recipes_FilterRecipesByItem_Smelter
|
||||
{
|
||||
public static void Postfix(List<Recipe> __result) => RecipeSmelterFilter.Apply(__result);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(XUiM_Recipes), nameof(XUiM_Recipes.FilterRecipesByID))]
|
||||
public static class Patch_XUiM_Recipes_FilterRecipesByID_Smelter
|
||||
{
|
||||
public static void Postfix(List<Recipe> __result) => RecipeSmelterFilter.Apply(__result);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(XUiM_Recipes), nameof(XUiM_Recipes.FilterRecipesByIngredient))]
|
||||
public static class Patch_XUiM_Recipes_FilterRecipesByIngredient_Smelter
|
||||
{
|
||||
public static void Postfix(List<Recipe> __result) => RecipeSmelterFilter.Apply(__result);
|
||||
}
|
||||
|
||||
public class ConsoleCmdNecroSmelterFilter : ConsoleCmdAbstract
|
||||
{
|
||||
public override bool IsExecuteOnClient => true;
|
||||
|
||||
public override bool AllowedInMainMenu => true;
|
||||
|
||||
public override string[] getCommands()
|
||||
{
|
||||
return new string[] { "necrosmelterfilter" };
|
||||
}
|
||||
|
||||
public override string getDescription()
|
||||
{
|
||||
return "Necromancer's Tome: hide forge recipe versions that do not match the smelter setting in item/ingredient recipe lists.";
|
||||
}
|
||||
|
||||
public override string getHelp()
|
||||
{
|
||||
return "necrosmelterfilter on|off - off: lists by item/ingredient show both forge versions, as in the base game";
|
||||
}
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
SdtdConsole con = SingletonMonoBehaviour<SdtdConsole>.Instance;
|
||||
if (_params.Count > 0 && (_params[0] == "on" || _params[0] == "off"))
|
||||
RecipeSmelterFilter.Enabled = _params[0] == "on";
|
||||
con.Output("necrosmelterfilter: " + (RecipeSmelterFilter.Enabled ? "on" : "off"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ namespace NecromancerTome
|
||||
public static class SnakeControl
|
||||
{
|
||||
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;
|
||||
/// <summary>Прыжок на Пробел (указание 28.09: «иначе не выбраться из ям»). 10 м/с при g = 20 -
|
||||
/// подъём на 2,5 м: вместе с заползанием на блок хватает на стену в три блока.</summary>
|
||||
|
||||
@@ -75,8 +75,15 @@ namespace NecromancerTome
|
||||
/// Время - ванильное GameUtils.IsBloodMoonTime (от заката дня луны до рассвета), оно считается по часам
|
||||
/// мира и одинаково у хоста и клиента; AIDirector с его BloodMoonActive есть только у сервера.
|
||||
/// </summary>
|
||||
/// <summary>Прилив для проверок (necrotide on|off|auto): null - по часам мира.</summary>
|
||||
public static bool? ForceTide;
|
||||
|
||||
public static bool IsBloodTide()
|
||||
{
|
||||
if (ForceTide.HasValue)
|
||||
{
|
||||
return ForceTide.Value;
|
||||
}
|
||||
World world = GameManager.Instance?.World;
|
||||
return world != null && GameUtils.IsBloodMoonTime(world.worldTime, (world.DuskHour, world.DawnHour),
|
||||
GameStats.GetInt(EnumGameStats.BloodMoonDay));
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NecromancerTome
|
||||
}
|
||||
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)
|
||||
{
|
||||
continue;
|
||||
@@ -74,7 +74,7 @@ namespace NecromancerTome
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>Ноша одержимого стервятника (курица, кролик): высота под лапами и запрет разгона с грузом (22.10.2026).</summary>
|
||||
public class ConsoleCmdNecroCarry : ConsoleCmdAbstract
|
||||
{
|
||||
public override string[] getCommands() => new[] { "necrocarry" };
|
||||
public override string getDescription() => "NecromancerTome: ноша стервятника - gap <м, минус - выше>|run on|off|show";
|
||||
|
||||
void Out(string _s)
|
||||
{
|
||||
SdtdConsole.Instance.Output(_s);
|
||||
Debug.Log("[NecromancerTome] necrocarry: " + _s);
|
||||
}
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
string cmd = _params.Count > 0 ? _params[0] : "show";
|
||||
switch (cmd)
|
||||
{
|
||||
case "gap":
|
||||
if (_params.Count > 1 && float.TryParse(_params[1].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out float g))
|
||||
{
|
||||
Possession.CarryGap = Mathf.Clamp(g, -0.6f, 0.3f);
|
||||
}
|
||||
break;
|
||||
case "run":
|
||||
if (_params.Count > 1 && (_params[1] == "on" || _params[1] == "off"))
|
||||
{
|
||||
Possession.CarryNoRun = _params[1] == "on";
|
||||
}
|
||||
break;
|
||||
}
|
||||
Out("gap " + Possession.CarryGap.ToString("0.###", CultureInfo.InvariantCulture) + ", без разгона с ношей - " +
|
||||
(Possession.CarryNoRun ? "да" : "нет"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.MoveFillStacks));
|
||||
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)
|
||||
@@ -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>
|
||||
[HarmonyPatch(typeof(XUiC_BagContainer), nameof(XUiC_BagContainer.GetBindingValueInternal))]
|
||||
public static class Patch_BagContainer_Binding_WolfBag
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace NecromancerTome
|
||||
/// - при выходе из одержимости зомби сбрасывает рюкзаком всё, что было в сумке (и на поясе - когда в поясе
|
||||
/// появятся предметы);
|
||||
/// - гибель одержимого - мешок на месте и метка на карте, как у сумки волка.
|
||||
/// ПОЯС (#37, 25.10) - нижний ряд той же сумки: Possession.BeltSlots ячеек под верхним рядом, оружие в руке берётся
|
||||
/// оттуда (ZombieWeapons). Хранение, выброс мешком и метка - общие с сумкой.
|
||||
/// Лут (указание 13.10: «раз у них E свободно, подключай лутание») - как у волка: E на ящик или мешок - обыск с
|
||||
/// переносом в сумку, E на предмет на земле - подобрать (WolfBag.Target - сумка зомби, пока она есть).
|
||||
/// Только зомби из списка вида от первого лица (FpArms.Allows), не питомцы. Окно - то же, что у сумки волка
|
||||
@@ -28,12 +30,41 @@ namespace NecromancerTome
|
||||
public const string WaypointName = "necroZombieBagWaypoint";
|
||||
public static bool WindowOpen;
|
||||
static Bag _bag;
|
||||
/// <summary>Ячеек в ряду: верхний ряд - сумка, нижний - пояс.</summary>
|
||||
static int _cols;
|
||||
|
||||
static string FilePath => Path.Combine(GameIO.GetSaveGameDir(), FileName);
|
||||
|
||||
public static bool Active => _bag != null;
|
||||
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>
|
||||
public static void SaveNow() => Save();
|
||||
|
||||
@@ -64,8 +95,9 @@ namespace NecromancerTome
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] ZombieBag: файл сумки не прочитан (" + e.Message + ") - оставлен как есть");
|
||||
}
|
||||
_bag = new Bag(new Vector2i(SlotsFor(_z), 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null);
|
||||
Debug.Log("[NecromancerTome] ZombieBag: сумка " + _z.EntityName + " - ячеек " + _bag.SlotCount);
|
||||
_cols = Mathf.Max(SlotsFor(_z), Possession.BeltSlots);
|
||||
_bag = new Bag(new Vector2i(_cols, 2), XUiC_ItemStack.StackLocationTypes.LootContainer, null);
|
||||
Debug.Log("[NecromancerTome] ZombieBag: сумка " + _z.EntityName + " - ячеек " + _bag.SlotCount + ", из них пояс " + Possession.BeltSlots);
|
||||
}
|
||||
|
||||
/// <summary>Вселение кончилось: всё из сумки - мешком на месте зомби; погиб - ещё и метка на карте.</summary>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
FIX: blood rose buds and blooming roses floated half a block above the planter. The plant model was lifted by the game's default model offset; now it stands on the soil.
|
||||
Nothing else changed. Drop-in over 1.6.0 or 1.5.x: saves carry over, roses already planted fix themselves.
|
||||
@@ -0,0 +1,3 @@
|
||||
COMPATIBILITY: the ghost look (grey and translucent) is now given only to the game's own traders. Before, it covered everything built on the game's trader type, so NPCs from other mods such as NPCCore turned nearly invisible. Thanks to Crozekanov for the report.
|
||||
FIX (from 1.6.1): blood rose buds and blooming roses no longer float half a block above the planter.
|
||||
Nothing else changed. Drop-in over 1.6.0, 1.6.1 or 1.5.x: saves carry over.
|
||||
@@ -42,7 +42,7 @@ Updating from 1.5.x: replace the folder, saves carry over. To uninstall, delete
|
||||
[size=5][b]Requirements[/b][/size]
|
||||
|
||||
[list]
|
||||
[*][b]7 Days to Die 3.3: version 1.6.0. 7 Days to Die 3.2: version 1.4.3.[/b] Both are on the Files tab. Each build tells you in the main menu if it is started on the wrong game version.
|
||||
[*][b]7 Days to Die 3.3: version 1.6.2. 7 Days to Die 3.2: version 1.4.3.[/b] Both are on the Files tab. Each build tells you in the main menu if it is started on the wrong game version.
|
||||
[*][b]EasyAntiCheat must be off[/b] - the mod uses Harmony patches.
|
||||
[*][b]No other mods required.[/b] Harmony itself ships with the game.
|
||||
[*][b]Single-player.[/b] The mod was made for single-player from the start. It reworks a number of the game's own mechanics - corpses, loot, zombie behaviour - and a server isn't necessarily built for that, so in multiplayer some things may not work. The story scenes also play there without stopping time, since vanilla pauses only in single-player.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Fix for 1.6.0: blood rose buds and flowers no longer float half a block above the planter. Nothing else changed - drop-in over 1.6.0 or 1.5.x, saves carry over. For game 3.3. Single-player, EAC off.
|
||||
@@ -0,0 +1 @@
|
||||
For 1.6.0: compatibility - only the game's own traders get the ghost look now, so NPCs from other mods (e.g. NPCCore) look normal; blood rose buds and flowers no longer float. Drop-in over 1.6.x or 1.5.x. Game 3.3, single-player, EAC off.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
*English version below — scroll past the Russian half.*
|
||||
|
||||
**Версия 1.6.0** — для 7 Days to Die 3.3. Для игры 3.2 — версия 1.4.3. Каждая сборка на чужой версии игры сама скажет, что несовместима. Автор: Alex Cube.
|
||||
**Версия 1.6.2** — для 7 Days to Die 3.3. Для игры 3.2 — версия 1.4.3. Каждая сборка на чужой версии игры сама скажет, что несовместима. Автор: Alex Cube.
|
||||
|
||||
- Страница мода: https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/
|
||||
- Nexus Mods: https://www.nexusmods.com/7daystodie/mods/12547
|
||||
@@ -63,7 +63,7 @@
|
||||
`%APPDATA%/7DaysToDie/` (подходят обе, если её нет — создайте).
|
||||
3. Запустите игру. Навык «Некромантия» появится на панели навыков крафта сразу.
|
||||
|
||||
Версия 1.6.0 — для игры 3.3, версия 1.4.3 — для игры 3.2 (обе на Nexus во вкладке Files).
|
||||
Версия 1.6.2 — для игры 3.3, версия 1.4.3 — для игры 3.2 (обе на Nexus во вкладке Files).
|
||||
Обновление поверх 1.5.x: сейвы переносятся.
|
||||
|
||||
Рассчитан на одиночную игру. Мод переделывает ряд механик самой игры (трупы, добычу, поведение
|
||||
@@ -75,6 +75,8 @@
|
||||
|
||||
## История версий
|
||||
|
||||
- **1.6.2** — совместимость: призрачный вид только у торговцев самой игры; NPC других модов (например, NPCCore) больше не становятся полупрозрачными.
|
||||
- **1.6.1** — исправление: бутоны и цветы кровавой розы висели над грядкой на полклетки.
|
||||
- **1.6.0** — дух змеи, одержимость зомби и своих питомцев, чертоги разума, книга некроманта с
|
||||
главами, слуги охраняют хозяина (один за раз), могильная крыса, костяной скакун, гео-оссуарий,
|
||||
кровавая роза и ступка, кровавый прилив и нож-меч, останки красных трупов, череп и
|
||||
@@ -98,7 +100,7 @@
|
||||
|
||||
# Necromancer's Tome (English)
|
||||
|
||||
**Version 1.6.0** — for 7 Days to Die 3.3. For game 3.2 use version 1.4.3. Each build tells you if it is started on the wrong game version. Author: Alex Cube.
|
||||
**Version 1.6.2** — for 7 Days to Die 3.3. For game 3.2 use version 1.4.3. Each build tells you if it is started on the wrong game version. Author: Alex Cube.
|
||||
|
||||
- Mod page: https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/
|
||||
- Nexus Mods: https://www.nexusmods.com/7daystodie/mods/12547
|
||||
@@ -161,7 +163,7 @@ Polish, Brazilian Portuguese, Russian, Turkish, Simplified and Traditional Chine
|
||||
`%APPDATA%/7DaysToDie/` (either works; create it if it is not there).
|
||||
3. Start the game. The Necromancy skill appears in the crafting skills panel right away.
|
||||
|
||||
Version 1.6.0 is for game 3.3, version 1.4.3 is for game 3.2 (both on the Nexus Files tab).
|
||||
Version 1.6.2 is for game 3.3, version 1.4.3 is for game 3.2 (both on the Nexus Files tab).
|
||||
Drop-in over 1.5.x: saves carry over.
|
||||
|
||||
Built for single-player. The mod reworks a number of the game's own mechanics (corpses, loot,
|
||||
@@ -173,6 +175,8 @@ To uninstall, delete the folder. The mod's items and blocks will be gone from th
|
||||
|
||||
## Version history
|
||||
|
||||
- **1.6.2** - compatibility: only the game's own traders get the ghost look; NPCs from other mods (for example NPCCore) no longer turn translucent.
|
||||
- **1.6.1** - fix: blood rose buds and flowers floated half a block above the planter.
|
||||
- **1.6.0** - the Snake Spirit, possession of zombies and of your own servants, the Mind Palace, the
|
||||
Necromancer's Tome with chapters, servants that guard their master (one at a time), the Grave
|
||||
Rat, the Bone Steed, the Geo-Ossuary, the blood rose and the mortar, the Blood Tide and the
|
||||
|
||||
|
Before Width: | Height: | Size: 28 KiB |
@@ -38,7 +38,7 @@
|
||||
<li>Запустите игру. Навык «Некромантия» появится на панели навыков крафта сразу.</li>
|
||||
</ol>
|
||||
|
||||
Версия 1.6.0 — для игры 3.3, версия 1.4.3 — для игры 3.2 (обе на Nexus во вкладке Files). Обновление поверх 1.5.x: сейвы переносятся.
|
||||
Версия 1.6.2 — для игры 3.3, версия 1.4.3 — для игры 3.2 (обе на Nexus во вкладке Files). Обновление поверх 1.5.x: сейвы переносятся.
|
||||
|
||||
Рассчитан на одиночную игру. Мод переделывает ряд механик самой игры (трупы, добычу, поведение зомби), а сервер не обязательно на такое рассчитан, поэтому в мультиплеере часть вещей может не работать. Сюжетные сцены там проиграются без остановки времени: пауза игры работает только в сингле.
|
||||
|
||||
@@ -97,7 +97,7 @@ Fully localized into 13 languages: English, German, Spanish, French, Italian, Ja
|
||||
<li>Start the game. The Necromancy skill appears in the crafting skills panel right away.</li>
|
||||
</ol>
|
||||
|
||||
Version 1.6.0 is for game 3.3, version 1.4.3 is for game 3.2 (both on the Nexus Files tab). Drop-in over 1.5.x: saves carry over.
|
||||
Version 1.6.2 is for game 3.3, version 1.4.3 is for game 3.2 (both on the Nexus Files tab). Drop-in over 1.5.x: saves carry over.
|
||||
|
||||
Built for single-player. The mod reworks a number of the game's own mechanics (corpses, loot, zombie behaviour), and a server isn't necessarily built for that, so in multiplayer some things may not work. The story scenes also play there without stopping time, since the game pauses only in single-player.
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 631 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB |