Compare commits
45
Commits
v1.6.2
..
e7a9d93cba
+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>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
FIX: compatibility with NPC mods. The ghost look (grey and translucent) was given to every entity built on the game's trader type, so NPCs from other mods such as NPCCore became nearly invisible. Now only the game's own traders look like ghosts. Thanks to Crozekanov for the report.
|
||||
Also in this file: the 1.6.1 fix - blood rose buds and blooming roses no longer float half a block above the planter.
|
||||
Nothing else changed. Drop-in over 1.6.0, 1.6.1 or 1.5.x: saves carry over.
|
||||
@@ -42,7 +42,7 @@ Updating from 1.5.x: replace the folder, saves carry over. To uninstall, delete
|
||||
[size=5][b]Requirements[/b][/size]
|
||||
|
||||
[list]
|
||||
[*][b]7 Days to Die 3.3: version 1.6.2. 7 Days to Die 3.2: version 1.4.3.[/b] Both are on the Files tab. Each build tells you in the main menu if it is started on the wrong game version.
|
||||
[*][b]7 Days to Die 3.3: version 1.6.1. 7 Days to Die 3.2: version 1.4.3.[/b] Both are on the Files tab. Each build tells you in the main menu if it is started on the wrong game version.
|
||||
[*][b]EasyAntiCheat must be off[/b] - the mod uses Harmony patches.
|
||||
[*][b]No other mods required.[/b] Harmony itself ships with the game.
|
||||
[*][b]Single-player.[/b] The mod was made for single-player from the start. It reworks a number of the game's own mechanics - corpses, loot, zombie behaviour - and a server isn't necessarily built for that, so in multiplayer some things may not work. The story scenes also play there without stopping time, since vanilla pauses only in single-player.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Fixes for 1.6.0: NPCs from other mods (e.g. NPCCore) no longer turn into translucent ghosts - only the game's own traders do; blood rose buds and flowers no longer float. Drop-in over 1.6.x or 1.5.x, saves carry over. Game 3.3, single-player, EAC off.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
|
||||
*English version below — scroll past the Russian half.*
|
||||
|
||||
**Версия 1.6.2** — для 7 Days to Die 3.3. Для игры 3.2 — версия 1.4.3. Каждая сборка на чужой версии игры сама скажет, что несовместима. Автор: Alex Cube.
|
||||
**Версия 1.6.1** — для 7 Days to Die 3.3. Для игры 3.2 — версия 1.4.3. Каждая сборка на чужой версии игры сама скажет, что несовместима. Автор: Alex Cube.
|
||||
|
||||
- Страница мода: https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/
|
||||
- Nexus Mods: https://www.nexusmods.com/7daystodie/mods/12547
|
||||
@@ -63,7 +63,7 @@
|
||||
`%APPDATA%/7DaysToDie/` (подходят обе, если её нет — создайте).
|
||||
3. Запустите игру. Навык «Некромантия» появится на панели навыков крафта сразу.
|
||||
|
||||
Версия 1.6.2 — для игры 3.3, версия 1.4.3 — для игры 3.2 (обе на Nexus во вкладке Files).
|
||||
Версия 1.6.1 — для игры 3.3, версия 1.4.3 — для игры 3.2 (обе на Nexus во вкладке Files).
|
||||
Обновление поверх 1.5.x: сейвы переносятся.
|
||||
|
||||
Рассчитан на одиночную игру. Мод переделывает ряд механик самой игры (трупы, добычу, поведение
|
||||
@@ -75,7 +75,6 @@
|
||||
|
||||
## История версий
|
||||
|
||||
- **1.6.2** — совместимость: призрачный вид только у торговцев самой игры; NPC других модов (например, NPCCore) больше не становятся полупрозрачными.
|
||||
- **1.6.1** — исправление: бутоны и цветы кровавой розы висели над грядкой на полклетки.
|
||||
- **1.6.0** — дух змеи, одержимость зомби и своих питомцев, чертоги разума, книга некроманта с
|
||||
главами, слуги охраняют хозяина (один за раз), могильная крыса, костяной скакун, гео-оссуарий,
|
||||
@@ -100,7 +99,7 @@
|
||||
|
||||
# Necromancer's Tome (English)
|
||||
|
||||
**Version 1.6.2** — for 7 Days to Die 3.3. For game 3.2 use version 1.4.3. Each build tells you if it is started on the wrong game version. Author: Alex Cube.
|
||||
**Version 1.6.1** — for 7 Days to Die 3.3. For game 3.2 use version 1.4.3. Each build tells you if it is started on the wrong game version. Author: Alex Cube.
|
||||
|
||||
- Mod page: https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/
|
||||
- Nexus Mods: https://www.nexusmods.com/7daystodie/mods/12547
|
||||
@@ -163,7 +162,7 @@ Polish, Brazilian Portuguese, Russian, Turkish, Simplified and Traditional Chine
|
||||
`%APPDATA%/7DaysToDie/` (either works; create it if it is not there).
|
||||
3. Start the game. The Necromancy skill appears in the crafting skills panel right away.
|
||||
|
||||
Version 1.6.2 is for game 3.3, version 1.4.3 is for game 3.2 (both on the Nexus Files tab).
|
||||
Version 1.6.1 is for game 3.3, version 1.4.3 is for game 3.2 (both on the Nexus Files tab).
|
||||
Drop-in over 1.5.x: saves carry over.
|
||||
|
||||
Built for single-player. The mod reworks a number of the game's own mechanics (corpses, loot,
|
||||
@@ -175,7 +174,6 @@ To uninstall, delete the folder. The mod's items and blocks will be gone from th
|
||||
|
||||
## Version history
|
||||
|
||||
- **1.6.2** - compatibility: only the game's own traders get the ghost look; NPCs from other mods (for example NPCCore) no longer turn translucent.
|
||||
- **1.6.1** - fix: blood rose buds and flowers floated half a block above the planter.
|
||||
- **1.6.0** - the Snake Spirit, possession of zombies and of your own servants, the Mind Palace, the
|
||||
Necromancer's Tome with chapters, servants that guard their master (one at a time), the Grave
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
@@ -38,7 +38,7 @@
|
||||
<li>Запустите игру. Навык «Некромантия» появится на панели навыков крафта сразу.</li>
|
||||
</ol>
|
||||
|
||||
Версия 1.6.2 — для игры 3.3, версия 1.4.3 — для игры 3.2 (обе на Nexus во вкладке Files). Обновление поверх 1.5.x: сейвы переносятся.
|
||||
Версия 1.6.1 — для игры 3.3, версия 1.4.3 — для игры 3.2 (обе на Nexus во вкладке Files). Обновление поверх 1.5.x: сейвы переносятся.
|
||||
|
||||
Рассчитан на одиночную игру. Мод переделывает ряд механик самой игры (трупы, добычу, поведение зомби), а сервер не обязательно на такое рассчитан, поэтому в мультиплеере часть вещей может не работать. Сюжетные сцены там проиграются без остановки времени: пауза игры работает только в сингле.
|
||||
|
||||
@@ -97,7 +97,7 @@ Fully localized into 13 languages: English, German, Spanish, French, Italian, Ja
|
||||
<li>Start the game. The Necromancy skill appears in the crafting skills panel right away.</li>
|
||||
</ol>
|
||||
|
||||
Version 1.6.2 is for game 3.3, version 1.4.3 is for game 3.2 (both on the Nexus Files tab). Drop-in over 1.5.x: saves carry over.
|
||||
Version 1.6.1 is for game 3.3, version 1.4.3 is for game 3.2 (both on the Nexus Files tab). Drop-in over 1.5.x: saves carry over.
|
||||
|
||||
Built for single-player. The mod reworks a number of the game's own mechanics (corpses, loot, zombie behaviour), and a server isn't necessarily built for that, so in multiplayer some things may not work. The story scenes also play there without stopping time, since the game pauses only in single-player.
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Запись состояния урны-цинерария (TEFeatureCinerarium, HarmonySrc/Cinerarium.cs). Здесь, а не в
|
||||
/// основном проекте, по той же причине, что и PyramidWardWriteHelper: PooledBinaryWriter.Write
|
||||
/// оттуда не компилируется (см. NecromancerTEPersistence.csproj). Первым байтом - версия, чтобы
|
||||
/// поля урны (дух, его работа) можно было дописывать, не ломая старые сейвы.
|
||||
/// </summary>
|
||||
public static class CinerariumWriteHelper
|
||||
{
|
||||
public static void Write(PooledBinaryWriter _bw, byte _version, bool _hasSkull, bool _zoneShown)
|
||||
{
|
||||
_bw.Write(_version);
|
||||
_bw.Write(_hasSkull);
|
||||
_bw.Write(_zoneShown);
|
||||
}
|
||||
|
||||
/// <summary>Версия 2 (16.10, нити): число, затем сами числа - координаты нитей (см. TEFeatureCinerarium.Write).</summary>
|
||||
public static void WriteInts(PooledBinaryWriter _bw, int[] _values)
|
||||
{
|
||||
_bw.Write(_values.Length);
|
||||
foreach (int v in _values)
|
||||
{
|
||||
_bw.Write(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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