Files
necromants-tome-7d2d-3-2/HarmonySrc/BloodTide.cs
T

315 lines
14 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 System.Collections.Generic;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Кровавый прилив и нож (указание 10.10): «раз некроэнергии становится много, она должна воздействовать не
/// только на зомби и некроманта». Пока идёт прилив (SnakeMode.IsBloodTide - ночь кровавой луны):
/// - на игроке бафф buffNecroBloodTide (buffs.xml): по нему нож (effect_group necroKnifeBloodTide, items.xml) бьёт
/// вдвое сильнее и вдвое чаще, лечит вдвое, плата за силовую вдвое меньше, выносливость на удар вчетверо
/// меньше, дальность - как у меча; с ножом в руке бег на 50% быстрее (сам бафф);
/// - хват ножа - как у мачете, модель - нож в фиолетовом мече (ItemClass ножа подменяется на всю ночь,
/// нож в руке пересоздаётся);
/// и, только если нож в руке:
/// - восстановление здоровья и выносливости вдвое (лишь положительное - кровотечение и прочий урон не трогаем);
/// - полоски здоровья и выносливости пульсируют фиолетовым в такт сердцу;
/// - фоном бьётся сердце, 120 ударов в минуту (синтез в коде: два низких удара синусом, «лаб» и «даб»,
/// с затуханием и спадом тона - по образцу, одобренному пользователем 10.10).
/// Всё - у локального игрока (одиночная игра и хост).
/// </summary>
public static class BloodTide
{
public const string BuffName = "buffNecroBloodTide";
/// <summary>Хват мачете (items.xml игры, meleeWpnBladeT3Machete HoldType=47); у ножа - 36.</summary>
public const int TideHoldType = 47;
/// <summary>Модель меча на прилив. Пока не собрана - null, остаётся модель ножа (меняется только хват).</summary>
public static string TideMeshFile = null;
public const float HeartBpm = 120f;
public const float HeartVolume = 0.55f;
/// <summary>Цвет пульса полосок - фиолетовый меча.</summary>
public static readonly Color PulseColor = new Color(0.67f, 0.24f, 1f);
/// <summary>Идёт прилив (кэш на кадр).</summary>
public static bool Active;
/// <summary>Прилив и нож в руке.</summary>
public static bool Armed;
static bool _swapped;
static int _origHoldType = -1;
static string _origMesh;
static float _nextBuffCheck;
static GameObject _heartGo;
static AudioSource _heart;
static AudioClip _heartClip;
public static void Init()
{
ModEvents.UnityUpdate.RegisterHandler(OnUnityUpdate);
}
static void OnUnityUpdate(ref ModEvents.SUnityUpdateData _data)
{
World world = GameManager.Instance?.World;
EntityPlayerLocal player = world?.GetPrimaryPlayer();
if (world == null || player == null)
{
Active = Armed = false;
_swapped = false; // классы предметов игра грузит заново с миром
_origHoldType = -1;
Heart(false);
Patch_XUiC_HUDStatBar_Update_BloodTide.Forget();
return;
}
bool tide = SnakeMode.IsBloodTide();
if (tide != Active)
{
Active = tide;
Debug.Log("[NecromancerTome] BloodTide: " + (tide ? "прилив начался" : "прилив схлынул"));
}
SwapKnife(player, Active);
if (Time.time >= _nextBuffCheck && !player.IsDead())
{
_nextBuffCheck = Time.time + 0.5f;
bool has = player.Buffs.HasBuff(BuffName);
if (Active && !has)
{
player.Buffs.AddBuff(BuffName);
}
else if (!Active && has)
{
player.Buffs.RemoveBuff(BuffName);
}
}
Armed = Active && !player.IsDead() && SnakeMode.IsKnife(player.inventory?.holdingItem);
Heart(Armed && !GameManager.Instance.IsPaused());
}
/// <summary>Хват и модель ножа на прилив и обратно. Подмена - в общем ItemClass, поэтому видна у любого
/// ножа; нож в руке пересоздаём (снять и достать тот же слот), иначе он до смены оружия остался бы прежним.</summary>
static void SwapKnife(EntityPlayerLocal _player, bool _tide)
{
if (_tide == _swapped)
{
return;
}
ItemClass knife = ItemClass.GetItemClass(SnakeMode.KnifeName);
if (knife == null)
{
return;
}
if (_origHoldType < 0)
{
_origHoldType = knife.HoldType.Value;
_origMesh = knife.MeshFile;
}
knife.HoldType.Value = _tide ? TideHoldType : _origHoldType;
knife.MeshFile = _tide && !string.IsNullOrEmpty(TideMeshFile) ? TideMeshFile : _origMesh;
_swapped = _tide;
Inventory inv = _player.inventory;
ItemInventoryData held = inv?.holdingItemData;
if (held != null && held.item == knife)
{
// NeedsRebuild - «собранный» предмет не совпадает с тем, что в слоте: сбрасываем собранный, и
// повторный выбор того же слота снимает нож и собирает его заново (Hand.SelectHoldingMode).
held.physicalItemValue = ItemValue.None.Clone();
inv.SetHoldingItemIdx(inv.holdingItemIdx);
}
Debug.Log("[NecromancerTome] BloodTide: нож - хват " + knife.HoldType.Value + (held != null && held.item == knife ? ", в руке пересоздан" : ""));
}
// ---------------------------------------------------------------- сердце
static void Heart(bool _on)
{
if (!_on)
{
if (_heart != null && _heart.isPlaying)
{
_heart.Stop();
}
return;
}
if (_heart == null)
{
_heartGo = new GameObject("NecroBloodTideHeart");
Object.DontDestroyOnLoad(_heartGo);
_heart = _heartGo.AddComponent<AudioSource>();
_heart.clip = _heartClip ?? (_heartClip = MakeHeartClip());
_heart.loop = true;
_heart.spatialBlend = 0f;
_heart.playOnAwake = false;
}
_heart.volume = HeartVolume * GamePrefs.GetFloat(EnumGamePrefs.OptionsOverallAudioVolumeLevel);
if (!_heart.isPlaying)
{
_heart.Play();
}
}
/// <summary>Доля удара сердца для пульса полосок: 1 в момент «лаба», дальше спад; без звука - 0.</summary>
public static float Beat()
{
if (_heart == null || !_heart.isPlaying)
{
return 0f;
}
float t = _heart.time;
float dub = 0.17f;
return Mathf.Max(Mathf.Exp(-9f * t), t >= dub ? 0.6f * Mathf.Exp(-12f * (t - dub)) : 0f);
}
/// <summary>Один цикл сердца (60/HeartBpm с): «лаб» 62 Гц и «даб» 52 Гц через 0.17 с, каждый - синус с
/// второй гармоникой, атака 4 мс, экспоненциальное затухание, тон к концу падает на треть.</summary>
static AudioClip MakeHeartClip()
{
const int sr = 44100;
int n = Mathf.RoundToInt(sr * 60f / HeartBpm);
float[] buf = new float[n];
Thump(buf, 0, 62f, 0.16f, 1f, 22f, sr);
Thump(buf, Mathf.RoundToInt(sr * 0.17f), 52f, 0.12f, 0.6f, 28f, sr);
float max = 0f;
foreach (float v in buf)
{
max = Mathf.Max(max, Mathf.Abs(v));
}
for (int i = 0; i < n; i++)
{
buf[i] = buf[i] / max * 0.9f;
}
AudioClip clip = AudioClip.Create("necroHeartbeat", n, 1, sr, false);
clip.SetData(buf, 0);
return clip;
}
static void Thump(float[] _buf, int _start, float _freq, float _dur, float _amp, float _decay, int _sr)
{
int n = Mathf.Min(Mathf.RoundToInt(_sr * _dur), _buf.Length - _start);
double ph = 0;
for (int i = 0; i < n; i++)
{
float t = (float)i / _sr;
float env = Mathf.Min(t / 0.004f, 1f) * Mathf.Exp(-_decay * t);
ph += 2 * Mathf.PI * _freq * (1f - 0.35f * t / _dur) / _sr;
_buf[_start + i] += _amp * env * (float)(System.Math.Sin(ph) + 0.25 * System.Math.Sin(2 * ph));
}
}
}
/// <summary>Восстановление вдвое (прилив и нож в руке). Игра сама задаёт RegenerationAmount и вызывает Stat.Tick в
/// PlayerEntityStats.UpdatePlayerHealthOT/StaminaOT; удваиваем только свежее положительное восстановление этого
/// вызова (у здоровья оно считается, лишь когда здоровье не полное и HealthChangeOT больше нуля - иначе в поле
/// остаётся прошлая разница, её трогать нельзя).</summary>
[HarmonyPatch(typeof(PlayerEntityStats), nameof(PlayerEntityStats.UpdatePlayerHealthOT))]
public static class Patch_PlayerEntityStats_UpdatePlayerHealthOT_BloodTide
{
public static void Prefix(PlayerEntityStats __instance)
{
Patch_Stat_Tick_BloodTide.Next = null;
if (BloodTide.Armed && __instance.m_entity is EntityPlayerLocal p && __instance.Health.ValuePercent < 1f &&
EffectManager.GetValue(PassiveEffects.HealthChangeOT, null, 0f, p) > 0f)
{
Patch_Stat_Tick_BloodTide.Next = __instance.Health;
}
}
}
[HarmonyPatch(typeof(PlayerEntityStats), nameof(PlayerEntityStats.UpdatePlayerStaminaOT))]
public static class Patch_PlayerEntityStats_UpdatePlayerStaminaOT_BloodTide
{
public static void Prefix(PlayerEntityStats __instance)
{
Patch_Stat_Tick_BloodTide.Next = BloodTide.Armed && __instance.m_entity is EntityPlayerLocal ? __instance.Stamina : null;
}
}
[HarmonyPatch(typeof(Stat), nameof(Stat.Tick))]
public static class Patch_Stat_Tick_BloodTide
{
/// <summary>Стат, чей следующий Tick удвоить (ставят префиксы выше).</summary>
public static Stat Next;
public static void Prefix(Stat __instance)
{
if (Next == null || Next != __instance)
{
return;
}
Next = null;
if (__instance.regenAmount > 0f)
{
__instance.regenAmount *= 2f;
}
}
}
/// <summary>
/// Крит меча (указание 10.10: «шанс на критический удар бы тоже повысить»). Случайного крита у ближнего боя в игре
/// нет: ItemActionDynamic помечает критическим каждый силовой удар (действие 1) и ни одного обычного, а
/// CriticalChance уходит в параметр _criticalHitChanceOLD функции Hit, которую та не читает. Сам крит урон не
/// поднимает - только чаще сбивает с ног и ставит на колено, вдвое быстрее копит урон по ногам, красит цифру урона.
/// Наш крит: в прилив обычный удар ножом по существу с шансом CritChance - критический, урон x CritMultiplier.
/// </summary>
[HarmonyPatch(typeof(ItemActionAttack), nameof(ItemActionAttack.Hit))]
public static class Patch_ItemActionAttack_Hit_BloodTideCrit
{
public const float CritChance = 0.25f;
public const float CritMultiplier = 2f;
public static void Prefix(WorldRayHitInfo hitInfo, int _attackerEntityId, ref float _entityDamage,
ItemActionAttack.AttackHitInfo _attackDetails, ItemValue damagingItemValue, bool _isGrazingHit)
{
if (!BloodTide.Armed || _isGrazingHit || _attackDetails == null || _attackDetails.isCriticalHit || _entityDamage <= 0f ||
hitInfo?.tag == null || !hitInfo.tag.StartsWith("E_") || damagingItemValue?.ItemClass?.Name != SnakeMode.KnifeName)
{
return;
}
World world = GameManager.Instance?.World;
if (world == null || world.GetPrimaryPlayer()?.entityId != _attackerEntityId || Random.value >= CritChance)
{
return;
}
_entityDamage *= CritMultiplier;
_attackDetails.isCriticalHit = true;
}
}
/// <summary>Пульс полосок здоровья и выносливости (прилив и нож в руке): цвет полосы (BarContent в HUDLeftStatBars
/// игры, у здоровья 255,0,0,160, у выносливости 0,54,255,160) уходит в фиолетовый на каждом ударе сердца.</summary>
[HarmonyPatch(typeof(XUiC_HUDStatBar), nameof(XUiC_HUDStatBar.Update))]
public static class Patch_XUiC_HUDStatBar_Update_BloodTide
{
static readonly Dictionary<XUiC_HUDStatBar, (XUiV_Sprite bar, Color orig)> _bars = new Dictionary<XUiC_HUDStatBar, (XUiV_Sprite, Color)>();
public static void Forget()
{
_bars.Clear();
}
public static void Postfix(XUiC_HUDStatBar __instance)
{
if (__instance.statType != HUDStatTypes.Health && __instance.statType != HUDStatTypes.Stamina)
{
return;
}
if (!_bars.TryGetValue(__instance, out var e))
{
if (!BloodTide.Armed || !(__instance.GetChildById("BarContent")?.ViewComponent is XUiV_Sprite s))
{
return;
}
e = (s, s.Color);
_bars[__instance] = e;
}
Color c = BloodTide.Armed
? Color.Lerp(e.orig, new Color(BloodTide.PulseColor.r, BloodTide.PulseColor.g, BloodTide.PulseColor.b, Mathf.Max(e.orig.a, 0.8f)), BloodTide.Beat())
: e.orig;
if (e.bar.Color != c)
{
e.bar.Color = c;
}
}
}
}