Files
necromants-tome-7d2d-3-2/HarmonySrc/GeoOssuaryPatch.cs
T
AlexCubeandClaude Opus 5.5 20ee89323d Пирамида отрицания: своя костяная модель, новое имя, иконка
- модель: ступени из костей, черепа с тлеющими глазницами, костяная ось насквозь,
  наверху цветок из рваной плоти на коротком шипастом стебле из скрученных жил,
  в нём фиолетовый кристалл; бандл necroward (копия префаба оссуария)
- блок на Shape="ModelEntity" как оссуарий; шейдер и теги коллайдера ставит
  GeoOssuaryPatch (теперь для обоих префабов); свой коллайдер-коробка
- «Пирамида духов» переименована в «Пирамиду отрицания» (13 языков, и в описаниях);
  внутреннее имя блока прежнее - уже поставленные пирамиды в сейвах сохранятся
- иконка - рендер модели

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 17:08:52 +03:00

255 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 VanillaModel = "@:Entities/Furniture/collectorDewPrefab.prefab";
// Пирамида отрицания (01.10) собрана из копии префаба оссуария - те же имена объектов,
// тот же запасной шейдер, поэтому лечится тем же постфиксом.
static readonly System.Collections.Generic.HashSet<string> models =
new System.Collections.Generic.HashSet<string> { "necroOssuaryPrefab", "necroWardPrefab" };
static readonly System.Collections.Generic.HashSet<string> done = new System.Collections.Generic.HashSet<string>();
public static void Postfix(Transform __result)
{
if (__result == null || !models.Contains(__result.name) || !done.Add(__result.name))
return;
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;
}
}
}
}
/// <summary>
/// Расход сферы - по прочности, как в пространственном хранилище (указание 01.10: «одна
/// сфера на стак - это жирно; 1/10 прочности на стак»). Ваниль в removeFuel вычитает ШТУКИ
/// топлива, то есть съедала бы сферу целиком за каждый стак. Здесь вместо этого у первой
/// сферы в отсеке списывается ChargePerStack прочности (DegradationMax сферы 500 -> 10 стаков
/// со сферы); выработанная до нуля сфера исчезает из ячейки, как из браслета. Наличие топлива
/// ваниль по-прежнему считает штуками (getFuelCount) - любая сфера с остатком годится.
/// </summary>
[HarmonyPatch(typeof(TileEntityCollector), nameof(TileEntityCollector.removeFuel))]
public static class Patch_TileEntityCollector_removeFuel_GeoOssuary
{
public const int ChargePerStack = 50;
public static bool Prefix(TileEntityCollector __instance)
{
if (!GeoOssuary.Is(__instance))
return true;
ItemStack[] fuel = __instance.FuelSlots;
// Кровавый камень в отсеке - вечное топливо (01.10: «полная замена сферы»): не тратится
// ни он, ни сферы рядом.
for (int i = 0; i < fuel.Length; i++)
if (fuel[i] != null && !fuel[i].IsEmpty() && fuel[i].itemValue.ItemClass != null &&
fuel[i].itemValue.ItemClass.Name == BoneSteedFuel.StoneItem)
return false;
for (int i = 0; i < fuel.Length; i++)
{
ItemStack stack = fuel[i];
if (stack == null || stack.IsEmpty() || stack.itemValue.ItemClass == null ||
stack.itemValue.ItemClass.Name != SpatialVaultPickup.ChargeItemName)
continue;
int max = stack.itemValue.MaxUseTimes;
if (max <= 0)
{
// Прочность не задалась (DegradationMax) - ведём себя как ваниль: сфера за стак.
fuel[i] = ItemStack.Empty;
Debug.LogWarning("[NecromancerTome] GeoOssuary: у сферы MaxUseTimes 0 - списана целиком");
return false;
}
stack.itemValue.UseTimes += ChargePerStack;
if (stack.itemValue.UseTimes >= max)
{
fuel[i] = ItemStack.Empty;
Debug.Log("[NecromancerTome] GeoOssuary: сфера в ячейке " + i + " выработана и исчезла");
}
return false;
}
return false;
}
}
[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;
}
}
}