1.7.0: костяной скакун, книга Духа крысы, турели, расход сфер в оссуарии

- Костяной скакун (necroBoneSteed): мотоцикл из костей, сухожилий и сосудов
  с черепом-циклопом вместо фары; своя модель (бандл necrobonesteed),
  ванильная система предмета-транспорта (necroBoneSteedPlaceable, предпросмотр
  при установке); топливо - кровавая сфера, одна сфера = полный бак; теги
  коллайдеров и шейдер копируются с ванильного мотоцикла; рецепт на верстаке,
  Подмастерье
- Книга Духа крысы: своя модель (бандл necrobooks) - чёрная кожа, светящийся
  узор со скелетом крысы и семиконечной звездой, ржавые уголки, ремешок
- Турели не стреляют по существам мода (питомцы, рой, змея)
- Гео-оссуарий тратит 1/10 прочности сферы на стак вместо целой сферы

В игре не проверено.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-01 01:58:18 +03:00
co-authored by Claude Opus 5.5
parent f2dfeed126
commit 11db1116b0
15 changed files with 448 additions and 16 deletions
+148
View File
@@ -0,0 +1,148 @@
using System.Collections.Generic;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Костяной скакун (1.7.0, 01.10.2026): мотоцикл из костей, призывается книгой, ездит на
/// кровавых сферах. Сущность necroBoneSteed в entityclasses.xml - наследник vehicleMotorcycle
/// со своей моделью (бандл necrobonesteed, собран из копии ванильного VMotorcycleP.prefab).
///
/// Топливо у транспорта игры в XML не задаётся: Vehicle.GetFuelItem() возвращает "ammoGasCan"
/// строкой в коде. Отсюда два патча: предмет топлива - сфера, и заправка - одна сфера на полный
/// бак (ваниль берёт по канистре на 1/25 единицы бака - 120 сфер на бак было бы абсурдом).
/// Без сфер скакун глохнет как обычный транспорт (решение 01.10) - это ваниль, кода не нужно.
///
/// Модель выросла из рипнутого префаба, и у неё те же две беды, что у оссуария: игровые теги
/// коллайдеров потеряны (без них на скакуна не сесть и по нему не попасть) и шейдер-заглушка.
/// Оба лечатся с ванильного мотоцикла - теги по путям объектов, шейдер у его материала.
/// </summary>
public static class BoneSteed
{
public const string EntityClassName = "necroBoneSteed";
public const string FuelItem = "resourceBloodSphere";
const string VanillaPrefab = "@:Entities/Vehicles/VMotorcycle/VMotorcycleP.prefab";
public static bool Is(Entity e)
{
return e != null && EntityClass.list.TryGetValue(e.entityClass, out EntityClass cls) &&
cls != null && cls.entityClassName == EntityClassName;
}
static Transform vanilla;
static Shader vanillaShader;
public static void FixModel(EntityVehicle ev)
{
if (vanilla == null)
{
vanilla = DataLoader.LoadAsset<Transform>(VanillaPrefab);
if (vanilla == null)
{
Debug.LogWarning("[NecromancerTome] BoneSteed: ванильный мотоцикл не найден - теги и шейдер не поправлены");
return;
}
foreach (Renderer r in vanilla.GetComponentsInChildren<Renderer>(true))
{
if (r.sharedMaterial != null && r.sharedMaterial.HasProperty("_RMOM"))
{
vanillaShader = r.sharedMaterial.shader;
break;
}
}
}
var byPath = new Dictionary<string, Transform>();
foreach (Transform t in vanilla.GetComponentsInChildren<Transform>(true))
byPath[PathOf(t, vanilla)] = t;
int tags = 0, mats = 0;
foreach (Transform t in ev.transform.GetComponentsInChildren<Transform>(true))
{
if (!byPath.TryGetValue(PathOf(t, ev.transform), out Transform src))
continue;
if (t.tag != src.tag || t.gameObject.layer != src.gameObject.layer)
{
t.tag = src.tag;
t.gameObject.layer = src.gameObject.layer;
tags++;
}
}
if (vanillaShader != null)
{
foreach (Renderer r in ev.transform.GetComponentsInChildren<Renderer>(true))
{
foreach (Material m in r.sharedMaterials)
{
if (m != null && m.HasProperty("_RMOM") && m.shader != vanillaShader)
{
m.shader = vanillaShader;
m.enableInstancing = true;
mats++;
}
}
}
}
Debug.Log("[NecromancerTome] BoneSteed: теги поправлены у " + tags + " объектов, шейдер '" +
(vanillaShader != null ? vanillaShader.name : "-") + "' поставлен " + mats + " материалам");
}
/// <summary>Путь объекта от корня префаба. Корень - пустая строка: у экземпляра корень
/// называется иначе (necroBoneSteedP(Clone)), чем у ванильного префаба.</summary>
static string PathOf(Transform t, Transform root)
{
if (t == root)
return "";
string p = t.name;
for (Transform x = t.parent; x != null && x != root; x = x.parent)
p = x.name + "/" + p;
return p;
}
}
[HarmonyPatch(typeof(EntityVehicle), nameof(EntityVehicle.PostInit))]
public static class Patch_EntityVehicle_PostInit_BoneSteed
{
public static void Postfix(EntityVehicle __instance)
{
if (BoneSteed.Is(__instance))
BoneSteed.FixModel(__instance);
}
}
[HarmonyPatch(typeof(Vehicle), nameof(Vehicle.GetFuelItem))]
public static class Patch_Vehicle_GetFuelItem_BoneSteed
{
public static void Postfix(Vehicle __instance, ref string __result)
{
if (__result != "" && BoneSteed.Is(__instance.entity))
__result = BoneSteed.FuelItem;
}
}
/// <summary>Одна сфера - полный бак.</summary>
[HarmonyPatch(typeof(EntityVehicle), nameof(EntityVehicle.AddFuelFromInventory))]
public static class Patch_EntityVehicle_AddFuelFromInventory_BoneSteed
{
public static bool Prefix(EntityVehicle __instance, EntityAlive entity, ref bool __result)
{
if (!BoneSteed.Is(__instance))
return true;
__result = false;
Vehicle v = __instance.GetVehicle();
if (v.GetFuelPercent() >= 1f || !(entity is EntityPlayer player))
return false;
ItemValue sphere = ItemClass.GetItem(BoneSteed.FuelItem);
int taken = player.inventory.DecItem(sphere, 1);
if (taken == 0)
taken = player.bag.DecItem(sphere, 1);
if (taken == 0)
return false;
v.AddFuel(v.GetMaxFuelLevel() - v.GetFuelLevel());
__instance.PlayOneShot("useactions/gas_refill");
Debug.Log("[NecromancerTome] BoneSteed: сфера залита - полный бак");
__result = true;
return false;
}
}
}
+44
View File
@@ -190,6 +190,50 @@ namespace NecromancerTome
}
}
/// <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;
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
{
+32
View File
@@ -0,0 +1,32 @@
using HarmonyLib;
namespace NecromancerTome
{
/// <summary>
/// Турели не стреляют по существам мода. Жалоба 01.10: «мои питомцы враги, их атакуют
/// турели». Питомцы, рой и змея сделаны на зомби-основе и для игры остаются EntityEnemy, а
/// обе турели (стационарная AutoTurretFireController и переносная MiniTurretFireController)
/// решают в shouldIgnoreTarget: EntityEnemy - стрелять, если включено «по зомби». Здесь
/// ответ «пропустить» для всех necro*-сущностей (SnakeControl.IsNecroEntity) - тот же признак,
/// по которому их не выбирает целью охрана.
/// </summary>
[HarmonyPatch(typeof(AutoTurretFireController), nameof(AutoTurretFireController.shouldIgnoreTarget))]
public static class Patch_AutoTurretFireController_shouldIgnoreTarget_Pets
{
public static void Postfix(Entity _target, ref bool __result)
{
if (!__result && _target is EntityAlive alive && SnakeControl.IsNecroEntity(alive))
__result = true;
}
}
[HarmonyPatch(typeof(MiniTurretFireController), nameof(MiniTurretFireController.shouldIgnoreTarget))]
public static class Patch_MiniTurretFireController_shouldIgnoreTarget_Pets
{
public static void Postfix(Entity _target, ref bool __result)
{
if (!__result && _target is EntityAlive alive && SnakeControl.IsNecroEntity(alive))
__result = true;
}
}
}