Красные трупы горят последние 30 с (рядом с игроком, до 6, обжигают); necroheld - только для своего предмета

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-10 22:32:23 +03:00
co-authored by Claude Opus 5.5
parent 0efa1b8db4
commit bf4b1819da
2 changed files with 168 additions and 3 deletions
+15 -2
View File
@@ -17,6 +17,9 @@ namespace NecromancerTome
public static Vector3 Pos; // метры, в осях модели public static Vector3 Pos; // метры, в осях модели
public static Vector3 Rot; // градусы, добавка к исходному повороту public static Vector3 Rot; // градусы, добавка к исходному повороту
public static float Scale = 1f; public static float Scale = 1f;
/// <summary>Предмет, для которого подбирается посадка (тот, что был в руке при первой команде). Другие предметы
/// не трогаются: 10.10 без этого поворот черепа применился и к книге, взятой следом.</summary>
public static string Item;
/// <summary>Исходные локальные значения детей модели (по экземпляру узла).</summary> /// <summary>Исходные локальные значения детей модели (по экземпляру узла).</summary>
static readonly Dictionary<int, (Vector3 p, Quaternion r, Vector3 s)> _base = new Dictionary<int, (Vector3, Quaternion, Vector3)>(); static readonly Dictionary<int, (Vector3 p, Quaternion r, Vector3 s)> _base = new Dictionary<int, (Vector3, Quaternion, Vector3)>();
@@ -27,10 +30,16 @@ namespace NecromancerTome
return pl?.inventory?.GetHoldingItemTransform(); return pl?.inventory?.GetHoldingItemTransform();
} }
public static string HeldName()
{
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
return pl?.inventory?.holdingItem?.Name;
}
public static void Apply() public static void Apply()
{ {
Transform m = Model(); Transform m = Model();
if (m == null) if (m == null || (Enabled && HeldName() != Item))
{ {
return; return;
} }
@@ -123,10 +132,14 @@ namespace NecromancerTome
} }
if (cmd != "show") if (cmd != "show")
{ {
if (!HeldTweak.Enabled)
{
HeldTweak.Item = HeldTweak.HeldName();
}
HeldTweak.Enabled = true; HeldTweak.Enabled = true;
} }
Transform m = HeldTweak.Model(); Transform m = HeldTweak.Model();
Out("модель " + (m != null ? m.name : "нет") + ": сдвиг (см) " + HeldTweak.F(HeldTweak.Pos * 100f) + ", поворот " + Out("предмет " + HeldTweak.Item + ", модель " + (m != null ? m.name : "нет") + ": сдвиг (см) " + HeldTweak.F(HeldTweak.Pos * 100f) + ", поворот " +
HeldTweak.F(HeldTweak.Rot) + ", масштаб " + HeldTweak.F(HeldTweak.Scale) + (HeldTweak.Enabled ? "" : " (выключено)")); HeldTweak.F(HeldTweak.Rot) + ", масштаб " + HeldTweak.F(HeldTweak.Scale) + (HeldTweak.Enabled ? "" : " (выключено)"));
} }
+153 -1
View File
@@ -14,6 +14,9 @@ namespace NecromancerTome
/// Лут переходит как у ванильного DropBagServer (Entity.cs): нетронутый - список zPackVictimRemains (наполнится при /// Лут переходит как у ванильного DropBagServer (Entity.cs): нетронутый - список zPackVictimRemains (наполнится при
/// первом открытии), открытый - предметы и отметка «трогали». Остаток срока трупа переходит в срок останков. /// первом открытии), открытый - предметы и отметка «трогали». Остаток срока трупа переходит в срок останков.
/// Звук - разбитое пятно крови (блоки bloodDecor из материала Morganic: organicdestroy). Только сервер/одиночная игра. /// Звук - разбитое пятно крови (блоки bloodDecor из материала Morganic: organicdestroy). Только сервер/одиночная игра.
/// Последние BurnSeconds труп горит (ванильное пламя p_onFire по форме тела, как у горящего зомби) и обжигает
/// стоящих вплотную (ванильный buffBurningEnvironment - как костёр). Ради нагрузки горят только трупы ближе BurnNear
/// к игроку и не больше MaxBurning сразу (ближайшие); ушёл игрок - пламя гаснет. Пламя видно у хоста (сервер).
/// </summary> /// </summary>
public static class RemainsDecay public static class RemainsDecay
{ {
@@ -23,11 +26,21 @@ namespace NecromancerTome
public const float FarDistance = 80f; public const float FarDistance = 80f;
public const int MaxCorpses = 30; public const int MaxCorpses = 30;
public const string Sound = "organicdestroy"; 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> /// <summary>Когда мод впервые увидел красный труп (Time.time).</summary>
static readonly Dictionary<int, float> _since = new Dictionary<int, float>(); static readonly Dictionary<int, float> _since = new Dictionary<int, float>();
static float _next; static float _next;
static readonly List<int> _tmp = new List<int>(); 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() public static void Init()
{ {
@@ -40,6 +53,7 @@ namespace NecromancerTome
if (world == null) if (world == null)
{ {
_since.Clear(); _since.Clear();
_burning.Clear();
BundleShaderFix.Forget(); BundleShaderFix.Forget();
return; return;
} }
@@ -60,6 +74,7 @@ namespace NecromancerTome
foreach (int id in _tmp) foreach (int id in _tmp)
{ {
_since.Remove(id); _since.Remove(id);
_burning.Remove(id);
} }
foreach (int id in CorpseLoot.SearchableCorpses) foreach (int id in CorpseLoot.SearchableCorpses)
{ {
@@ -104,7 +119,139 @@ namespace NecromancerTome
Convert(world, c); Convert(world, c);
} }
_since.Remove(id); _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) static bool FarFromPlayers(World _world, Vector3 _p)
@@ -175,7 +322,7 @@ namespace NecromancerTome
public class ConsoleCmdNecroRemains : ConsoleCmdAbstract public class ConsoleCmdNecroRemains : ConsoleCmdAbstract
{ {
public override string[] getCommands() => new[] { "necroremains" }; public override string[] getCommands() => new[] { "necroremains" };
public override string getDescription() => "NecromancerTome: красные трупы рядом сразу истлевают в останки"; public override string getDescription() => "NecromancerTome: красные трупы сразу истлевают в останки; burn - сразу последние 30 с (горят)";
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{ {
@@ -184,6 +331,11 @@ namespace NecromancerTome
{ {
return; return;
} }
if (_params.Count > 0 && _params[0] == "burn")
{
SdtdConsole.Instance.Output("подожжено трупов (последние " + RemainsDecay.BurnSeconds + " с): " + RemainsDecay.IgniteAll());
return;
}
var ids = new List<int>(CorpseLoot.SearchableCorpses); var ids = new List<int>(CorpseLoot.SearchableCorpses);
int n = 0; int n = 0;
foreach (int id in ids) foreach (int id in ids)