- Топливо скакуна - кровавые сферы в 4 ячейках модификаций, тратятся по очереди (одна сфера = 120 ед., бак 480); уровень бака считается по прочности сфер, выработанная сфера исчезает. Заправка канистрой убрана. В ячейки - только сферы. - Причина «не едет» и «-2147483648%»: Extends не наследует effect_group, у предмета скакуна не было ни ячеек, ни прочности. Своя группа: ModSlots 4, DegradationMax 4000; здоровье закреплено кодом (DegradationMax сфер в ячейках его перебивал бы). - Ремонт - 50 костей за ход вместо ремкомплекта; подсказка при нехватке, 13 языков. - Модель скакуна: сиденье и рама прогнуты по ванили, рога по оси руля, череп меньше и короче, свет фары в глазнице, крыло над передним колесом, зубы под черепом убраны. - Книга крысы: повёрнута как ванильная (палец на корешке), без металлических уголков. В игре не проверено. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
261 lines
10 KiB
C#
261 lines
10 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|