Останки красного трупа: модель, сущность, истлевание через 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}根骨頭。
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,抹除者,抹除者
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ı,记忆之书,記憶之書
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,记忆,記憶
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>
</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>
</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">
<item group="groupZpackVictim"/>
</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>
</config>
+1
View File
@@ -67,6 +67,7 @@ namespace NecromancerTome
Possession.Init();
BloodTide.Init();
FreezeProbe.Init();
RemainsDecay.Init();
// PyramidWardPatch.cs's TEFeaturePyramidWard needs no Init() call - it's discovered
// automatically by the engine's own TileEntityCompositeData reflection scan (see that
// 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.