using UnityEngine; namespace NecromancerTome { /// /// Череп из красного трупа (BACKLOG «Некротехнолог v2», указание 06.10): тот же жест, что снятие /// блока браслетом, - зажатая обычная атака с браслетом в руке, круговой таймер. Только целью /// здесь не блок, а труп зомби, погибшего под дебаффом жертвы (CorpseLoot.IsSearchable). Труп - /// сущность, а не блок, поэтому SpatialVaultPickup.Begin сперва спрашивает этот класс. /// /// Шанс 2% («так ресурс станет ценнее»): удача - череп в пространственное хранилище, труп остаётся /// (его можно обыскать), второй попытки на нём нет (cvar без точки - пишется в сейв вместе с трупом); /// неудача - труп уходит в чёрный разрыв (NecroBurst), вместе с недобранной добычей. /// /// Цена та же, что у блоков: без сферы или камня в слоте браслет не работает, сфера платит /// прочностью за секунды канала. Разрыв делает сервер - в мультиплеере клиенту пока только удача /// (труп не исчезает); решать вместе с прочим мультиплеером урн. /// public static class NecroSkull { public const string ItemName = "necroSkull"; /// На трупе: попытка уже была удачной. public const string TakenCVar = "necroSkullTaken"; public const float Chance = 0.02f; /// Короче десяти секунд блока: шанс мал, попытка должна быть дешёвой. public const float ChannelSeconds = 3f; /// Дальше этого тянуть череп нельзя - как дистанция обыска трупа у игры. public static float MaxDistance => Constants.cCollectItemDistance; public const string MsgChanneling = "necroSkullChanneling"; public const string MsgTaken = "necroSkullAlreadyTaken"; public const string MsgExtracted = "necroSkullExtracted"; public const string MsgFailed = "necroSkullFailed"; public class SkullJob : SpatialVaultPickup.PickupJob { public int CorpseId; } /// Красный труп под прицелом (так же, как игра ищет сущность для E - /// PlayerMoveController.TryHandleEntity), иначе null. public static EntityAlive CorpseAtCrosshair(EntityPlayerLocal _player) { WorldRayHitInfo hit = _player.HitInfo; if (hit == null || !hit.bHitValid || hit.tag == null || !hit.tag.StartsWith("E_") || hit.hit.distanceSq > MaxDistance * MaxDistance) { return null; } Transform root = GameUtils.GetHitRootTransform(hit.tag, hit.transform); EntityAlive corpse = root != null ? root.GetComponent() as EntityAlive : null; return corpse != null && corpse.IsDead() && CorpseLoot.IsSearchable(corpse) ? corpse : null; } /// Обычная атака браслетом: true - под прицелом красный труп, нажатие наше. public static bool TryBegin(EntityPlayerLocal _player, ItemValue _bracelet) { EntityAlive corpse = CorpseAtCrosshair(_player); if (corpse == null) { return false; } if (corpse.Buffs.GetCustomVar(TakenCVar) > 0f) { SpatialVaultPickup.Deny(_player, MsgTaken); return true; } Bag bag = SpatialVaultPickup.GetVault(_player); ItemValue skull = ItemClass.GetItem(ItemName); if (bag == null || skull == null || skull.IsEmpty()) { return true; } if (!bag.CanTakeItem(new ItemStack(skull, 1))) { SpatialVaultPickup.Deny(_player, SpatialVaultPickup.MsgVaultFull); return true; } TimerEventData timerData = new TimerEventData { Data = new SkullJob { Player = _player, Bracelet = _bracelet, ChannelSeconds = ChannelSeconds, CorpseId = corpse.entityId }, CloseOnHit = true, CancelWithActivateButton = true }; timerData.FullTimeFinishEvent += OnChannelComplete; timerData.CloseEvent += delegate { ChannelVision.End(_player); }; LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPlayer(_player); XUiC_Timer.OpenTimer(playerUI.xui, ChannelSeconds, timerData, -1f, Localization.Get(MsgChanneling)); ChannelVision.Begin(_player); Debug.Log("[NecromancerTome] NecroSkull: " + _player.entityId + " тянет череп из трупа " + corpse.entityId); return true; } static void OnChannelComplete(TimerEventData _timerData) { if (!(_timerData.Data is SkullJob job) || job.Player == null) { return; } ChannelVision.End(job.Player); World world = GameManager.Instance != null ? GameManager.Instance.World : null; EntityAlive corpse = world?.GetEntity(job.CorpseId) as EntityAlive; // Труп обыскали до дна и он исчез, его разорвали или уже вытянули череп - пока шёл круг. if (corpse == null || !corpse.IsDead() || !CorpseLoot.IsSearchable(corpse)) { SpatialVaultPickup.Deny(job.Player, SpatialVaultPickup.MsgBlockMissing); return; } if (corpse.Buffs.GetCustomVar(TakenCVar) > 0f) { SpatialVaultPickup.Deny(job.Player, MsgTaken); return; } Bag bag = SpatialVaultPickup.GetVault(job.Player); ItemValue skull = ItemClass.GetItem(ItemName); if (bag == null || skull == null || skull.IsEmpty()) { return; } float roll = Random.value; bool success = roll < Chance; Debug.Log("[NecromancerTome] NecroSkull: труп " + corpse.entityId + " - бросок " + roll.ToString("0.000") + " против " + Chance.ToString("0.00") + " -> " + (success ? "ЧЕРЕП" : "разрыв")); if (success) { if (!bag.AddItem(new ItemStack(skull, 1))) { SpatialVaultPickup.Deny(job.Player, SpatialVaultPickup.MsgVaultFull); return; } corpse.Buffs.SetCustomVar(TakenCVar, 1f); GameManager.ShowTooltip(job.Player, MsgExtracted); } else { GameManager.ShowTooltip(job.Player, MsgFailed); NecroBurst.Burst(corpse, job.Player); } // Попытка оплачена в обоих исходах: канал отработан. SpatialVaultPickup.SpendCharge(job); if (success) { GameManager.Instance.SaveLocalPlayerData(); } } } }