- Одержимость питомца: ПКМ с книгой - вселиться, E - отозвать (и изнутри питомца), пояс скрыт, надпись о враге у тела. - Одержимый стервятник: полёт, рывок на ЛКМ с 10 м с прямым уроном, звуком и кровотечением, обычный прицел краснеет на цели; посадка в позу 0 и взлёт на пробел; ПКМ - клич-приманка (паникующая курица, откат 0.5 с); F - питаться трупами и блоками-падалью, доеденное исчезает. - Дух змеи атакует только нежить; фиолетовая метка на карте у змеи и одержимого. - Череп из красного трупа (этап 1 цинерария); глава книги «Письмо архимага» с видео. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
159 lines
6.7 KiB
C#
159 lines
6.7 KiB
C#
using UnityEngine;
|
|
|
|
namespace NecromancerTome
|
|
{
|
|
/// <summary>
|
|
/// Череп из красного трупа (BACKLOG «Некротехнолог v2», указание 06.10): тот же жест, что снятие
|
|
/// блока браслетом, - зажатая обычная атака с браслетом в руке, круговой таймер. Только целью
|
|
/// здесь не блок, а труп зомби, погибшего под дебаффом жертвы (CorpseLoot.IsSearchable). Труп -
|
|
/// сущность, а не блок, поэтому SpatialVaultPickup.Begin сперва спрашивает этот класс.
|
|
///
|
|
/// Шанс 2% («так ресурс станет ценнее»): удача - череп в пространственное хранилище, труп остаётся
|
|
/// (его можно обыскать), второй попытки на нём нет (cvar без точки - пишется в сейв вместе с трупом);
|
|
/// неудача - труп уходит в чёрный разрыв (NecroBurst), вместе с недобранной добычей.
|
|
///
|
|
/// Цена та же, что у блоков: без сферы или камня в слоте браслет не работает, сфера платит
|
|
/// прочностью за секунды канала. Разрыв делает сервер - в мультиплеере клиенту пока только удача
|
|
/// (труп не исчезает); решать вместе с прочим мультиплеером урн.
|
|
/// </summary>
|
|
public static class NecroSkull
|
|
{
|
|
public const string ItemName = "necroSkull";
|
|
/// <summary>На трупе: попытка уже была удачной.</summary>
|
|
public const string TakenCVar = "necroSkullTaken";
|
|
public const float Chance = 0.02f;
|
|
/// <summary>Короче десяти секунд блока: шанс мал, попытка должна быть дешёвой.</summary>
|
|
public const float ChannelSeconds = 3f;
|
|
/// <summary>Дальше этого тянуть череп нельзя - как дистанция обыска трупа у игры.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Красный труп под прицелом (так же, как игра ищет сущность для E -
|
|
/// PlayerMoveController.TryHandleEntity), иначе null.</summary>
|
|
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<Entity>() as EntityAlive : null;
|
|
return corpse != null && corpse.IsDead() && CorpseLoot.IsSearchable(corpse) ? corpse : null;
|
|
}
|
|
|
|
/// <summary>Обычная атака браслетом: true - под прицелом красный труп, нажатие наше.</summary>
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|