1.7.0: охрана питомцами, один питомец за раз, прицел книг, гео-оссуарий
Питомцы: - охрана хозяина: враг ближе 20 м (в режиме змеи - к телу) атакуется сам, поводок 40 м, охраняют все кроме крысы; приказ прицелом охрана не перебивает - IsFoe исключает существ мода (necro*): питомец выбирал целью себя - призыв нового спутника отзывает прежнего - прицел книг призыва - как у арбалета, краснеет на годной цели Гео-оссуарий (necroGeoOssuary): станция добычи на кровавой сфере - основа - коллектор росы; в 3 ячейки модов любой из 5 ресурсов (камень, уголь, селитра, железо, сланец), 1 сфера = стак 6000 за 5-8 ч - работает без открытого неба; верстак, Подмастерье (ур. 25) - своя модель (бандл necroossuary): кости, корни, сосуды, клыки, черепа с глазами-углями; шейдер и теги коллайдера берутся у ванильного коллектора Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b737dd43d0
commit
f2dfeed126
+63
-5
File diff suppressed because one or more lines are too long
@@ -179,5 +179,52 @@
|
|||||||
<property name="SellableToTrader" value="false" />
|
<property name="SellableToTrader" value="false" />
|
||||||
<drop event="Destroy" name="resourceScrapIron" count="10,20" />
|
<drop event="Destroy" name="resourceScrapIron" count="10,20" />
|
||||||
</block>
|
</block>
|
||||||
|
<!-- Гео-оссуарий (1.7.0, 2026-09-30): станция добычи ресурсов на кровавой сфере.
|
||||||
|
Тело - ванильный коллектор росы (Class=Collector): окно, топливо, таймеры, работа без
|
||||||
|
игрока рядом - всё его. Своего кода два места, оба в HarmonySrc/GeoOssuaryPatch.cs:
|
||||||
|
- каждая из трёх ячеек модификаций принимает любой из пяти ресурсов (ваниль даёт
|
||||||
|
ячейке ровно один предмет из RequiredMods);
|
||||||
|
- ячейка продукции i выдаёт полный стак того ресурса, что лежит в ячейке
|
||||||
|
модификации i (ваниль смотрит только "занята/пуста", а не на предмет), пустая
|
||||||
|
модификация = ячейка стоит и сферу не ест; проверка неба снята.
|
||||||
|
Отсюда три OutputType os1..os3 на три ячейки продукции: у каждого свой таймер и своя
|
||||||
|
ячейка. OutputItem в них - заглушка, настоящий ресурс подставляет патч.
|
||||||
|
Топливо общее: 1 сфера = 1 стак (FuelCost 1, AdditionalFuelCost 0). Время как у росы.
|
||||||
|
ModTypes=Modify - флаг, который ваниль ни на что не тратит: Speed/Count/Expand
|
||||||
|
изменили бы скорость, выход или размер сетки.
|
||||||
|
Модель своя (Blender: кости, корни, сосуды, клыки, черепа с глазами-углями), бандл
|
||||||
|
necroossuary, собрана из префаба коллектора росы: LODGroup, слои и коллайдер ванильные.
|
||||||
|
Шейдер материалу ставит GeoOssuaryPatch (шейдер ванильного коллектора). -->
|
||||||
|
<block name="necroGeoOssuary">
|
||||||
|
<property name="Extends" value="cntDewCollector" />
|
||||||
|
<property name="UnlockedBy" value="craftingNecroNecromancy" />
|
||||||
|
<property name="DescriptionKey" value="necroGeoOssuaryDesc" />
|
||||||
|
<property name="LootLabelKey" value="xuiNecroGeoOssuary" />
|
||||||
|
<property name="ItemIconBackdrop" value="ui_game_symbol_mining" />
|
||||||
|
<property name="ItemBackgroundColor" value="140,20,20,255" />
|
||||||
|
<property name="FuelTypes" value="{blood,resourceBloodSphere}" />
|
||||||
|
<property name="OutputTypes" value="{os1,blood,1,0,1,resourceRockSmall,resourceRockSmall,1,1,6000,10000,collector_complete_item}{os2,blood,1,0,1,resourceRockSmall,resourceRockSmall,1,1,6000,10000,collector_complete_item}{os3,blood,1,0,1,resourceRockSmall,resourceRockSmall,1,1,6000,10000,collector_complete_item}" />
|
||||||
|
<property name="Outputs" value="os1,os2,os3" />
|
||||||
|
<property name="OutputGridHeight" value="1" />
|
||||||
|
<property name="FuelGridHeight" value="2" />
|
||||||
|
<property name="RequiredMods" value="resourceRockSmall,resourceRockSmall,resourceRockSmall" />
|
||||||
|
<property name="RequiredModsOnly" value="true" />
|
||||||
|
<property name="ModTypes" value="Modify,Modify,Modify" />
|
||||||
|
<property name="ModTransformEnableNames" value=",," />
|
||||||
|
<property name="EmptyText" value="necroGeoOssuaryEmpty" />
|
||||||
|
<property name="HasItem1Text" value="necroGeoOssuaryHasItems" />
|
||||||
|
<property name="HasItem2Text" value="necroGeoOssuaryHasItems" />
|
||||||
|
<property name="FuelTitleText" value="necroGeoOssuaryFuel" />
|
||||||
|
<property name="FuelTitleSprite" value="ui_game_symbol_skull" />
|
||||||
|
<property name="FuelTitleSpriteAtlas" value="UIAtlas" />
|
||||||
|
<property name="FuelTypesSprites" value="BloodSphere,ItemIconAtlasGreyscale" />
|
||||||
|
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necroossuary?necroOssuaryPrefab.prefab" />
|
||||||
|
<property name="CustomIcon" value="necroGeoOssuary" />
|
||||||
|
<property name="EconomicValue" value="0" />
|
||||||
|
<property name="SellableToTrader" value="false" />
|
||||||
|
<property class="RepairItems">
|
||||||
|
<property name="resourceBone" value="50" />
|
||||||
|
</property>
|
||||||
|
</block>
|
||||||
</append>
|
</append>
|
||||||
</config>
|
</config>
|
||||||
|
|||||||
@@ -235,7 +235,7 @@
|
|||||||
ОБЯЗАНЫ совпадать. display_entry рисует замок сам по себе, по unlock_tier, и про
|
ОБЯЗАНЫ совпадать. display_entry рисует замок сам по себе, по unlock_tier, и про
|
||||||
теги не знает - разойдутся, и на панели будет замок на доступном рецепте. -->
|
теги не знает - разойдутся, и на панели будет замок на доступном рецепте. -->
|
||||||
<display_entry icon="ScrollOfDeviation" name_key="craftingNecroNecromancyTier2Name" has_quality="false" unlock_level="25,65,70,85">
|
<display_entry icon="ScrollOfDeviation" name_key="craftingNecroNecromancyTier2Name" has_quality="false" unlock_level="25,65,70,85">
|
||||||
<unlock_entry item="thrownBookGrimoireDeviation,bookSummonZombieGriffin" unlock_tier="1"/>
|
<unlock_entry item="thrownBookGrimoireDeviation,bookSummonZombieGriffin,necroGeoOssuary" unlock_tier="1"/>
|
||||||
<!-- Зомбогриф, 18.09: "гриф пусть тоже будет на подмастерье, но 1300 (65 уровень
|
<!-- Зомбогриф, 18.09: "гриф пусть тоже будет на подмастерье, но 1300 (65 уровень
|
||||||
некромантии)". 65 вставлен в unlock_level МЕЖДУ 25 и 70, поэтому unlock_tier
|
некромантии)". 65 вставлен в unlock_level МЕЖДУ 25 и 70, поэтому unlock_tier
|
||||||
у двух модов ножа ниже сдвинулись на единицу (были 2 и 3, стали 3 и 4).
|
у двух модов ножа ниже сдвинулись на единицу (были 2 и 3, стали 3 и 4).
|
||||||
|
|||||||
@@ -473,4 +473,19 @@
|
|||||||
<ingredient name="resourceNecromancerBlood" count="1"/>
|
<ingredient name="resourceNecromancerBlood" count="1"/>
|
||||||
</recipe>
|
</recipe>
|
||||||
</append>
|
</append>
|
||||||
|
<!-- Гео-оссуарий (1.7.0, 2026-09-30). Рецепт придуман по указанию «туда явно нужен флакон
|
||||||
|
крови некроманта и много костей»; дерево и волокна - корни, которыми
|
||||||
|
оплетены кости (добавлено тем же вечером). Порог - «Подмастерье» (уровень 25, как Гримуар
|
||||||
|
девиации): к этому времени игрок строит базу и кузницу, и камень с железом ему нужны
|
||||||
|
именно тогда (правило «порог по нужде, а не по силе»). Тег и unlock_level группы 2 в
|
||||||
|
progression.xml обязаны совпадать. -->
|
||||||
|
<append xpath="/recipes">
|
||||||
|
<recipe name="necroGeoOssuary" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
|
||||||
|
<ingredient name="resourceBone" count="400"/>
|
||||||
|
<ingredient name="resourceZombieAsh" count="200"/>
|
||||||
|
<ingredient name="resourceNecromancerBlood" count="1"/>
|
||||||
|
<ingredient name="resourceWood" count="100"/>
|
||||||
|
<ingredient name="resourceYuccaFibers" count="50"/>
|
||||||
|
</recipe>
|
||||||
|
</append>
|
||||||
</config>
|
</config>
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
using HarmonyLib;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NecromancerTome
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Гео-оссуарий (1.7.0, 2026-09-30): станция добычи ресурсов на кровавой сфере. Блок -
|
||||||
|
/// necroGeoOssuary в blocks.xml, наследник ванильного коллектора росы; всё, что коллектор
|
||||||
|
/// умеет сам (окно, топливо, таймеры, работа без игрока рядом), остаётся его. Здесь только то,
|
||||||
|
/// чего ваниль не умеет, - разобрано по декомпилятору 3.3:
|
||||||
|
///
|
||||||
|
/// - ЯЧЕЙКА МОДИФИКАЦИИ ПРИНИМАЕТ ОДИН ПРЕДМЕТ. XUiC_DewCollectorModGrid.SetTileEntity
|
||||||
|
/// зовёт SetAllowedItemClassSingle(RequiredMods[i]). У оссуария в любую из трёх ячеек идёт
|
||||||
|
/// любой из пяти ресурсов, по одной штуке (OverrideStackCount = 1). Окно у всех
|
||||||
|
/// коллекторов одно и то же, поэтому для чужих коллекторов предел штук сбрасывается
|
||||||
|
/// обратно - иначе коллектор росы после оссуария тоже брал бы по одной.
|
||||||
|
/// - ВАНИЛЬ НЕ СМОТРИТ, ЧТО ЛЕЖИТ В ЯЧЕЙКЕ. ModTypes=Type - просто флаг HasModConvert
|
||||||
|
/// (OutputItem -> OutputItemModded), одна замена на весь блок. У оссуария три OutputType
|
||||||
|
/// os1..os3, по одному на ячейку продукции; os(i+1) выдаёт полный стак ресурса из ячейки
|
||||||
|
/// модификации i. Пустая или чужая модификация - этот OutputType выключен (isDisabled),
|
||||||
|
/// значит таймер стоит и сферу он не ест.
|
||||||
|
/// - ПРОВЕРКА НЕБА. HandleSkyCheck (7 блоков вверх) у ванили безусловна для всех
|
||||||
|
/// коллекторов; оссуарий работает где угодно, в том числе под землёй (решение 30.09).
|
||||||
|
/// </summary>
|
||||||
|
public static class GeoOssuary
|
||||||
|
{
|
||||||
|
public const string BlockName = "necroGeoOssuary";
|
||||||
|
|
||||||
|
/// <summary>Камень, уголь, селитра, железо (его дают железные жилы), сланец.</summary>
|
||||||
|
public static readonly string[] ResourceNames =
|
||||||
|
{
|
||||||
|
"resourceRockSmall", "resourceCoal", "resourcePotassiumNitratePowder",
|
||||||
|
"resourceScrapIron", "resourceOilShale",
|
||||||
|
};
|
||||||
|
|
||||||
|
static ItemClass[] resourceClasses;
|
||||||
|
|
||||||
|
public static ItemClass[] ResourceClasses
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (resourceClasses == null)
|
||||||
|
{
|
||||||
|
resourceClasses = new ItemClass[ResourceNames.Length];
|
||||||
|
for (int i = 0; i < ResourceNames.Length; i++)
|
||||||
|
resourceClasses[i] = ItemClass.GetItemClass(ResourceNames[i]);
|
||||||
|
}
|
||||||
|
return resourceClasses;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Is(Block block) => block != null && block.GetBlockName() == BlockName;
|
||||||
|
|
||||||
|
public static bool Is(TileEntityCollector te) => te != null && Is(te.blockValue.Block);
|
||||||
|
|
||||||
|
/// <summary>os1 -> 0, os2 -> 1, os3 -> 2; иначе -1.</summary>
|
||||||
|
public static int ModSlotOf(BlockCollector.OutputType outputType)
|
||||||
|
{
|
||||||
|
string name = outputType?.Name;
|
||||||
|
if (name == null || name.Length != 3 || !name.StartsWith("os"))
|
||||||
|
return -1;
|
||||||
|
return name[2] - '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ресурс в ячейке модификации, если он из списка; иначе null.</summary>
|
||||||
|
public static ItemClass ResourceIn(TileEntityCollector te, int modSlot)
|
||||||
|
{
|
||||||
|
ItemStack[] mods = te.ModSlots;
|
||||||
|
if (modSlot < 0 || mods == null || modSlot >= mods.Length)
|
||||||
|
return null;
|
||||||
|
ItemStack stack = mods[modSlot];
|
||||||
|
if (stack == null || stack.IsEmpty())
|
||||||
|
return null;
|
||||||
|
ItemClass cls = stack.itemValue.ItemClass;
|
||||||
|
return cls != null && System.Array.IndexOf(ResourceNames, cls.Name) >= 0 ? cls : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(XUiC_DewCollectorModGrid), nameof(XUiC_DewCollectorModGrid.SetTileEntity))]
|
||||||
|
public static class Patch_XUiC_DewCollectorModGrid_SetTileEntity_GeoOssuary
|
||||||
|
{
|
||||||
|
public static void Postfix(XUiC_DewCollectorModGrid __instance, TileEntityCollector te)
|
||||||
|
{
|
||||||
|
bool ossuary = GeoOssuary.Is(te);
|
||||||
|
foreach (XUiC_ItemStack slot in __instance.itemControllers)
|
||||||
|
{
|
||||||
|
if (!(slot is XUiC_RequiredItemStack required))
|
||||||
|
continue;
|
||||||
|
required.OverrideStackCount = ossuary ? 1 : -1;
|
||||||
|
if (ossuary)
|
||||||
|
{
|
||||||
|
required.SetAllowedItemClasses(GeoOssuary.ResourceClasses);
|
||||||
|
required.RequiredItemOnly = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(TileEntityCollector), nameof(TileEntityCollector.isDisabled))]
|
||||||
|
public static class Patch_TileEntityCollector_isDisabled_GeoOssuary
|
||||||
|
{
|
||||||
|
public static void Postfix(TileEntityCollector __instance, BlockCollector.OutputType _outputType, ref bool __result)
|
||||||
|
{
|
||||||
|
if (__result || !GeoOssuary.Is(__instance))
|
||||||
|
return;
|
||||||
|
if (GeoOssuary.ResourceIn(__instance, GeoOssuary.ModSlotOf(_outputType)) == null)
|
||||||
|
__result = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(TileEntityCollector), nameof(TileEntityCollector.newItem))]
|
||||||
|
public static class Patch_TileEntityCollector_newItem_GeoOssuary
|
||||||
|
{
|
||||||
|
public static void Postfix(TileEntityCollector __instance, BlockCollector.OutputType outputType, ref ItemStack __result)
|
||||||
|
{
|
||||||
|
if (__result == null || !GeoOssuary.Is(__instance))
|
||||||
|
return;
|
||||||
|
ItemClass resource = GeoOssuary.ResourceIn(__instance, GeoOssuary.ModSlotOf(outputType));
|
||||||
|
if (resource == null)
|
||||||
|
{
|
||||||
|
// isDisabled не пускает сюда без ресурса; если всё же дошло - сфера уже списана,
|
||||||
|
// и отдать хотя бы заглушку честнее, чем молча съесть топливо.
|
||||||
|
Debug.LogWarning("[NecromancerTome] GeoOssuary: " + outputType.Name + " сработал без ресурса в ячейке модификации");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
__result = new ItemStack(new ItemValue(resource.Id), resource.MaxCount);
|
||||||
|
Debug.Log("[NecromancerTome] GeoOssuary: " + outputType.Name + " -> " + resource.Name + " x" + resource.MaxCount + " в " + __instance.ToWorldPos());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Шейдер модели. Своя модель оссуария лежит в нашем бандле с запасным шейдером
|
||||||
|
/// NecromancerTome/OssuaryProxy (Standard-поверхность), а у пирамиды 10.09 именно
|
||||||
|
/// Standard из бандла на расстоянии рисовался маджентой. Поэтому при первой загрузке модели
|
||||||
|
/// материалу ставится тот шейдер, которым игра рисует ванильный коллектор росы: имена текстур
|
||||||
|
/// у них общие (_MainTex, _Normal, _RMOM, _Emissive), модель выросла из его префаба. Префаб из
|
||||||
|
/// бандла - общий ассет, так что правка одна на все экземпляры.
|
||||||
|
/// </summary>
|
||||||
|
[HarmonyPatch(typeof(BlockShapeModelEntity), nameof(BlockShapeModelEntity.getPrefab))]
|
||||||
|
public static class Patch_BlockShapeModelEntity_getPrefab_GeoOssuaryShader
|
||||||
|
{
|
||||||
|
const string ModelName = "necroOssuaryPrefab";
|
||||||
|
const string VanillaModel = "@:Entities/Furniture/collectorDewPrefab.prefab";
|
||||||
|
static bool done;
|
||||||
|
|
||||||
|
public static void Postfix(Transform __result)
|
||||||
|
{
|
||||||
|
if (done || __result == null || __result.name != ModelName)
|
||||||
|
return;
|
||||||
|
done = true;
|
||||||
|
Transform vanilla = DataLoader.LoadAsset<Transform>(VanillaModel);
|
||||||
|
Renderer src = vanilla != null ? vanilla.GetComponentInChildren<Renderer>(true) : null;
|
||||||
|
Shader shader = src != null && src.sharedMaterial != null ? src.sharedMaterial.shader : null;
|
||||||
|
if (shader == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[NecromancerTome] GeoOssuary: шейдер коллектора росы не найден - остаётся запасной");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int n = 0;
|
||||||
|
foreach (Renderer r in __result.GetComponentsInChildren<Renderer>(true))
|
||||||
|
foreach (Material m in r.sharedMaterials)
|
||||||
|
if (m != null && m.shader != shader) { m.shader = shader; m.enableInstancing = true; n++; }
|
||||||
|
Debug.Log("[NecromancerTome] GeoOssuary: материалам модели (" + n + ") поставлен шейдер '" + shader.name + "'");
|
||||||
|
CopyTags(vanilla, __result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Теги коллайдера. У ванильного префаба корень и объект collision помечены
|
||||||
|
/// игровым тегом (AssetRipper записал его как unknown_20004 - имени он не знал), и именно
|
||||||
|
/// по тегу игра понимает, что луч попал в блок, с которым можно взаимодействовать. В нашем
|
||||||
|
/// Unity-проекте такого тега нет, при сборке он стал Untagged - и оссуарий не реагировал
|
||||||
|
/// на наведение (тест 30.09). Берём теги у ванильного коллектора по именам объектов.</summary>
|
||||||
|
static void CopyTags(Transform vanilla, Transform ours)
|
||||||
|
{
|
||||||
|
var byName = new System.Collections.Generic.Dictionary<string, Transform>();
|
||||||
|
foreach (Transform t in vanilla.GetComponentsInChildren<Transform>(true))
|
||||||
|
byName[t == vanilla ? "<root>" : t.name] = t;
|
||||||
|
foreach (Transform t in ours.GetComponentsInChildren<Transform>(true))
|
||||||
|
{
|
||||||
|
Transform src;
|
||||||
|
if (!byName.TryGetValue(t == ours ? "<root>" : t.name, out src))
|
||||||
|
continue;
|
||||||
|
if (t.tag != src.tag || t.gameObject.layer != src.gameObject.layer)
|
||||||
|
{
|
||||||
|
Debug.Log("[NecromancerTome] GeoOssuary: '" + t.name + "' тег " + t.tag + " -> " + src.tag +
|
||||||
|
", слой " + t.gameObject.layer + " -> " + src.gameObject.layer);
|
||||||
|
t.tag = src.tag;
|
||||||
|
t.gameObject.layer = src.gameObject.layer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HarmonyPatch(typeof(TileEntityCollector), nameof(TileEntityCollector.HandleSkyCheck))]
|
||||||
|
public static class Patch_TileEntityCollector_HandleSkyCheck_GeoOssuary
|
||||||
|
{
|
||||||
|
public static void Postfix(TileEntityCollector __instance, ref bool __result)
|
||||||
|
{
|
||||||
|
if (__result && GeoOssuary.Is(__instance))
|
||||||
|
__result = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,11 @@
|
|||||||
<HintPath>..\..\..\7DaysToDie_Data\Managed\Assembly-CSharp.dll</HintPath>
|
<HintPath>..\..\..\7DaysToDie_Data\Managed\Assembly-CSharp.dll</HintPath>
|
||||||
<Private>false</Private>
|
<Private>false</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
<!-- DataLoader.LoadAsset: у метода есть перегрузка с AssetReference (гео-оссуарий, 30.09) -->
|
||||||
|
<Reference Include="Unity.Addressables">
|
||||||
|
<HintPath>..\..\..\7DaysToDie_Data\Managed\Unity.Addressables.dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
<Reference Include="UnityEngine.CoreModule">
|
<Reference Include="UnityEngine.CoreModule">
|
||||||
<HintPath>..\..\..\7DaysToDie_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
|
<HintPath>..\..\..\7DaysToDie_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
|
||||||
<Private>false</Private>
|
<Private>false</Private>
|
||||||
|
|||||||
+129
-17
@@ -42,6 +42,32 @@ namespace NecromancerTome
|
|||||||
/// смысл не в таймере, а в том, чтобы приказ не висел вечно, если цель уйдёт из мира.</summary>
|
/// смысл не в таймере, а в том, чтобы приказ не висел вечно, если цель уйдёт из мира.</summary>
|
||||||
public const int OrderTicks = 6000;
|
public const int OrderTicks = 6000;
|
||||||
|
|
||||||
|
/// <summary>Кого игрок сейчас держит в прицеле и кого можно назначить целью. null - некого.
|
||||||
|
/// Общее для приказа и для покраснения прицела (PetCrosshair): прицел краснеет ровно тогда,
|
||||||
|
/// когда приказ будет принят. Свои питомцы целью не бывают.</summary>
|
||||||
|
public static EntityAlive TargetUnderCrosshair(EntityPlayerLocal _local)
|
||||||
|
{
|
||||||
|
WorldRayHitInfo hit = _local.HitInfo;
|
||||||
|
if (hit == null || !hit.bHitValid || hit.transform == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
EntityAlive target = ItemActionAttack.GetEntityFromHit(hit) as EntityAlive;
|
||||||
|
if (target == null || target.IsDead() || target == _local)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (target is EntityPlayer || target is EntityTrader)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (PetFollowPatch.FindOwnerOfPet(target.entityId) != null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Отдаёт питомцу приказ атаковать то, на что смотрит игрок. false - приказа не
|
/// <summary>Отдаёт питомцу приказ атаковать то, на что смотрит игрок. false - приказа не
|
||||||
/// вышло, и вызывающая сторона показывает "Нет цели для атаки".</summary>
|
/// вышло, и вызывающая сторона показывает "Нет цели для атаки".</summary>
|
||||||
public static bool TryOrderAttack(EntityAlive _owner, EntityAlive _pet)
|
public static bool TryOrderAttack(EntityAlive _owner, EntityAlive _pet)
|
||||||
@@ -59,23 +85,8 @@ namespace NecromancerTome
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
WorldRayHitInfo hit = local.HitInfo;
|
EntityAlive target = TargetUnderCrosshair(local);
|
||||||
if (hit == null || !hit.bHitValid || hit.transform == null)
|
if (target == null || target == _pet)
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Entity hitEntity = ItemActionAttack.GetEntityFromHit(hit);
|
|
||||||
EntityAlive target = hitEntity as EntityAlive;
|
|
||||||
if (target == null || target.IsDead())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (target == _pet || target == _owner)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (target is EntityPlayer || target is EntityTrader)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -84,9 +95,110 @@ namespace NecromancerTome
|
|||||||
// следующем тике перебить наш приказ тем, кто ударил крысу последним.
|
// следующем тике перебить наш приказ тем, кто ударил крысу последним.
|
||||||
_pet.SetRevengeTarget(null);
|
_pet.SetRevengeTarget(null);
|
||||||
_pet.SetAttackTarget(target, OrderTicks);
|
_pet.SetAttackTarget(target, OrderTicks);
|
||||||
|
// Охрана (PetFollowPatch.Guard) сама раздаёт цели; приказ игрока она перебивать не должна.
|
||||||
|
PetFollowPatch.NoteOrder(_pet.entityId, target.entityId);
|
||||||
Debug.Log("[NecromancerTome] PetAttackCommand: pet " + _pet.entityId + " ordered to attack " +
|
Debug.Log("[NecromancerTome] PetAttackCommand: pet " + _pet.entityId + " ordered to attack " +
|
||||||
target.entityId + " (" + target.EntityClass.entityClassName + ")");
|
target.entityId + " (" + target.EntityClass.entityClassName + ")");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ПРИЦЕЛ КНИГ И СВИТКОВ ПРИЗЫВА - указание 30.09.2026: «сделать другой прицел (более явный, из
|
||||||
|
/// уже имеющихся в игре) и чтобы он краснел при попадании в него врага. Как у змеи краснеет».
|
||||||
|
///
|
||||||
|
/// У ItemActionSpawnEntity своего прицела нет, он получал общий маленький «плюс». Игра рисует
|
||||||
|
/// прицел одним методом, EntityPlayerLocal.guiDrawCrosshair, и цвет там берётся из настроек,
|
||||||
|
/// так что подменить один лишь ВИД прицела мало - покраснеть он не сможет. Поэтому для наших
|
||||||
|
/// книг метод заменяется целиком: те же условия показа, тот же размер и та же готовая
|
||||||
|
/// прицел арбалета (DrawDynamicCrosshair, 4 штриха), а цвет - наш.
|
||||||
|
///
|
||||||
|
/// Красный = приказ будет принят: питомец этой книги призван и под прицелом годная цель
|
||||||
|
/// (PetAttackCommand.TargetUnderCrosshair - тот же разбор, что у самого приказа). Пока питомец
|
||||||
|
/// не призван, основное действие - это призыв, и красить прицел незачем.
|
||||||
|
///
|
||||||
|
/// Только у книг с приказом (PetInfo.CommandsAttack). Свиток Роя оставлен как был.
|
||||||
|
/// </summary>
|
||||||
|
[HarmonyLib.HarmonyPatch(typeof(EntityPlayerLocal), "guiDrawCrosshair")]
|
||||||
|
public static class Patch_EntityPlayerLocal_guiDrawCrosshair_PetCrosshair
|
||||||
|
{
|
||||||
|
/// <summary>Тот же красный, что у прицела змеи (SnakeCrosshair.OnTarget).</summary>
|
||||||
|
static readonly Color OnTarget = new Color(1f, 0.15f, 0.1f, 1f);
|
||||||
|
|
||||||
|
public static bool Prefix(EntityPlayerLocal __instance, NGuiWdwInGameHUD _guiInGame, bool bModalWindowOpen)
|
||||||
|
{
|
||||||
|
if (__instance.inventory == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
ItemClass holding = __instance.inventory.holdingItem;
|
||||||
|
ItemActionSpawnEntity action = holding != null && holding.Actions != null && holding.Actions.Length > 0
|
||||||
|
? holding.Actions[0] as ItemActionSpawnEntity
|
||||||
|
: null;
|
||||||
|
if (action == null ||
|
||||||
|
!Patch_ItemActionSpawnEntity_Spawn_PetLimit.LimitedPets.TryGetValue(action.entityToSpawn,
|
||||||
|
out Patch_ItemActionSpawnEntity_Spawn_PetLimit.PetInfo petInfo) ||
|
||||||
|
!petInfo.CommandsAttack)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Дальше - условия показа из самого guiDrawCrosshair, в том же порядке.
|
||||||
|
if (!_guiInGame.showCrosshair || Event.current.type != EventType.Repaint || __instance.IsDead() ||
|
||||||
|
__instance.emodel.IsRagdollActive || __instance.AttachedToEntity != null ||
|
||||||
|
!GamePrefs.GetBool(EnumGamePrefs.OptionsCrosshairEnabled) || bModalWindowOpen ||
|
||||||
|
_guiInGame.CrosshairSquare == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float opacity = GamePrefs.GetFloat(EnumGamePrefs.OptionsCrosshairOpacity);
|
||||||
|
int colorIndex = GamePrefs.GetInt(EnumGamePrefs.OptionsCrosshairColor);
|
||||||
|
if (colorIndex < 0 || colorIndex >= EntityPlayerLocal.crosshairColors.Count)
|
||||||
|
{
|
||||||
|
colorIndex = 0;
|
||||||
|
}
|
||||||
|
Color color = EntityPlayerLocal.crosshairColors[colorIndex];
|
||||||
|
color.a = opacity;
|
||||||
|
if (PetIsOut(__instance, action.entityToSpawn) && PetAttackCommand.TargetUnderCrosshair(__instance) != null)
|
||||||
|
{
|
||||||
|
color = OnTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 center = __instance.GetCrosshairPosition2D();
|
||||||
|
center.y = Screen.height - center.y;
|
||||||
|
float size = Screen.height * 0.059f * GamePrefs.GetFloat(EnumGamePrefs.OptionsCrosshairScale);
|
||||||
|
// Прицел арбалета (тест 30.09: CrosshairDamage выглядел «кружком со стрелкой вниз»):
|
||||||
|
// четыре штриха игрового DrawDynamicCrosshair, длина та же, что у стрелкового оружия
|
||||||
|
// (size / 3); разброс у книги не меняется, поэтому зазор постоянный.
|
||||||
|
EntityPlayerLocal.DrawDynamicCrosshair(center.x, center.y, size * 0.22f, size * 0.22f, size / 3f,
|
||||||
|
GamePrefs.GetFloat(EnumGamePrefs.OptionsCrosshairThickness), color.a, color);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Призван ли сейчас питомец этой книги у этого игрока. По списку PetFollowPatch, а не
|
||||||
|
/// GetOwnedEntities: тот на каждый вызов заводит новый список, а мы здесь каждый кадр.</summary>
|
||||||
|
static bool PetIsOut(EntityPlayerLocal _player, string _entityClassName)
|
||||||
|
{
|
||||||
|
int classId = EntityClass.GetId(_entityClassName);
|
||||||
|
World world = _player.world;
|
||||||
|
if (classId == -1 || world == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < PetFollowPatch.TrackedPets.Count; i++)
|
||||||
|
{
|
||||||
|
PetFollowPatch.TrackedPet tracked = PetFollowPatch.TrackedPets[i];
|
||||||
|
if (tracked.OwnerEntityId != _player.entityId)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Entity pet = world.GetEntity(tracked.PetEntityId);
|
||||||
|
if (pet != null && pet.entityClass == classId && !pet.IsDead())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,17 @@ namespace NecromancerTome
|
|||||||
/// <summary>Радиус "дома" для летающих (см. PetInfo.FlyingHomeRadius). Ноль -
|
/// <summary>Радиус "дома" для летающих (см. PetInfo.FlyingHomeRadius). Ноль -
|
||||||
/// питомец не летающий, дом ему не переставляем.</summary>
|
/// питомец не летающий, дом ему не переставляем.</summary>
|
||||||
public int FlyingHomeRadius;
|
public int FlyingHomeRadius;
|
||||||
|
|
||||||
|
/// <summary>Охраняет ли хозяина сам - копия PetInfo.Guards.</summary>
|
||||||
|
public bool Guards;
|
||||||
|
|
||||||
|
/// <summary>На кого игрок указал прицелом (PetAttackCommand). Пока эта цель жива и питомец
|
||||||
|
/// занят именно ею, охрана его не перенацеливает. -1 - приказа нет.</summary>
|
||||||
|
public int OrderedTargetId = -1;
|
||||||
|
|
||||||
|
/// <summary>Питомец ушёл дальше GuardLeash и возвращается: пока он не подойдёт к хозяину
|
||||||
|
/// на GuardRange, любые цели с него снимаются - иначе укус в спину разворачивал бы его.</summary>
|
||||||
|
public bool Returning;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly List<TrackedPet> TrackedPets = new List<TrackedPet>();
|
public static readonly List<TrackedPet> TrackedPets = new List<TrackedPet>();
|
||||||
@@ -106,7 +117,7 @@ namespace NecromancerTome
|
|||||||
|
|
||||||
/// <summary>Called from SummonPatch.cs right after a pet is created and owned.</summary>
|
/// <summary>Called from SummonPatch.cs right after a pet is created and owned.</summary>
|
||||||
public static void Register(EntityAlive owner, Entity pet, bool ownFollowTask, bool collisionIgnored,
|
public static void Register(EntityAlive owner, Entity pet, bool ownFollowTask, bool collisionIgnored,
|
||||||
bool wantsLitEyes, int flyingHomeRadius)
|
bool wantsLitEyes, int flyingHomeRadius, bool guards)
|
||||||
{
|
{
|
||||||
TrackedPets.Add(new TrackedPet
|
TrackedPets.Add(new TrackedPet
|
||||||
{
|
{
|
||||||
@@ -116,6 +127,7 @@ namespace NecromancerTome
|
|||||||
CollisionIgnored = collisionIgnored,
|
CollisionIgnored = collisionIgnored,
|
||||||
WantsLitEyes = wantsLitEyes,
|
WantsLitEyes = wantsLitEyes,
|
||||||
FlyingHomeRadius = flyingHomeRadius,
|
FlyingHomeRadius = flyingHomeRadius,
|
||||||
|
Guards = guards,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +202,121 @@ namespace NecromancerTome
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>ОХРАНА ХОЗЯИНА - указание 30.09.2026. Образ: «ночь, мы в виде змеи ползаем на
|
||||||
|
/// другом конце карты, а тело охраняет стервятник». До этого питомец дрался только по приказу
|
||||||
|
/// (PetAttackCommand) - отзыв sanovskiy 23.09: «приказ нужен на каждую цель».
|
||||||
|
///
|
||||||
|
/// Решения пользователя по пунктам:
|
||||||
|
/// - радиус охраны 20 м от хозяина (GuardRange); в режиме змеи хозяин - это лежащее тело,
|
||||||
|
/// позиция игрока там и остаётся;
|
||||||
|
/// - охраняют все, кроме крысы (PetInfo.Guards);
|
||||||
|
/// - отходить можно до 40 м (GuardLeash), дальше - возвращается;
|
||||||
|
/// - бить ближайшего к телу, если не было приказа атаковать конкретного.
|
||||||
|
///
|
||||||
|
/// Враг - тот же, что для тревоги у тела в режиме змеи (SnakeControl.IsFoe): живой EntityEnemy,
|
||||||
|
/// не питомец и не девиант. СПЯЩИЕ НЕ В СЧЁТ: иначе питомец, проходящий сквозь стены, будил бы
|
||||||
|
/// всё здание, пока игрок крадётся, - к спящему слово «приближается» не относится.
|
||||||
|
///
|
||||||
|
/// Уже начатую цель охрана не бросает, пока та в радиусе: выбор «ближайшего» заново каждую
|
||||||
|
/// секунду заставил бы питомца метаться между двумя зомби и не добить ни одного.</summary>
|
||||||
|
public const float GuardRange = 20f;
|
||||||
|
public const float GuardLeash = 40f;
|
||||||
|
|
||||||
|
public static void NoteOrder(int petEntityId, int targetEntityId)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < TrackedPets.Count; i++)
|
||||||
|
{
|
||||||
|
if (TrackedPets[i].PetEntityId == petEntityId)
|
||||||
|
{
|
||||||
|
TrackedPets[i].OrderedTargetId = targetEntityId;
|
||||||
|
TrackedPets[i].Returning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EntityAlive NearestFoe(World world, Vector3 center, float range)
|
||||||
|
{
|
||||||
|
EntityAlive nearest = null;
|
||||||
|
float best = range * range;
|
||||||
|
for (int i = 0; i < world.EntityAlives.Count; i++)
|
||||||
|
{
|
||||||
|
EntityAlive e = world.EntityAlives[i];
|
||||||
|
if (!SnakeControl.IsFoe(e, null) || e.IsSleeping)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float distSq = (e.position - center).sqrMagnitude;
|
||||||
|
if (distSq < best)
|
||||||
|
{
|
||||||
|
best = distSq;
|
||||||
|
nearest = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Guard(TrackedPet tracked, EntityAlive pet, EntityAlive owner, World world)
|
||||||
|
{
|
||||||
|
if (owner.IsDead())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float fromOwnerSq = (pet.position - owner.position).sqrMagnitude;
|
||||||
|
EntityAlive target = pet.GetAttackTarget();
|
||||||
|
|
||||||
|
// Поводок 40 м - для всех спутников, и для приказа игрока тоже.
|
||||||
|
if (tracked.Returning)
|
||||||
|
{
|
||||||
|
if (fromOwnerSq > GuardRange * GuardRange)
|
||||||
|
{
|
||||||
|
if (target != null)
|
||||||
|
{
|
||||||
|
pet.SetRevengeTarget(null);
|
||||||
|
pet.SetAttackTarget(null, 0);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tracked.Returning = false;
|
||||||
|
Debug.Log("[NecromancerTome] PetFollowPatch: pet " + pet.entityId + " вернулся к хозяину");
|
||||||
|
}
|
||||||
|
else if (fromOwnerSq > GuardLeash * GuardLeash)
|
||||||
|
{
|
||||||
|
tracked.Returning = true;
|
||||||
|
tracked.OrderedTargetId = -1;
|
||||||
|
pet.SetRevengeTarget(null);
|
||||||
|
pet.SetAttackTarget(null, 0);
|
||||||
|
Debug.Log("[NecromancerTome] PetFollowPatch: pet " + pet.entityId + " ушёл на " +
|
||||||
|
Mathf.Sqrt(fromOwnerSq).ToCultureInvariantString("0") + " м от хозяина - цель снята, возвращается");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tracked.Guards)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool live = target != null && !target.IsDead();
|
||||||
|
if (live && target.entityId == tracked.OrderedTargetId)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tracked.OrderedTargetId = -1;
|
||||||
|
if (live && SnakeControl.IsFoe(target, null) &&
|
||||||
|
(target.position - owner.position).sqrMagnitude <= GuardRange * GuardRange)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EntityAlive foe = NearestFoe(world, owner.position, GuardRange);
|
||||||
|
if (foe == null || foe == target)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pet.SetRevengeTarget(null);
|
||||||
|
pet.SetAttackTarget(foe, PetAttackCommand.OrderTicks);
|
||||||
|
Debug.Log("[NecromancerTome] PetFollowPatch: охрана - pet " + pet.entityId + " идёт на " + foe.entityId +
|
||||||
|
" (" + foe.EntityClass.entityClassName + "), до хозяина " +
|
||||||
|
(foe.position - owner.position).magnitude.ToCultureInvariantString("0.0") + " м");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>ПИТАНИЕ ПАДАЛЬЮ, 2026-09-18. Наблюдение пользователя: "Дух крысы после того как
|
/// <summary>ПИТАНИЕ ПАДАЛЬЮ, 2026-09-18. Наблюдение пользователя: "Дух крысы после того как
|
||||||
/// зомби погибает, начинает есть труп. Пусть тогда этот процесс восстанавливает ей здоровье".
|
/// зомби погибает, начинает есть труп. Пусть тогда этот процесс восстанавливает ей здоровье".
|
||||||
///
|
///
|
||||||
@@ -392,6 +519,13 @@ namespace NecromancerTome
|
|||||||
tracked.CollisionIgnored = TryIgnoreCollisionWithOwner(owner, pet);
|
tracked.CollisionIgnored = TryIgnoreCollisionWithOwner(owner, pet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Охрана и поводок 40 м - до блока про «дом» летающих: тот в этом же тике должен
|
||||||
|
// увидеть уже назначенную или снятую цель. Рой (не спутник) сюда не попадает.
|
||||||
|
if (tracked.Guards || tracked.OwnFollowTask || tracked.FlyingHomeRadius > 0)
|
||||||
|
{
|
||||||
|
Guard(tracked, pet, owner, world);
|
||||||
|
}
|
||||||
|
|
||||||
// СЛЕДОВАНИЕ ДЛЯ ЛЕТАЮЩИХ, 2026-09-18. Дом переставляется на хозяина каждую
|
// СЛЕДОВАНИЕ ДЛЯ ЛЕТАЮЩИХ, 2026-09-18. Дом переставляется на хозяина каждую
|
||||||
// секунду, а возвращается питомец САМ, своим полётом: у EntityVulture проверка
|
// секунду, а возвращается питомец САМ, своим полётом: у EntityVulture проверка
|
||||||
// "не ушёл ли из дома" встроена и работает каждые 60 тиков. Ни телепорта, ни
|
// "не ушёл ли из дома" встроена и работает каждые 60 тиков. Ни телепорта, ни
|
||||||
|
|||||||
@@ -495,12 +495,23 @@ namespace NecromancerTome
|
|||||||
/// <summary>Враг для змеи: живой EntityEnemy, не питомец (призрачный) и не девиант - зомби под
|
/// <summary>Враг для змеи: живой EntityEnemy, не питомец (призрачный) и не девиант - зомби под
|
||||||
/// баффом Девиатора на стороне некроманта (указание 28.09: «если под бафом девианта, то это
|
/// баффом Девиатора на стороне некроманта (указание 28.09: «если под бафом девианта, то это
|
||||||
/// не враг»). Общее для тревоги у тела и для прицела.</summary>
|
/// не враг»). Общее для тревоги у тела и для прицела.</summary>
|
||||||
static bool IsFoe(EntityAlive _e, EntityAlive _snake)
|
public static bool IsFoe(EntityAlive _e, EntityAlive _snake)
|
||||||
{
|
{
|
||||||
return _e != null && _e != _snake && _e is EntityEnemy && !_e.IsDead() &&
|
return _e != null && _e != _snake && _e is EntityEnemy && !_e.IsDead() &&
|
||||||
|
!IsNecroEntity(_e) &&
|
||||||
!GhostTraderPatch.ShouldBeGhost(_e) && !NecroCharmSide.IsCharmed(_e);
|
!GhostTraderPatch.ShouldBeGhost(_e) && !NecroCharmSide.IsCharmed(_e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Питомцы и змея мода - тоже EntityEnemy (зомби-основа), и без этой проверки
|
||||||
|
/// охрана выбирала целью самого питомца: он ближе всех к хозяину (тест 30.09, лог
|
||||||
|
/// «pet 3838 идёт на 3838»). Все наши классы сущностей начинаются с necro.</summary>
|
||||||
|
public static bool IsNecroEntity(EntityAlive _e)
|
||||||
|
{
|
||||||
|
EntityClass cls;
|
||||||
|
return EntityClass.list.TryGetValue(_e.entityClass, out cls) && cls != null &&
|
||||||
|
cls.entityClassName != null && cls.entityClassName.StartsWith("necro");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Раз в полсекунды: есть ли живой враг ближе BodyDangerRange к телу игрока.</summary>
|
/// <summary>Раз в полсекунды: есть ли живой враг ближе BodyDangerRange к телу игрока.</summary>
|
||||||
static void CheckBody(World _world, EntityPlayerLocal _player, EntityAlive _snake, float _dt)
|
static void CheckBody(World _world, EntityPlayerLocal _player, EntityAlive _snake, float _dt)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -133,6 +133,11 @@ namespace NecromancerTome
|
|||||||
/// таких питомцев выключается - две системы на одно и то же дёргали бы питомца в
|
/// таких питомцев выключается - две системы на одно и то же дёргали бы питомца в
|
||||||
/// разные стороны.</summary>
|
/// разные стороны.</summary>
|
||||||
public bool UsesFollowTask;
|
public bool UsesFollowTask;
|
||||||
|
|
||||||
|
/// <summary>Охраняет ли питомец хозяина сам (PetFollowPatch.Guard) - указание 30.09.2026:
|
||||||
|
/// «охраняют все, кроме крысы». Рой сюда не входит: он не спутник, а разовое заклинание
|
||||||
|
/// со своим поиском целей.</summary>
|
||||||
|
public bool Guards;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly Dictionary<string, PetInfo> LimitedPets = new Dictionary<string, PetInfo>
|
public static readonly Dictionary<string, PetInfo> LimitedPets = new Dictionary<string, PetInfo>
|
||||||
@@ -153,6 +158,7 @@ namespace NecromancerTome
|
|||||||
// AlreadyActiveKey у всех четверых теперь не читается никогда.
|
// AlreadyActiveKey у всех четверых теперь не читается никогда.
|
||||||
CommandsAttack = true,
|
CommandsAttack = true,
|
||||||
NoTargetKey = "necroPetNoTarget",
|
NoTargetKey = "necroPetNoTarget",
|
||||||
|
Guards = true,
|
||||||
UsesFollowTask = true,
|
UsesFollowTask = true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -199,6 +205,7 @@ namespace NecromancerTome
|
|||||||
// AlreadyActiveKey у всех четверых теперь не читается никогда.
|
// AlreadyActiveKey у всех четверых теперь не читается никогда.
|
||||||
CommandsAttack = true,
|
CommandsAttack = true,
|
||||||
NoTargetKey = "necroPetNoTarget",
|
NoTargetKey = "necroPetNoTarget",
|
||||||
|
Guards = true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -217,6 +224,7 @@ namespace NecromancerTome
|
|||||||
// AlreadyActiveKey у всех четверых теперь не читается никогда.
|
// AlreadyActiveKey у всех четверых теперь не читается никогда.
|
||||||
CommandsAttack = true,
|
CommandsAttack = true,
|
||||||
NoTargetKey = "necroPetNoTarget",
|
NoTargetKey = "necroPetNoTarget",
|
||||||
|
Guards = true,
|
||||||
UsesFollowTask = true,
|
UsesFollowTask = true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -236,6 +244,7 @@ namespace NecromancerTome
|
|||||||
// AlreadyActiveKey у всех четверых теперь не читается никогда.
|
// AlreadyActiveKey у всех четверых теперь не читается никогда.
|
||||||
CommandsAttack = true,
|
CommandsAttack = true,
|
||||||
NoTargetKey = "necroPetNoTarget",
|
NoTargetKey = "necroPetNoTarget",
|
||||||
|
Guards = true,
|
||||||
UsesFollowTask = true,
|
UsesFollowTask = true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -340,10 +349,48 @@ namespace NecromancerTome
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
RecallOtherCompanions(holdingEntity, __instance.entityToSpawn, tooltips);
|
||||||
summonAllowedThisCall = true;
|
summonAllowedThisCall = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>ОДИН ПИТОМЕЦ ЗА РАЗ - указание 30.09.2026: «за один раз можно было призвать только
|
||||||
|
/// одного питомца. А то слишком жирно сразу и собаку, и крысу, и стервятника, и медведя».
|
||||||
|
/// Раньше предел был по одному НА ВИД, и все пятеро могли ходить вместе.
|
||||||
|
///
|
||||||
|
/// Сделано заменой, а не отказом: призыв нового сам отзывает прежнего. Игроку не надо
|
||||||
|
/// искать в поясе другую книгу, чтобы отозвать, и новых строк перевода не нужно - о прежнем
|
||||||
|
/// сообщает его же готовая надпись «… отозван».
|
||||||
|
///
|
||||||
|
/// Спутник - это вид с отзывом (RecalledKey). Рой Жуков не спутник: он разовое заклинание
|
||||||
|
/// без отзыва, сам никого не вытесняет, и его не вытесняют.</summary>
|
||||||
|
public static void RecallOtherCompanions(EntityAlive _owner, string _summoned, PetInfo _summonedInfo)
|
||||||
|
{
|
||||||
|
if (_summonedInfo.RecalledKey == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach (KeyValuePair<string, PetInfo> entry in LimitedPets)
|
||||||
|
{
|
||||||
|
if (entry.Key == _summoned || entry.Value.RecalledKey == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int classId = EntityClass.GetId(entry.Key);
|
||||||
|
if (classId == -1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<OwnedEntityData> others = _owner.GetOwnedEntities(classId);
|
||||||
|
for (int i = others.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
Debug.Log("[NecromancerTome] SummonPatch: один питомец за раз - " + entry.Key + " " + others[i].Id +
|
||||||
|
" отозван ради " + _summoned);
|
||||||
|
RecallPet(_owner, others[i].Id, entry.Value.RecalledKey, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>ДОБАВЛЕНО 2026-09-18, после чтения ItemActionSpawnEntity.Spawn целиком.
|
/// <summary>ДОБАВЛЕНО 2026-09-18, после чтения ItemActionSpawnEntity.Spawn целиком.
|
||||||
/// Последней строкой ваниль делает вот что:
|
/// Последней строкой ваниль делает вот что:
|
||||||
///
|
///
|
||||||
@@ -634,7 +681,7 @@ namespace NecromancerTome
|
|||||||
// Поэтому результат передаётся в Register, и тик повторяет попытку до успеха.
|
// Поэтому результат передаётся в Register, и тик повторяет попытку до успеха.
|
||||||
bool collisionIgnored = PetFollowPatch.TryIgnoreCollisionWithOwner(owner, __result);
|
bool collisionIgnored = PetFollowPatch.TryIgnoreCollisionWithOwner(owner, __result);
|
||||||
PetFollowPatch.Register(owner, __result, petInfo.UsesFollowTask, collisionIgnored, petInfo.LitEyes,
|
PetFollowPatch.Register(owner, __result, petInfo.UsesFollowTask, collisionIgnored, petInfo.LitEyes,
|
||||||
petInfo.FlyingHomeRadius);
|
petInfo.FlyingHomeRadius, petInfo.Guards);
|
||||||
AttachFollowTask(owner, __result, petInfo);
|
AttachFollowTask(owner, __result, petInfo);
|
||||||
// Полупрозрачность больше НЕ делается здесь. Бывший ApplyGhostlyTransparency ставил
|
// Полупрозрачность больше НЕ делается здесь. Бывший ApplyGhostlyTransparency ставил
|
||||||
// альфу в material.color и не работал ни разу: шейдеры моделей мобов альфу не
|
// альфу в material.color и не работал ни разу: шейдеры моделей мобов альфу не
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Reference in New Issue
Block a user