Files
necromants-tome-7d2d-3-2/HarmonySrc/BoneSteedFuelPatch.cs
T
AlexCubeandClaude Opus 5.5 24d6e9f675 1.7.0: скакун на сферах в ячейках, ремонт костями; правки моделей скакуна и книги крысы
- Топливо скакуна - кровавые сферы в 4 ячейках модификаций, тратятся по очереди
  (одна сфера = 120 ед., бак 480); уровень бака считается по прочности сфер,
  выработанная сфера исчезает. Заправка канистрой убрана. В ячейки - только сферы.
- Причина «не едет» и «-2147483648%»: Extends не наследует effect_group, у предмета
  скакуна не было ни ячеек, ни прочности. Своя группа: ModSlots 4, DegradationMax 4000;
  здоровье закреплено кодом (DegradationMax сфер в ячейках его перебивал бы).
- Ремонт - 50 костей за ход вместо ремкомплекта; подсказка при нехватке, 13 языков.
- Модель скакуна: сиденье и рама прогнуты по ванили, рога по оси руля,
  череп меньше и короче, свет фары в глазнице, крыло над передним колесом,
  зубы под черепом убраны.
- Книга крысы: повёрнута как ванильная (палец на корешке), без металлических уголков.

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

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

261 lines
10 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 -> VPFuelTank.AddFuel ->
/// SetFuelLevel(меньше). Префикс SetFuelLevel переводит разницу в прочность первой сферы
/// с остатком (ячейки по порядку); выработанная сфера исчезает из ячейки, как в браслете и
/// оссуарии. Рост уровня (ванильная заправка, загрузка из Meta) игнорируется - кровь
/// приходит только сферами.
///
/// Ячейки: у ванили в слот модификации идёт только модификация с подходящими тегами и не
/// больше 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>Остаток сферы 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;
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;
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;
public static void Prefix() => Loading = true;
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.##"));
}
}
/// <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;
__result = BoneSteedFuel.IsSphere(stack.itemValue) &&
stack.itemValue.UseTimes < stack.itemValue.MaxUseTimes &&
(here == null || here.IsEmpty() || BoneSteedFuel.IsSphere(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;
}
}
}