Одержимость питомцев: роли медведя, волка, собаки и крысы; ноша стервятника; череп при обыске
- Могильная крыса: своя модель без волчьей шерсти; стоит на земле (центр капсулы); E - осмотр контейнеров без права взять (окно игры, рюкзак скрыт, отказ «инвентарь заполнен», контейнер не разрушается); ПКМ - проход сквозь постройки, полупрозрачная. - Стервятник: курица в когтях - слепок с обвисшими крыльями и шеей, без хитбокса. - Зомбособака: нюх - все существа рядом на компасе и карте. - Медведь: толстая шкура (вдвое меньше урона), рёв-провокация, удар лапой по блокам. - Лютоволк: бросок на цель с опрокидыванием, след крови раненых, сумка на 3 ячейки (лут, выгрузка у тела, выпадает мешком при гибели и отзыве; отдельный файл сейва). - Череп (останки): 2% при обычном обыске красного трупа вместо браслета. - Прицел книги не рисуется в одержимости; подсказки вселения на 13 языках. - Замер EntityLeakProbe: удалённые, но живые в памяти сущности - в лог. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
beab1640b0
commit
31aa99860c
+20
-131
@@ -3,155 +3,44 @@ using UnityEngine;
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Череп из красного трупа (BACKLOG «Некротехнолог v2», указание 06.10): тот же жест, что снятие
|
||||
/// блока браслетом, - зажатая обычная атака с браслетом в руке, круговой таймер. Только целью
|
||||
/// здесь не блок, а труп зомби, погибшего под дебаффом жертвы (CorpseLoot.IsSearchable). Труп -
|
||||
/// сущность, а не блок, поэтому SpatialVaultPickup.Begin сперва спрашивает этот класс.
|
||||
/// Череп (останки) из красного трупа - сырьё для цинерария (дух-работник).
|
||||
///
|
||||
/// Шанс 2% («так ресурс станет ценнее»): удача - череп в пространственное хранилище, труп остаётся
|
||||
/// (его можно обыскать), второй попытки на нём нет (cvar без точки - пишется в сейв вместе с трупом);
|
||||
/// неудача - труп уходит в чёрный разрыв (NecroBurst), вместе с недобранной добычей.
|
||||
/// С 09.10 - находка при обычном обыске (указание: «браслет больше не собирает красные трупы,
|
||||
/// выпиливаем; останки дропаются с шансом 2% при обычном лутании красного трупа»). Раньше (06.10) череп
|
||||
/// вырывали пространственным браслетом: зажатая атака по трупу, три секунды канала, при неудаче труп
|
||||
/// уходил в чёрный разрыв. Сняли из-за подозрения на утечку памяти на этом пути (тест 09.10: после
|
||||
/// разрывов в памяти копились экземпляры сущностей - 31 при 7 живых).
|
||||
///
|
||||
/// Цена та же, что у блоков: без сферы или камня в слоте браслет не работает, сфера платит
|
||||
/// прочностью за секунды канала. Разрыв делает сервер - в мультиплеере клиенту пока только удача
|
||||
/// (труп не исчезает); решать вместе с прочим мультиплеером урн.
|
||||
/// Бросок - один раз на труп, при первом открытии его сумки, на сервере, сразу после того, как игра
|
||||
/// наполнила её добычей (Patch_EntityAlive_OnLockRequestServer_Corpse). Шанс прежний - 2%
|
||||
/// («так ресурс станет ценнее»).
|
||||
/// </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
|
||||
/// <summary>Бросок на череп в только что наполненную сумку трупа. Места нет - сумка на ячейку больше.</summary>
|
||||
public static void Roll(EntityAlive _corpse, Bag _bag)
|
||||
{
|
||||
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)
|
||||
if (_bag == null || skull == null || skull.IsEmpty())
|
||||
{
|
||||
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)
|
||||
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);
|
||||
return;
|
||||
}
|
||||
else
|
||||
ItemStack stack = new ItemStack(skull, 1);
|
||||
if (!_bag.AddItem(stack.Clone()))
|
||||
{
|
||||
GameManager.ShowTooltip(job.Player, MsgFailed);
|
||||
NecroBurst.Burst(corpse, job.Player);
|
||||
}
|
||||
// Попытка оплачена в обоих исходах: канал отработан.
|
||||
SpatialVaultPickup.SpendCharge(job);
|
||||
if (success)
|
||||
{
|
||||
GameManager.Instance.SaveLocalPlayerData();
|
||||
_bag.Resize(new Vector2i(_bag.SlotCount + 1, 1));
|
||||
_bag.AddItem(stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user