diff --git a/HarmonySrc/HeldTweak.cs b/HarmonySrc/HeldTweak.cs
index a3b461c..d9d66b1 100644
--- a/HarmonySrc/HeldTweak.cs
+++ b/HarmonySrc/HeldTweak.cs
@@ -17,6 +17,9 @@ namespace NecromancerTome
public static Vector3 Pos; // метры, в осях модели
public static Vector3 Rot; // градусы, добавка к исходному повороту
public static float Scale = 1f;
+ /// Предмет, для которого подбирается посадка (тот, что был в руке при первой команде). Другие предметы
+ /// не трогаются: 10.10 без этого поворот черепа применился и к книге, взятой следом.
+ public static string Item;
/// Исходные локальные значения детей модели (по экземпляру узла).
static readonly Dictionary _base = new Dictionary();
@@ -27,10 +30,16 @@ namespace NecromancerTome
return pl?.inventory?.GetHoldingItemTransform();
}
+ public static string HeldName()
+ {
+ EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
+ return pl?.inventory?.holdingItem?.Name;
+ }
+
public static void Apply()
{
Transform m = Model();
- if (m == null)
+ if (m == null || (Enabled && HeldName() != Item))
{
return;
}
@@ -123,10 +132,14 @@ namespace NecromancerTome
}
if (cmd != "show")
{
+ if (!HeldTweak.Enabled)
+ {
+ HeldTweak.Item = HeldTweak.HeldName();
+ }
HeldTweak.Enabled = true;
}
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 ? "" : " (выключено)"));
}
diff --git a/HarmonySrc/RemainsDecay.cs b/HarmonySrc/RemainsDecay.cs
index a3ac670..26ba67b 100644
--- a/HarmonySrc/RemainsDecay.cs
+++ b/HarmonySrc/RemainsDecay.cs
@@ -14,6 +14,9 @@ namespace NecromancerTome
/// Лут переходит как у ванильного DropBagServer (Entity.cs): нетронутый - список zPackVictimRemains (наполнится при
/// первом открытии), открытый - предметы и отметка «трогали». Остаток срока трупа переходит в срок останков.
/// Звук - разбитое пятно крови (блоки bloodDecor из материала Morganic: organicdestroy). Только сервер/одиночная игра.
+ /// Последние BurnSeconds труп горит (ванильное пламя p_onFire по форме тела, как у горящего зомби) и обжигает
+ /// стоящих вплотную (ванильный buffBurningEnvironment - как костёр). Ради нагрузки горят только трупы ближе BurnNear
+ /// к игроку и не больше MaxBurning сразу (ближайшие); ушёл игрок - пламя гаснет. Пламя видно у хоста (сервер).
///
public static class RemainsDecay
{
@@ -23,11 +26,21 @@ namespace NecromancerTome
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";
/// Когда мод впервые увидел красный труп (Time.time).
static readonly Dictionary _since = new Dictionary();
static float _next;
static readonly List _tmp = new List();
+ static readonly HashSet _burning = new HashSet();
+ static readonly List> _cand = new List>();
+ static GameObject _fire;
public static void Init()
{
@@ -40,6 +53,7 @@ namespace NecromancerTome
if (world == null)
{
_since.Clear();
+ _burning.Clear();
BundleShaderFix.Forget();
return;
}
@@ -60,6 +74,7 @@ namespace NecromancerTome
foreach (int id in _tmp)
{
_since.Remove(id);
+ _burning.Remove(id);
}
foreach (int id in CorpseLoot.SearchableCorpses)
{
@@ -104,7 +119,139 @@ namespace NecromancerTome
Convert(world, c);
}
_since.Remove(id);
+ _burning.Remove(id);
}
+ Burn(world);
+ }
+
+ /// Тест: все красные трупы сразу в последние BurnSeconds (necroremains burn).
+ 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(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(_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;
+ }
+
+ /// Пламя по форме тела - как MinEventActionAttachParticleEffectToEntity (parent_transform LOD0, shape_mesh).
+ static bool Ignite(EntityAlive _c)
+ {
+ Transform parent = _c.emodel?.meshTransform;
+ if (parent == null)
+ {
+ return false;
+ }
+ if (_fire == null)
+ {
+ _fire = LoadManager.LoadAssetFromAddressables("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();
+ foreach (ParticleSystem ps in go.GetComponentsInChildren())
+ {
+ 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)
@@ -175,7 +322,7 @@ namespace NecromancerTome
public class ConsoleCmdNecroRemains : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necroremains" };
- public override string getDescription() => "NecromancerTome: красные трупы рядом сразу истлевают в останки";
+ public override string getDescription() => "NecromancerTome: красные трупы сразу истлевают в останки; burn - сразу последние 30 с (горят)";
public override void Execute(List _params, CommandSenderInfo _senderInfo)
{
@@ -184,6 +331,11 @@ namespace NecromancerTome
{
return;
}
+ if (_params.Count > 0 && _params[0] == "burn")
+ {
+ SdtdConsole.Instance.Output("подожжено трупов (последние " + RemainsDecay.BurnSeconds + " с): " + RemainsDecay.IgniteAll());
+ return;
+ }
var ids = new List(CorpseLoot.SearchableCorpses);
int n = 0;
foreach (int id in ids)