using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
///
/// Топливо костяного скакуна - кровавые сферы в ячейках модификаций (указание 01.10.2026:
/// «4 ячейки под кровавые сферы, которые будут постепенно по очереди тратиться; заправку
/// через канистру убрать»). Ванильный бак (VPFuelTank) остаётся частью машины - двигатель,
/// HUD и глохнутие читают его, - но для скакуна его уровень не хранится, а СЧИТАЕТСЯ по
/// сферам: каждая сфера - FuelPerSphere единиц, пропорционально остатку прочности.
///
/// Расход: двигатель снимает топливо событием FuelRemove -> VPFuelTank.AddFuel ->
/// SetFuelLevel(меньше). Префикс SetFuelLevel переводит разницу в прочность первой сферы
/// с остатком (ячейки по порядку); выработанная сфера исчезает из ячейки, как в браслете и
/// оссуарии. Рост уровня (ванильная заправка, загрузка из Meta) игнорируется - кровь
/// приходит только сферами.
///
/// Ячейки: у ванили в слот модификации идёт только модификация с подходящими тегами и не
/// больше MaxModsAllowed одного вида. Сфера - модификация браслета, поэтому для скакуна
/// CanSwap решает сам: только сфера с остатком прочности, прочие моды не ставятся.
///
/// Здоровье: Extends не наследует effect_group (ItemClassesFromXml парсит эффекты только
/// со своего узла), поэтому у necroBoneSteedPlaceable свой DegradationMax в items.xml. Но
/// эффекты установленных модификаций применяются к хозяину, а у сферы DegradationMax
/// base_set 500 - со сферами здоровье скакуна упало бы до 500 и прыгало бы при их смене.
/// Поэтому MaxUseTimesBase скакуна закреплён кодом.
///
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;
/// Остаток сферы 0..1.
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;
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;
}
/// Снимает топливо со сфер по очереди. Возвращает остаток топлива.
public static float Burn(Vehicle v, float amount)
{
ItemValue item = v.itemValue;
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;
}
}
[HarmonyPatch(typeof(VPFuelTank), nameof(VPFuelTank.SetFuelLevel))]
public static class Patch_VPFuelTank_SetFuelLevel_BoneSteed
{
public static bool Prefix(VPFuelTank __instance, float _fuelLevel)
{
if (!BoneSteedFuel.IsSteedTank(__instance))
return true;
Vehicle v = __instance.vehicle;
float have = BoneSteedFuel.FuelOf(v);
// Загрузка (SetItemValue -> Meta/50) приходит, пока LoadingItem; её не трогаем.
if (Patch_Vehicle_SetItemValue_BoneSteed.Loading || _fuelLevel >= have)
{
__instance.fuelLevel = have;
return false;
}
float left = BoneSteedFuel.Burn(v, have - Mathf.Max(0f, _fuelLevel));
__instance.fuelLevel = left;
if (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;
/// Скакун, сделанный до появления ячеек (или выданный без них), получает свои
/// 4 ячейки здесь: ItemValue размечает ячейки один раз при создании и сам не дорастает.
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))
Debug.Log("[NecromancerTome] BoneSteed: ячеек " + __instance.itemValue.ModificationCount +
", топливо " + BoneSteedFuel.FuelOf(__instance).ToString("0.#") + "/" + BoneSteedFuel.MaxFuel +
", здоровье " + __instance.entity.Health + "/" + __instance.GetMaxHealth() +
", расход x" + __instance.EffectFuelUsePer.ToString("0.##"));
}
}
/// В ячейки скакуна - только кровавые сферы с остатком прочности.
[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;
__result = BoneSteedFuel.IsSphere(stack.itemValue) &&
stack.itemValue.UseTimes < stack.itemValue.MaxUseTimes &&
(here == null || here.IsEmpty() || BoneSteedFuel.IsSphere(here));
return false;
}
}
/// Здоровье скакуна не зависит от сфер в ячейках (у сферы DegradationMax 500).
[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;
}
}
/// Ремонт скакуна - костями вместо ремкомплекта (указание 01.10): BonesPerRepair
/// костей за ход, как ванильный ремкомплект (1000 здоровья + 10% за уровень «Механика»).
[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;
}
}
/// Кнопка «Заправить» в окне обслуживания: у скакуна выключена, текст - «нельзя».
[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");
}
}
/// Страховка: любая ванильная заправка скакуна (кнопка, меню) ничего не делает.
[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;
}
}
}