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)