diff --git a/Config/Localization.csv b/Config/Localization.csv index 44ddb7a..61036fd 100644 --- a/Config/Localization.csv +++ b/Config/Localization.csv @@ -324,6 +324,7 @@ Dört modifikasyon yuvasına takılan kan küreleriyle çalışır: küreler sı necroBoneSteedNeedBones,UI,Tooltip,,,,You need {0} bones to repair the Bone Steed.,,"Du brauchst {0} Knochen, um das Knochenross zu reparieren.",Necesitas {0} huesos para reparar el Corcel de hueso.,Il te faut {0} os pour réparer le Destrier d'os.,Servono {0} ossa per riparare il Destriero d'ossa.,骨の駿馬の修理には骨が{0}個必要だ。,뼈 군마를 수리하려면 뼈 {0}개가 필요하다.,Do naprawy Kościanego rumaka potrzeba {0} kości.,Você precisa de {0} ossos para reparar o Corcel de osso.,Для ремонта Костяного скакуна нужно {0} костей.,Kemik Atı onarmak için {0} kemik gerekir.,修理骨骸坐骑需要{0}根骨头。,修理骨骸坐騎需要{0}根骨頭。 necroBoneSteed,entityclasses,Entity,,,,Bone Steed,,Knochenross,Corcel de hueso,Destrier d'os,Destriero d'ossa,骨の駿馬,뼈 군마,Kościany rumak,Corcel de osso,Костяной скакун,Kemik At,骨骸坐骑,骨骸坐騎 zombieEraser,entityclasses,Entity,,,,Eraser,,Radierer,Borrador,Effaceur,Cancellatore,イレイサー,이레이저,Wymazywacz,Apagador,Стиратель,Silici,抹除者,抹除者 +necroRemains,entityclasses,Entity,,,,Remains,,Überreste,Restos,Restes,Resti,遺骸,유해,Szczątki,Restos,Останки,Kalıntılar,遗骸,遺骸 necroMemoryBook,blocks,Block,,,,Book of Memory,,Buch der Erinnerung,Libro de la memoria,Livre de la mémoire,Libro della memoria,記憶の書,기억의 책,Księga pamięci,Livro da memória,Книга памяти,Hafıza Kitabı,记忆之书,記憶之書 necroMemoryBookPrompt,blocks,Block,,,,Press {0} to read,,Drücke {0} zum Lesen,Pulsa {0} para leer,Appuyez sur {0} pour lire,Premi {0} per leggere,{0} で読む,{0} 키를 눌러 읽기,"Naciśnij {0}, aby przeczytać",Pressione {0} para ler,"Нажмите {0}, чтобы прочитать",Okumak için {0} tuşuna basın,按 {0} 阅读,按 {0} 閱讀 necroMemoryTitle,XUi,Menu,,,,Memory,,Erinnerung,Memoria,Mémoire,Memoria,記憶,기억,Pamięć,Memória,Память,Hafıza,记忆,記憶 diff --git a/Config/entityclasses.xml b/Config/entityclasses.xml index 22de7c9..4fb9c21 100644 --- a/Config/entityclasses.xml +++ b/Config/entityclasses.xml @@ -482,5 +482,21 @@ + + + + + + + + + + + + + diff --git a/Config/loot.xml b/Config/loot.xml index 60f66b2..46e6ada 100644 --- a/Config/loot.xml +++ b/Config/loot.xml @@ -39,5 +39,10 @@ + + + + diff --git a/HarmonySrc/ModEntry.cs b/HarmonySrc/ModEntry.cs index 71c898d..b0429bc 100644 --- a/HarmonySrc/ModEntry.cs +++ b/HarmonySrc/ModEntry.cs @@ -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. diff --git a/HarmonySrc/RemainsDecay.cs b/HarmonySrc/RemainsDecay.cs new file mode 100644 index 0000000..6cab293 --- /dev/null +++ b/HarmonySrc/RemainsDecay.cs @@ -0,0 +1,198 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace NecromancerTome +{ + /// + /// Истлевание красного трупа в останки (указание 10.10.2026). Красный труп «Жертвы» - полноценный мёртвый зомби + /// (модель со скелетом, физика, перекрашенные текстуры): много таких трупов в загруженной зоне - много памяти, а при + /// выгрузке участка игра трупы не сохраняет (лут пропадал). Поэтому труп истлевает в лёгкий контейнер necroRemains + /// (entityclasses.xml, своя модель: остов, обтянутый обугленной плёнкой; бандл necroremains): + /// - через DecaySeconds после того, как стал красным трупом (пользователь: «2 минуты будет более чем достаточно»); + /// - раньше, если до ближайшего игрока дальше FarDistance - участок скоро выгрузится; + /// - раньше, если красных трупов больше MaxCorpses - сначала самые старые. + /// Лут переходит как у ванильного DropBagServer (Entity.cs): нетронутый - список zPackVictimRemains (наполнится при + /// первом открытии), открытый - предметы и отметка «трогали». Остаток срока трупа переходит в срок останков. + /// Звук - разбитое пятно крови (блоки bloodDecor из материала Morganic: organicdestroy). Только сервер/одиночная игра. + /// + 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"; + + /// Когда мод впервые увидел красный труп (Time.time). + static readonly Dictionary _since = new Dictionary(); + static float _next; + static readonly List _tmp = new List(); + + 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.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>(); + 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; + } + + /// Красный труп -> останки. True - получилось (труп убран). + 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; + } + } + + /// necroremains - мгновенно истлеть все красные трупы рядом (проверка без ожидания 2 минут). + [UnityEngine.Scripting.Preserve] + public class ConsoleCmdNecroRemains : ConsoleCmdAbstract + { + public override string[] getCommands() => new[] { "necroremains" }; + public override string getDescription() => "NecromancerTome: красные трупы рядом сразу истлевают в останки"; + + public override void Execute(List _params, CommandSenderInfo _senderInfo) + { + World world = GameManager.Instance?.World; + if (world == null) + { + return; + } + var ids = new List(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); + } + } +} diff --git a/NecromancerHarmony.dll b/NecromancerHarmony.dll index 42c1096..720becf 100644 Binary files a/NecromancerHarmony.dll and b/NecromancerHarmony.dll differ diff --git a/NecromancerHarmony.pdb b/NecromancerHarmony.pdb index 6441013..7309122 100644 Binary files a/NecromancerHarmony.pdb and b/NecromancerHarmony.pdb differ diff --git a/Resources/necroremains b/Resources/necroremains new file mode 100644 index 0000000..4cf445c Binary files /dev/null and b/Resources/necroremains differ