- Камень в любой ячейке скакуна - вечно полный бак, сферы рядом не тратятся. - Камень в отсеке топлива гео-оссуария - работает вечно (FuelTypes + removeFuel). - Кровавая сфера чинится пакетом крови: 250 прочности за пакет (2 пакета - полная). - Описания сферы, камня, оссуария и скакуна дополнены (13 языков); в описании скакуна по-корейски и по-китайски название сферы приведено к принятому в моде. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
354 lines
15 KiB
C#
354 lines
15 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|