Compare commits
46
Commits
v1.6.2
...
a3f825303e
+65
-2
File diff suppressed because one or more lines are too long
@@ -208,4 +208,62 @@
|
||||
<sprite depth="9" pos="0,20" height="8" color="[black]" sprite="menu_empty2px" globalopacitymod="0" type="sliced" fillcenter="false" />
|
||||
<sprite depth="4" pos="0,20" height="8" width="750" color="150,60,220,200" globalopacitymod="0" type="filled" fill="{% cvar('necroNecromancyProgressCVar') / 20 }" />
|
||||
</append>
|
||||
<!-- Фильтр урны-логиста (16.10.2026, HarmonySrc/CinerariumFilterWindow.cs, CinerariumGroups.cs): значки групп предметов
|
||||
в окне «Содержимое» рядом с ячейкой черепа. Окно в группе bagStorage (xui.xml) - контроллер прячет его, если окно
|
||||
открыто не урной-логистом (сумки транспорта и волка). f0..f21 - рамки, g0..g21 - значки (иконки ставит контроллер). -->
|
||||
<append xpath="/windows">
|
||||
<window name="necroUrnFilter" width="348" height="279" depth="5" panel="Left" backgroundcolor="[black]" cursor_area="true" controller="NecromancerTome.NecroUrnFilter, NecromancerHarmony">
|
||||
<rect name="header" height="43" depth="0">
|
||||
<sprite depth="1" name="headerbackground" sprite="ui_game_panel_header" width="348" height="43" />
|
||||
<sprite depth="3" name="windowIcon" style="icon32px" pivot="center" pos="21,-21" sprite="ui_game_symbol_sort" />
|
||||
<label depth="2" name="title" pos="39,-6" width="303" height="32" text_key="necroUrnFilterTitle" font_size="28" />
|
||||
</rect>
|
||||
<sprite depth="2" name="f0" pos="6,-49" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g0" pos="8,-51" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f1" pos="62,-49" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g1" pos="64,-51" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f2" pos="118,-49" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g2" pos="120,-51" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f3" pos="174,-49" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g3" pos="176,-51" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f4" pos="230,-49" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g4" pos="232,-51" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f5" pos="286,-49" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g5" pos="288,-51" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f6" pos="6,-105" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g6" pos="8,-107" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f7" pos="62,-105" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g7" pos="64,-107" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f8" pos="118,-105" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g8" pos="120,-107" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f9" pos="174,-105" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g9" pos="176,-107" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f10" pos="230,-105" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g10" pos="232,-107" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f11" pos="286,-105" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g11" pos="288,-107" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f12" pos="6,-161" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g12" pos="8,-163" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f13" pos="62,-161" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g13" pos="64,-163" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f14" pos="118,-161" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g14" pos="120,-163" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f15" pos="174,-161" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g15" pos="176,-163" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f16" pos="230,-161" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g16" pos="232,-163" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f17" pos="286,-161" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g17" pos="288,-163" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f18" pos="6,-217" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g18" pos="8,-219" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f19" pos="62,-217" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g19" pos="64,-219" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f20" pos="118,-217" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g20" pos="120,-219" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<sprite depth="2" name="f21" pos="174,-217" width="52" height="52" sprite="menu_empty2px" type="sliced" fillcenter="false" />
|
||||
<sprite depth="3" name="g21" pos="176,-219" width="48" height="48" atlas="ItemIconAtlas" sprite="oldCash" on_press="true" sound="[paging_click]" foregroundlayer="true" />
|
||||
<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" />
|
||||
</window>
|
||||
</append>
|
||||
</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>
|
||||
|
||||
@@ -519,4 +519,68 @@
|
||||
<property name="ItemTypeIcon" value="all_blocks"/>
|
||||
</block>
|
||||
</append>
|
||||
|
||||
<!-- Урна-цинерарий (Harvest Cinerarium), 1.7.0, 16.10.2026: этап 2 некротехнолога (BACKLOG «Некротехнолог v2»).
|
||||
Составной блок, как земельный камень: TEFeatureStorage - сумка духа (окно хранилища 6x2, «Открыть»),
|
||||
TEFeatureCinerarium (HarmonySrc/Cinerarium.cs) - череп (положить/забрать), показ зоны 5x5, -1..+4,
|
||||
лимит ceil(убийств / 500) и запрет пересечения зон при постановке. Всё - в радиальном меню на E (решение 16.10).
|
||||
Модель (16.10) - своя: бандл necrocinerarium (BlenderWork/necroCinerarium, make_urn.py): чёрная глазурь, черепа по
|
||||
четырём сторонам, руны футарка кровью «SERVU ĜIS LA LASTA CINDRO», меандр, бусины; свечение рун и глазниц
|
||||
включает TEFeatureCinerarium по черепу. Иконка - рендер той же модели (make_icon.py). -->
|
||||
<append xpath="/blocks">
|
||||
<block name="necroCinerarium">
|
||||
<property name="CreativeMode" value="Player"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumDesc"/>
|
||||
<property name="UnlockedBy" value="craftingNecroNecromancy"/>
|
||||
<property name="Class" value="CompositeTileEntity"/>
|
||||
<property class="CompositeFeatures">
|
||||
<property class="TEFeatureStorage">
|
||||
<property name="LootList" value="necroCinerariumBag"/>
|
||||
</property>
|
||||
<property class="TEFeatureCinerarium"/>
|
||||
<property class="TEFeaturePickup">
|
||||
<property name="TakeDelay" value="2"/>
|
||||
</property>
|
||||
</property>
|
||||
<property name="Material" value="Mstone"/>
|
||||
<property name="Shape" value="ModelEntity"/>
|
||||
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necrocinerarium?necroCinerariumPrefab.prefab"/>
|
||||
<property name="ModelOffset" value="0,0,0"/> <!-- меш стоит основанием на y=0; без этого ModelEntity поднимает на полклетки -->
|
||||
<property name="CustomIcon" value="necroCinerarium"/>
|
||||
<property name="WaterFlow" value="permitted"/>
|
||||
<property name="OnlySimpleRotations" value="true"/>
|
||||
<property name="Collide" value="movement,melee,bullet,arrow,rocket"/>
|
||||
<property name="StabilitySupport" value="false"/>
|
||||
<property name="MaxDamage" value="500"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
<property name="SellableToTrader" value="false"/>
|
||||
<property name="FilterTags" value="MC_playerBlocks,SC_decor"/>
|
||||
<property class="RepairItems">
|
||||
<property name="resourceBone" value="5"/>
|
||||
</property>
|
||||
</block>
|
||||
<!-- Урна, поднятая с черепом внутри (как транспорт держит модификации): тот же блок, при постановке череп
|
||||
уже в багажнике. Получается только подъёмом (Patch_Block_OnBlockPickedUp_Cinerarium). -->
|
||||
<block name="necroCinerariumSkull">
|
||||
<property name="Extends" value="necroCinerarium"/>
|
||||
<property name="CreativeMode" value="None"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumDesc"/>
|
||||
</block>
|
||||
<!-- Урна-логист (16.10.2026): «Я не могу ставить урны рядом. Может нам для логиста вообще сделать отдельные урны?»
|
||||
Без зоны (ставится вплотную к любым урнам), только нити и перевозка (CinerariumThreads.cs), лимит урн общий.
|
||||
Модель - та же; отличие - код (TEFeatureCinerarium.SyncGlow): зелёные руны и глазницы
|
||||
(Resources/necroCinerarium_e_logist.png), зеленоватая глазурь. Иконка - своя (зелёные руны, нити из горлышка).
|
||||
Своя модель логиста - позже, в Blender. -->
|
||||
<block name="necroCinerariumLogist">
|
||||
<property name="Extends" value="necroCinerarium"/>
|
||||
<property name="CreativeMode" value="Player"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumLogistDesc"/>
|
||||
<property name="CustomIcon" value="necroCinerariumLogist"/>
|
||||
</block>
|
||||
<block name="necroCinerariumLogistSkull">
|
||||
<property name="Extends" value="necroCinerariumLogist"/>
|
||||
<property name="CreativeMode" value="None"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumLogistDesc"/>
|
||||
</block>
|
||||
</append>
|
||||
</config>
|
||||
|
||||
@@ -2018,4 +2018,21 @@
|
||||
<property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necroblood?necroJuicePrefab.prefab"/>
|
||||
</item>
|
||||
</append>
|
||||
<!-- Нож некроманта «хватом для нитей» (16.10.2026, HarmonySrc/CinerariumThreads.cs): «нож некроманта, но взятый особым
|
||||
хватом... как кисточка или как тот же стриппер». Отдельный предмет - класс ножа в сейве менять нельзя; переключение -
|
||||
меню по R (качество, износ и моды переносятся). Не бьёт: ПКМ - тянуть нить урна-ящик, ЛКМ - снять нити. Удары
|
||||
перехвачены в ItemClass.ExecuteAction, поэтому действия ножа (Extends) остаются, но не срабатывают. Держится пока как
|
||||
обычный нож (хват стриппера HoldType 52 не подошёл - 16.10). effect_group через Extends не наследуется -
|
||||
у этого предмета нет урона, лечения и слотов модов (моды в ItemValue сохраняются и вернутся с обычным хватом). -->
|
||||
<append xpath="/items">
|
||||
<item name="necroWpnBladeNecroKnifeThread">
|
||||
<property name="Extends" value="necroWpnBladeNecroKnife"/>
|
||||
<property name="CreativeMode" value="None"/>
|
||||
<!-- Хват для нитей (16.10, подбор в игре): основа - 68 (деревянный лук), поверх - поза рук мода
|
||||
(HandPose.PresetThread): нож в левой руке, правая раскрытой ладонью. 52 (стриппер) не подошёл. -->
|
||||
<property name="HoldType" value="68"/>
|
||||
<property name="DescriptionKey" value="necroWpnBladeNecroKnifeThreadDesc"/>
|
||||
<property name="CustomIconTint" value="150, 205, 255"/>
|
||||
</item>
|
||||
</append>
|
||||
</config>
|
||||
|
||||
@@ -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,7 @@
|
||||
пользуется, вода сидит на necroNecromancyLvl20. -->
|
||||
<unlock_entry item="necroModKnifeScavengersFeast,bookSummonRatSpirit" unlock_tier="3"/>
|
||||
<unlock_entry item="necroModKnifeGravesRepose" unlock_tier="4"/>
|
||||
<unlock_entry item="necroModKnifeDarkSense" unlock_tier="5"/>
|
||||
<unlock_entry item="necroModKnifeDarkSense,necroCinerarium,necroCinerariumLogist" unlock_tier="5"/>
|
||||
</display_entry>
|
||||
<!-- Ступенчатая разблокировка внутри группы. ПЕРЕРАСПРЕДЕЛЕНО 2026-09-09: четыре из
|
||||
шести модов ножа (вода/еда/покой/чутьё) уехали отсюда в группу 1 - см. большой
|
||||
|
||||
+34
-2
@@ -493,7 +493,7 @@
|
||||
гео-оссуария: транспорт нужен, когда карта уже велика для пеших ходок. Состав - по
|
||||
указанию пользователя: кости, гнилое мясо, прах, кожа жертвы, два флакона крови. -->
|
||||
<append xpath="/recipes">
|
||||
<recipe name="necroBoneSteedPlaceable" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
|
||||
<recipe name="necroBoneSteedPlaceable" count="1" craft_time="120" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
|
||||
<ingredient name="resourceBone" count="300"/>
|
||||
<ingredient name="foodRottingFlesh" count="30"/>
|
||||
<ingredient name="resourceZombieAsh" count="150"/>
|
||||
@@ -587,7 +587,7 @@
|
||||
<ingredient name="resourceWood" count="100"/>
|
||||
<ingredient name="resourceYuccaFibers" count="50"/>
|
||||
</recipe>
|
||||
<recipe name="necroBoneSteedPlaceable" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
|
||||
<recipe name="necroBoneSteedPlaceable" count="1" craft_time="120" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
|
||||
<ingredient name="resourceBone" count="300"/>
|
||||
<ingredient name="foodRottingFlesh" count="30"/>
|
||||
<ingredient name="resourceZombieAsh" count="150"/>
|
||||
@@ -605,4 +605,36 @@
|
||||
<ingredient name="necroSkullDecor" count="1"/>
|
||||
</recipe>
|
||||
</append>
|
||||
|
||||
<!-- Урна-цинерарий (1.7.0, 16.10): конец Адепта - уровень 15, 300 убийств (решение 06.10). Кровь - 16.10: «Да, кровью» (руны на урне расписаны кровью некроманта). -->
|
||||
<append xpath="/recipes">
|
||||
<recipe name="necroCinerarium" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceClayLump" count="30"/>
|
||||
<ingredient name="resourceZombieAsh" count="100"/>
|
||||
<ingredient name="resourceBone" count="10"/>
|
||||
<ingredient name="resourceNecromancerBlood" count="1"/>
|
||||
</recipe>
|
||||
<!-- Копия на соке чёрной розы (правило 05.10: у каждого рецепта на крови - копия; одна банка крови = одна банка сока). -->
|
||||
<recipe name="necroCinerarium" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceClayLump" count="30"/>
|
||||
<ingredient name="resourceZombieAsh" count="100"/>
|
||||
<ingredient name="resourceBone" count="10"/>
|
||||
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
|
||||
</recipe>
|
||||
<!-- Урна-логист (16.10: «Рецепт с набором для шитья») - рецепт урны и набор для шитья; копия на соке чёрной розы. -->
|
||||
<recipe name="necroCinerariumLogist" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceClayLump" count="30"/>
|
||||
<ingredient name="resourceZombieAsh" count="100"/>
|
||||
<ingredient name="resourceBone" count="10"/>
|
||||
<ingredient name="resourceNecromancerBlood" count="1"/>
|
||||
<ingredient name="resourceSewingKit" count="1"/>
|
||||
</recipe>
|
||||
<recipe name="necroCinerariumLogist" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceClayLump" count="30"/>
|
||||
<ingredient name="resourceZombieAsh" count="100"/>
|
||||
<ingredient name="resourceBone" count="10"/>
|
||||
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
|
||||
<ingredient name="resourceSewingKit" count="1"/>
|
||||
</recipe>
|
||||
</append>
|
||||
</config>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,626 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Дух-работник урны - «джинн из урны» (решение 16.10): кончик хвоста уходит в горлышко, дух вырастает над урной,
|
||||
/// без работы висит, чуть покачиваясь и медленно поворачиваясь. Этап 3 некротехнолога.
|
||||
///
|
||||
/// Не сущность игры, а видимая модель - дочерний объект модели блока урны, как каркас зоны (решение 16.10 вместо
|
||||
/// плана 15.10 «сущность на префабе медсестры»): хитбокса, ИИ и сохранения у неё нет, поэтому «убить нельзя»,
|
||||
/// «зомби не замечают», «сквозь стены» выходят сами, а сейвы дух не трогает - он появляется по черепу в урне
|
||||
/// (TEFeatureCinerarium.HasSkull) и пропадает без него. Сдвиги Origin и выгрузку чанка переживает с моделью урны.
|
||||
///
|
||||
/// Модель - бандл necrotechnologist (NecroTechnologistSetup.cs): своя сетка на своём скелете NT_rig в позе покоя,
|
||||
/// глаза - отдельные рендереры (в имени «eye»). Тело - светящаяся дымка (16.10: Fade давал пятна; сложение One/One,
|
||||
/// холодный оттенок Tint, своё слабое свечение Glow); текстура уже серая (сделана под обесцвечивание). Глаза - свой
|
||||
/// непрозрачный светящийся материал из бандла, очередь отрисовки 3100 - после тела.
|
||||
/// </summary>
|
||||
public static class CinerariumGenie
|
||||
{
|
||||
public const string ModelPath = "#@modfolder(NecromancerTome):Resources/necrotechnologist?necroTechnologistPrefab.prefab";
|
||||
|
||||
/// <summary>Высота корня модели над низом блока урны, м. 16.10 в Blender по модели урны: -0.15 («духа чуть приподними»).
|
||||
/// Подгоняется командой necrogenie.</summary>
|
||||
public static float OffsetY = -0.15f;
|
||||
/// <summary>Сдвиг вперёд в системе самого духа, м (16.10: «чуть вперёд, чтобы хвост не просвечивал сквозь урну сзади»
|
||||
/// - загнутый назад кончик хвоста; в Blender 0.06 - ни одной вершины за стенкой, взято 0.07). Поворачивается вместе
|
||||
/// с духом, поэтому хвост всегда в горлышке. Знак проверить в игре: necrogenie fwd.</summary>
|
||||
public static float Forward = 0.07f;
|
||||
/// <summary>Поворот без работы, градусов в секунду.</summary>
|
||||
public static float TurnSpeed = 6f;
|
||||
/// <summary>Покачивание: размах (м) и период (с).</summary>
|
||||
public static float BobHeight = 0.04f;
|
||||
public static float BobPeriod = 4f;
|
||||
public static float Alpha = 0.3f;
|
||||
/// <summary>Засыпание (16.10): игрок дальше SleepDistance - дух замирает дома, огоньки нитей стоят. Работа идёт.</summary>
|
||||
public static float SleepDistance = 40f;
|
||||
public static bool Hidden;
|
||||
|
||||
/// <summary>Далеко ли игрок от точки сцены (без Origin). Для засыпания духа и нитей.</summary>
|
||||
public static bool PlayerFar(Vector3 _scenePos)
|
||||
{
|
||||
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||||
if (pl == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return (pl.GetPosition() - Origin.position - _scenePos).sqrMagnitude > SleepDistance * SleepDistance;
|
||||
}
|
||||
/// <summary>Руки (16.10: «Т-поза бесит»): плечо от вертикали вбок и вперёд, локоть согнут вперёд, градусы.
|
||||
/// Модель в позе покоя, аниматора нет - кости ставятся один раз при появлении.</summary>
|
||||
public static float ArmOut = 12f;
|
||||
public static float ArmFwd = 15f;
|
||||
public static float Elbow = 35f;
|
||||
|
||||
static GameObject _prefab;
|
||||
static bool _tried;
|
||||
static readonly Dictionary<Material, Material> _ghostMats = new Dictionary<Material, Material>();
|
||||
/// <summary>Все джинны на сцене - команда necrogenie правит их сразу.</summary>
|
||||
public static readonly List<CinerariumGenieMotion> Live = new List<CinerariumGenieMotion>();
|
||||
|
||||
static bool Load()
|
||||
{
|
||||
if (!_tried)
|
||||
{
|
||||
_tried = true;
|
||||
_prefab = DataLoader.LoadAsset<GameObject>(ModelPath);
|
||||
Debug.Log("[NecromancerTome] CinerariumGenie: модель " + (_prefab != null ? "загружена" : "НЕ загружена: " + ModelPath));
|
||||
}
|
||||
return _prefab != null;
|
||||
}
|
||||
|
||||
public static GameObject Spawn(Transform _urnModel)
|
||||
{
|
||||
if (_urnModel == null || !Load())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
GameObject g = Object.Instantiate(_prefab, _urnModel, false);
|
||||
g.name = "NecroCinerariumGenie";
|
||||
foreach (Renderer r in g.GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
r.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
r.receiveShadows = false;
|
||||
if (r is SkinnedMeshRenderer smr)
|
||||
{
|
||||
smr.updateWhenOffscreen = false; // 16.10: вне экрана скелет не считать (экономия); границы - от сетки
|
||||
}
|
||||
if (r.name.Contains("eye"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Material[] mats = r.sharedMaterials;
|
||||
for (int i = 0; i < mats.Length; i++)
|
||||
{
|
||||
mats[i] = Ghost(mats[i]);
|
||||
}
|
||||
r.sharedMaterials = mats;
|
||||
}
|
||||
foreach (Collider c in g.GetComponentsInChildren<Collider>(true))
|
||||
{
|
||||
Object.Destroy(c);
|
||||
}
|
||||
g.AddComponent<CinerariumGenieMotion>().Pose();
|
||||
return g;
|
||||
}
|
||||
|
||||
/// <summary>Полупрозрачная копия материала тела - одна на исходный материал на всю игру.</summary>
|
||||
static Material Ghost(Material _src)
|
||||
{
|
||||
if (_src == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (_ghostMats.TryGetValue(_src, out Material m) && m != null)
|
||||
{
|
||||
return m;
|
||||
}
|
||||
m = new Material(_src) { name = _src.name + "_ghost" };
|
||||
Shader std = Shader.Find("Standard");
|
||||
if (std != null && m.shader != std)
|
||||
{
|
||||
m.shader = std;
|
||||
}
|
||||
// 16.10: Fade (SrcAlpha/OneMinusSrcAlpha) давал «прозрачный, но какими-то пятнами» и «не выглядит бесплотным
|
||||
// духом»: без записи глубины треугольники одной сетки ложатся в случайном порядке. Свечение (One/One) от порядка
|
||||
// не зависит: дух только добавляет свет к тому, что за ним, - дымка. Альфа здесь не нужна, яркость - цветом.
|
||||
m.SetFloat("_Mode", 2f);
|
||||
m.SetOverrideTag("RenderType", "Transparent");
|
||||
m.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
|
||||
m.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.One);
|
||||
m.SetInt("_ZWrite", 0);
|
||||
m.DisableKeyword("_ALPHATEST_ON");
|
||||
m.DisableKeyword("_ALPHABLEND_ON");
|
||||
m.DisableKeyword("_ALPHAPREMULTIPLY_ON");
|
||||
if (m.HasProperty("_Glossiness"))
|
||||
{
|
||||
m.SetFloat("_Glossiness", 0f); // без бликов: блик при сложении - белое пятно
|
||||
}
|
||||
if (m.HasProperty("_Metallic"))
|
||||
{
|
||||
m.SetFloat("_Metallic", 0f);
|
||||
}
|
||||
if (m.HasProperty("_EmissionMap") && m.HasProperty("_MainTex"))
|
||||
{
|
||||
m.SetTexture("_EmissionMap", m.GetTexture("_MainTex")); // своё свечение - чтобы ночью не пропадал
|
||||
m.EnableKeyword("_EMISSION");
|
||||
m.globalIlluminationFlags = MaterialGlobalIlluminationFlags.None;
|
||||
}
|
||||
m.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
|
||||
SetLook(m);
|
||||
_ghostMats[_src] = m;
|
||||
return m;
|
||||
}
|
||||
|
||||
/// <summary>Холодный бледный оттенок духа (умножается на серую текстуру).</summary>
|
||||
public static Color Tint = new Color(0.55f, 0.75f, 1f, 1f);
|
||||
/// <summary>Своё свечение тела, доля от Tint.</summary>
|
||||
public static float Glow = 0.35f;
|
||||
|
||||
static void SetLook(Material _m)
|
||||
{
|
||||
if (_m == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_m.HasProperty("_Color"))
|
||||
{
|
||||
_m.color = new Color(Tint.r * Alpha, Tint.g * Alpha, Tint.b * Alpha, 1f);
|
||||
}
|
||||
if (_m.HasProperty("_EmissionColor"))
|
||||
{
|
||||
_m.SetColor("_EmissionColor", new Color(Tint.r * Glow, Tint.g * Glow, Tint.b * Glow, 1f));
|
||||
}
|
||||
}
|
||||
|
||||
public static void ApplyAlpha()
|
||||
{
|
||||
foreach (Material m in _ghostMats.Values)
|
||||
{
|
||||
SetLook(m);
|
||||
}
|
||||
}
|
||||
|
||||
static readonly Dictionary<Material, Material> _stuckMats = new Dictionary<Material, Material>();
|
||||
|
||||
/// <summary>Почерневший дух (класть некуда): свечением чёрное не нарисовать, поэтому обычная Fade-копия, тёмная.</summary>
|
||||
public static Material Stuck(Material _src)
|
||||
{
|
||||
if (_src == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (_stuckMats.TryGetValue(_src, out Material m) && m != null)
|
||||
{
|
||||
return m;
|
||||
}
|
||||
m = new Material(_src) { name = _src.name + "_stuck" };
|
||||
Shader std = Shader.Find("Standard");
|
||||
if (std != null && m.shader != std)
|
||||
{
|
||||
m.shader = std;
|
||||
}
|
||||
m.SetFloat("_Mode", 2f);
|
||||
m.SetOverrideTag("RenderType", "Transparent");
|
||||
m.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
|
||||
m.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
|
||||
m.SetInt("_ZWrite", 1); // с глубиной: пятен нет, тёмный и почти плотный
|
||||
m.DisableKeyword("_ALPHATEST_ON");
|
||||
m.EnableKeyword("_ALPHABLEND_ON");
|
||||
m.DisableKeyword("_ALPHAPREMULTIPLY_ON");
|
||||
m.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
|
||||
if (m.HasProperty("_Color"))
|
||||
{
|
||||
m.color = new Color(0.04f, 0.035f, 0.045f, 0.8f);
|
||||
}
|
||||
_stuckMats[_src] = m;
|
||||
return m;
|
||||
}
|
||||
|
||||
/// <summary>Исходный материал тела по его светящейся копии.</summary>
|
||||
public static Material Source(Material _ghost)
|
||||
{
|
||||
foreach (KeyValuePair<Material, Material> p in _ghostMats)
|
||||
{
|
||||
if (p.Value == _ghost)
|
||||
{
|
||||
return p.Key;
|
||||
}
|
||||
}
|
||||
return _ghost;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Покачивание и поворот джинна над урной. Только вид - никакой логики.</summary>
|
||||
[Preserve]
|
||||
public class CinerariumGenieMotion : MonoBehaviour
|
||||
{
|
||||
float _yaw;
|
||||
float _phase;
|
||||
/// <summary>Полёт к месту работы и обратно (16.10: «возвращается задом», «делал действие не сразу, а хоть руками
|
||||
/// пошевелив»): туда - лицом к месту, над местом - жест руками (дело делается в его середине, см. Visit),
|
||||
/// обратно - развернувшись лицом к урне. Точка - в координатах модели урны; _t - время с начала полёта, -1 - дома.</summary>
|
||||
struct Stop
|
||||
{
|
||||
public Vector3 Hover, Look; // в осях модели урны
|
||||
public float Fly;
|
||||
}
|
||||
|
||||
readonly List<Stop> _stops = new List<Stop>();
|
||||
float _backFly;
|
||||
float _t = -1f;
|
||||
/// <summary>Скорость полёта, м/с: время перелёта - не меньше OutSeconds/BackSeconds, дальше - по расстоянию (нити до 15 м).</summary>
|
||||
public static float FlySpeed = 4f;
|
||||
public static float OutSeconds = 1.3f;
|
||||
public static float WorkSeconds = 1.6f;
|
||||
public static float BackSeconds = 1.3f;
|
||||
/// <summary>Жест: плечи вперёд до GestureFwd градусов, локти ходят на ±GestureWave вразнобой.</summary>
|
||||
public static float GestureFwd = 55f;
|
||||
public static float GestureWave = 30f;
|
||||
bool _gesturing;
|
||||
/// <summary>Почернел и замер (готовое класть некуда, решение 06.10).</summary>
|
||||
bool _stuck;
|
||||
/// <summary>Кости рук (плечо, предплечье, кисть по сторонам) и их поворот в позе покоя.</summary>
|
||||
Transform[] _arm;
|
||||
Quaternion[] _armRest;
|
||||
static bool _armLogged;
|
||||
|
||||
/// <summary>Опустить руки из позы покоя: направление плеча и предплечья задаётся в осях модели (вперёд - +Z),
|
||||
/// поворот - от текущего направления кости к нужному, так что оси самих костей значения не имеют.</summary>
|
||||
public void Pose()
|
||||
{
|
||||
Pose(CinerariumGenie.ArmFwd, CinerariumGenie.Elbow, CinerariumGenie.Elbow);
|
||||
}
|
||||
|
||||
void Pose(float _fwd, float _elbowL, float _elbowR)
|
||||
{
|
||||
if (_arm == null)
|
||||
{
|
||||
string[] names = { "LeftArm", "LeftForeArm", "LeftHand", "RightArm", "RightForeArm", "RightHand" };
|
||||
_arm = new Transform[names.Length];
|
||||
_armRest = new Quaternion[names.Length];
|
||||
foreach (Transform t in GetComponentsInChildren<Transform>(true))
|
||||
{
|
||||
for (int i = 0; i < names.Length; i++)
|
||||
{
|
||||
if (_arm[i] == null && t.name.EndsWith(names[i]))
|
||||
{
|
||||
_arm[i] = t;
|
||||
_armRest[i] = t.localRotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!_armLogged)
|
||||
{
|
||||
_armLogged = true;
|
||||
System.Text.StringBuilder sb = new System.Text.StringBuilder();
|
||||
for (int i = 0; i < names.Length; i++)
|
||||
{
|
||||
sb.Append(names[i]).Append('=').Append(_arm[i] != null ? "ok" : "НЕТ").Append(' ');
|
||||
}
|
||||
Debug.Log("[NecromancerTome] CinerariumGenie: кости рук " + sb);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < _arm.Length; i++)
|
||||
{
|
||||
if (_arm[i] != null)
|
||||
{
|
||||
_arm[i].localRotation = _armRest[i];
|
||||
}
|
||||
}
|
||||
for (int side = 0; side < 2; side++)
|
||||
{
|
||||
Transform up = _arm[side * 3], fore = _arm[side * 3 + 1], hand = _arm[side * 3 + 2];
|
||||
if (up == null || fore == null || hand == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
float sign = Mathf.Sign(transform.InverseTransformPoint(up.position).x);
|
||||
float o = CinerariumGenie.ArmOut * Mathf.Deg2Rad, f = _fwd * Mathf.Deg2Rad;
|
||||
Vector3 upDir = new Vector3(sign * Mathf.Sin(o), -Mathf.Cos(o) * Mathf.Cos(f), Mathf.Sin(f)).normalized;
|
||||
Vector3 foreDir = Vector3.RotateTowards(upDir, Vector3.forward, (side == 0 ? _elbowL : _elbowR) * Mathf.Deg2Rad, 0f);
|
||||
up.rotation = Quaternion.FromToRotation(fore.position - up.position, transform.TransformDirection(upDir)) * up.rotation;
|
||||
fore.rotation = Quaternion.FromToRotation(hand.position - fore.position, transform.TransformDirection(foreDir)) * fore.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Светящиеся материалы рендереров - вернуть после почернения.</summary>
|
||||
readonly Dictionary<Renderer, Material[]> _glowing = new Dictionary<Renderer, Material[]>();
|
||||
|
||||
/// <summary>Облёт мест работы по порядку (у перевозки по нити - ящик «откуда», потом «куда»), у каждого - жест, затем
|
||||
/// назад к урне. Точки - в координатах сцены (без Origin): stand - где встать (низ клетки), look - низ клетки места,
|
||||
/// beside - встал сбоку, а не над местом. Возвращает, через сколько секунд дело (середина последнего жеста).</summary>
|
||||
public float Visit(List<(Vector3 stand, Vector3 look, bool beside)> _list)
|
||||
{
|
||||
if (transform.parent == null || _list == null || _list.Count == 0)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
_stops.Clear();
|
||||
Vector3 home = Home();
|
||||
Vector3 prev = home;
|
||||
float delay = 0f;
|
||||
foreach ((Vector3 stand, Vector3 look, bool beside) in _list)
|
||||
{
|
||||
Stop st = new Stop
|
||||
{
|
||||
Hover = transform.parent.InverseTransformPoint(stand) + new Vector3(0f, CinerariumGenie.OffsetY + (beside ? 0.15f : 0.8f), 0f),
|
||||
Look = transform.parent.InverseTransformPoint(look)
|
||||
};
|
||||
st.Fly = Mathf.Max(OutSeconds, Vector3.Distance(prev, st.Hover) / FlySpeed);
|
||||
delay += st.Fly + WorkSeconds;
|
||||
prev = st.Hover;
|
||||
_stops.Add(st);
|
||||
}
|
||||
_backFly = Mathf.Max(BackSeconds, Vector3.Distance(prev, home) / FlySpeed);
|
||||
_t = 0f;
|
||||
return delay - WorkSeconds * 0.5f;
|
||||
}
|
||||
|
||||
Vector3 Home()
|
||||
{
|
||||
return new Vector3(0f, CinerariumGenie.OffsetY, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward);
|
||||
}
|
||||
|
||||
/// <summary>Повернуться лицом по направлению _dir (горизонтально), не быстрее 300°/с.</summary>
|
||||
void Face(Vector3 _dir)
|
||||
{
|
||||
if (_dir.x * _dir.x + _dir.z * _dir.z > 0.01f)
|
||||
{
|
||||
_yaw = Mathf.MoveTowardsAngle(_yaw, Mathf.Atan2(_dir.x, _dir.z) * Mathf.Rad2Deg, 300f * Time.deltaTime) % 360f;
|
||||
}
|
||||
}
|
||||
|
||||
void Gesture(float _w)
|
||||
{
|
||||
if (_w > 0f)
|
||||
{
|
||||
float wave = Mathf.Sin(_t * Mathf.PI * 2f * 1.5f) * GestureWave * _w;
|
||||
Pose(Mathf.Lerp(CinerariumGenie.ArmFwd, GestureFwd, _w), CinerariumGenie.Elbow + 15f * _w + wave, CinerariumGenie.Elbow + 15f * _w - wave);
|
||||
_gesturing = true;
|
||||
}
|
||||
else if (_gesturing)
|
||||
{
|
||||
Pose();
|
||||
_gesturing = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetStuck(bool _on)
|
||||
{
|
||||
if (_stuck == _on)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_stuck = _on;
|
||||
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
if (r.name.Contains("eye"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!_glowing.ContainsKey(r))
|
||||
{
|
||||
_glowing[r] = r.sharedMaterials;
|
||||
}
|
||||
Material[] glow = _glowing[r];
|
||||
if (_on)
|
||||
{
|
||||
Material[] dark = new Material[glow.Length];
|
||||
for (int i = 0; i < glow.Length; i++)
|
||||
{
|
||||
dark[i] = CinerariumGenie.Stuck(CinerariumGenie.Source(glow[i]));
|
||||
}
|
||||
r.sharedMaterials = dark;
|
||||
}
|
||||
else
|
||||
{
|
||||
r.sharedMaterials = glow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
CinerariumGenie.Live.Add(this);
|
||||
_phase = Random.value * 10f;
|
||||
_yaw = Random.value * 360f;
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
CinerariumGenie.Live.Remove(this);
|
||||
}
|
||||
|
||||
float _nextSleepCheck;
|
||||
bool _asleep;
|
||||
bool _hiddenApplied;
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (Time.time >= _nextSleepCheck)
|
||||
{
|
||||
_nextSleepCheck = Time.time + 0.5f;
|
||||
bool asleep = CinerariumGenie.PlayerFar(transform.position);
|
||||
if (asleep && !_asleep)
|
||||
{
|
||||
_t = -1f; // полёт прерван - дело всё равно выполнится
|
||||
Gesture(0f);
|
||||
transform.localPosition = Home();
|
||||
}
|
||||
_asleep = asleep;
|
||||
if (_hiddenApplied != CinerariumGenie.Hidden)
|
||||
{
|
||||
// Только при смене (16.10, проверка на утечки: раньше список рендереров собирался каждые 0.5 с - мусор для GC).
|
||||
_hiddenApplied = CinerariumGenie.Hidden;
|
||||
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
r.enabled = !_hiddenApplied;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_stuck || _asleep)
|
||||
{
|
||||
return; // замер как был
|
||||
}
|
||||
float yawTarget = _yaw + CinerariumGenie.TurnSpeed * Time.deltaTime;
|
||||
Vector3 home = Home();
|
||||
if (_t >= 0f && _stops.Count > 0)
|
||||
{
|
||||
_t += Time.deltaTime;
|
||||
float acc = 0f;
|
||||
Vector3 from = home;
|
||||
foreach (Stop st in _stops)
|
||||
{
|
||||
if (_t < acc + st.Fly)
|
||||
{
|
||||
transform.localPosition = Vector3.Lerp(from, st.Hover, Mathf.SmoothStep(0f, 1f, (_t - acc) / st.Fly));
|
||||
Face(st.Hover - from);
|
||||
Gesture(0f);
|
||||
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
|
||||
return;
|
||||
}
|
||||
acc += st.Fly;
|
||||
if (_t < acc + WorkSeconds)
|
||||
{
|
||||
transform.localPosition = st.Hover;
|
||||
Face(st.Look - st.Hover); // за работой - лицом к месту
|
||||
Gesture(Mathf.Sin((_t - acc) / WorkSeconds * Mathf.PI));
|
||||
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
|
||||
return;
|
||||
}
|
||||
acc += WorkSeconds;
|
||||
from = st.Hover;
|
||||
}
|
||||
if (_t < acc + _backFly)
|
||||
{
|
||||
transform.localPosition = Vector3.Lerp(from, home, Mathf.SmoothStep(0f, 1f, (_t - acc) / _backFly));
|
||||
Face(home - from); // назад - лицом к урне
|
||||
Gesture(0f);
|
||||
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
|
||||
return;
|
||||
}
|
||||
_t = -1f;
|
||||
Gesture(0f);
|
||||
}
|
||||
_yaw = yawTarget % 360f;
|
||||
float bob = CinerariumGenie.BobPeriod > 0.01f
|
||||
? Mathf.Sin((Time.time + _phase) * Mathf.PI * 2f / CinerariumGenie.BobPeriod) * CinerariumGenie.BobHeight
|
||||
: 0f;
|
||||
transform.localPosition = home + new Vector3(0f, bob, 0f);
|
||||
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Подгонка джинна в игре: высота над урной, скорость поворота, покачивание, прозрачность. Не сохраняется -
|
||||
/// найденные значения переносятся в константы CinerariumGenie.</summary>
|
||||
[Preserve]
|
||||
public class ConsoleCmdNecroGenie : ConsoleCmdAbstract
|
||||
{
|
||||
public override bool IsExecuteOnClient => true;
|
||||
|
||||
public override bool AllowedInMainMenu => false;
|
||||
|
||||
public override string[] getCommands()
|
||||
{
|
||||
return new string[] { "necrogenie" };
|
||||
}
|
||||
|
||||
public override string getDescription()
|
||||
{
|
||||
return "Necromancer's Tome: tune the cinerarium spirit (height over the urn, turning, bobbing, transparency).";
|
||||
}
|
||||
|
||||
public override string getHelp()
|
||||
{
|
||||
return "necrogenie - show the current values and how many spirits are on screen\n" +
|
||||
"necrogenie y <m> - model root height above the urn block's bottom (tail tip is 0.47 m above the root)\n" +
|
||||
"necrogenie fwd <m> - shift along the spirit's own forward (keeps the curled tail inside the neck)\n" +
|
||||
"necrogenie turn <deg/s> - idle turning speed\n" +
|
||||
"necrogenie bob <m> <s> - bobbing height and period\n" +
|
||||
"necrogenie alpha <0-1> - body brightness (the body adds light, it is not alpha-blended)\n" +
|
||||
"necrogenie glow <0-1> - body's own glow (visible at night)\n" +
|
||||
"necrogenie tint <r> <g> <b> - body colour\n" +
|
||||
"necrogenie arms <out> <fwd> <elbow> - arm pose, degrees: upper arm from vertical sideways and forward, elbow bend\n" +
|
||||
"necrogenie sleep <m> - spirits and threads freeze when the player is farther (work goes on)\n" +
|
||||
"necrogenie hide|show - hide all spirits (weak PCs, video), work goes on";
|
||||
}
|
||||
|
||||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||||
{
|
||||
SdtdConsole con = SingletonMonoBehaviour<SdtdConsole>.Instance;
|
||||
if (_params.Count == 1 && (_params[0] == "hide" || _params[0] == "show"))
|
||||
{
|
||||
CinerariumGenie.Hidden = _params[0] == "hide";
|
||||
}
|
||||
if (_params.Count >= 2 && float.TryParse(_params[1], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float v))
|
||||
{
|
||||
switch (_params[0].ToLowerInvariant())
|
||||
{
|
||||
case "y":
|
||||
CinerariumGenie.OffsetY = v;
|
||||
break;
|
||||
case "fwd":
|
||||
CinerariumGenie.Forward = v;
|
||||
break;
|
||||
case "turn":
|
||||
CinerariumGenie.TurnSpeed = v;
|
||||
break;
|
||||
case "bob":
|
||||
CinerariumGenie.BobHeight = v;
|
||||
if (_params.Count >= 3 && float.TryParse(_params[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float per))
|
||||
{
|
||||
CinerariumGenie.BobPeriod = per;
|
||||
}
|
||||
break;
|
||||
case "alpha":
|
||||
CinerariumGenie.Alpha = Mathf.Clamp01(v);
|
||||
CinerariumGenie.ApplyAlpha();
|
||||
break;
|
||||
case "glow":
|
||||
CinerariumGenie.Glow = Mathf.Clamp01(v);
|
||||
CinerariumGenie.ApplyAlpha();
|
||||
break;
|
||||
case "sleep":
|
||||
CinerariumGenie.SleepDistance = Mathf.Max(5f, v);
|
||||
break;
|
||||
case "arms":
|
||||
CinerariumGenie.ArmOut = v;
|
||||
if (_params.Count >= 3 && float.TryParse(_params[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float af))
|
||||
{
|
||||
CinerariumGenie.ArmFwd = af;
|
||||
}
|
||||
if (_params.Count >= 4 && float.TryParse(_params[3], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float ae))
|
||||
{
|
||||
CinerariumGenie.Elbow = ae;
|
||||
}
|
||||
foreach (CinerariumGenieMotion m in CinerariumGenie.Live)
|
||||
{
|
||||
m.Pose();
|
||||
}
|
||||
break;
|
||||
case "tint":
|
||||
if (_params.Count >= 4 &&
|
||||
float.TryParse(_params[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float g) &&
|
||||
float.TryParse(_params[3], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float b))
|
||||
{
|
||||
CinerariumGenie.Tint = new Color(v, g, b, 1f);
|
||||
CinerariumGenie.ApplyAlpha();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
con.Output("necrogenie: unknown '" + _params[0] + "' - see 'help necrogenie'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
con.Output("necrogenie: y " + CinerariumGenie.OffsetY.ToString("F2") + ", fwd " + CinerariumGenie.Forward.ToString("F2") + ", turn " + CinerariumGenie.TurnSpeed.ToString("F1") +
|
||||
", bob " + CinerariumGenie.BobHeight.ToString("F2") + " / " + CinerariumGenie.BobPeriod.ToString("F1") + " s, alpha " +
|
||||
CinerariumGenie.Alpha.ToString("F2") + ", glow " + CinerariumGenie.Glow.ToString("F2") + ", tint " +
|
||||
CinerariumGenie.Tint.r.ToString("F2") + " " + CinerariumGenie.Tint.g.ToString("F2") + " " + CinerariumGenie.Tint.b.ToString("F2") + ", arms " + CinerariumGenie.ArmOut.ToString("F0") + " " +
|
||||
CinerariumGenie.ArmFwd.ToString("F0") + " " + CinerariumGenie.Elbow.ToString("F0") + ", sleep " + CinerariumGenie.SleepDistance.ToString("F0") + " m" + (CinerariumGenie.Hidden ? ", HIDDEN" : "") + "; spirits on screen: " + CinerariumGenie.Live.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
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;
|
||||
public const int Count = 23;
|
||||
public const uint All = (1u << Count) - 1;
|
||||
|
||||
/// <summary>Значок группы - иконка типичного предмета (ItemIconAtlas).</summary>
|
||||
public static readonly string[] Icons =
|
||||
{
|
||||
"foodMeatStew", "foodCanChili", "foodRawMeat", "foodCropCorn", "plantedChrysanthemum3Harvest", "plantedCorn1",
|
||||
"drinkJarBoiledWater", "medicalFirstAidKit", "resourceRockSmall", "resourceForgedSteel", "resourceMechanicalParts",
|
||||
"resourceWood", "resourceGunPowder", "meleeToolPickT1IronPickaxe", "gunHandgunT1Pistol", "ammo9mmBulletBall",
|
||||
"armorMinerHelmet", "modGunLaserSight", "bookArtOfMiningLuckyStrike", "cobblestoneMaster", "necroSkull", "oldCash",
|
||||
"resourceChickenFeed"
|
||||
};
|
||||
|
||||
/// <summary>Ключ названия группы в Localization.csv: necroGroup0..necroGroup22.</summary>
|
||||
public static string NameKey(int _g) => "necroGroup" + _g;
|
||||
|
||||
static readonly HashSet<string> OreNames = new HashSet<string>
|
||||
{
|
||||
"resourceRockSmall", "resourceRockSmallBundle", "resourceScrapIron", "resourceScrapIronBundle", "resourceScrapLead",
|
||||
"resourceLeadBundle", "resourceCoal", "resourceCoalBundle", "resourcePotassiumNitratePowder",
|
||||
"resourcePotassiumNitratePowderBundle", "resourceOilShale", "resourceOilShaleBundle", "resourceClayLump",
|
||||
"resourceCrushedSand", "resourceCobblestones", "resourceGoldNugget", "resourceSilverNugget", "resourceRawDiamond",
|
||||
"resourceSnowBall"
|
||||
};
|
||||
static readonly HashSet<string> IngotNames = new HashSet<string> { "resourceForgedIron", "resourceForgedSteel" };
|
||||
static readonly HashSet<string> ChemNames = new HashSet<string>
|
||||
{
|
||||
"resourceGunPowder", "resourceGunPowderBundle", "resourceOil", "ammoGasCan", "ammoGasCanBundle", "resourceAcid",
|
||||
"resourceGlue", "resourcePaint", "resourceTestosteroneExtract"
|
||||
};
|
||||
static readonly HashSet<string> MaterialNames = new HashSet<string>
|
||||
{
|
||||
"resourceWood", "resourceWoodBundle", "resourceCloth", "resourceLeather", "resourceBone", "resourceFeather",
|
||||
"resourceYuccaFibers", "resourcePaper", "resourceCement", "resourceConcreteMix", "resourceBeeswax", "resourceCandleStick",
|
||||
"resourceAnimalFat"
|
||||
};
|
||||
/// <summary>Детали, которые теги отнесли бы не туда (гвозди - патроны гвоздомёта).</summary>
|
||||
static readonly HashSet<string> PartNames = new HashSet<string> { "resourceNail" };
|
||||
static readonly HashSet<string> AmmoParts = new HashSet<string>
|
||||
{
|
||||
"resourceArrowHeadIron", "resourceArrowHeadSteelAP", "resourceBuckshot", "resourceBulletCasing", "resourceBulletTip",
|
||||
"resourceRocketCasing", "resourceRocketTip"
|
||||
};
|
||||
static readonly HashSet<string> RawNames = new HashSet<string>
|
||||
{
|
||||
"foodRawMeat", "foodRawMeatBundle", "foodEgg", "foodHoney", "foodRottingFlesh"
|
||||
};
|
||||
|
||||
/// <summary>Предметы и блоки самого мода - по его Config/items.xml и blocks.xml (у многих нет «necro» в имени: книги
|
||||
/// призыва, браслет, камни портала, прах зомби, кожа жертвы). Читается один раз.</summary>
|
||||
static HashSet<string> _modNames;
|
||||
|
||||
static HashSet<string> ModNames()
|
||||
{
|
||||
if (_modNames != null)
|
||||
{
|
||||
return _modNames;
|
||||
}
|
||||
_modNames = new HashSet<string>();
|
||||
foreach (string f in new[] { "items.xml", "blocks.xml" })
|
||||
{
|
||||
try
|
||||
{
|
||||
XmlDocument doc = new XmlDocument();
|
||||
doc.Load(Path.Combine(ModEntry.Instance.Path, "Config", f));
|
||||
foreach (XmlNode n in doc.SelectNodes("//item[@name] | //block[@name]"))
|
||||
{
|
||||
_modNames.Add(n.Attributes["name"].Value);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning("[NecromancerTome] CinerariumGroups: " + f + " мода не прочитан (" + e.Message + ")");
|
||||
}
|
||||
}
|
||||
return _modNames;
|
||||
}
|
||||
|
||||
static readonly Dictionary<int, int> _cache = new Dictionary<int, int>();
|
||||
|
||||
public static int Of(ItemValue _iv)
|
||||
{
|
||||
ItemClass ic = _iv?.ItemClass;
|
||||
if (ic == null)
|
||||
{
|
||||
return Other;
|
||||
}
|
||||
if (!_cache.TryGetValue(ic.Id, out int g))
|
||||
{
|
||||
g = Classify(ic, _iv);
|
||||
_cache[ic.Id] = g;
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
static bool Tag(ItemClass _ic, string _t)
|
||||
{
|
||||
return _ic.ItemTags.Test_AnySet(FastTags<TagGroup.Global>.Parse(_t));
|
||||
}
|
||||
|
||||
static int Classify(ItemClass _ic, ItemValue _iv)
|
||||
{
|
||||
string n = _ic.GetItemName() ?? "";
|
||||
if (n.IndexOf("necro", StringComparison.OrdinalIgnoreCase) >= 0 || ModNames().Contains(n))
|
||||
{
|
||||
return Necro;
|
||||
}
|
||||
if (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 (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,418 @@
|
||||
using System.Collections.Generic;
|
||||
using GUI_2;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Нити цинерария (16.10.2026, BACKLOG «Нити цинерария»): перевозка между ящиками, как провода электрики. «Ящик на
|
||||
/// территории одного цинерария и присоединяю к нему другой цинерарий проводом. А затем от цинерария тяну провод до другого
|
||||
/// ящика. Дух в цинерарии будет забирать всё что лежит в ящике из которого в него тянется провод, и тащить в ящик, в
|
||||
/// который тянется провод.» Решения: у урны несколько нитей «откуда» и одна «куда»; везётся по образцу (только то, что уже
|
||||
/// лежит в ящике «куда»; пустой - всё подряд); «куда» полон - дух ждёт; нить призрачная; длина - как у провода (15 м);
|
||||
/// ящики - любые. Нити - только у отдельного блока «урна-логист» (necroCinerariumLogist, 16.10): без зоны, дух только перевозит,
|
||||
/// глазницы всегда зелёные.
|
||||
///
|
||||
/// Инструмент - нож некроманта «хватом для нитей»: отдельный предмет necroWpnBladeNecroKnifeThread (класс ножа в сейве
|
||||
/// менять нельзя), переключение - пункт в меню ножа по R (то же меню, что «человек/змея/чертоги», SnakeMode.cs; 16.10:
|
||||
/// своё меню по R перекрывало его - «ты убрал хваты чертогов памяти и духа змеи»); качество и моды переносятся
|
||||
/// (ItemValue.Clone со сменой типа). Как у стриппера: ПКМ - начать нить и закончить её (порядок = направление: ящик, потом
|
||||
/// урна - «откуда»; урна, потом ящик - «куда»), ЛКМ по урне или ящику - снять его нити.
|
||||
/// Первая версия - одиночная игра и хост (связи меняются на месте, клиентам сети - позже).
|
||||
/// </summary>
|
||||
public static class CinerariumThreads
|
||||
{
|
||||
public const string KnifeName = Patch_ItemClass_ExecuteAction_KnifePowerCost.KnifeName;
|
||||
public const string ThreadKnifeName = "necroWpnBladeNecroKnifeThread";
|
||||
/// <summary>Как у провода электрики: ItemActionConnectPower.maxWireLength = 15.</summary>
|
||||
public const float MaxLength = 15f;
|
||||
public const int MaxFrom = 8;
|
||||
public const int GripNormal = 0, GripThread = 1;
|
||||
/// <summary>Цвет нити - в цвет духа (CinerariumGenie.Tint).</summary>
|
||||
public static readonly Color ThreadColor = new Color(0.55f, 0.8f, 1f, 0.45f);
|
||||
public static readonly Color SparkColor = new Color(0.8f, 0.95f, 1f, 1f);
|
||||
|
||||
public static bool IsKnife(ItemClass _i) => _i != null && (_i.Name == KnifeName || _i.Name == ThreadKnifeName);
|
||||
public static bool IsThreadKnife(ItemClass _i) => _i != null && _i.Name == ThreadKnifeName;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Хват.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>Нож в руке меняется на _name: тот же ItemValue (качество, износ, моды), другой тип.</summary>
|
||||
public static void Convert(EntityPlayerLocal _pl, string _name)
|
||||
{
|
||||
ItemStack cur = _pl?.inventory?.holdingItemStack;
|
||||
if (cur == null || cur.IsEmpty() || !IsKnife(cur.itemValue.ItemClass) || cur.itemValue.ItemClass.Name == _name)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ItemValue target = ItemClass.GetItem(_name);
|
||||
if (target == null || target.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
ItemValue iv = cur.itemValue.Clone();
|
||||
iv.type = target.type;
|
||||
_pl.inventory.SetItem(_pl.inventory.holdingItemIdx, new ItemStack(iv, cur.count));
|
||||
Cancel();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Соединение.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
static Vector3i? _start;
|
||||
static bool _startUrn;
|
||||
static GameObject _preview;
|
||||
|
||||
public static Vector3i? Start => _start;
|
||||
public static bool StartIsUrn => _startUrn;
|
||||
|
||||
public static void Cancel()
|
||||
{
|
||||
_start = null;
|
||||
if (_preview != null)
|
||||
{
|
||||
Object.Destroy(_preview);
|
||||
_preview = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Урна или ящик (составной блок игрока с хранилищем) в клетке _p; _owner - главная клетка блока.</summary>
|
||||
public static bool Resolve(World _w, Vector3i _p, out Vector3i _owner, out TEFeatureCinerarium _urn)
|
||||
{
|
||||
BlockValue bv = _w.GetBlock(_p);
|
||||
_owner = bv.ischild ? _p + bv.parent : _p;
|
||||
_urn = null;
|
||||
if (!(_w.GetTileEntity(_owner) is TileEntityComposite comp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (comp.TryGetSelfOrFeature<TEFeatureCinerarium>(out _urn))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return comp.PlayerPlaced && comp.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage _);
|
||||
}
|
||||
|
||||
public static TEFeatureCinerarium UrnAt(World _w, Vector3i _p)
|
||||
{
|
||||
return _w.GetTileEntity(_p) is TileEntityComposite c && c.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium u) ? u : null;
|
||||
}
|
||||
|
||||
/// <summary>Отпущена кнопка с ножом-для-нитей: _connect - ПКМ (тянуть нить), иначе ЛКМ (снять нити).</summary>
|
||||
public static void Click(EntityPlayerLocal _pl, bool _connect)
|
||||
{
|
||||
World w = GameManager.Instance?.World;
|
||||
WorldRayHitInfo hi = _pl?.HitInfo;
|
||||
if (w == null || hi == null || !hi.bHitValid || !GameUtils.IsBlockOrTerrain(hi.tag) || hi.hit.distanceSq > 36f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
bool ok = Resolve(w, hi.hit.blockPos, out Vector3i owner, out TEFeatureCinerarium urn);
|
||||
if (!_connect)
|
||||
{
|
||||
if (_start.HasValue)
|
||||
{
|
||||
Cancel();
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadCancel"));
|
||||
return;
|
||||
}
|
||||
if (ok && RemoveAll(w, owner, urn) > 0)
|
||||
{
|
||||
_pl.PlayOneShot("wiretool_grab");
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadRemoved"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!ok)
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedPair"));
|
||||
return;
|
||||
}
|
||||
if (!_start.HasValue)
|
||||
{
|
||||
if (urn != null && !urn.IsLogist)
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedLogist"));
|
||||
return;
|
||||
}
|
||||
_start = owner;
|
||||
_startUrn = urn != null;
|
||||
_preview = new GameObject("NecroThreadPreview");
|
||||
_preview.AddComponent<CinerariumThreadPreview>();
|
||||
_pl.PlayOneShot("wiretool_grab");
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadStart"));
|
||||
return;
|
||||
}
|
||||
Vector3i s = _start.Value;
|
||||
bool sUrn = _startUrn;
|
||||
Cancel();
|
||||
if (s == owner)
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadCancel"));
|
||||
return;
|
||||
}
|
||||
if (sUrn == (urn != null))
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedPair"));
|
||||
return;
|
||||
}
|
||||
TEFeatureCinerarium end = sUrn ? UrnAt(w, s) : urn;
|
||||
if (end == null || !end.IsLogist)
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedLogist"));
|
||||
return;
|
||||
}
|
||||
if (Vector3.Distance(s.ToVector3(), owner.ToVector3()) > MaxLength)
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTooFar"));
|
||||
return;
|
||||
}
|
||||
if (sUrn)
|
||||
{
|
||||
TEFeatureCinerarium u = UrnAt(w, s);
|
||||
if (u == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
u.SetThreadTo(owner);
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTo"));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!urn.AddThreadFrom(s))
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadLimit"));
|
||||
return;
|
||||
}
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadFrom"));
|
||||
}
|
||||
_pl.PlayOneShot("wiretool_place");
|
||||
}
|
||||
|
||||
/// <summary>Снять все нити урны или все нити, идущие к ящику (у всех урн сейва). Сколько снято.</summary>
|
||||
static int RemoveAll(World _w, Vector3i _owner, TEFeatureCinerarium _urn)
|
||||
{
|
||||
if (_urn != null)
|
||||
{
|
||||
return _urn.ClearThreads();
|
||||
}
|
||||
int n = 0;
|
||||
foreach (Vector3i p in CinerariumRegistry.All())
|
||||
{
|
||||
TEFeatureCinerarium u = UrnAt(_w, p);
|
||||
if (u != null)
|
||||
{
|
||||
n += u.RemoveThreadsTo(_owner);
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Вид нитей.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
static Material _mat;
|
||||
|
||||
public static Material Mat()
|
||||
{
|
||||
if (_mat == null)
|
||||
{
|
||||
_mat = new Material(Shader.Find("Sprites/Default")) { name = "NecroThread" };
|
||||
}
|
||||
return _mat;
|
||||
}
|
||||
|
||||
/// <summary>Где нить крепится: у урны - горлышко, у ящика - край со стороны урны, чуть выше середины. Сцена (без Origin).</summary>
|
||||
public static Vector3 UrnPoint(Vector3i _urn) => World.blockToTransformPos(_urn) - Origin.position + Vector3.up * 0.5f;
|
||||
|
||||
public static Vector3 BoxPoint(Vector3i _box, Vector3i _urn)
|
||||
{
|
||||
Vector3 c = World.blockToTransformPos(_box) - Origin.position + Vector3.up * 0.6f;
|
||||
Vector3 d = (World.blockToTransformPos(_urn) - World.blockToTransformPos(_box));
|
||||
d.y = 0f;
|
||||
if (d.sqrMagnitude > 0.01f)
|
||||
{
|
||||
d.Normalize();
|
||||
c += d * 0.5f;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
/// <summary>Провисающая нить: точка по доле пути _u.</summary>
|
||||
public static Vector3 Curve(Vector3 _a, Vector3 _b, float _u)
|
||||
{
|
||||
float sag = Mathf.Min(0.6f, Vector3.Distance(_a, _b) * 0.05f);
|
||||
return Vector3.Lerp(_a, _b, _u) + Vector3.down * (sag * 4f * _u * (1f - _u));
|
||||
}
|
||||
|
||||
public static LineRenderer NewLine(Transform _parent, string _name, float _width, Color _c)
|
||||
{
|
||||
GameObject g = new GameObject(_name);
|
||||
g.transform.SetParent(_parent, false);
|
||||
LineRenderer lr = g.AddComponent<LineRenderer>();
|
||||
lr.useWorldSpace = false;
|
||||
lr.widthMultiplier = _width;
|
||||
lr.sharedMaterial = Mat();
|
||||
lr.startColor = _c;
|
||||
lr.endColor = _c;
|
||||
lr.numCapVertices = 2;
|
||||
lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
lr.receiveShadows = false;
|
||||
return lr;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Нити одной урны: провисающие линии и бегущие по ним огоньки (направление). Видны, только пока в руках нож
|
||||
/// хватом для нитей. Дочерний объект модели урны -
|
||||
/// сдвиги Origin и выгрузка чанка его не теряют; точки - в осях модели.</summary>
|
||||
[Preserve]
|
||||
public class CinerariumThreadView : MonoBehaviour
|
||||
{
|
||||
struct Thread
|
||||
{
|
||||
public Vector3 A, B; // в осях модели урны, огоньки бегут от A к B
|
||||
public LineRenderer[] Sparks;
|
||||
}
|
||||
|
||||
readonly List<Thread> _threads = new List<Thread>();
|
||||
const int Segments = 16;
|
||||
const int SparksPerThread = 3;
|
||||
public static float SparkSpeed = 1.2f; // м/с
|
||||
|
||||
/// <summary>_pairs - концы в координатах сцены (без Origin). Видимость - заново по ножу в руке (Update).</summary>
|
||||
public void Build(List<(Vector3 a, Vector3 b)> _pairs)
|
||||
{
|
||||
foreach (Transform c in transform)
|
||||
{
|
||||
Destroy(c.gameObject);
|
||||
}
|
||||
_threads.Clear();
|
||||
Transform t = transform;
|
||||
foreach ((Vector3 a, Vector3 b) in _pairs)
|
||||
{
|
||||
Thread th = new Thread { A = t.InverseTransformPoint(a), B = t.InverseTransformPoint(b), Sparks = new LineRenderer[SparksPerThread] };
|
||||
LineRenderer lr = CinerariumThreads.NewLine(t, "thread", 0.025f, CinerariumThreads.ThreadColor);
|
||||
lr.positionCount = Segments + 1;
|
||||
for (int i = 0; i <= Segments; i++)
|
||||
{
|
||||
lr.SetPosition(i, CinerariumThreads.Curve(th.A, th.B, i / (float)Segments));
|
||||
}
|
||||
for (int k = 0; k < SparksPerThread; k++)
|
||||
{
|
||||
th.Sparks[k] = CinerariumThreads.NewLine(t, "spark", 0.06f, CinerariumThreads.SparkColor);
|
||||
th.Sparks[k].positionCount = 2;
|
||||
}
|
||||
_threads.Add(th);
|
||||
}
|
||||
_shown = -1;
|
||||
}
|
||||
|
||||
float _nextSleepCheck;
|
||||
bool _asleep;
|
||||
int _shown = -1;
|
||||
|
||||
/// <summary>16.10: «Нити должны быть видны, только когда нож в режиме нитей и в руках».</summary>
|
||||
public static bool ToolInHand()
|
||||
{
|
||||
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||||
return pl != null && CinerariumThreads.IsThreadKnife(pl.inventory?.holdingItem);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (Time.time >= _nextSleepCheck)
|
||||
{
|
||||
_nextSleepCheck = Time.time + 0.25f;
|
||||
_asleep = CinerariumGenie.PlayerFar(transform.position); // засыпание (16.10): огоньки стоят
|
||||
int show = ToolInHand() ? 1 : 0;
|
||||
if (show != _shown)
|
||||
{
|
||||
_shown = show;
|
||||
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
r.enabled = show == 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_asleep || _shown != 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (Thread th in _threads)
|
||||
{
|
||||
float len = Mathf.Max(0.5f, Vector3.Distance(th.A, th.B));
|
||||
float step = 0.2f / len;
|
||||
for (int k = 0; k < th.Sparks.Length; k++)
|
||||
{
|
||||
float u = Mathf.Repeat(Time.time * SparkSpeed / len + k / (float)th.Sparks.Length, 1f);
|
||||
th.Sparks[k].SetPosition(0, CinerariumThreads.Curve(th.A, th.B, Mathf.Max(0f, u - step)));
|
||||
th.Sparks[k].SetPosition(1, CinerariumThreads.Curve(th.A, th.B, u));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Нить, которую тянут: от начала до руки игрока; дальше 15 м - красная. Нож убрали - нить брошена.</summary>
|
||||
[Preserve]
|
||||
public class CinerariumThreadPreview : MonoBehaviour
|
||||
{
|
||||
LineRenderer _lr;
|
||||
|
||||
void Start()
|
||||
{
|
||||
_lr = CinerariumThreads.NewLine(transform, "line", 0.025f, CinerariumThreads.ThreadColor);
|
||||
_lr.useWorldSpace = true;
|
||||
_lr.positionCount = 17;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||||
if (pl == null || !CinerariumThreads.Start.HasValue || !CinerariumThreads.IsThreadKnife(pl.inventory?.holdingItem))
|
||||
{
|
||||
CinerariumThreads.Cancel();
|
||||
return;
|
||||
}
|
||||
if (_lr == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Vector3i s = CinerariumThreads.Start.Value;
|
||||
Vector3 a = World.blockToTransformPos(s) - Origin.position + Vector3.up * (CinerariumThreads.StartIsUrn ? 0.5f : 0.6f);
|
||||
Transform cam = pl.playerCamera != null ? pl.playerCamera.transform : null;
|
||||
Vector3 b = cam != null ? cam.position + cam.forward * 0.6f + cam.right * 0.2f - cam.up * 0.25f : pl.GetPosition() - Origin.position + Vector3.up * 1.2f;
|
||||
bool far = Vector3.Distance(s.ToVector3(), pl.GetPosition()) > CinerariumThreads.MaxLength;
|
||||
Color c = far ? new Color(1f, 0.3f, 0.3f, 0.7f) : CinerariumThreads.ThreadColor;
|
||||
_lr.startColor = c;
|
||||
_lr.endColor = c;
|
||||
for (int i = 0; i <= 16; i++)
|
||||
{
|
||||
_lr.SetPosition(i, CinerariumThreads.Curve(a, b, i / 16f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Нож для нитей не бьёт: ЛКМ и ПКМ - нити (на отпускание кнопки).</summary>
|
||||
[HarmonyPatch(typeof(ItemClass), nameof(ItemClass.ExecuteAction))]
|
||||
public static class Patch_ItemClass_ExecuteAction_ThreadKnife
|
||||
{
|
||||
public static bool Prefix(ItemClass __instance, int _actionIdx, ItemInventoryData _data, bool _bReleased)
|
||||
{
|
||||
if (!CinerariumThreads.IsThreadKnife(__instance))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (_bReleased && _data?.holdingEntity is EntityPlayerLocal pl)
|
||||
{
|
||||
CinerariumThreads.Click(pl, _actionIdx == 1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// «Содержимое» урны-цинерария - ячейка черепа (решение 16.10: «урна как транспорт»; переименовано из «Багажника»).
|
||||
/// Окно - родное окно хранилища игры (XUiC_BagStorageWindowGroup, как сумка транспорта и сумка волка) с сумкой на одну
|
||||
/// ячейку: перетаскивание, Shift+клик и правый клик - кодом игры (16.10: «Сумки, ящики в игре есть» - своё окно с
|
||||
/// ячейкой коллектора не принимало череп ни перетаскиванием, ни Shift+кликом). Своё здесь одно: после каждого изменения
|
||||
/// в ячейке остаётся один череп, всё остальное (лишние черепа, чужие предметы) - назад в рюкзак, и урна узнаёт, есть ли
|
||||
/// в ней череп. Череп - обычный предмет (ItemClass): он уже в сейвах, менять ему класс на модификацию нельзя.
|
||||
/// </summary>
|
||||
public static class CinerariumContents
|
||||
{
|
||||
public const string LootTemplate = "roboticDrone"; // звуки и время открытия, как у сумки волка
|
||||
|
||||
static TEFeatureCinerarium _urn;
|
||||
/// <summary>Урна, чьё «Содержимое» сейчас открыто (окно фильтра логиста), или null.</summary>
|
||||
public static TEFeatureCinerarium Urn => _urn;
|
||||
static Bag _bag;
|
||||
static EntityPlayerLocal _player;
|
||||
|
||||
public static void Open(EntityPlayerLocal _p, TEFeatureCinerarium _u)
|
||||
{
|
||||
LocalPlayerUI ui = _p != null ? LocalPlayerUI.GetUIForPlayer(_p) : null;
|
||||
ItemValue skull = ItemClass.GetItem(TEFeatureCinerarium.SkullItemName);
|
||||
if (ui == null || _u == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_urn = _u;
|
||||
_player = _p;
|
||||
_bag = new Bag(new Vector2i(1, 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null);
|
||||
if (_u.HasSkull && skull != null && skull.type > 0)
|
||||
{
|
||||
_bag.SetSlot(0, new ItemStack(skull, 1));
|
||||
}
|
||||
XUiC_BagStorageWindowGroup.Open(ui.xui, _p, _bag, LootContainer.GetLootContainer(LootTemplate),
|
||||
Localization.Get("necroCinerariumTrunk"), Check, () =>
|
||||
{
|
||||
Check();
|
||||
_urn = null;
|
||||
_bag = null;
|
||||
_player = null;
|
||||
});
|
||||
Debug.Log("[NecromancerTome] Cinerarium: «Содержимое» урны " + _u.ToWorldPos() + " открыто, череп " + (_u.HasSkull ? "есть" : "нет"));
|
||||
}
|
||||
|
||||
/// <summary>Оставить в ячейке один череп, остальное - игроку; отметить в урне.</summary>
|
||||
static void Check()
|
||||
{
|
||||
if (_bag == null || _urn == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ItemStack s = _bag.ItemGrid.items[0];
|
||||
bool has = false;
|
||||
if (s != null && !s.IsEmpty())
|
||||
{
|
||||
ItemStack back = null;
|
||||
if (s.itemValue.ItemClass?.GetItemName() != TEFeatureCinerarium.SkullItemName)
|
||||
{
|
||||
back = s.Clone();
|
||||
_bag.SetSlot(0, ItemStack.Empty.Clone());
|
||||
}
|
||||
else
|
||||
{
|
||||
has = true;
|
||||
if (s.count > 1)
|
||||
{
|
||||
back = s.Clone();
|
||||
back.count = s.count - 1;
|
||||
ItemStack one = s.Clone();
|
||||
one.count = 1;
|
||||
_bag.SetSlot(0, one);
|
||||
}
|
||||
}
|
||||
if (back != null)
|
||||
{
|
||||
GiveBack(back);
|
||||
}
|
||||
}
|
||||
_urn.SetSkull(has);
|
||||
}
|
||||
|
||||
static void GiveBack(ItemStack _s)
|
||||
{
|
||||
if (_player == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_player.bag != null && _player.bag.AddItem(_s))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_player.inventory != null && _player.inventory.AddItem(_s))
|
||||
{
|
||||
return;
|
||||
}
|
||||
_player.world.gameManager.ItemDropServer(_s, _player.GetPosition(), Vector3.zero);
|
||||
}
|
||||
|
||||
/// <summary>Урна убрана или выгружена, пока открыто её «Содержимое», - закрыть окно (череп из ячейки не потеряется:
|
||||
/// Check успеет отметить его в урне, а урна при разрушении роняет череп).</summary>
|
||||
public static void CloseIfShowing(TEFeatureCinerarium _u)
|
||||
{
|
||||
if (_urn == null || _urn != _u)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Check();
|
||||
LocalPlayerUI ui = _player != null ? LocalPlayerUI.GetUIForPlayer(_player) : null;
|
||||
_urn = null;
|
||||
if (ui != null && ui.windowManager.IsWindowOpen(XUiC_BagStorageWindowGroup.ID))
|
||||
{
|
||||
ui.windowManager.Close(XUiC_BagStorageWindowGroup.ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Работа духа урны - этап 4 некротехнолога (указание 16.10: дух - УНИВЕРСАЛЬНЫЙ работник зоны; BACKLOG «Некротехнолог v2»).
|
||||
/// Раз в StepSeconds - одно дело, по порядку:
|
||||
/// 1. пополнить расходники коллекторов из ящиков зоны: ячейки топлива (банки, цветы, корм, сферы - что коллектор
|
||||
/// принимает, GetFuelTypes) и катализатора (куры - GetCatalystTypes; по одной в пустую ячейку). Модификации
|
||||
/// коллекторов не трогаются;
|
||||
/// 2. забрать готовое из коллекторов (Items) и станций (TileEntityWorkstation.Output - верстак, кузня, химстанция,
|
||||
/// костёр, бетономешалка) и сложить в ящики зоны: сначала к таким же предметам, потом в свободное место; ящики полны -
|
||||
/// в сумку урны; некуда - дух чернеет и замирает над урной, оживает сам, когда место появится;
|
||||
/// 3. грядки - последними: собрать созревшее (блок с меткой crops и добычей Harvest) с «идеальными бонусами»
|
||||
/// (06.10: все бонусные доли урожая и семя - всегда), семя сразу посадить на то же место; затем засадить пустые
|
||||
/// грядки случайными семенами из ящиков и сумки (семя - предмет-блок стадии 1, BlockPlantGrowing; можно ли сажать,
|
||||
/// решает сам блок семени - CanPlaceBlockAt проверяет плодородие под ним, то есть грядку).
|
||||
/// Коллектор или станция подходят любые - будущий крольчатник обслуживается тем же кодом. Ящик - составной блок игрока
|
||||
/// с хранилищем (TEFeatureStorage, PlayerPlaced), кроме урн. Открытое у игрока (IsUserAccessing) не трогается.
|
||||
/// Работает на сервере: зовётся из TEFeatureCinerarium.UpdateTick.
|
||||
/// </summary>
|
||||
public static class CinerariumWork
|
||||
{
|
||||
public static float StepSeconds = 3f;
|
||||
|
||||
/// <summary>Что в зоне: собирается заново на каждом шаге (150 клеток - дёшево, а ящики могут переставить).</summary>
|
||||
sealed class Zone
|
||||
{
|
||||
public readonly List<TEFeatureStorage> Boxes = new List<TEFeatureStorage>();
|
||||
public readonly List<TileEntityCollector> Collectors = new List<TileEntityCollector>();
|
||||
public readonly List<TileEntityWorkstation> Stations = new List<TileEntityWorkstation>();
|
||||
public readonly Dictionary<TileEntity, Vector3i> Where = new Dictionary<TileEntity, Vector3i>();
|
||||
public TEFeatureStorage Bag;
|
||||
public readonly List<Vector3i> Ripe = new List<Vector3i>();
|
||||
public readonly List<Vector3i> Empty = new List<Vector3i>();
|
||||
}
|
||||
|
||||
static readonly FastTags<TagGroup.Global> CropTag = FastTags<TagGroup.Global>.Parse("crops");
|
||||
static readonly GameRandom _rnd = GameRandomManager.Instance.CreateGameRandom();
|
||||
|
||||
static readonly HashSet<TileEntity> _seen = new HashSet<TileEntity>();
|
||||
|
||||
/// <summary>Пробный шаг: найти дело и место, ничего не меняя.</summary>
|
||||
static bool _dry;
|
||||
|
||||
/// <summary>Одно дело. Возвращает, где дух работал (для полёта модели), или null; _stuck - есть что забрать, но некуда.
|
||||
/// _dryRun - только узнать, где будет дело (дух летит туда заранее), ничего не трогая.</summary>
|
||||
public static Vector3i? Step(World _world, TEFeatureCinerarium _urn, out bool _stuck, bool _dryRun)
|
||||
{
|
||||
_dry = _dryRun;
|
||||
try
|
||||
{
|
||||
return DoStep(_world, _urn, out _stuck);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dry = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>У перевозки по нити - ящик «откуда» последнего шага (дух летит сначала к нему), иначе null.</summary>
|
||||
public static Vector3i? LastFrom;
|
||||
|
||||
static Vector3i? DoStep(World _world, TEFeatureCinerarium _urn, out bool _stuck)
|
||||
{
|
||||
_stuck = false;
|
||||
LastFrom = null;
|
||||
// Урна с нитями - логист (16.10: «занимается только перетаскиванием из ящиков в ящики»).
|
||||
if (_urn.IsLogist)
|
||||
{
|
||||
return Ferry(_world, _urn);
|
||||
}
|
||||
Zone z = Scan(_world, _urn);
|
||||
// 1. Расходники.
|
||||
foreach (TileEntityCollector c in z.Collectors)
|
||||
{
|
||||
if (Refill(c, c.FuelSlots, c.GetFuelTypes(), false, z) || Refill(c, c.CatalystSlots, c.GetCatalystTypes(), true, z))
|
||||
{
|
||||
if (!_dry)
|
||||
{
|
||||
c.SetModified();
|
||||
}
|
||||
return z.Where[c];
|
||||
}
|
||||
}
|
||||
// 2. Готовое.
|
||||
bool blocked = false;
|
||||
foreach (TileEntityCollector c in z.Collectors)
|
||||
{
|
||||
ItemStack[] items = c.Items;
|
||||
if (Unload(items, z, ref blocked))
|
||||
{
|
||||
if (!_dry)
|
||||
{
|
||||
c.Items = items;
|
||||
c.SetModified();
|
||||
}
|
||||
return z.Where[c];
|
||||
}
|
||||
}
|
||||
foreach (TileEntityWorkstation w in z.Stations)
|
||||
{
|
||||
ItemStack[] output = w.Output;
|
||||
if (Unload(output, z, ref blocked))
|
||||
{
|
||||
if (!_dry)
|
||||
{
|
||||
w.Output = output;
|
||||
}
|
||||
return z.Where[w];
|
||||
}
|
||||
}
|
||||
// 3. Грядки.
|
||||
foreach (Vector3i p in z.Ripe)
|
||||
{
|
||||
if (Harvest(_world, p, z, ref blocked))
|
||||
{
|
||||
return p;
|
||||
}
|
||||
}
|
||||
if (z.Empty.Count > 0)
|
||||
{
|
||||
Vector3i? planted = PlantRandom(_world, z);
|
||||
if (planted.HasValue)
|
||||
{
|
||||
return planted;
|
||||
}
|
||||
}
|
||||
_stuck = blocked;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Перевозка по нитям: одна стопка из ящика «откуда» в ящик «куда». Только предметы включённых в урне групп
|
||||
/// (фильтр логиста, CinerariumGroups; 16.10 заменил прежнее правило «по образцу»); «куда» полон - ждём (дух не чернеет). Открытые у игрока ящики и закреплённые
|
||||
/// ячейки не трогаются. Возвращает ящик «куда», LastFrom - откуда взято.</summary>
|
||||
static Vector3i? Ferry(World _world, TEFeatureCinerarium _urn)
|
||||
{
|
||||
if (!_urn.HasThreadTo)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
TEFeatureStorage dst = BoxAt(_world, _urn.ThreadTo);
|
||||
if (dst == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
foreach (Vector3i f in _urn.ThreadFrom)
|
||||
{
|
||||
if (f == _urn.ThreadTo)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
TEFeatureStorage src = BoxAt(_world, f);
|
||||
if (src == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (int k = 0; k < src.ItemGrid.Length; k++)
|
||||
{
|
||||
ItemStack s = src.ItemGrid[k];
|
||||
if (s == null || s.IsEmpty() || Locked(src, k) || !_urn.GroupOn(CinerariumGroups.Of(s.itemValue)) || !Fits(dst, s))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
LastFrom = f;
|
||||
if (_dry)
|
||||
{
|
||||
return _urn.ThreadTo;
|
||||
}
|
||||
ItemStack moving = s.Clone();
|
||||
Place(moving, new[] { dst });
|
||||
int moved = s.count - moving.count;
|
||||
if (moved <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Take(src, k, moved);
|
||||
dst.SetModified();
|
||||
Debug.Log("[NecromancerTome] Cinerarium: дух перенёс " + moved + " " + s.itemValue.ItemClass?.GetItemName() + " из " + f + " в " + _urn.ThreadTo);
|
||||
return _urn.ThreadTo;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static TEFeatureStorage BoxAt(World _world, Vector3i _p)
|
||||
{
|
||||
return _world.GetTileEntity(_p) is TileEntityComposite c && !c.IsUserAccessing() &&
|
||||
!c.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium _) &&
|
||||
c.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage s) ? s : null;
|
||||
}
|
||||
|
||||
static bool IsRipe(Block _b)
|
||||
{
|
||||
return _b != null && !(_b is BlockPlantGrowing) && _b.HasAnyFastTags(CropTag) &&
|
||||
_b.itemsToDrop.TryGetValue(EnumDropEvent.Harvest, out List<Block.SItemDropProb> l) && l != null && l.Count > 0;
|
||||
}
|
||||
|
||||
static bool IsSeed(ItemValue _iv, out BlockValue _bv)
|
||||
{
|
||||
_bv = BlockValue.Air;
|
||||
if (_iv == null || _iv.type == 0 || !_iv.ItemClass.IsBlock())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_bv = _iv.ToBlockValue();
|
||||
return _bv.Block is BlockPlantGrowing;
|
||||
}
|
||||
|
||||
/// <summary>Урожай: все доли добычи Harvest (бонусы - тоже, вероятность только у обычных долей), множители сервера на
|
||||
/// урожай и семена. Одно семя - сразу в землю на то же место, остальное - в ящики/сумку. Нет места ни для чего -
|
||||
/// грядку не трогаем, дух чернеет.</summary>
|
||||
static bool Harvest(World _world, Vector3i _p, Zone _z, ref bool _blocked)
|
||||
{
|
||||
Block crop = _world.GetBlock(_p).Block;
|
||||
List<ItemStack> got = new List<ItemStack>();
|
||||
BlockValue replant = BlockValue.Air;
|
||||
foreach (Block.SItemDropProb d in crop.itemsToDrop[EnumDropEvent.Harvest])
|
||||
{
|
||||
ItemValue iv = ItemClass.GetItem(d.name);
|
||||
if (iv == null || iv.type == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
bool seed = IsSeed(iv, out BlockValue seedBv);
|
||||
bool bonus = d.tag != null && (d.tag.Contains("Bonus") || d.tag.Contains("bonus"));
|
||||
if (!bonus && d.prob < 1f && _rnd.RandomFloat > d.prob)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int n = _rnd.RandomRange(d.minCount, d.maxCount + 1);
|
||||
n = Mathf.RoundToInt(n * (seed ? XUiM_Recipes.SeedDropOutputModifier : XUiM_Recipes.CropOutputModifier));
|
||||
if (seed && replant.isair && n > 0)
|
||||
{
|
||||
replant = seedBv;
|
||||
n--;
|
||||
}
|
||||
if (n > 0)
|
||||
{
|
||||
got.Add(new ItemStack(iv, n));
|
||||
}
|
||||
}
|
||||
if (got.Count > 0 && !HasRoom(got[0], _z))
|
||||
{
|
||||
_blocked = true;
|
||||
return false;
|
||||
}
|
||||
if (_dry)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_world.SetBlockRPC(_p, BlockValue.Air);
|
||||
if (!replant.isair)
|
||||
{
|
||||
replant.rotation = (byte)_rnd.RandomRange(0, 4);
|
||||
_world.SetBlockRPC(_p, replant);
|
||||
}
|
||||
foreach (ItemStack s in got)
|
||||
{
|
||||
Place(s, _z.Boxes);
|
||||
if (s.count > 0 && _z.Bag != null)
|
||||
{
|
||||
Place(s, new[] { _z.Bag });
|
||||
}
|
||||
if (s.count > 0)
|
||||
{
|
||||
// Влезло не всё (проверяли только первую долю) - остаток падает у урны, не пропадает.
|
||||
GameManager.Instance.ItemDropServer(s.Clone(), World.blockToTransformPos(_p) + Vector3.up * 0.5f, Vector3.zero);
|
||||
}
|
||||
}
|
||||
Debug.Log("[NecromancerTome] Cinerarium: дух собрал " + crop.GetBlockName() + " в " + _p + (replant.isair ? "" : ", посадил снова"));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool HasRoom(ItemStack _s, Zone _z)
|
||||
{
|
||||
foreach (TEFeatureStorage t in _z.Boxes)
|
||||
{
|
||||
if (Fits(t, _s))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return _z.Bag != null && Fits(_z.Bag, _s);
|
||||
}
|
||||
|
||||
static bool Fits(TEFeatureStorage _t, ItemStack _s)
|
||||
{
|
||||
for (int i = 0; i < _t.ItemGrid.Length; i++)
|
||||
{
|
||||
ItemStack x = _t.ItemGrid[i];
|
||||
if (x == null || x.IsEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
int n = _s.count;
|
||||
if (x.itemValue.type == _s.itemValue.type && x.CanStackPartly(ref n))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Засадить одну пустую грядку случайным семенем из сумки и ящиков (06.10: «случайно сажает имеющиеся семена»).</summary>
|
||||
static Vector3i? PlantRandom(World _world, Zone _z)
|
||||
{
|
||||
List<(TEFeatureStorage src, int slot, BlockValue bv)> seeds = new List<(TEFeatureStorage, int, BlockValue)>();
|
||||
foreach (TEFeatureStorage src in Sources(_z))
|
||||
{
|
||||
for (int k = 0; k < src.ItemGrid.Length; k++)
|
||||
{
|
||||
ItemStack st = src.ItemGrid[k];
|
||||
if (st != null && !st.IsEmpty() && !Locked(src, k) && IsSeed(st.itemValue, out BlockValue bv))
|
||||
{
|
||||
seeds.Add((src, k, bv));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (seeds.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
foreach (Vector3i p in _z.Empty)
|
||||
{
|
||||
var pick = seeds[_rnd.RandomRange(0, seeds.Count)];
|
||||
BlockValue bv = pick.bv;
|
||||
bv.rotation = (byte)_rnd.RandomRange(0, 4);
|
||||
if (!bv.Block.CanPlaceBlockAt(_world, p, bv))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (_dry)
|
||||
{
|
||||
return p;
|
||||
}
|
||||
Take(pick.src, pick.slot, 1);
|
||||
_world.SetBlockRPC(p, bv);
|
||||
Debug.Log("[NecromancerTome] Cinerarium: дух посадил " + bv.Block.GetBlockName() + " в " + p);
|
||||
return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Zone Scan(World _world, TEFeatureCinerarium _urn)
|
||||
{
|
||||
Zone z = new Zone();
|
||||
_seen.Clear();
|
||||
Vector3i c = _urn.ToWorldPos();
|
||||
if (_urn.Parent != null)
|
||||
{
|
||||
_urn.Parent.TryGetSelfOrFeature<TEFeatureStorage>(out z.Bag);
|
||||
_seen.Add(_urn.Parent);
|
||||
}
|
||||
int w = TEFeatureCinerarium.HalfWidth;
|
||||
for (int dy = -TEFeatureCinerarium.Below; dy <= TEFeatureCinerarium.Above; dy++)
|
||||
{
|
||||
for (int dx = -w; dx <= w; dx++)
|
||||
{
|
||||
for (int dz = -w; dz <= w; dz++)
|
||||
{
|
||||
Vector3i p = new Vector3i(c.x + dx, c.y + dy, c.z + dz);
|
||||
BlockValue bv = _world.GetBlock(p);
|
||||
if (bv.isair)
|
||||
{
|
||||
// Пустая клетка над плодородным блоком - кандидат в грядку (окончательно решит блок семени).
|
||||
if (_world.GetBlock(p - Vector3i.up).Block?.blockMaterial?.FertileLevel > 0)
|
||||
{
|
||||
z.Empty.Add(p);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!bv.ischild && IsRipe(bv.Block))
|
||||
{
|
||||
z.Ripe.Add(p);
|
||||
continue;
|
||||
}
|
||||
Vector3i owner = bv.ischild ? p + bv.parent : p; // часть составного блока (верстак 2x1) - к главной клетке
|
||||
TileEntity te = _world.GetTileEntity(owner);
|
||||
if (te == null || !_seen.Add(te) || te.IsUserAccessing())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (te is TileEntityCollector col)
|
||||
{
|
||||
z.Collectors.Add(col);
|
||||
z.Where[col] = owner;
|
||||
}
|
||||
else if (te is TileEntityWorkstation ws)
|
||||
{
|
||||
z.Stations.Add(ws);
|
||||
z.Where[ws] = owner;
|
||||
}
|
||||
else if (te is TileEntityComposite comp && comp.PlayerPlaced &&
|
||||
!comp.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium _) &&
|
||||
comp.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage box))
|
||||
{
|
||||
z.Boxes.Add(box);
|
||||
z.Where[comp] = owner;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return z;
|
||||
}
|
||||
|
||||
/// <summary>Дополнить ячейки коллектора тем, что он принимает, из ящиков и сумки. _single - по одному предмету в
|
||||
/// пустую ячейку (куры), иначе до полного стака. true - что-то положено.</summary>
|
||||
static bool Refill(TileEntityCollector _c, ItemStack[] _slots, string[] _allowed, bool _single, Zone _z)
|
||||
{
|
||||
if (_slots == null || _allowed == null || _allowed.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < _slots.Length; i++)
|
||||
{
|
||||
ItemStack slot = _slots[i];
|
||||
ItemClass want = null;
|
||||
int room;
|
||||
if (slot == null || slot.IsEmpty())
|
||||
{
|
||||
room = -1; // любой допустимый
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_single)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
want = slot.itemValue.ItemClass;
|
||||
room = want != null ? want.MaxCount - slot.count : 0;
|
||||
if (room <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
foreach (TEFeatureStorage src in Sources(_z))
|
||||
{
|
||||
for (int k = 0; k < src.ItemGrid.Length; k++)
|
||||
{
|
||||
ItemStack s = src.ItemGrid[k];
|
||||
if (s == null || s.IsEmpty() || Locked(src, k))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ItemClass ic = s.itemValue.ItemClass;
|
||||
if (ic == null || (want != null ? ic != want : System.Array.IndexOf(_allowed, ic.GetItemName()) < 0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int take = _single ? 1 : (room < 0 ? Mathf.Min(s.count, ic.MaxCount) : Mathf.Min(s.count, room));
|
||||
if (take <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (_dry)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (want == null)
|
||||
{
|
||||
_slots[i] = new ItemStack(s.itemValue.Clone(), take);
|
||||
}
|
||||
else
|
||||
{
|
||||
_slots[i].count += take;
|
||||
}
|
||||
Take(src, k, take);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Одна стопка готового - в ящики (к таким же, потом в пустое), затем в сумку урны. true - что-то перенесено;
|
||||
/// _blocked - была стопка, которой не нашлось места.</summary>
|
||||
static bool Unload(ItemStack[] _items, Zone _z, ref bool _blocked)
|
||||
{
|
||||
if (_items == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < _items.Length; i++)
|
||||
{
|
||||
ItemStack s = _items[i];
|
||||
if (s == null || s.IsEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (_dry)
|
||||
{
|
||||
if (HasRoom(s, _z))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_blocked = true;
|
||||
continue;
|
||||
}
|
||||
int before = s.count;
|
||||
// Сначала ящики зоны (к таким же, потом в пустое), они полны - тогда сумка урны (решение 06.10).
|
||||
Place(s, _z.Boxes);
|
||||
if (s.count > 0 && _z.Bag != null)
|
||||
{
|
||||
Place(s, new[] { _z.Bag });
|
||||
}
|
||||
if (s.count == 0)
|
||||
{
|
||||
_items[i] = ItemStack.Empty.Clone();
|
||||
return true;
|
||||
}
|
||||
if (s.count != before)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_blocked = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void Place(ItemStack _s, IList<TEFeatureStorage> _dst)
|
||||
{
|
||||
for (int i = 0; i < _dst.Count && _s.count > 0; i++)
|
||||
{
|
||||
_dst[i].TryStackItem(0, _s);
|
||||
}
|
||||
for (int i = 0; i < _dst.Count && _s.count > 0; i++)
|
||||
{
|
||||
if (_dst[i].AddItem(_s.Clone()))
|
||||
{
|
||||
_s.count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Откуда брать расходники: сумка урны (там то, что дух накопил), потом ящики зоны.</summary>
|
||||
static IEnumerable<TEFeatureStorage> Sources(Zone _z)
|
||||
{
|
||||
if (_z.Bag != null)
|
||||
{
|
||||
yield return _z.Bag;
|
||||
}
|
||||
foreach (TEFeatureStorage b in _z.Boxes)
|
||||
{
|
||||
yield return b;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ячейка, закреплённая игроком (замок в окне ящика), - не трогать.</summary>
|
||||
static bool Locked(TEFeatureStorage _src, int _slot)
|
||||
{
|
||||
PackedBoolArray locks = _src.ItemGrid.SlotLocks;
|
||||
return locks != null && _slot < locks.Length && locks[_slot];
|
||||
}
|
||||
|
||||
static void Take(TEFeatureStorage _src, int _slot, int _count)
|
||||
{
|
||||
ItemStack s = _src.ItemGrid[_slot].Clone();
|
||||
s.count -= _count;
|
||||
_src.UpdateSlot(_slot, s.count > 0 ? s : ItemStack.Empty.Clone());
|
||||
_src.SetModified();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ namespace NecromancerTome
|
||||
|
||||
static bool Ours(string _name)
|
||||
{
|
||||
return _name == "necroBookMindPrefab" || _name == "necroKnifeDecorPrefab" || _name == "necroMortarPrefab" || _name.StartsWith("necroBloodRose") || _name.StartsWith("necroPoster") || _name == "necroBookDoorPrefab" || _name.StartsWith("necroSkullDecor");
|
||||
return _name == "necroBookMindPrefab" || _name == "necroKnifeDecorPrefab" || _name == "necroMortarPrefab" || _name.StartsWith("necroBloodRose") || _name.StartsWith("necroPoster") || _name == "necroBookDoorPrefab" || _name.StartsWith("necroSkullDecor") || _name == "necroCinerariumPrefab";
|
||||
}
|
||||
|
||||
public static void Postfix(Transform __result)
|
||||
|
||||
+22
-1
@@ -23,7 +23,10 @@ namespace NecromancerTome
|
||||
public static class HandHolds
|
||||
{
|
||||
public const string HandItem = "meleeHandPlayer";
|
||||
public const int None = 0, Deny = 1, PifPaf = 2;
|
||||
public const int None = 0, Deny = 1, PifPaf = 2, Thread = 3;
|
||||
/// <summary>Хват для нитей - не сразу: снимок замороженных костей должен быть в покое хвата 68, после доставания.</summary>
|
||||
const float ThreadDelay = 0.6f;
|
||||
static float _threadSince = -1f;
|
||||
|
||||
/// <summary>Дальность и конус выбора зомби для «отрицания».</summary>
|
||||
public const float DenyRange = 25f;
|
||||
@@ -59,6 +62,19 @@ namespace NecromancerTome
|
||||
|
||||
public static int Current(EntityPlayerLocal _player)
|
||||
{
|
||||
// Только от первого лица (16.10: «В виде от третьего лица что-то страшное»): поза и перенос ножа - для рук первого
|
||||
// лица; от третьего лица модель в руке - у тела, и нож улетал к невидимой руке первого лица.
|
||||
if (_player != null && _player.inventory?.holdingItem?.Name == CinerariumThreads.ThreadKnifeName &&
|
||||
(_player.emodel as EModelSDCS)?.IsFPV == true && !SnakeMode.Active && !MindPalace.Active && !Possession.Active && !_player.IsDead())
|
||||
{
|
||||
if (_threadSince < 0f)
|
||||
{
|
||||
_threadSince = Time.time;
|
||||
}
|
||||
bool ready = Time.time - _threadSince >= ThreadDelay && !(_player.Hand != null && _player.Hand.isSwitching);
|
||||
return ready ? Thread : None;
|
||||
}
|
||||
_threadSince = -1f;
|
||||
if (_player == null || _player.Buffs == null || !IsHand(_player.inventory?.holdingItem) ||
|
||||
SnakeMode.Active || MindPalace.Active || Possession.Active || _player.IsDead())
|
||||
{
|
||||
@@ -104,9 +120,14 @@ namespace NecromancerTome
|
||||
{
|
||||
HandPose.PresetPifPaf();
|
||||
}
|
||||
else if (want == Thread)
|
||||
{
|
||||
HandPose.PresetThread();
|
||||
}
|
||||
else
|
||||
{
|
||||
HandPose.Enabled = false;
|
||||
HandPose.KnifeToLeft = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+127
-1
@@ -76,6 +76,8 @@ namespace NecromancerTome
|
||||
/// положение ВСЕХ костей под Hips, и каждый кадр они возвращаются на место, а поверх - поза.
|
||||
/// Снимок только в покое аниматора (ItemActionIndex = -1), иначе застыл бы кадр удара.</summary>
|
||||
public static bool Freeze = true;
|
||||
/// <summary>Замереть без своего хвата - на время осмотра со стороны (NecroView).</summary>
|
||||
public static bool HoldStill;
|
||||
static readonly List<Transform> _frozen = new List<Transform>();
|
||||
static readonly List<Vector3> _frozenPos = new List<Vector3>();
|
||||
static readonly List<Quaternion> _frozenRot = new List<Quaternion>();
|
||||
@@ -86,6 +88,8 @@ namespace NecromancerTome
|
||||
public static void ResetAll()
|
||||
{
|
||||
Enabled = false;
|
||||
KnifeToLeft = false;
|
||||
NecroView.Stop();
|
||||
Overrides.Clear();
|
||||
CamOverrides.Clear();
|
||||
_jabStart = -1f;
|
||||
@@ -142,10 +146,18 @@ namespace NecromancerTome
|
||||
{
|
||||
if (!Enabled || Overrides.Count == 0 && CamOverrides.Count == 0)
|
||||
{
|
||||
if (_frozen.Count > 0)
|
||||
if (HoldStill)
|
||||
{
|
||||
FreezeBones(); // осмотр со стороны (necroview): поза замирает и без своего хвата
|
||||
}
|
||||
else if (_frozen.Count > 0)
|
||||
{
|
||||
Unfreeze();
|
||||
}
|
||||
if (KnifeToLeft && Enabled)
|
||||
{
|
||||
ApplyKnifeToLeft();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Freeze)
|
||||
@@ -160,6 +172,10 @@ namespace NecromancerTome
|
||||
b.localRotation = Quaternion.Euler(kv.Value);
|
||||
}
|
||||
}
|
||||
if (KnifeToLeft)
|
||||
{
|
||||
ApplyKnifeToLeft();
|
||||
}
|
||||
Transform cam = CamOverrides.Count > 0 || _jabStart >= 0f ? Camera() : null;
|
||||
ApplyJab(cam);
|
||||
if (cam != null)
|
||||
@@ -265,6 +281,7 @@ namespace NecromancerTome
|
||||
/// (сгиб - как у поджатых пальцев «пиф-пафа»), указательный и мизинец прямые.</summary>
|
||||
public static void PresetDeny()
|
||||
{
|
||||
KnifeToLeft = false;
|
||||
Overrides.Clear();
|
||||
CamOverrides.Clear();
|
||||
Overrides["RightArm"] = new Vector3(345.8f, 34.9f, 24.3f);
|
||||
@@ -291,6 +308,7 @@ namespace NecromancerTome
|
||||
/// сомкнуты и смотрят к прицелу, безымянный и мизинец поджаты, большой вверх. Кисть - от камеры.</summary>
|
||||
public static void PresetPifPaf()
|
||||
{
|
||||
KnifeToLeft = false;
|
||||
Overrides.Clear();
|
||||
CamOverrides.Clear();
|
||||
Overrides["RightArm"] = new Vector3(330.8f, 34.9f, 24.3f);
|
||||
@@ -313,6 +331,111 @@ namespace NecromancerTome
|
||||
Enabled = true;
|
||||
}
|
||||
|
||||
/// <summary>Нож в левой кисти (хват для нитей): корень модели в руке ставится каждый кадр от LeftHand. Смещение -
|
||||
/// снимок хвата 68 (деревянный лук) 16.10, где нож лежал в левой руке («Нож в левой руке»).</summary>
|
||||
public static bool KnifeToLeft;
|
||||
static readonly Vector3 KnifeLeftPos = new Vector3(-0.0832f, -0.0354f, 0.0186f);
|
||||
static readonly Quaternion KnifeLeftRot = new Quaternion(0.56471f, -0.62192f, -0.41297f, 0.35182f);
|
||||
|
||||
static void ApplyKnifeToLeft()
|
||||
{
|
||||
Transform lh = Bone("LeftHand");
|
||||
Transform m = HeldTweak.Model();
|
||||
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||||
if (lh != null && m != null && (pl?.emodel as EModelSDCS)?.IsFPV == true)
|
||||
{
|
||||
m.SetPositionAndRotation(lh.TransformPoint(KnifeLeftPos), lh.rotation * KnifeLeftRot);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Хват для нитей (16.10, подбор с пользователем: «Нож в левую руку, а правую сделай как в том хвате, где
|
||||
/// ты говорил что она перебирает нити»): основа - HoldType 68 (деревянный лук, items.xml); левая рука поднята и
|
||||
/// отведена влево, нож в ней; правая - раскрытая ладонь хвата 64 (бинт), пересчитанная под плечо хвата 68 так,
|
||||
/// чтобы кисть стояла там же относительно камеры (scratchpad ik.py).</summary>
|
||||
public static void PresetThread()
|
||||
{
|
||||
Overrides.Clear();
|
||||
CamOverrides.Clear();
|
||||
Overrides["RightShoulder"] = new Vector3(0.0f, 180.0f, 180.0f);
|
||||
Overrides["Right_sup_shoulder_01"] = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
Overrides["Right_sup_shoulder_02"] = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
Overrides["Right_sup_trap_01"] = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
Overrides["RightArm"] = new Vector3(311.4f, 24.9f, 39.2f);
|
||||
Overrides["Right_sup_arm_01"] = new Vector3(0.0f, 0.0f, 358.2f);
|
||||
Overrides["RightArmRoll"] = new Vector3(357.9f, 0.0f, 0.0f);
|
||||
Overrides["RightForeArm"] = new Vector3(0.0f, 79.9f, 0.0f);
|
||||
Overrides["Right_sup_elbow_00"] = new Vector3(0.0f, 332.8f, 0.0f);
|
||||
Overrides["RightForeArmRoll"] = new Vector3(8.7f, 0.0f, 0.0f);
|
||||
Overrides["RightHand"] = new Vector3(7.2f, 346.5f, 5.4f);
|
||||
Overrides["RightHandIndex0"] = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
Overrides["Right_sup_index_00"] = new Vector3(0.0f, 0.0f, 17.3f);
|
||||
Overrides["Right_sup_index_00_palm"] = new Vector3(0.0f, 0.0f, 15.6f);
|
||||
Overrides["RightHandIndex1"] = new Vector3(2.6f, 2.9f, 31.3f);
|
||||
Overrides["Right_sup_index_01_under"] = new Vector3(0.0f, 0.0f, 3.5f);
|
||||
Overrides["RightHandIndex2"] = new Vector3(0.0f, 0.0f, 52.8f);
|
||||
Overrides["Right_sup_index_02"] = new Vector3(0.0f, 0.0f, 333.6f);
|
||||
Overrides["Right_sup_index_02_under"] = new Vector3(0.0f, 0.0f, 5.1f);
|
||||
Overrides["RightHandIndex3"] = new Vector3(0.0f, 0.0f, 77.0f);
|
||||
Overrides["Right_sup_index_03"] = new Vector3(0.0f, 0.0f, 321.5f);
|
||||
Overrides["RightHandIndex4"] = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
Overrides["RightHandMiddle0"] = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
Overrides["Right_sup_middle_00"] = new Vector3(0.0f, 0.0f, 32.1f);
|
||||
Overrides["Right_sup_middle_00_palm"] = new Vector3(0.0f, 0.0f, 28.8f);
|
||||
Overrides["RightHandMiddle1"] = new Vector3(0.0f, 0.0f, 57.7f);
|
||||
Overrides["Right_sup_middle_01_under"] = new Vector3(0.0f, 0.0f, 5.7f);
|
||||
Overrides["RightHandMiddle2"] = new Vector3(0.0f, 0.0f, 85.0f);
|
||||
Overrides["Right_sup_middle_02"] = new Vector3(0.0f, 0.0f, 317.5f);
|
||||
Overrides["Right_sup_middle_02_under"] = new Vector3(0.0f, 0.0f, 5.1f);
|
||||
Overrides["RightHandMiddle3"] = new Vector3(0.0f, 0.0f, 77.0f);
|
||||
Overrides["Right_sup_middle_03"] = new Vector3(0.0f, 0.0f, 321.5f);
|
||||
Overrides["RightHandMiddle4"] = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
Overrides["RightHandPinky0"] = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
Overrides["Right_sup_pinky_00"] = new Vector3(0.0f, 0.0f, 45.0f);
|
||||
Overrides["Right_sup_pinky_00_palm"] = new Vector3(0.0f, 0.0f, 45.0f);
|
||||
Overrides["RightHandPinky1"] = new Vector3(0.2f, 353.1f, 92.0f);
|
||||
Overrides["Right_sup_pinky_01_under"] = new Vector3(0.0f, 0.0f, 5.6f);
|
||||
Overrides["RightHandPinky2"] = new Vector3(0.0f, 0.0f, 84.0f);
|
||||
Overrides["Right_sup_pinky_02"] = new Vector3(0.0f, 0.0f, 318.0f);
|
||||
Overrides["Right_sup_pinky_02_under"] = new Vector3(0.0f, 0.0f, 5.2f);
|
||||
Overrides["RightHandPinky3"] = new Vector3(0.0f, 0.0f, 78.0f);
|
||||
Overrides["Right_sup_pinky_03"] = new Vector3(0.0f, 0.0f, 321.0f);
|
||||
Overrides["RightHandPinky4"] = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
Overrides["RightHandRing0"] = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
Overrides["Right_sup_ring_00"] = new Vector3(0.0f, 0.0f, 33.0f);
|
||||
Overrides["Right_sup_ring_00_palm"] = new Vector3(0.0f, 0.0f, 33.0f);
|
||||
Overrides["RightHandRing1"] = new Vector3(358.6f, 355.2f, 66.1f);
|
||||
Overrides["Right_sup_ring_01_under"] = new Vector3(0.0f, 0.0f, 5.5f);
|
||||
Overrides["RightHandRing2"] = new Vector3(0.0f, 0.0f, 83.0f);
|
||||
Overrides["Right_sup_ring_02"] = new Vector3(0.0f, 0.0f, 318.5f);
|
||||
Overrides["Right_sup_ring_02_under"] = new Vector3(0.0f, 0.0f, 5.1f);
|
||||
Overrides["RightHandRing3"] = new Vector3(0.0f, 0.0f, 77.0f);
|
||||
Overrides["Right_sup_ring_03"] = new Vector3(0.0f, 0.0f, 321.5f);
|
||||
Overrides["RightHandRing4"] = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
Overrides["RightHandThumb1"] = new Vector3(27.9f, 75.4f, 33.2f);
|
||||
Overrides["RightHandThumb2"] = new Vector3(0.0f, 0.0f, 15.8f);
|
||||
Overrides["Right_sup_thumb_01"] = new Vector3(0.0f, 0.0f, 352.1f);
|
||||
Overrides["Right_sup_thumb_01_under"] = new Vector3(0.0f, 0.0f, 1.2f);
|
||||
Overrides["RightHandThumb3"] = new Vector3(0.0f, 0.0f, 17.9f);
|
||||
Overrides["Right_sup_thumb_02"] = new Vector3(0.0f, 0.0f, 351.0f);
|
||||
Overrides["RightHandThumb4"] = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
Overrides["necroKnifePrefab(Clone)"] = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
Overrides["boneShiv_LOD0"] = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
Overrides["boneShiv_LOD1"] = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
Overrides["boneShiv_LOD2"] = new Vector3(0.0f, 0.0f, 0.0f);
|
||||
Overrides["RightForeArmRoll1"] = new Vector3(3.5f, 0.0f, 0.0f);
|
||||
Overrides["RightForeArmRoll2"] = new Vector3(7.0f, 0.0f, 0.0f);
|
||||
Overrides["RightForeArmRoll3"] = new Vector3(10.5f, 0.0f, 0.0f);
|
||||
Overrides["RightForeArmRoll4"] = new Vector3(14.0f, 0.0f, 0.0f);
|
||||
Overrides["Right_sup_wrist_00"] = new Vector3(0.0f, 0.0f, 357.5f);
|
||||
Overrides["Right_sup_wrist_01"] = new Vector3(0.0f, 347.6f, 0.0f);
|
||||
Overrides["Right_sup_wrist_02"] = new Vector3(0.0f, 0.0f, 356.8f);
|
||||
Overrides["Right_sup_wrist_03"] = new Vector3(0.0f, 347.6f, 0.0f);
|
||||
Overrides["LeftArm"] = new Vector3(329.6f, 42.5f, 8.5f);
|
||||
KnifeToLeft = true;
|
||||
Unfreeze();
|
||||
Enabled = true;
|
||||
}
|
||||
|
||||
public static string F(float _v) => _v.ToString("0.#", CultureInfo.InvariantCulture);
|
||||
public static string F(Vector3 _v) => F(_v.x) + " " + F(_v.y) + " " + F(_v.z);
|
||||
}
|
||||
@@ -324,6 +447,9 @@ namespace NecromancerTome
|
||||
HandHolds.Update();
|
||||
HandPose.Apply();
|
||||
HeldTweak.Apply();
|
||||
NecroView.Update();
|
||||
NecroBody.LateUpdate();
|
||||
ThreadKnifeBody.LateUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+37
-2
@@ -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,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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -825,6 +825,11 @@ namespace NecromancerTome
|
||||
}
|
||||
if (IsPet && RatModel.Is(z))
|
||||
{
|
||||
// E на зомби в прицеле - дух переходит в него, крыса развеивается (указание 16.10).
|
||||
if (detonate && TryRatJump(world, z))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// E - осмотреть контейнер без права взять (указание 08.10: крыса - разведчик).
|
||||
RatLoot.Tick(world, _player, z, detonate);
|
||||
// ПКМ (держать) - сквозь постройки, без полёта (BACKLOG, «Могильная крыса» п.4).
|
||||
@@ -1524,6 +1529,42 @@ namespace NecromancerTome
|
||||
return best;
|
||||
}
|
||||
|
||||
/// <summary>Перенос духа из крысы в зомби под прицелом (решения 16.10): E, зомби не дальше RatJumpRange от
|
||||
/// крысы и в прямой видимости (GroundAimTarget не видит сквозь стены, во время прохода - не работает);
|
||||
/// цена - как у «пиф-пафа» (никакой). Крыса отзывается и развеивается; цепочки нет - после зомби дух
|
||||
/// вернётся в тело ГГ (Exit). True - E израсходована: перенос сделан или начат.</summary>
|
||||
static bool TryRatJump(World _world, EntityAlive _rat)
|
||||
{
|
||||
if (RatPhase.On)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
EntityAlive victim = GroundAimTarget(_world, _rat);
|
||||
if (victim == null || !CanPossess(victim) ||
|
||||
(victim.position - _rat.position).sqrMagnitude > RatJumpRange * RatJumpRange)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
EntityPlayerLocal player = _player;
|
||||
int ratId = _rat.entityId;
|
||||
Exit("крыса: дух переходит в " + victim.EntityName + " (" + victim.entityId + ")");
|
||||
if (!Enter(player, victim))
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Possession: перенос из крысы не удался - дух в теле, крыса осталась");
|
||||
return true;
|
||||
}
|
||||
if (NecroBurst.EnsureRegistered())
|
||||
{
|
||||
ParticleEffect pe = new ParticleEffect(NecroBurst.ParticleName, _rat.position,
|
||||
_world.GetLightBrightness(_rat.GetBlockPosition()), Color.white, null, null, _OLDCreateColliders: false);
|
||||
GameManager.Instance.SpawnParticleEffectServer(pe, ratId);
|
||||
}
|
||||
Patch_ItemActionSpawnEntity_Spawn_PetLimit.RecallPet(player, ratId, null, null); // без подсказки «отозвана» - её место у подсказки вселения
|
||||
return true;
|
||||
}
|
||||
|
||||
public const float RatJumpRange = 3f;
|
||||
|
||||
public const float GroundAimRange = 30f; // зрение зомби, как TauntRange
|
||||
public const float GroundAimRadius = 0.6f;
|
||||
|
||||
@@ -1678,7 +1719,8 @@ namespace NecromancerTome
|
||||
}
|
||||
if (IsPet && RatModel.Is(_z))
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintLook") + ", " + Localization.Get("necroPossessHintPhase");
|
||||
text += ", " + Localization.Get("necroPossessHintLook") + ", " + Localization.Get("necroPossessHintRatJump") + ", " +
|
||||
Localization.Get("necroPossessHintPhase");
|
||||
}
|
||||
if (IsPet && DogSense.Is(_z))
|
||||
{
|
||||
|
||||
+31
-1
@@ -356,8 +356,22 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
public const int GripThreadIdx = 3;
|
||||
|
||||
public static void OnRadialCommand(XUiC_Radial _sender, int _commandIndex, XUiC_Radial.RadialContextAbs _context)
|
||||
{
|
||||
EntityPlayerLocal pl = _sender.xui.playerUI.entityPlayer;
|
||||
// Хват ножа (16.10, нити цинерария): 3 - хват для нитей; 0 у ножа-для-нитей - обычный хват.
|
||||
if (_commandIndex == GripThreadIdx)
|
||||
{
|
||||
CinerariumThreads.Convert(pl, CinerariumThreads.ThreadKnifeName);
|
||||
return;
|
||||
}
|
||||
if (_commandIndex == 0 && CinerariumThreads.IsThreadKnife(pl?.inventory?.holdingItem))
|
||||
{
|
||||
CinerariumThreads.Convert(pl, CinerariumThreads.KnifeName);
|
||||
return;
|
||||
}
|
||||
if (_commandIndex == 1)
|
||||
{
|
||||
Enter(_sender.xui.playerUI.entityPlayer);
|
||||
@@ -374,7 +388,8 @@ namespace NecromancerTome
|
||||
{
|
||||
public static void Postfix(ItemAction __instance, ref bool __result)
|
||||
{
|
||||
if (!__result && !SnakeMode.Active && !MindPalace.Active && SnakeMode.IsKnife(__instance.item))
|
||||
if (!__result && !SnakeMode.Active && !MindPalace.Active &&
|
||||
(SnakeMode.IsKnife(__instance.item) || CinerariumThreads.IsThreadKnife(__instance.item)))
|
||||
{
|
||||
__result = true;
|
||||
}
|
||||
@@ -389,6 +404,18 @@ namespace NecromancerTome
|
||||
|
||||
public static bool Prefix(ItemAction __instance, XUiC_Radial _xuiRadialWindow, EntityPlayerLocal _epl)
|
||||
{
|
||||
if (CinerariumThreads.IsThreadKnife(__instance.item))
|
||||
{
|
||||
// Нож хватом для нитей: вернуть обычный хват (0) или остаться (3).
|
||||
_xuiRadialWindow.ResetRadialEntries();
|
||||
_xuiRadialWindow.CreateRadialEntry(0, "NecromancersKnife", "ItemIconAtlas", "",
|
||||
Localization.Get("necroKnifeGripNormal"), _highlighted: false);
|
||||
_xuiRadialWindow.CreateRadialEntry(SnakeMode.GripThreadIdx, "resourceSewingKit", "ItemIconAtlas", "",
|
||||
Localization.Get("necroKnifeGripThread"), _highlighted: true);
|
||||
_xuiRadialWindow.SetCommonData(UIUtils.GetButtonIconForAction(_epl.playerInput.Reload),
|
||||
SnakeMode.OnRadialCommand, null, SnakeMode.GripThreadIdx);
|
||||
return false;
|
||||
}
|
||||
if (!SnakeMode.IsKnife(__instance.item))
|
||||
{
|
||||
return true;
|
||||
@@ -417,6 +444,9 @@ namespace NecromancerTome
|
||||
_xuiRadialWindow.CreateRadialEntry(2, SnakeMode.DayIcon, "UIAtlas", "",
|
||||
Localization.Get("necroSnakeModeDay"), _highlighted: false);
|
||||
}
|
||||
// Хват для нитей (16.10: «Есть в игре пиктограмма ниток, катушки?» - набор для шитья) - в любое время суток.
|
||||
_xuiRadialWindow.CreateRadialEntry(SnakeMode.GripThreadIdx, "resourceSewingKit", "ItemIconAtlas", "",
|
||||
Localization.Get("necroKnifeGripThread"), _highlighted: false);
|
||||
_xuiRadialWindow.SetCommonData(UIUtils.GetButtonIconForAction(_epl.playerInput.Reload),
|
||||
SnakeMode.OnRadialCommand, null, 0);
|
||||
return false;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4,6 +4,6 @@
|
||||
<DisplayName value="Necromancer's Tome" />
|
||||
<Description value="A dark necromancy progression for 7 Days to Die 3.3: a skill that grows from the dead you put to rest, a bone knife, undead servants, possession of zombies and beasts, the Snake Spirit, the Mind Palace, the Blood Tide and a story-ending Black Portal. Fully localized into 13 languages. Single-player; requires EAC off." />
|
||||
<Author value="Alex Cube" />
|
||||
<Version value="1.6.2" />
|
||||
<Version value="1.7.0" />
|
||||
<Website value="https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/" />
|
||||
</xml>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
@@ -0,0 +1,28 @@
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Запись состояния урны-цинерария (TEFeatureCinerarium, HarmonySrc/Cinerarium.cs). Здесь, а не в
|
||||
/// основном проекте, по той же причине, что и PyramidWardWriteHelper: PooledBinaryWriter.Write
|
||||
/// оттуда не компилируется (см. NecromancerTEPersistence.csproj). Первым байтом - версия, чтобы
|
||||
/// поля урны (дух, его работа) можно было дописывать, не ломая старые сейвы.
|
||||
/// </summary>
|
||||
public static class CinerariumWriteHelper
|
||||
{
|
||||
public static void Write(PooledBinaryWriter _bw, byte _version, bool _hasSkull, bool _zoneShown)
|
||||
{
|
||||
_bw.Write(_version);
|
||||
_bw.Write(_hasSkull);
|
||||
_bw.Write(_zoneShown);
|
||||
}
|
||||
|
||||
/// <summary>Версия 2 (16.10, нити): число, затем сами числа - координаты нитей (см. TEFeatureCinerarium.Write).</summary>
|
||||
public static void WriteInts(PooledBinaryWriter _bw, int[] _values)
|
||||
{
|
||||
_bw.Write(_values.Length);
|
||||
foreach (int v in _values)
|
||||
{
|
||||
_bw.Write(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Reference in New Issue
Block a user