Труп «Жертвы» открывается сам, как ящик: без мешка
Обыск больше не превращает труп в мешок. Труп получает сумку со списком добычи zPackVictim и открывается игровым путём (LockRequestLocal -> LootBagOpened -> окно добычи с игровым таймером обыска). Запрет открывать мёртвых у EntityAlive снят префиксами только для таких трупов. Открытый и не опустошённый труп лежит час, опустошённый убирается сразу. После загрузки труп узнаётся по сохранённой сумке (баффы зомби в сейв не пишутся). LockRequestLocal вызывается из TEPersistence (перегрузка с ReadOnlySpan не компилируется в основном проекте). Класс EntityLootContainerVictim оставлен ради старых сейвов. Описание ножа исправлено на всех языках, README - текущее описание. Проверено в игре на 3.3: открытие, таймер, уборка опустошённых, восстановление после загрузки. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1c2e3f2cdf
commit
e25544521d
File diff suppressed because one or more lines are too long
@@ -388,7 +388,10 @@
|
|||||||
uses this one directly or one of the three OTHER confirmed variants, blue/red/gold) per
|
uses this one directly or one of the three OTHER confirmed variants, blue/red/gold) per
|
||||||
the fallback plan agreed on before this was ever guessed at - not visually distinct from a
|
the fallback plan agreed on before this was ever guessed at - not visually distinct from a
|
||||||
normal zombie drop, but a real, working bag with the right guaranteed contents.
|
normal zombie drop, but a real, working bag with the right guaranteed contents.
|
||||||
Force-spawned by HarmonySrc/VictimPatch.cs when a zombie with buffNecroVictim dies -
|
С 1.5.0 (2026-09-26) НЕ СОЗДАЁТСЯ: труп «Жертвы» сам открывается как контейнер
|
||||||
|
(HarmonySrc/CorpseLootPatch.cs). Класс оставлен ради сейвов - в мирах игроков могут
|
||||||
|
лежать уже выпавшие мешки, и без класса они не загрузятся. Не удалять.
|
||||||
|
Был: force-spawned by HarmonySrc/VictimPatch.cs when a zombie with buffNecroVictim dies -
|
||||||
never rolled by the normal LootDropProb/LootDropEntityClass system (not referenced from
|
never rolled by the normal LootDropProb/LootDropEntityClass system (not referenced from
|
||||||
any zombie's own entity_class), so its own IsEnemyEntity/Faction/etc. just mirror the
|
any zombie's own entity_class), so its own IsEnemyEntity/Faction/etc. just mirror the
|
||||||
other loot containers for consistency, not because anything reads them via that path. -->
|
other loot containers for consistency, not because anything reads them via that path. -->
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
<config>
|
<config>
|
||||||
<!-- "Жертва" (Victim) green loot bag - BACKLOG.md item 5, Necromancer's Knife. Guaranteed
|
<!-- "Жертва" (Victim) loot - BACKLOG.md item 5, Necromancer's Knife. С 1.5.0 это список
|
||||||
drop off a zombie killed while carrying buffNecroVictim (see HarmonySrc/VictimPatch.cs,
|
добычи САМОГО ТРУПА зомби, умершего с buffNecroVictim: труп получает сумку с этим
|
||||||
which force-spawns EntityLootContainerVictim from entityclasses.xml on death, bypassing
|
списком и открывается как ящик (HarmonySrc/CorpseLootPatch.cs, CorpseLoot.VictimLootList).
|
||||||
the normal LootDropProb roll entirely).
|
До 1.5.0 список наполнял мешок EntityLootContainerVictim, который появлялся при обыске.
|
||||||
|
|
||||||
Contents per user: random hides (usually one or none), rotten flesh, bones, and a chance
|
Contents per user: random hides (usually one or none), rotten flesh, bones, and a chance
|
||||||
at "Кожа жертвы" (resourceVictimSkin, items.xml) - a crafting resource for summon books.
|
at "Кожа жертвы" (resourceVictimSkin, items.xml) - a crafting resource for summon books.
|
||||||
|
|||||||
+202
-54
@@ -33,15 +33,16 @@ namespace NecromancerTome
|
|||||||
/// подпись на экране, и значок берутся готовые, локализованные на все языки игры, и
|
/// подпись на экране, и значок берутся готовые, локализованные на все языки игры, и
|
||||||
/// игроку это выглядит ровно как обыск мешка, потому что это и есть он.
|
/// игроку это выглядит ровно как обыск мешка, потому что это и есть он.
|
||||||
///
|
///
|
||||||
/// 3. ВНУТРИ ЛУТ. А вот здесь сознательный обход. Само хранилище живёт внутри
|
/// 3. ВНУТРИ ЛУТ, И ОТКРЫВАЕТСЯ САМ ТРУП (1.5.0, 2026-09-26; пользователь: «труп сам
|
||||||
/// `EntityLootContainer`: его `bag`, его `Read`/`Write` для сейва, его сетевая
|
/// становится обыскиваемым объектом», как машина или ящик). До 1.5.0 обыск превращал
|
||||||
/// синхронизация. Приделать всё это трупу зомби — работа масштаба Пространственного
|
/// труп в мешок «Жертва» - оказалось, что это лишнее: сумка есть у ЛЮБОЙ сущности.
|
||||||
/// хранилища, с выходом на сейвы, то есть на риск номер один этого проекта.
|
/// `Entity.bag` + `Entity.lootList`, первое открытие наполняет сумку по списку
|
||||||
///
|
/// (LootManager.LootBagOpened -> GetLootList()), окно - XUiC_BagStorageWindowGroup, тот
|
||||||
/// Поэтому обыск НЕ открывает труп, а ПРЕВРАЩАЕТ его в мешок «Жертва»: мешок спавнится
|
/// же путь, что у мешков, машин и дронов. Сумка сущности пишется в сейв
|
||||||
/// на месте трупа, труп убирается. Снаружи читается как «обыскал и нашёл», внутри —
|
/// (EntityCreationData.bagData), а мёртвые зомби сохраняются (EntityEnemy.IsSavedToFile).
|
||||||
/// обычный ванильный контейнер, который сам себя сохраняет, синхронизирует и открывает.
|
/// Единственный запрет - EntityAlive.OnLockRequestServer/OnLockResponseLocal не пускают
|
||||||
/// Ни одной новой сущности с состоянием.
|
/// мёртвых (`if (!IsDead())`); для помеченных трупов его снимают два префикса ниже.
|
||||||
|
/// Таймер обыска - игровой, окна добычи (runOpenTimer при первом открытии).
|
||||||
///
|
///
|
||||||
/// ОБЫЧНЫЙ МЕШОК ЭТО НЕ ОТМЕНЯЕТ. Бросок вероятности в VictimPatch.cs остаётся как был: выпал
|
/// ОБЫЧНЫЙ МЕШОК ЭТО НЕ ОТМЕНЯЕТ. Бросок вероятности в VictimPatch.cs остаётся как был: выпал
|
||||||
/// — обычный мешок лежит рядом, не выпал — только труп. «Жертва» переехала из «падает сразу» в
|
/// — обычный мешок лежит рядом, не выпал — только труп. «Жертва» переехала из «падает сразу» в
|
||||||
@@ -71,12 +72,21 @@ namespace NecromancerTome
|
|||||||
public const string SearchCommand = "search";
|
public const string SearchCommand = "search";
|
||||||
public const string SearchIcon = "hand";
|
public const string SearchIcon = "hand";
|
||||||
|
|
||||||
/// <summary>Секунды удержания. Круговой индикатор рисует сама игра - XUiC_Timer, тот же,
|
/// <summary>Тег, по которому после загрузки отличаем наш труп (RestoreLoadedCorpses).</summary>
|
||||||
/// что крутится при снятии верстака.</summary>
|
public static readonly FastTags<TagGroup.Global> ZombieTag = FastTags<TagGroup.Global>.Parse("zombie");
|
||||||
public const float SearchSeconds = 3f;
|
|
||||||
|
|
||||||
/// <summary>Трупы, которые ещё не обысканы. Ключ — entityId. Чистится сама: запись
|
/// <summary>Список добычи трупа - тот же, что был у мешка «Жертва» (Config/loot.xml).</summary>
|
||||||
/// снимается при обыске, а остальные уходят вместе с миром.</summary>
|
public const string VictimLootList = "zPackVictim";
|
||||||
|
|
||||||
|
/// <summary>Сколько лежит труп, который уже открывали, пока в нём что-то осталось. Столько
|
||||||
|
/// же лежал мешок «Жертва» (TimeStayAfterDeath 3600): добыча не должна пропадать через
|
||||||
|
/// 5 минут только потому, что игрок отошёл. Таких трупов единицы, а опустошённый убирается
|
||||||
|
/// сразу (RemoveEmptiedCorpses), так что 5-минутное правило 1.4.1 про FPS не нарушается.</summary>
|
||||||
|
public const float OpenedCorpseSecondsAfterDeath = 3600f;
|
||||||
|
|
||||||
|
/// <summary>Трупы, которые можно открыть. Ключ — entityId. Запись снимается, когда труп
|
||||||
|
/// исчезает (ReleaseGoneCorpses, RemoveEmptiedCorpses); после загрузки сейва
|
||||||
|
/// восстанавливается по метке «Жертвы» (RestoreLoadedCorpses).</summary>
|
||||||
public static readonly HashSet<int> SearchableCorpses = new HashSet<int>();
|
public static readonly HashSet<int> SearchableCorpses = new HashSet<int>();
|
||||||
|
|
||||||
/// <summary>Трупы, которые ещё не удалось перекрасить: модель бывает не собрана на том
|
/// <summary>Трупы, которые ещё не удалось перекрасить: модель бывает не собрана на том
|
||||||
@@ -118,6 +128,11 @@ namespace NecromancerTome
|
|||||||
/// <summary>Повторная попытка перекраски. Зовётся из общего тика раз в секунду.</summary>
|
/// <summary>Повторная попытка перекраски. Зовётся из общего тика раз в секунду.</summary>
|
||||||
public static void RetryPendingTints(World _world)
|
public static void RetryPendingTints(World _world)
|
||||||
{
|
{
|
||||||
|
if (_world != null)
|
||||||
|
{
|
||||||
|
RestoreLoadedCorpses(_world);
|
||||||
|
RemoveEmptiedCorpses(_world);
|
||||||
|
}
|
||||||
ReleaseGoneCorpses(_world);
|
ReleaseGoneCorpses(_world);
|
||||||
if (PendingTint.Count == 0 || _world == null)
|
if (PendingTint.Count == 0 || _world == null)
|
||||||
{
|
{
|
||||||
@@ -237,6 +252,7 @@ namespace NecromancerTome
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SearchableCorpses.Add(_victim.entityId);
|
SearchableCorpses.Add(_victim.entityId);
|
||||||
|
GiveBag(_victim);
|
||||||
|
|
||||||
// timeStayAfterDeath хранится в ТИКАХ: игра заполняет его как TimeStayAfterDeath * 20
|
// timeStayAfterDeath хранится в ТИКАХ: игра заполняет его как TimeStayAfterDeath * 20
|
||||||
// (EntityAlive.CopyPropertiesFromEntityClass), то есть двадцать тиков на секунду.
|
// (EntityAlive.CopyPropertiesFromEntityClass), то есть двадцать тиков на секунду.
|
||||||
@@ -413,15 +429,101 @@ namespace NecromancerTome
|
|||||||
return painted;
|
return painted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Обыск: труп превращается в мешок «Жертва» на своём же месте.</summary>
|
/// <summary>Сумка и список добычи трупа. Сумка пустая и нетронутая: наполнит её сама игра
|
||||||
public static void SearchCorpse(EntityAlive _corpse)
|
/// при первом открытии. Уже существующую (труп из сейва) не трогаем - в ней то, что игрок
|
||||||
|
/// не забрал.</summary>
|
||||||
|
public static void GiveBag(EntityAlive _corpse)
|
||||||
{
|
{
|
||||||
SearchableCorpses.Remove(_corpse.entityId);
|
_corpse.lootList = VictimLootList;
|
||||||
Patch_EntityAlive_dropItemOnDeath_VictimBag.SpawnVictimBag(_corpse, false);
|
if (_corpse.bag != null && _corpse.bag.SlotCount > 0)
|
||||||
Debug.Log("[NecromancerTome] CorpseLoot: труп " + _corpse.entityId + " обыскан, заменён мешком");
|
|
||||||
if (_corpse.world != null)
|
|
||||||
{
|
{
|
||||||
_corpse.world.RemoveEntity(_corpse.entityId, EnumRemoveEntityReason.Despawned);
|
return;
|
||||||
|
}
|
||||||
|
LootContainer container = LootContainer.GetLootContainer(VictimLootList, false);
|
||||||
|
if (container == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[NecromancerTome] CorpseLoot: нет списка добычи '" + VictimLootList +
|
||||||
|
"' - труп " + _corpse.entityId + " откроется пустым");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_corpse.bag == null)
|
||||||
|
{
|
||||||
|
_corpse.bag = new Bag(container.size, XUiC_ItemStack.StackLocationTypes.LootContainer, _corpse);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_corpse.bag.Resize(container.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Трупы из сейва. Наша пометка живёт только в памяти, а срок трупа при загрузке
|
||||||
|
/// снова берётся из entity_class (30 с у зомби) - без этого труп с добычей исчез бы через
|
||||||
|
/// полминуты после загрузки. Тик раз в секунду.
|
||||||
|
///
|
||||||
|
/// УЗНАЁМ ПО СУМКЕ, А НЕ ПО МЕТКЕ «ЖЕРТВЫ» (проверено в игре 2026-09-26: по метке труп после
|
||||||
|
/// загрузки не узнавался и становился обычным). Баффы зомби в сейв НЕ пишутся:
|
||||||
|
/// EntityAlive.Write кладёт только deathHealth и StressAmount, EntityCreationData - stats и
|
||||||
|
/// bagData. Сумка же сохраняется (bagData), а своей сумки у зомби не бывает: ни у одного из
|
||||||
|
/// 154 классов с тегом zombie в Data/Config/entityclasses.xml нет LootList (сверено 3.3).
|
||||||
|
/// Значит, мёртвый зомби с непустой по размеру сумкой - это наш труп.</summary>
|
||||||
|
public static void RestoreLoadedCorpses(World _world)
|
||||||
|
{
|
||||||
|
List<Entity> list = _world.Entities.list;
|
||||||
|
for (int i = 0; i < list.Count; i++)
|
||||||
|
{
|
||||||
|
if (!(list[i] is EntityAlive alive) || !alive.IsDead() || SearchableCorpses.Contains(alive.entityId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (alive.bag == null || alive.bag.SlotCount == 0 || !alive.HasAnyTags(ZombieTag))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
MarkCorpse(alive);
|
||||||
|
bool opened = alive.bag != null && alive.bag.Touched;
|
||||||
|
if (opened)
|
||||||
|
{
|
||||||
|
alive.timeStayAfterDeath = (int)(OpenedCorpseSecondsAfterDeath * 20f);
|
||||||
|
}
|
||||||
|
Debug.Log("[NecromancerTome] CorpseLoot: труп " + alive.entityId + " восстановлен после загрузки" +
|
||||||
|
(opened ? " (уже открывали)" : ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Опустошённый и закрытый труп убирается сразу - как игра убирает пустой мешок
|
||||||
|
/// (EntityContainerAbs.OnUpdateEntity: `bag.Touched && bag.IsEmpty()`). Только сервер.</summary>
|
||||||
|
public static void RemoveEmptiedCorpses(World _world)
|
||||||
|
{
|
||||||
|
if (SearchableCorpses.Count == 0 || !SingletonMonoBehaviour<ConnectionManager>.Instance.IsServer)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<int> emptied = null;
|
||||||
|
foreach (int id in SearchableCorpses)
|
||||||
|
{
|
||||||
|
if (!(_world.GetEntity(id) is EntityAlive corpse) || corpse.bag == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!corpse.bag.Touched || !corpse.bag.IsEmpty() || LockManager.Instance.IsLockedServer(corpse, 0))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (emptied == null)
|
||||||
|
{
|
||||||
|
emptied = new List<int>();
|
||||||
|
}
|
||||||
|
emptied.Add(id);
|
||||||
|
}
|
||||||
|
if (emptied == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach (int id in emptied)
|
||||||
|
{
|
||||||
|
SearchableCorpses.Remove(id);
|
||||||
|
_world.RemoveEntity(id, EnumRemoveEntityReason.Despawned);
|
||||||
|
Debug.Log("[NecromancerTome] CorpseLoot: труп " + id + " опустошён и убран");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -496,56 +598,102 @@ namespace NecromancerTome
|
|||||||
PlayerActionsLocal input = player.playerInput;
|
PlayerActionsLocal input = player.playerInput;
|
||||||
string key = input.Activate.GetBindingXuiMarkupString() +
|
string key = input.Activate.GetBindingXuiMarkupString() +
|
||||||
input.PermanentActions.Activate.GetBindingXuiMarkupString();
|
input.PermanentActions.Activate.GetBindingXuiMarkupString();
|
||||||
__result = string.Format(Localization.Get("lootTooltipNew"), key,
|
// Как у мешка (EntityItem.GetActivationText): нетронуто / пусто / обыскано.
|
||||||
|
Bag bag = __instance.bag;
|
||||||
|
string tooltip = bag == null || !bag.Touched ? "lootTooltipNew"
|
||||||
|
: bag.IsEmpty() ? "lootTooltipEmpty" : "lootTooltipTouched";
|
||||||
|
__result = string.Format(Localization.Get(tooltip), key,
|
||||||
Localization.Get(__instance.LocalizedEntityName));
|
Localization.Get(__instance.LocalizedEntityName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Запускает обыск - НЕ мгновенно, а с трёхсекундным круговым индикатором (указание
|
/// <summary>Открывает труп так же, как игра открывает мешок: EntityItem.OnEntityActivated по
|
||||||
/// 2026-09-18). Индикатор рисует сама игра: XUiC_Timer.OpenTimer, тот же механизм, что крутится
|
/// команде "search" делает ровно `LockManager.Instance.LockRequestLocal(this, 0)` (у нас - через
|
||||||
/// при снятии верстака, и знакомый игроку без объяснений.
|
/// LockRequestHelper в TEPersistenceSrc, иначе не компилируется). Дальше всё
|
||||||
///
|
/// игровое: сервер наполняет сумку при первом открытии, окно добычи показывает свой таймер
|
||||||
/// ПОЧЕМУ НЕ ЧЕРЕЗ activateTime У КОМАНДЫ. У EntityActivationCommand есть поле activateTime, и
|
/// обыска. До 1.5.0 здесь был свой 3-секундный таймер и превращение трупа в мешок.</summary>
|
||||||
/// Entity.ActivateEntityCommand действительно открывает по нему таймер - но по завершении он
|
|
||||||
/// дёргает ИГРОВОЕ СОБЫТИЕ из entity_class (`onActivateEvent`), а не наш код, и запускается
|
|
||||||
/// только если у команды заполнено eventName. Нам нужен свой обработчик в конце, поэтому таймер
|
|
||||||
/// открывается вручную, а не описанием команды.
|
|
||||||
///
|
|
||||||
/// CloseOnHit=true: если по игроку ударили посреди обыска, полоса закрывается и труп остаётся
|
|
||||||
/// нетронутым. Обыскивать труп посреди боя не должно быть бесплатно.</summary>
|
|
||||||
[HarmonyPatch(typeof(EntityAlive), "OnEntityActivated")]
|
[HarmonyPatch(typeof(EntityAlive), "OnEntityActivated")]
|
||||||
public static class Patch_EntityAlive_OnEntityActivated_Corpse
|
public static class Patch_EntityAlive_OnEntityActivated_Corpse
|
||||||
{
|
{
|
||||||
public static void Postfix(EntityAlive __instance, EntityActivationCommand _command,
|
public static void Postfix(EntityAlive __instance, EntityActivationCommand _command)
|
||||||
EntityPlayerLocal _playerFocusing)
|
|
||||||
{
|
{
|
||||||
if (_command.commandId != CorpseLoot.SearchCommand || !CorpseLoot.IsSearchable(__instance))
|
if (_command.commandId != CorpseLoot.SearchCommand || !CorpseLoot.IsSearchable(__instance))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_playerFocusing == null || _playerFocusing.PlayerUI == null)
|
LockRequestHelper.RequestLocal(__instance);
|
||||||
{
|
}
|
||||||
CorpseLoot.SearchCorpse(__instance);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Сервер: пускает к сумке помеченного трупа. У EntityAlive тут `if (!IsDead())`, и
|
||||||
|
/// мёртвому отказано всегда; для нашего трупа выполняется тело базового Entity.OnLockRequestServer
|
||||||
|
/// (без веток Twitch-ящиков - зомби не бывает «чужим» ящиком). Заодно продлевает жизнь трупа:
|
||||||
|
/// открытый, но не опустошённый лежит как мешок.</summary>
|
||||||
|
[HarmonyPatch(typeof(EntityAlive), "OnLockRequestServer")]
|
||||||
|
public static class Patch_EntityAlive_OnLockRequestServer_Corpse
|
||||||
|
{
|
||||||
|
public static bool Prefix(EntityAlive __instance, int _lockingPlayerID, ref bool __result)
|
||||||
|
{
|
||||||
|
if (!__instance.IsDead() || !CorpseLoot.IsSearchable(__instance))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (__instance.bag == null)
|
||||||
|
{
|
||||||
|
__result = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!__instance.bag.Touched)
|
||||||
|
{
|
||||||
|
GameManager.Instance.lootManager.LootBagOpened(__instance.bag, __instance, _lockingPlayerID);
|
||||||
|
}
|
||||||
|
__instance.timeStayAfterDeath = (int)(CorpseLoot.OpenedCorpseSecondsAfterDeath * 20f);
|
||||||
|
Debug.Log("[NecromancerTome] CorpseLoot: труп " + __instance.entityId + " открыт");
|
||||||
|
__result = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Клиент: показывает окно добычи трупа. Повтор тела Entity.OnLockResponseLocal
|
||||||
|
/// (у EntityAlive снова `if (!IsDead())`). OnLockResponseServer у EntityAlive не переопределён -
|
||||||
|
/// базовый сам пишет, открывали ли сумку раньше (это и включает игровой таймер обыска), и её
|
||||||
|
/// содержимое.</summary>
|
||||||
|
[HarmonyPatch(typeof(EntityAlive), "OnLockResponseLocal")]
|
||||||
|
public static class Patch_EntityAlive_OnLockResponseLocal_Corpse
|
||||||
|
{
|
||||||
|
public static bool Prefix(EntityAlive __instance, bool _success, PooledBinaryReader _brServerContext, ref bool __result)
|
||||||
|
{
|
||||||
|
if (!__instance.IsDead() || !CorpseLoot.IsSearchable(__instance))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
__result = false;
|
||||||
|
LocalPlayerUI playerUI = LocalPlayerUI.GetUIForPrimaryPlayer();
|
||||||
|
Bag bag = __instance.bag;
|
||||||
|
if (bag == null || !_success)
|
||||||
|
{
|
||||||
|
if (playerUI != null)
|
||||||
|
{
|
||||||
|
GameManager.ShowTooltip(playerUI.entityPlayer, Localization.Get("ttNoInteractItem"), string.Empty, "ui_denied");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool runOpenTimer = _brServerContext.ReadBoolean();
|
||||||
|
bag.ItemGrid.ReadInto(_brServerContext, StreamModeRead.FromServer);
|
||||||
|
LootContainer lootContainer = LootContainer.GetLootContainer(__instance.GetLootList());
|
||||||
EntityAlive corpse = __instance;
|
EntityAlive corpse = __instance;
|
||||||
TimerEventData timer = new TimerEventData
|
XUiC_BagStorageWindowGroup.Open(playerUI.xui, corpse, bag, lootContainer,
|
||||||
|
Localization.Get(corpse.LocalizedEntityName), corpse.OnBagModified,
|
||||||
|
() => LockManager.Instance.UnlockRequestLocal(),
|
||||||
|
() => corpse.GetDistanceSq(playerUI.entityPlayer) <= Constants.cPlayerInteractDistance * Constants.cPlayerInteractDistance,
|
||||||
|
runOpenTimer);
|
||||||
|
if (lootContainer != null && playerUI.entityPlayer != null)
|
||||||
{
|
{
|
||||||
Data = corpse,
|
lootContainer.ExecuteBuffActions(corpse.entityId, playerUI.entityPlayer);
|
||||||
CloseOnHit = true,
|
|
||||||
};
|
|
||||||
timer.FullTimeFinishEvent += delegate
|
|
||||||
{
|
|
||||||
// Повторная проверка на финише: за три секунды труп мог истлеть, а игрок - уйти.
|
|
||||||
if (CorpseLoot.IsSearchable(corpse))
|
|
||||||
{
|
|
||||||
CorpseLoot.SearchCorpse(corpse);
|
|
||||||
}
|
}
|
||||||
};
|
bag.ItemGrid.Touch();
|
||||||
XUiC_Timer.OpenTimer(_playerFocusing.PlayerUI.xui, CorpseLoot.SearchSeconds, timer);
|
__result = true;
|
||||||
Debug.Log("[NecromancerTome] CorpseLoot: начат обыск трупа " + corpse.entityId +
|
return false;
|
||||||
", " + CorpseLoot.SearchSeconds + " с");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ namespace NecromancerTome
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Нож некроманта (BACKLOG.md, п.5): зомби, задетый ножом, носит buffNecroVictim (buffs.xml) до
|
/// Нож некроманта (BACKLOG.md, п.5): зомби, задетый ножом, носит buffNecroVictim (buffs.xml) до
|
||||||
/// самой смерти. На смерти он роняет мешок «Жертва» (EntityLootContainerVictim в
|
/// самой смерти. На смерти его труп становится обыскиваемым контейнером с добычей «Жертвы»
|
||||||
/// entityclasses.xml).
|
/// (CorpseLootPatch.cs; до 1.5.0 обыск превращал труп в мешок EntityLootContainerVictim).
|
||||||
///
|
///
|
||||||
/// ПОВЕДЕНИЕ ИСПРАВЛЕНО 2026-09-18, И ЭТО ВОЗВРАТ К ТОМУ, О ЧЁМ ДОГОВАРИВАЛИСЬ. Баг-репорт
|
/// ПОВЕДЕНИЕ ИСПРАВЛЕНО 2026-09-18, И ЭТО ВОЗВРАТ К ТОМУ, О ЧЁМ ДОГОВАРИВАЛИСЬ. Баг-репорт
|
||||||
/// пользователя: «мы договаривались, что он выпадает только если не должен был выпасть мешок с
|
/// пользователя: «мы договаривались, что он выпадает только если не должен был выпасть мешок с
|
||||||
@@ -41,8 +41,11 @@ namespace NecromancerTome
|
|||||||
/// от этого не изменились: они по-прежнему решают судьбу ОБЫЧНОГО мешка.
|
/// от этого не изменились: они по-прежнему решают судьбу ОБЫЧНОГО мешка.
|
||||||
///
|
///
|
||||||
/// Второй префикс (на DropBagServer) УДАЛЁН ЦЕЛИКОМ - он теперь вреден: подменял бы и тот
|
/// Второй префикс (на DropBagServer) УДАЛЁН ЦЕЛИКОМ - он теперь вреден: подменял бы и тот
|
||||||
/// обычный мешок, который мы как раз хотим оставить. Спавн «Жертвы» переехал сюда, в
|
/// обычный мешок, который мы как раз хотим оставить.
|
||||||
/// SpawnVictimBag.
|
///
|
||||||
|
/// С 1.5.0 мешка «Жертва» нет совсем: сам труп открывается как ящик (CorpseLootPatch.cs), и
|
||||||
|
/// метод SpawnVictimBag удалён. Класс EntityLootContainerVictim в entityclasses.xml ОСТАВЛЕН
|
||||||
|
/// ради сейвов: в мирах игроков могут лежать уже выпавшие мешки, и без класса они не загрузятся.
|
||||||
///
|
///
|
||||||
/// ПОЧЕМУ ВООБЩЕ НУЖЕН ПРЕФИКС, А НЕ ПОСТФИКС. Узнать «выпал ли мешок» после оригинала нечем:
|
/// ПОЧЕМУ ВООБЩЕ НУЖЕН ПРЕФИКС, А НЕ ПОСТФИКС. Узнать «выпал ли мешок» после оригинала нечем:
|
||||||
/// dropItemOnDeath ничего не возвращает и следов не оставляет. Поэтому бросок повторяется
|
/// dropItemOnDeath ничего не возвращает и следов не оставляет. Поэтому бросок повторяется
|
||||||
@@ -57,7 +60,6 @@ namespace NecromancerTome
|
|||||||
public static class Patch_EntityAlive_dropItemOnDeath_VictimBag
|
public static class Patch_EntityAlive_dropItemOnDeath_VictimBag
|
||||||
{
|
{
|
||||||
public const string VictimBuffName = "buffNecroVictim";
|
public const string VictimBuffName = "buffNecroVictim";
|
||||||
public const string VictimContainerClassName = "EntityLootContainerVictim";
|
|
||||||
|
|
||||||
public static bool Prefix(EntityAlive __instance)
|
public static bool Prefix(EntityAlive __instance)
|
||||||
{
|
{
|
||||||
@@ -122,43 +124,5 @@ namespace NecromancerTome
|
|||||||
CorpseLoot.MarkCorpse(__instance);
|
CorpseLoot.MarkCorpse(__instance);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Спавнит мешок «Жертва». Раньше это жило в префиксе на Entity.DropBagServer и
|
|
||||||
/// подменяло собой обычный мешок; теперь ничего не подменяет, поэтому тот патч снят.
|
|
||||||
///
|
|
||||||
/// _offsetAside - сдвинуть в сторону, потому что рядом уже лёг обычный мешок. Ваниль
|
|
||||||
/// кладёт свой ровно в GetPosition() + 0.9 по Y (декомпилировано, Entity.DropBagServer),
|
|
||||||
/// то есть без сдвига два мешка встали бы в одну точку и выглядели как один.</summary>
|
|
||||||
public static void SpawnVictimBag(EntityAlive _victim, bool _offsetAside)
|
|
||||||
{
|
|
||||||
int containerClassId = EntityClass.GetId(VictimContainerClassName);
|
|
||||||
if (containerClassId == -1)
|
|
||||||
{
|
|
||||||
Debug.LogWarning("[NecromancerTome] VictimPatch: класс '" + VictimContainerClassName +
|
|
||||||
"' не найден, мешок не выпадет");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector3 pos = _victim.GetPosition();
|
|
||||||
pos.y += 0.9f;
|
|
||||||
if (_offsetAside)
|
|
||||||
{
|
|
||||||
// Полметра вбок - достаточно, чтобы мешки читались как два и оба брались
|
|
||||||
// прицелом, и мало, чтобы «Жертва» уехала за стену или в соседнюю комнату.
|
|
||||||
pos += _victim.qrotation * new Vector3(0.6f, 0f, 0f);
|
|
||||||
}
|
|
||||||
Entity spawned = EntityFactory.CreateEntity(containerClassId, pos, Vector3.zero);
|
|
||||||
if (spawned is EntityLootContainer lootContainer)
|
|
||||||
{
|
|
||||||
GameManager.Instance.World.SpawnEntityInWorld(lootContainer);
|
|
||||||
Debug.Log("[NecromancerTome] VictimPatch: " + _victim.entityId + " уронил «Жертву» " +
|
|
||||||
lootContainer.entityId + (_offsetAside ? " (вторым мешком, со сдвигом)" : " (единственным мешком)"));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.LogWarning("[NecromancerTome] VictimPatch: созданная сущность для '" +
|
|
||||||
VictimContainerClassName + "' не оказалась EntityLootContainer");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -58,7 +58,8 @@ HUD за этим следят два индикатора: череп в ста
|
|||||||
ножа, костяная гарда, рукоять в верёвочной обмотке и светящиеся руны. Урон растёт вместе с
|
ножа, костяная гарда, рукоять в верёвочной обмотке и светящиеся руны. Урон растёт вместе с
|
||||||
навыком Некромантии: почти бесполезен в неопытных руках, смертоносен у прокачанного игрока.
|
навыком Некромантии: почти бесполезен в неопытных руках, смертоносен у прокачанного игрока.
|
||||||
Лечит владельца на половину нанесённого урона и метит раненого зомби как Жертву — труп такого
|
Лечит владельца на половину нанесённого урона и метит раненого зомби как Жертву — труп такого
|
||||||
зомби краснеет и лежит час; при обыске он исчезает, оставляя мешок Жертвы с ингредиентами.
|
зомби краснеет, и его можно обыскать, как ящик: внутри ингредиенты Жертвы. Нетронутый труп
|
||||||
|
лежит 5 минут, открытый, но не опустошённый, — час, опустошённый исчезает сразу.
|
||||||
- **Шесть модов только для Ножа некроманта** — обычные моды для ножей в это оружие не встают, а
|
- **Шесть модов только для Ножа некроманта** — обычные моды для ножей в это оружие не встают, а
|
||||||
эти не встают ни во что другое: **Слёзы мертвеца** (2 воды с каждого убитого клинком зомби),
|
эти не встают ни во что другое: **Слёзы мертвеца** (2 воды с каждого убитого клинком зомби),
|
||||||
**Пир падальщика** (2 еды за труп), **Могильный покой** (защита от перегрева и переохлаждения,
|
**Пир падальщика** (2 еды за труп), **Могильный покой** (защита от перегрева и переохлаждения,
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace NecromancerTome
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Та же причина, что у PyramidWardWriteHelper: среди перегрузок LockManager.LockRequestLocal
|
||||||
|
/// есть вариант с ReadOnlySpan<ILockTarget>, и основной проект (netstandard2.1) не может
|
||||||
|
/// разобрать набор перегрузок целиком - CS7069 «ReadOnlySpan<> требует определения в
|
||||||
|
/// mscorlib». Отсюда вызов собирается.
|
||||||
|
///
|
||||||
|
/// Зовётся из HarmonySrc/CorpseLootPatch.cs: открытие трупа «Жертвы» тем же запросом, которым
|
||||||
|
/// игра открывает мешок (EntityItem.OnEntityActivated, команда "search").
|
||||||
|
/// </summary>
|
||||||
|
public static class LockRequestHelper
|
||||||
|
{
|
||||||
|
public static void RequestLocal(ILockTarget _target)
|
||||||
|
{
|
||||||
|
LockManager.Instance.LockRequestLocal(_target, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user