Files
necromants-tome-7d2d-3-2/HarmonySrc/RemainsDecay.cs
T

352 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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). Только сервер/одиночная игра.
/// Последние BurnSeconds труп горит (ванильное пламя p_onFire по форме тела, как у горящего зомби) и обжигает
/// стоящих вплотную (ванильный buffBurningEnvironment - как костёр). Ради нагрузки горят только трупы ближе BurnNear
/// к игроку и не больше MaxBurning сразу (ближайшие); ушёл игрок - пламя гаснет. Пламя видно у хоста (сервер).
/// </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";
public const float BurnSeconds = 30f;
public const float BurnNear = 25f;
public const int MaxBurning = 6;
public const float BurnRadius = 1.3f;
const string FireParticle = "p_onFire";
const string FireName = "Ptl_" + FireParticle;
const string BurnBuff = "buffBurningEnvironment";
/// <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>();
static readonly HashSet<int> _burning = new HashSet<int>();
static readonly List<KeyValuePair<float, EntityAlive>> _cand = new List<KeyValuePair<float, EntityAlive>>();
static GameObject _fire;
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();
_burning.Clear();
BundleShaderFix.Forget();
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);
_burning.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);
_burning.Remove(id);
}
Burn(world);
}
/// <summary>Тест: все красные трупы сразу в последние BurnSeconds (necroremains burn).</summary>
public static int IgniteAll()
{
_tmp.Clear();
_tmp.AddRange(_since.Keys);
foreach (int id in _tmp)
{
_since[id] = Mathf.Min(_since[id], Time.time - (DecaySeconds - BurnSeconds));
}
_next = 0f;
return _tmp.Count;
}
static void Burn(World _world)
{
_cand.Clear();
foreach (var kv in _since)
{
if (Time.time - kv.Value < DecaySeconds - BurnSeconds || !(_world.GetEntity(kv.Key) is EntityAlive c) || !c.IsDead())
{
continue;
}
float d = NearestPlayer(_world, c.position);
if (d < BurnNear)
{
_cand.Add(new KeyValuePair<float, EntityAlive>(d, c));
}
}
_cand.Sort((a, b) => a.Key.CompareTo(b.Key));
_tmp.Clear();
for (int i = 0; i < _cand.Count && i < MaxBurning; i++)
{
_tmp.Add(_cand[i].Value.entityId);
}
// погасить лишние
foreach (int id in new List<int>(_burning))
{
if (!_tmp.Contains(id))
{
(_world.GetEntity(id) as EntityAlive)?.RemoveParticle(FireName);
_burning.Remove(id);
}
}
for (int i = 0; i < _cand.Count && i < MaxBurning; i++)
{
EntityAlive c = _cand[i].Value;
if (!_burning.Contains(c.entityId) && Ignite(c))
{
_burning.Add(c.entityId);
}
if (!_burning.Contains(c.entityId))
{
continue;
}
// обжечь вплотную стоящих (как костёр: от 3 касаний подряд - горит сам)
Vector3 hip = c.emodel != null ? c.emodel.GetHipPosition() : c.position;
float r2 = BurnRadius * BurnRadius;
foreach (EntityPlayer pl in _world.Players.list)
{
if (pl != null && !pl.IsDead() && (pl.position - hip).sqrMagnitude < r2)
{
pl.Buffs.AddBuff(BurnBuff);
}
}
}
}
static float NearestPlayer(World _world, Vector3 _p)
{
float best = float.MaxValue;
foreach (EntityPlayer pl in _world.Players.list)
{
if (pl != null)
{
best = Mathf.Min(best, (pl.position - _p).magnitude);
}
}
return best;
}
/// <summary>Пламя по форме тела - как MinEventActionAttachParticleEffectToEntity (parent_transform LOD0, shape_mesh).</summary>
static bool Ignite(EntityAlive _c)
{
Transform parent = _c.emodel?.meshTransform;
if (parent == null)
{
return false;
}
if (_fire == null)
{
_fire = LoadManager.LoadAssetFromAddressables<GameObject>("ParticleEffects/" + FireParticle + ".prefab", null, null, _deferLoading: false, _loadSync: true).Asset;
if (_fire == null)
{
return false;
}
}
if (parent.Find(FireName) != null)
{
return true;
}
GameObject go = Object.Instantiate(_fire);
go.name = FireName;
Utils.SetLayerRecursively(go, parent.gameObject.layer);
Transform t = go.transform;
t.SetParent(parent, false);
t.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
_c.AddParticle(FireName, t);
SkinnedMeshRenderer smr = parent.GetComponentInChildren<SkinnedMeshRenderer>();
foreach (ParticleSystem ps in go.GetComponentsInChildren<ParticleSystem>())
{
ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
ParticleSystem.ShapeModule sh = ps.shape;
if (sh.shapeType == ParticleSystemShapeType.SkinnedMeshRenderer || sh.shapeType == ParticleSystemShapeType.Mesh)
{
if (smr != null)
{
sh.skinnedMeshRenderer = smr;
}
else
{
sh.shapeType = ParticleSystemShapeType.Sphere;
}
}
ParticleSystem.MainModule main = ps.main;
main.duration = 900000f;
ps.Play();
}
return true;
}
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.19f; // модель x1.2 (NecroRemainsSetup.Scale)
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: красные трупы сразу истлевают в останки; burn - сразу последние 30 с (горят)";
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{
World world = GameManager.Instance?.World;
if (world == null)
{
return;
}
if (_params.Count > 0 && _params[0] == "burn")
{
SdtdConsole.Instance.Output("подожжено трупов (последние " + RemainsDecay.BurnSeconds + " с): " + RemainsDecay.IgniteAll());
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);
}
}
}