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