Останки красного трупа: модель, сущность, истлевание через 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
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user