Останки красного трупа: модель, сущность, истлевание через 2 минуты

Красный труп «Жертвы» истлевает в лёгкий контейнер necroRemains (своя модель:
остов, обтянутый обугленной плёнкой, бандл necroremains) через 2 минуты,
раньше - если игрок дальше 80 м или трупов больше 30. Лут и остаток срока
переходят, звук разбитого пятна крови (organicdestroy). Команда necroremains
истлевает трупы рядом сразу (для проверки).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-10 21:00:11 +03:00
co-authored by Claude Opus 5.5
parent eba772123f
commit 8ad2f25c37
8 changed files with 221 additions and 0 deletions
+1
View File
@@ -324,6 +324,7 @@ Dört modifikasyon yuvasına takılan kan küreleriyle çalışır: küreler sı
necroBoneSteedNeedBones,UI,Tooltip,,,,You need {0} bones to repair the Bone Steed.,,"Du brauchst {0} Knochen, um das Knochenross zu reparieren.",Necesitas {0} huesos para reparar el Corcel de hueso.,Il te faut {0} os pour réparer le Destrier d'os.,Servono {0} ossa per riparare il Destriero d'ossa.,骨の駿馬の修理には骨が{0}個必要だ。,뼈 군마를 수리하려면 뼈 {0}개가 필요하다.,Do naprawy Kościanego rumaka potrzeba {0} kości.,Você precisa de {0} ossos para reparar o Corcel de osso.,Для ремонта Костяного скакуна нужно {0} костей.,Kemik Atı onarmak için {0} kemik gerekir.,修理骨骸坐骑需要{0}根骨头。,修理骨骸坐騎需要{0}根骨頭。 necroBoneSteedNeedBones,UI,Tooltip,,,,You need {0} bones to repair the Bone Steed.,,"Du brauchst {0} Knochen, um das Knochenross zu reparieren.",Necesitas {0} huesos para reparar el Corcel de hueso.,Il te faut {0} os pour réparer le Destrier d'os.,Servono {0} ossa per riparare il Destriero d'ossa.,骨の駿馬の修理には骨が{0}個必要だ。,뼈 군마를 수리하려면 뼈 {0}개가 필요하다.,Do naprawy Kościanego rumaka potrzeba {0} kości.,Você precisa de {0} ossos para reparar o Corcel de osso.,Для ремонта Костяного скакуна нужно {0} костей.,Kemik Atı onarmak için {0} kemik gerekir.,修理骨骸坐骑需要{0}根骨头。,修理骨骸坐騎需要{0}根骨頭。
necroBoneSteed,entityclasses,Entity,,,,Bone Steed,,Knochenross,Corcel de hueso,Destrier d'os,Destriero d'ossa,骨の駿馬,뼈 군마,Kościany rumak,Corcel de osso,Костяной скакун,Kemik At,骨骸坐骑,骨骸坐騎 necroBoneSteed,entityclasses,Entity,,,,Bone Steed,,Knochenross,Corcel de hueso,Destrier d'os,Destriero d'ossa,骨の駿馬,뼈 군마,Kościany rumak,Corcel de osso,Костяной скакун,Kemik At,骨骸坐骑,骨骸坐騎
zombieEraser,entityclasses,Entity,,,,Eraser,,Radierer,Borrador,Effaceur,Cancellatore,イレイサー,이레이저,Wymazywacz,Apagador,Стиратель,Silici,抹除者,抹除者 zombieEraser,entityclasses,Entity,,,,Eraser,,Radierer,Borrador,Effaceur,Cancellatore,イレイサー,이레이저,Wymazywacz,Apagador,Стиратель,Silici,抹除者,抹除者
necroRemains,entityclasses,Entity,,,,Remains,,Überreste,Restos,Restes,Resti,遺骸,유해,Szczątki,Restos,Останки,Kalıntılar,遗骸,遺骸
necroMemoryBook,blocks,Block,,,,Book of Memory,,Buch der Erinnerung,Libro de la memoria,Livre de la mémoire,Libro della memoria,記憶の書,기억의 책,Księga pamięci,Livro da memória,Книга памяти,Hafıza Kitabı,记忆之书,記憶之書 necroMemoryBook,blocks,Block,,,,Book of Memory,,Buch der Erinnerung,Libro de la memoria,Livre de la mémoire,Libro della memoria,記憶の書,기억의 책,Księga pamięci,Livro da memória,Книга памяти,Hafıza Kitabı,记忆之书,記憶之書
necroMemoryBookPrompt,blocks,Block,,,,Press {0} to read,,Drücke {0} zum Lesen,Pulsa {0} para leer,Appuyez sur {0} pour lire,Premi {0} per leggere,{0} で読む,{0} 키를 눌러 읽기,"Naciśnij {0}, aby przeczytać",Pressione {0} para ler,"Нажмите {0}, чтобы прочитать",Okumak için {0} tuşuna basın,按 {0} 阅读,按 {0} 閱讀 necroMemoryBookPrompt,blocks,Block,,,,Press {0} to read,,Drücke {0} zum Lesen,Pulsa {0} para leer,Appuyez sur {0} pour lire,Premi {0} per leggere,{0} で読む,{0} 키를 눌러 읽기,"Naciśnij {0}, aby przeczytać",Pressione {0} para ler,"Нажмите {0}, чтобы прочитать",Okumak için {0} tuşuna basın,按 {0} 阅读,按 {0} 閱讀
necroMemoryTitle,XUi,Menu,,,,Memory,,Erinnerung,Memoria,Mémoire,Memoria,記憶,기억,Pamięć,Memória,Память,Hafıza,记忆,記憶 necroMemoryTitle,XUi,Menu,,,,Memory,,Erinnerung,Memoria,Mémoire,Memoria,記憶,기억,Pamięć,Memória,Память,Hafıza,记忆,記憶
1 Key File Type UsedInMainMenu NoTranslate KeepLoaded english Context / Alternate Text german spanish french italian japanese koreana polish brazilian russian turkish schinese tchinese
324
325
326
327
328
329
330
+16
View File
@@ -482,5 +482,21 @@
</property> </property>
</property> </property>
</entity_class> </entity_class>
<!-- Останки красного трупа (10.10.2026): через 2 минуты (раньше - если игрок ушёл далеко или трупов больше
предела) красный труп «Жертвы» истлевает в лёгкий контейнер со своей моделью - остов, обтянутый обугленной
плёнкой (бандл necroremains). Лут и остаток срока переходят от трупа (RemainsDecay.cs). Сохраняется с
участком силами игры, как мешок. TimeStayAfterDeath ставит код (остаток срока трупа). -->
<entity_class name="necroRemains">
<property name="Class" value="EntityLootContainer"/>
<property name="UserSpawnType" value="Console"/>
<property name="Mesh" value="#@modfolder(NecromancerTome):Resources/necroremains?necroRemainsPrefab.prefab"/>
<property name="ModelType" value="Custom"/>
<property name="Prefab" value="Backpack"/>
<property name="Parent" value="Backpack"/>
<property name="IsEnemyEntity" value="false"/>
<property name="TimeStayAfterDeath" value="300"/>
<property name="LootList" value="zPackVictimRemains"/>
<property name="Faction" value="none"/>
</entity_class>
</append> </append>
</config> </config>
+5
View File
@@ -39,5 +39,10 @@
<lootcontainer name="zPackVictim" count="1" size="6,3" sound_open="UseActions/open_backpack" sound_close="UseActions/close_backpack" open_time="1" loot_quality_template="qualBaseTemplate"> <lootcontainer name="zPackVictim" count="1" size="6,3" sound_open="UseActions/open_backpack" sound_close="UseActions/close_backpack" open_time="1" loot_quality_template="qualBaseTemplate">
<item group="groupZpackVictim"/> <item group="groupZpackVictim"/>
</lootcontainer> </lootcontainer>
<!-- Останки красного трупа (10.10): та же добыча, но контейнер исчезает, когда опустошён (труп убирает
CorpseLoot.RemoveEmptiedCorpses, а останки - обычный контейнер игры). -->
<lootcontainer name="zPackVictimRemains" count="1" size="6,3" sound_open="UseActions/open_backpack" sound_close="UseActions/close_backpack" open_time="1" destroy_on_close="empty" loot_quality_template="qualBaseTemplate">
<item group="groupZpackVictim"/>
</lootcontainer>
</append> </append>
</config> </config>
+1
View File
@@ -67,6 +67,7 @@ namespace NecromancerTome
Possession.Init(); Possession.Init();
BloodTide.Init(); BloodTide.Init();
FreezeProbe.Init(); FreezeProbe.Init();
RemainsDecay.Init();
// PyramidWardPatch.cs's TEFeaturePyramidWard needs no Init() call - it's discovered // PyramidWardPatch.cs's TEFeaturePyramidWard needs no Init() call - it's discovered
// automatically by the engine's own TileEntityCompositeData reflection scan (see that // automatically by the engine's own TileEntityCompositeData reflection scan (see that
// file's class doc comment), not registered here like PetFollowPatch's UnityUpdate hook. // file's class doc comment), not registered here like PetFollowPatch's UnityUpdate hook.
+198
View File
@@ -0,0 +1,198 @@
using System.Collections.Generic;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Истлевание красного трупа в останки (указание 10.10.2026). Красный труп «Жертвы» - полноценный мёртвый зомби
/// (модель со скелетом, физика, перекрашенные текстуры): много таких трупов в загруженной зоне - много памяти, а при
/// выгрузке участка игра трупы не сохраняет (лут пропадал). Поэтому труп истлевает в лёгкий контейнер necroRemains
/// (entityclasses.xml, своя модель: остов, обтянутый обугленной плёнкой; бандл necroremains):
/// - через DecaySeconds после того, как стал красным трупом (пользователь: «2 минуты будет более чем достаточно»);
/// - раньше, если до ближайшего игрока дальше FarDistance - участок скоро выгрузится;
/// - раньше, если красных трупов больше MaxCorpses - сначала самые старые.
/// Лут переходит как у ванильного DropBagServer (Entity.cs): нетронутый - список zPackVictimRemains (наполнится при
/// первом открытии), открытый - предметы и отметка «трогали». Остаток срока трупа переходит в срок останков.
/// Звук - разбитое пятно крови (блоки bloodDecor из материала Morganic: organicdestroy). Только сервер/одиночная игра.
/// </summary>
public static class RemainsDecay
{
public const string EntityClassName = "necroRemains";
public const string RemainsLootList = "zPackVictimRemains";
public const float DecaySeconds = 120f;
public const float FarDistance = 80f;
public const int MaxCorpses = 30;
public const string Sound = "organicdestroy";
/// <summary>Когда мод впервые увидел красный труп (Time.time).</summary>
static readonly Dictionary<int, float> _since = new Dictionary<int, float>();
static float _next;
static readonly List<int> _tmp = new List<int>();
public static void Init()
{
ModEvents.UnityUpdate.RegisterHandler(OnUnityUpdate);
}
static void OnUnityUpdate(ref ModEvents.SUnityUpdateData _data)
{
World world = GameManager.Instance?.World;
if (world == null)
{
_since.Clear();
return;
}
if (Time.time < _next || !SingletonMonoBehaviour<ConnectionManager>.Instance.IsServer)
{
return;
}
_next = Time.time + 1f;
// забыть ушедшие
_tmp.Clear();
foreach (int id in _since.Keys)
{
if (!CorpseLoot.SearchableCorpses.Contains(id))
{
_tmp.Add(id);
}
}
foreach (int id in _tmp)
{
_since.Remove(id);
}
foreach (int id in CorpseLoot.SearchableCorpses)
{
if (!_since.ContainsKey(id))
{
_since[id] = Time.time;
}
}
// кого истлеть
_tmp.Clear();
foreach (var kv in _since)
{
if (!(world.GetEntity(kv.Key) is EntityAlive c) || !c.IsDead())
{
continue;
}
if (Time.time - kv.Value >= DecaySeconds || FarFromPlayers(world, c.position))
{
_tmp.Add(kv.Key);
}
}
if (_since.Count - _tmp.Count > MaxCorpses)
{
var rest = new List<KeyValuePair<int, float>>();
foreach (var kv in _since)
{
if (!_tmp.Contains(kv.Key))
{
rest.Add(kv);
}
}
rest.Sort((a, b) => a.Value.CompareTo(b.Value));
for (int i = 0; i < rest.Count - MaxCorpses; i++)
{
_tmp.Add(rest[i].Key);
}
}
foreach (int id in _tmp)
{
if (world.GetEntity(id) is EntityAlive c)
{
Convert(world, c);
}
_since.Remove(id);
}
}
static bool FarFromPlayers(World _world, Vector3 _p)
{
float far2 = FarDistance * FarDistance;
for (int i = 0; i < _world.Players.list.Count; i++)
{
EntityPlayer pl = _world.Players.list[i];
if (pl != null && (pl.position - _p).sqrMagnitude < far2)
{
return false;
}
}
return _world.Players.list.Count > 0;
}
/// <summary>Красный труп -> останки. True - получилось (труп убран).</summary>
public static bool Convert(World _world, EntityAlive _corpse)
{
int cls = EntityClass.FromString(EntityClassName);
if (cls == -1 || _corpse == null || _corpse.IsMarkedForUnload())
{
return false;
}
// где и как лежит тело: таз и направление таз -> голова (у модели останков голова - в сторону -Z)
Vector3 pos = _corpse.emodel != null ? _corpse.emodel.GetHipPosition() : _corpse.position;
Vector3 head = _corpse.emodel?.GetHeadTransform() != null ? _corpse.emodel.GetHeadTransform().position + Origin.position : pos + _corpse.transform.forward;
Vector3 dir = head - pos; dir.y = 0f;
float yaw = dir.sqrMagnitude > 1e-4f ? Mathf.Atan2(-dir.x, -dir.z) * Mathf.Rad2Deg : _corpse.rotation.y;
// на землю: луч вниз от таза
Vector3 g = pos;
if (Physics.Raycast(pos + Vector3.up * 0.6f - Origin.position, Vector3.down, out RaycastHit hit, 3f, SnakeControl.TerrainMask, QueryTriggerInteraction.Ignore))
{
g.y = hit.point.y + Origin.position.y;
}
// центр модели останков сдвинут от таза к груди: отодвинуть назад на полкорпуса, чтобы таз остался на месте
Vector3 back = Quaternion.Euler(0f, yaw, 0f) * Vector3.back;
g += back * 0.16f;
if (!(EntityFactory.CreateEntity(cls, g, new Vector3(0f, yaw, 0f)) is EntityLootContainer rem))
{
return false;
}
rem.OverrideLootList = RemainsLootList;
Bag bag = _corpse.bag;
bool touched = bag != null && bag.Touched;
if (touched)
{
rem.bag.Resize(new Vector2i(bag.SlotCount, 1));
rem.bag.ItemGrid.CopyItemsFrom(bag.ItemGrid);
rem.bag.ItemGrid.WorldTimeTouched = bag.ItemGrid.WorldTimeTouched;
}
_world.SpawnEntityInWorld(rem);
int left = Mathf.Max(20 * 30, _corpse.timeStayAfterDeath - _corpse.deathUpdateTime);
rem.timeStayAfterDeath = left;
rem.deathUpdateTime = 0;
Audio.Manager.BroadcastPlay(g, Sound);
CorpseLoot.SearchableCorpses.Remove(_corpse.entityId);
_world.RemoveEntity(_corpse.entityId, EnumRemoveEntityReason.Despawned);
Debug.Log("[NecromancerTome] RemainsDecay: труп " + _corpse.entityId + " -> останки " + rem.entityId +
(touched ? " (лут перенесён)" : " (нетронутый)") + ", срок " + left / 20 + " с");
return true;
}
}
/// <summary>necroremains - мгновенно истлеть все красные трупы рядом (проверка без ожидания 2 минут).</summary>
[UnityEngine.Scripting.Preserve]
public class ConsoleCmdNecroRemains : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necroremains" };
public override string getDescription() => "NecromancerTome: красные трупы рядом сразу истлевают в останки";
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{
World world = GameManager.Instance?.World;
if (world == null)
{
return;
}
var ids = new List<int>(CorpseLoot.SearchableCorpses);
int n = 0;
foreach (int id in ids)
{
if (world.GetEntity(id) is EntityAlive c && c.IsDead() && RemainsDecay.Convert(world, c))
{
n++;
}
}
SdtdConsole.Instance.Output("истлело трупов: " + n);
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.