Files
necromants-tome-7d2d-3-2/HarmonySrc/BoneSteedFuelPatch.cs
T
AlexCubeandClaude Opus 5.5 d35b77be1b Кровавый камень - вечное топливо скакуна и оссуария; сферы чинятся пакетом крови
- Камень в любой ячейке скакуна - вечно полный бак, сферы рядом не тратятся.
- Камень в отсеке топлива гео-оссуария - работает вечно (FuelTypes + removeFuel).
- Кровавая сфера чинится пакетом крови: 250 прочности за пакет (2 пакета - полная).
- Описания сферы, камня, оссуария и скакуна дополнены (13 языков); в описании
  скакуна по-корейски и по-китайски название сферы приведено к принятому в моде.

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

354 lines
15 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>
/// Топливо костяного скакуна - кровавые сферы в ячейках модификаций (указание 01.10.2026:
/// «4 ячейки под кровавые сферы, которые будут постепенно по очереди тратиться; заправку
/// через канистру убрать»). Ванильный бак (VPFuelTank) остаётся частью машины - двигатель,
/// HUD и глохнутие читают его, - но для скакуна его уровень не хранится, а СЧИТАЕТСЯ по
/// сферам: каждая сфера - FuelPerSphere единиц, пропорционально остатку прочности.
///
/// Расход: двигатель шлёт событие FuelRemove; его префикс переводит количество в прочность
/// первой сферы с остатком (ячейки по порядку); выработанная сфера исчезает из ячейки, как в
/// браслете и оссуарии. Прямые записи уровня (SetFuelLevel: загрузка, синхронизация,
/// заправка) игнорируются - кровь приходит только сферами.
///
/// Ячейки: у ванили в слот модификации идёт только модификация с подходящими тегами и не
/// больше MaxModsAllowed одного вида. Сфера - модификация браслета, поэтому для скакуна
/// CanSwap решает сам: только сфера с остатком прочности, прочие моды не ставятся.
///
/// Здоровье: Extends не наследует effect_group (ItemClassesFromXml парсит эффекты только
/// со своего узла), поэтому у necroBoneSteedPlaceable свой DegradationMax в items.xml. Но
/// эффекты установленных модификаций применяются к хозяину, а у сферы DegradationMax
/// base_set 500 - со сферами здоровье скакуна упало бы до 500 и прыгало бы при их смене.
/// Поэтому MaxUseTimesBase скакуна закреплён кодом.
/// </summary>
public static class BoneSteedFuel
{
public const int Slots = 4;
public const float FuelPerSphere = 120f;
public const float MaxFuel = Slots * FuelPerSphere;
public const int MaxHealth = 4000;
static int steedItemType = -1;
public static int SteedItemType
{
get
{
if (steedItemType < 0)
{
ItemValue iv = ItemClass.GetItem(BoneSteed.ItemName);
steedItemType = iv != null ? iv.type : 0;
}
return steedItemType;
}
}
public static bool IsSteedItem(ItemValue iv) =>
iv != null && iv.type != 0 && iv.type == SteedItemType;
public static bool IsSphere(ItemValue iv) =>
iv != null && !iv.IsEmpty() && iv.ItemClass != null && iv.ItemClass.Name == BoneSteed.FuelItem;
/// <summary>Кровавый камень - неиссякаемая сфера (01.10: «полная замена сферы»): один камень
/// в любой ячейке - вечный полный бак, сферы рядом с ним не тратятся.</summary>
public const string StoneItem = "resourceBloodStone";
public static bool IsStone(ItemValue iv) =>
iv != null && !iv.IsEmpty() && iv.ItemClass != null && iv.ItemClass.Name == StoneItem;
public static bool HasStone(ItemValue item)
{
if (item == null)
return false;
for (int i = 0; i < item.ModificationCount; i++)
if (IsStone(item.GetModification(i)))
return true;
return false;
}
/// <summary>Остаток сферы 0..1.</summary>
static float Left(ItemValue sphere)
{
int max = sphere.MaxUseTimes;
if (max <= 0)
return 1f;
return Mathf.Clamp01(1f - sphere.UseTimes / max);
}
public static float FuelOf(Vehicle v)
{
ItemValue item = v?.itemValue;
if (item == null)
return 0f;
if (HasStone(item))
return MaxFuel;
float fuel = 0f;
for (int i = 0; i < item.ModificationCount; i++)
{
ItemValue m = item.GetModification(i);
if (IsSphere(m))
fuel += Left(m) * FuelPerSphere;
}
return fuel;
}
/// <summary>Снимает топливо со сфер по очереди. Возвращает остаток топлива.</summary>
public static float Burn(Vehicle v, float amount)
{
ItemValue item = v.itemValue;
if (HasStone(item))
return MaxFuel;
for (int i = 0; i < item.ModificationCount && amount > 0f; i++)
{
ItemValue m = item.GetModification(i);
if (!IsSphere(m))
continue;
int max = m.MaxUseTimes;
if (max <= 0)
{
item.SetModification(i, ItemValue.None.Clone());
amount -= FuelPerSphere;
continue;
}
float have = Left(m) * FuelPerSphere;
float take = Mathf.Min(have, amount);
m.UseTimes += take / FuelPerSphere * max;
amount -= take;
if (m.UseTimes >= max - 0.001f)
{
item.SetModification(i, ItemValue.None.Clone());
Debug.Log("[NecromancerTome] BoneSteed: сфера в ячейке " + i + " выработана и исчезла");
}
}
return FuelOf(v);
}
public static bool IsSteedTank(VPFuelTank tank) =>
tank?.vehicle != null && BoneSteed.Is(tank.vehicle.entity);
}
[HarmonyPatch(typeof(VPFuelTank), nameof(VPFuelTank.GetFuelLevel))]
public static class Patch_VPFuelTank_GetFuelLevel_BoneSteed
{
public static bool Prefix(VPFuelTank __instance, ref float __result)
{
if (!BoneSteedFuel.IsSteedTank(__instance))
return true;
__result = BoneSteedFuel.FuelOf(__instance.vehicle);
return false;
}
}
[HarmonyPatch(typeof(VPFuelTank), nameof(VPFuelTank.GetMaxFuelLevel))]
public static class Patch_VPFuelTank_GetMaxFuelLevel_BoneSteed
{
public static bool Prefix(VPFuelTank __instance, ref float __result)
{
if (!BoneSteedFuel.IsSteedTank(__instance))
return true;
__result = BoneSteedFuel.MaxFuel;
return false;
}
}
[HarmonyPatch(typeof(VPFuelTank), nameof(VPFuelTank.GetFuelLevelPercent))]
public static class Patch_VPFuelTank_GetFuelLevelPercent_BoneSteed
{
public static bool Prefix(VPFuelTank __instance, ref float __result)
{
if (!BoneSteedFuel.IsSteedTank(__instance))
return true;
__result = Mathf.Clamp01(BoneSteedFuel.FuelOf(__instance.vehicle) / BoneSteedFuel.MaxFuel);
return false;
}
}
/// <summary>Любая прямая запись уровня бака (загрузка из Meta, сетевая синхронизация,
/// ванильная заправка) для скакуна ничего не меняет: уровень - производная от сфер. Тест 01.10:
/// после установки сферы игра записала в бак прежний 0, и сфера сгорала целиком.</summary>
[HarmonyPatch(typeof(VPFuelTank), nameof(VPFuelTank.SetFuelLevel))]
public static class Patch_VPFuelTank_SetFuelLevel_BoneSteed
{
public static bool Prefix(VPFuelTank __instance)
{
if (!BoneSteedFuel.IsSteedTank(__instance))
return true;
__instance.fuelLevel = BoneSteedFuel.FuelOf(__instance.vehicle);
return false;
}
}
/// <summary>Расход - только событием двигателя FuelRemove: снимается прочность сфер.</summary>
[HarmonyPatch(typeof(VPFuelTank), nameof(VPFuelTank.HandleEvent))]
public static class Patch_VPFuelTank_HandleEvent_BoneSteed
{
public static bool Prefix(VPFuelTank __instance, VehiclePart.Event _event, float _amount)
{
if (_event != VehiclePart.Event.FuelRemove || !BoneSteedFuel.IsSteedTank(__instance))
return true;
Vehicle v = __instance.vehicle;
bool had = BoneSteedFuel.FuelOf(v) > 0f;
float left = _amount > 0f ? BoneSteedFuel.Burn(v, _amount) : BoneSteedFuel.FuelOf(v);
__instance.fuelLevel = left;
if (had && left <= 0f && EntityVehicle.VehicleFuelUsageModifier > 0f)
v.FireEvent(VehiclePart.Event.FuelEmpty, __instance, 0f);
return false;
}
}
[HarmonyPatch(typeof(Vehicle), nameof(Vehicle.SetItemValue))]
public static class Patch_Vehicle_SetItemValue_BoneSteed
{
public static bool Loading;
/// <summary>Скакун, сделанный до появления ячеек (или выданный без них), получает свои
/// 4 ячейки здесь: ItemValue размечает ячейки один раз при создании и сам не дорастает.</summary>
public static void Prefix(ItemValue _itemValue)
{
Loading = true;
if (!BoneSteedFuel.IsSteedItem(_itemValue) || _itemValue.ModificationCount >= BoneSteedFuel.Slots)
return;
var mods = new ItemValue[BoneSteedFuel.Slots];
for (int i = 0; i < mods.Length; i++)
mods[i] = i < _itemValue.ModificationCount && _itemValue.modifications[i] != null
? _itemValue.modifications[i]
: ItemValue.None.Clone();
_itemValue.modifications = mods;
}
public static void Finalizer(Vehicle __instance)
{
Loading = false;
if (!BoneSteed.Is(__instance.entity))
return;
// Слота покраски у скакуна нет (01.10: в нём сам собой появлялся коричневый краситель,
// вынимался и появлялся снова). Ваниль в SetItemValue заводит его заново, если пусто,
// поэтому снимаем каждый раз после неё. Что там лежало - в лог, для поиска источника.
ItemValue iv = __instance.itemValue;
if (iv.CosmeticModCount > 0)
{
for (int i = 0; i < iv.CosmeticModCount; i++)
{
ItemValue c = iv.GetCosmeticMod(i);
if (c != null && !c.IsEmpty())
Debug.Log("[NecromancerTome] BoneSteed: в слоте покраски было '" + c.ItemClass?.Name + "' - снято");
}
iv.ResizeCosmeticMods(0);
}
Debug.Log("[NecromancerTome] BoneSteed: ячеек " + __instance.itemValue.ModificationCount +
", топливо " + BoneSteedFuel.FuelOf(__instance).ToString("0.#") + "/" + BoneSteedFuel.MaxFuel +
", здоровье " + __instance.entity.Health + "/" + __instance.GetMaxHealth() +
", расход x" + __instance.EffectFuelUsePer.ToString("0.##"));
}
}
/// <summary>В ячейки скакуна - только кровавые сферы с остатком прочности и кровавые камни.</summary>
[HarmonyPatch(typeof(XUiC_ItemPartStack), "CanSwap")]
public static class Patch_XUiC_ItemPartStack_CanSwap_BoneSteed
{
public static bool Prefix(XUiC_ItemPartStack __instance, ItemStack stack, ref bool __result)
{
ItemStack current = __instance.xui?.AssembleItem?.CurrentItem;
if (current == null || !BoneSteedFuel.IsSteedItem(current.itemValue))
return true;
ItemValue here = __instance.ItemValue;
ItemValue add = stack.itemValue;
bool blood = BoneSteedFuel.IsStone(add) ||
BoneSteedFuel.IsSphere(add) && add.UseTimes < add.MaxUseTimes;
__result = blood && (here == null || here.IsEmpty() || BoneSteedFuel.IsSphere(here) || BoneSteedFuel.IsStone(here));
return false;
}
}
/// <summary>Здоровье скакуна не зависит от сфер в ячейках (у сферы DegradationMax 500).</summary>
[HarmonyPatch(typeof(ItemValue), nameof(ItemValue.MaxUseTimesBase), MethodType.Getter)]
public static class Patch_ItemValue_MaxUseTimesBase_BoneSteed
{
public static void Postfix(ItemValue __instance, ref int __result)
{
if (BoneSteedFuel.IsSteedItem(__instance))
__result = BoneSteedFuel.MaxHealth;
}
}
/// <summary>Ремонт скакуна - костями вместо ремкомплекта (указание 01.10): BonesPerRepair
/// костей за ход, как ванильный ремкомплект (1000 здоровья + 10% за уровень «Механика»).</summary>
[HarmonyPatch(typeof(XUiM_Vehicle), nameof(XUiM_Vehicle.RepairVehicle))]
public static class Patch_XUiM_Vehicle_RepairVehicle_BoneSteed
{
public const string BoneItem = "resourceBone";
public const int BonesPerRepair = 50;
public static bool Prefix(XUi _xui, Vehicle _vehicle, ref bool __result)
{
Vehicle v = _vehicle ?? _xui.Vehicle.CurrentVehicle?.GetVehicle();
if (v == null || !BoneSteed.Is(v.entity))
return true;
__result = false;
EntityPlayerLocal player = _xui.playerUI.entityPlayer;
ItemValue bone = ItemClass.GetItem(BoneItem);
if (bone == null || bone.IsEmpty() || v.GetRepairAmountNeeded() <= 0)
return false;
int have = player.bag.GetItemCount(bone) + player.inventory.GetItemCount(bone);
if (have < BonesPerRepair)
{
Audio.Manager.PlayInsidePlayerHead("misc/missingitemtorepair");
GameManager.ShowTooltip(player, string.Format(Localization.Get("necroBoneSteedNeedBones"), BonesPerRepair),
string.Empty, "ui_denied");
return false;
}
float perk = 0f;
ProgressionValue pv = player.Progression.GetProgressionValue("perkGreaseMonkey");
if (pv != null)
perk += pv.Level * 0.1f;
v.RepairParts(1000, perk);
int left = BonesPerRepair;
left -= player.inventory.DecItem(bone, left);
if (left > 0)
player.bag.DecItem(bone, left);
_xui.CollectedItemList.RemoveItemStack(new ItemStack(bone, BonesPerRepair));
Audio.Manager.PlayInsidePlayerHead("craft_complete_item");
__result = true;
return false;
}
}
/// <summary>Кнопка «Заправить» в окне обслуживания: у скакуна выключена, текст - «нельзя».</summary>
[HarmonyPatch(typeof(XUiC_VehicleFrameWindow), nameof(XUiC_VehicleFrameWindow.Update))]
public static class Patch_XUiC_VehicleFrameWindow_Update_BoneSteed
{
public static void Postfix(XUiC_VehicleFrameWindow __instance)
{
if (__instance.btnRefuel_Background != null && __instance.Vehicle != null && BoneSteed.Is(__instance.Vehicle))
__instance.btnRefuel_Background.Enabled = false;
}
}
[HarmonyPatch(typeof(XUiC_VehicleFrameWindow), "GetBindingValueInternal")]
public static class Patch_XUiC_VehicleFrameWindow_Binding_BoneSteed
{
public static void Postfix(XUiC_VehicleFrameWindow __instance, ref string value, string bindingName)
{
if (bindingName == "refueltext" && __instance.Vehicle != null && BoneSteed.Is(__instance.Vehicle))
value = Localization.Get("xuiRefuelNotAllowed");
}
}
/// <summary>Страховка: любая ванильная заправка скакуна (кнопка, меню) ничего не делает.</summary>
[HarmonyPatch(typeof(EntityVehicle), nameof(EntityVehicle.AddFuelFromInventory))]
public static class Patch_EntityVehicle_AddFuelFromInventory_BoneSteed
{
public static bool Prefix(EntityVehicle __instance, ref bool __result)
{
if (!BoneSteed.Is(__instance))
return true;
__result = false;
return false;
}
}
}