Останки красного трупа: модель, сущность, истлевание через 2 минуты
Красный труп «Жертвы» истлевает в лёгкий контейнер necroRemains (своя модель: остов, обтянутый обугленной плёнкой, бандл necroremains) через 2 минуты, раньше - если игрок дальше 80 м или трупов больше 30. Лут и остаток срока переходят, звук разбитого пятна крови (organicdestroy). Команда necroremains истлевает трупы рядом сразу (для проверки). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
eba772123f
commit
8ad2f25c37
@@ -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,记忆,記憶
|
||||||
|
|||||||
|
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
Reference in New Issue
Block a user