В чертогах игра держала две полные области мира (у чертогов и у тела). Тело теперь сжато до 3 участков, как у змеи и одержимого; при выходе возвращается. FreezeProbe пишет каждый кадр дольше 100 мс: длительность, сборки мусора за кадр, режим, число наблюдателей областей, куча. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1175 lines
49 KiB
C#
1175 lines
49 KiB
C#
using System.Collections.Generic;
|
||
using System.IO;
|
||
using HarmonyLib;
|
||
using UnityEngine;
|
||
using UnityEngine.Scripting;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// Ментальная форма - «Чертоги разума» (решения пользователя 02-03.10, BACKLOG «Чертоги разума»).
|
||
/// Вход - третья строка меню ножа некроманта на R, только ночью, здоровье не тратит. Тело
|
||
/// падает рэгдоллом и лежит беззащитным, как в режиме змеи; дух - камера без сущности
|
||
/// (MindControl.cs): ходит по полу, видит только своё, с другими игроками не встречается.
|
||
/// Выход - R, утро или смерть тела.
|
||
///
|
||
/// ГДЕ ЧЕРТОГИ. Под землёй, в радиационном поясе у северо-восточного угла карты: сначала от
|
||
/// угла по диагонали ищется пояс (biomeProvider.GetRadiationAt > 2.5); пояс шире префаба -
|
||
/// чертоги в его середине, уже (Navezgane: 16 блоков) - в 45 блоках от края. Своё место у каждого
|
||
/// игрока - вдоль северного края с шагом SlotStep (дальше дальности показа сущностей). Места
|
||
/// пишутся в файл в папке сохранения мира, чтобы смена ширины пояса в новой версии игры их не
|
||
/// сдвинула.
|
||
///
|
||
/// КАК СТАВЯТСЯ. Префаб chertogi (PalacePrefab/, нарисован пользователем в редакторе игры)
|
||
/// грузится Prefab.Load и ставится поблочно через World.SetBlocksRPC - НЕ Prefab.CopyIntoLocal:
|
||
/// тот перед постановкой вычищает весь столб над префабом до неба от не-земляных блоков,
|
||
/// и над чертогами вырубило бы деревья (видно сверху). Заодно: внешний слой оболочки
|
||
/// (woodShapes:cube) -> terrBedrock (в меню творчества его нет, рисовали деревом),
|
||
/// одиночная книга на столе -> necroMemoryBook, на стол с реактивами - нож-декорация,
|
||
/// кровать (SleepingBag) -> necroPalaceBed: иначе игра делает её точкой возрождения игрока.
|
||
/// Построены ли - сверка с префабом каждой клетки (Mismatches: тип, плотность, покраска) при каждом входе:
|
||
/// расхождение - ставим заново (самопочинка; заодно новый, более широкий префаб сам перестраивает старые
|
||
/// чертоги в сейвах). Надписи на досках из префаба НЕ переносятся (указание 09.10: «текст будет добавлять мод по событиям»).
|
||
///
|
||
/// ОХРАНА. Внутри чертогов игра считает место «зоной торговца» (патч IsWithinTraderArea):
|
||
/// блоки не ломаются ни ударом, ни взрывом, строить нельзя. Чужие сущности в коробке чертогов
|
||
/// раз в секунду выбрасываются: змея развеивается, игрок - на поверхность, прочие - удаляются.
|
||
///
|
||
/// ОГРАНИЧЕНИЕ: одиночная игра и хост. Клиенту в мультиплеере вход пока не даётся - чертоги ставит
|
||
/// сервер, а камера живёт у клиента (решать отдельно).
|
||
/// </summary>
|
||
public static class MindPalace
|
||
{
|
||
public const string PrefabName = "chertogi";
|
||
public const string BookBlock = "necroMemoryBook";
|
||
public const string KnifeBlock = "necroKnifeDecor";
|
||
/// <summary>Кровать без класса SleepingBag - ею заменяется кровать префаба (bunkBedMadeWhite).</summary>
|
||
public const string BedBlock = "necroPalaceBed";
|
||
public const string ShellFrom = "woodShapes:cube";
|
||
public const string ShellTo = "terrBedrock";
|
||
/// <summary>Межкомнатные двери - их чертоги открывают сами при входе (кроме входной и дверей с номером).</summary>
|
||
public const string PassageDoor = "exteriorHouseDoorWhite";
|
||
/// <summary>Табличка с номером двери (указание 09.10): ставится в редакторе рядом с дверью или над ней,
|
||
/// в ней пишется номер (до двух цифр). В чертоги не ставится (вместо неё воздух) - видна только в редакторе.
|
||
/// Дверь с номером закрыта, пока событие не откроет её (SetDoorOpen).</summary>
|
||
public const string DoorTagBlock = "necroDoorNumber";
|
||
/// <summary>Двери с номером, открытые с самого начала (указание 09.10): 2 туалет, 4 ванная, 5 прихожая,
|
||
/// 6 лаборатория. Остальные закрыты до события: 1 входная - бутафорская, не открывается никогда; 3 лестница,
|
||
/// 7 шкаф-тайник, 8 за ним, 9 решётка, 10-11 сейфовые, 12, 14-17 второй этаж.</summary>
|
||
public static readonly HashSet<int> DefaultOpenDoors = new HashSet<int> { 2, 4, 5, 6 };
|
||
/// <summary>Как далеко от таблички искать её дверь (клеток по каждой оси; над дверью - 2).</summary>
|
||
public const int DoorTagReach = 2;
|
||
/// <summary>Одиночная книга на обеденном столе (cntBookPile09 в префабе).</summary>
|
||
public static readonly Vector3i BookAt = new Vector3i(12, 2, 9);
|
||
/// <summary>Место ножа на столе с реактивами (указание пользователя).</summary>
|
||
public static readonly Vector3i KnifeAt = new Vector3i(4, 2, 11);
|
||
/// <summary>Входная дверь чертогов - во внутреннюю оболочку, никуда не ведёт: остаётся закрытой
|
||
/// (указание 03.10). Остальные четыре - между комнатами - открываются.</summary>
|
||
public static readonly Vector3i EntranceDoorAt = new Vector3i(8, 1, 3);
|
||
/// <summary>Где появляется дух: комната с книгой, свободная клетка у стола.</summary>
|
||
public static readonly Vector3 SpawnAt = new Vector3(11.5f, 1.05f, 11.5f);
|
||
public const float SpawnYaw = 180f;
|
||
public const int SlotStep = 300;
|
||
public const int FallbackInset = 45;
|
||
public const int FloorY = 3;
|
||
/// <summary>Рамка вокруг чертогов, которая заливается камнем перед постановкой.</summary>
|
||
public const int LooseMargin = 3;
|
||
public const float RadiationThreshold = 2.5f;
|
||
/// <summary>Поворот ножа-декорации (0-23): 9 - плашмя на столешнице (подобрано пользователем
|
||
/// 03.10 командой necropalace knife N).</summary>
|
||
public static int KnifeRotation = 0;
|
||
|
||
public static bool Active;
|
||
static Vector3i _origin;
|
||
static Prefab _prefab;
|
||
static ChunkManager.ChunkObserver _observer;
|
||
enum Stage { None, Loading, Settling, Ready }
|
||
static Stage _stage;
|
||
static float _deadline;
|
||
static float _settleUntil;
|
||
static float _ragdollAt;
|
||
static bool _waitStandUp;
|
||
static float _standUpDeadline;
|
||
static float _guardCheck;
|
||
|
||
static readonly Dictionary<string, Vector3i> _slots = new Dictionary<string, Vector3i>();
|
||
static string _slotsFor;
|
||
|
||
public static void Init()
|
||
{
|
||
ModEvents.UnityUpdate.RegisterHandler(OnUnityUpdate);
|
||
MindHud.Create();
|
||
}
|
||
|
||
// ---------------------------------------------------------------- места
|
||
|
||
static string SlotsFile => Path.Combine(GameIO.GetSaveGameDir(), "NecromancerTome_palaces.txt");
|
||
|
||
static void LoadSlots()
|
||
{
|
||
string dir = GameIO.GetSaveGameDir();
|
||
if (_slotsFor == dir)
|
||
{
|
||
return;
|
||
}
|
||
_slotsFor = dir;
|
||
_slots.Clear();
|
||
try
|
||
{
|
||
if (File.Exists(SlotsFile))
|
||
{
|
||
foreach (string line in File.ReadAllLines(SlotsFile))
|
||
{
|
||
string[] p = line.Split(' ');
|
||
if (p.Length == 4 && int.TryParse(p[1], out int x) && int.TryParse(p[2], out int y) && int.TryParse(p[3], out int z))
|
||
{
|
||
_slots[p[0]] = new Vector3i(x, y, z);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (System.Exception ex)
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] MindPalace: не прочитать места - " + ex.Message);
|
||
}
|
||
}
|
||
|
||
static void SaveSlots()
|
||
{
|
||
try
|
||
{
|
||
var lines = new List<string>();
|
||
foreach (var kv in _slots)
|
||
{
|
||
lines.Add(kv.Key + " " + kv.Value.x + " " + kv.Value.y + " " + kv.Value.z);
|
||
}
|
||
File.WriteAllLines(SlotsFile, lines);
|
||
}
|
||
catch (System.Exception ex)
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] MindPalace: не записать места - " + ex.Message);
|
||
}
|
||
}
|
||
|
||
static string PlayerKey(EntityPlayer _player)
|
||
{
|
||
string id = GameManager.Instance.persistentLocalPlayer?.PrimaryId?.CombinedString;
|
||
return string.IsNullOrEmpty(id) ? "local" : id.Replace(' ', '_');
|
||
}
|
||
|
||
static Vector3i GetSlot(World _world, EntityPlayer _player)
|
||
{
|
||
LoadSlots();
|
||
string key = PlayerKey(_player);
|
||
if (_slots.TryGetValue(key, out Vector3i o))
|
||
{
|
||
return o;
|
||
}
|
||
o = ComputeSlot(_world, _slots.Count);
|
||
_slots[key] = o;
|
||
SaveSlots();
|
||
Debug.Log("[NecromancerTome] MindPalace: место " + _slots.Count + " для " + key + " - " + o);
|
||
return o;
|
||
}
|
||
|
||
/// <summary>Отступ от края: середина радиационного пояса, если он шире префаба, иначе 45.</summary>
|
||
static int Inset(World _world, Vector3i _min, Vector3i _max, int _size)
|
||
{
|
||
IBiomeProvider biomes = _world.ChunkCache?.ChunkProvider?.GetBiomeProvider();
|
||
int start = -1, end = -1;
|
||
if (biomes != null)
|
||
{
|
||
for (int t = 0; t < 600; t += 4)
|
||
{
|
||
bool rad = biomes.GetRadiationAt(_max.x - t, _max.z - t) > RadiationThreshold;
|
||
if (rad && start < 0) start = t;
|
||
if (!rad && start >= 0) { end = t; break; }
|
||
}
|
||
}
|
||
if (start >= 0 && end > start && end - start >= _size + 20)
|
||
{
|
||
return Mathf.Max(FallbackInset, (start + end) / 2 - _size / 2);
|
||
}
|
||
return FallbackInset;
|
||
}
|
||
|
||
static Vector3i ComputeSlot(World _world, int _index)
|
||
{
|
||
_world.GetWorldExtent(out Vector3i min, out Vector3i max);
|
||
Prefab p = LoadPrefab();
|
||
int size = p != null ? Mathf.Max(p.size.x, p.size.z) : 17;
|
||
int inset = Inset(_world, min, max, size);
|
||
int perEdge = Mathf.Max(1, (max.x - min.x - 2 * inset - size) / SlotStep);
|
||
int x, z;
|
||
if (_index < perEdge)
|
||
{
|
||
x = max.x - inset - size - _index * SlotStep;
|
||
z = max.z - inset - size;
|
||
}
|
||
else
|
||
{
|
||
x = max.x - inset - size;
|
||
z = max.z - inset - size - (_index - perEdge + 1) * SlotStep;
|
||
}
|
||
x = Mathf.FloorToInt(x / 16f) * 16;
|
||
z = Mathf.FloorToInt(z / 16f) * 16;
|
||
return new Vector3i(x, FloorY, z);
|
||
}
|
||
|
||
// ---------------------------------------------------------------- префаб и постройка
|
||
|
||
static string ModDir => Path.GetDirectoryName(typeof(MindPalace).Assembly.Location);
|
||
|
||
static Prefab LoadPrefab()
|
||
{
|
||
if (_prefab != null)
|
||
{
|
||
return _prefab;
|
||
}
|
||
string path = Path.Combine(ModDir, "PalacePrefab", PrefabName + ".tts");
|
||
var p = new Prefab();
|
||
if (!p.Load(new PathAbstractions.AbstractedLocation(PathAbstractions.EAbstractedLocationType.Mods, PrefabName, path, null, false)))
|
||
{
|
||
Debug.LogError("[NecromancerTome] MindPalace: префаб не загрузился - " + path);
|
||
return null;
|
||
}
|
||
_prefab = p;
|
||
Debug.Log("[NecromancerTome] MindPalace: префаб " + PrefabName + " " + p.size);
|
||
return _prefab;
|
||
}
|
||
|
||
static bool ChunksLoaded(World _world, Vector3i _o, Vector3i _size)
|
||
{
|
||
for (int cx = World.toChunkXZ(_o.x); cx <= World.toChunkXZ(_o.x + _size.x - 1); cx++)
|
||
{
|
||
for (int cz = World.toChunkXZ(_o.z); cz <= World.toChunkXZ(_o.z + _size.z - 1); cz++)
|
||
{
|
||
// Не просто «есть в памяти», а сгенерирован и украшен: иначе генерация досыпала
|
||
// землю поверх поставленного (тест 03.10: «внутри блоки камня»).
|
||
Chunk c = _world.GetChunkSync(cx, cz) as Chunk;
|
||
if (c == null || !c.IsInitialized || c.NeedsDecoration)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/// <summary>Стоит ли блок уже в самом префабе (03.10: пользователь расставляет книгу и нож в редакторе
|
||
/// сам) - тогда старые принудительные замены (книга в BookAt, нож в KnifeAt) не делаем.</summary>
|
||
static bool PrefabHas(Prefab _p, string _block)
|
||
{
|
||
if (_hasCache == null || _hasCacheFor != _p)
|
||
{
|
||
_hasCache = new Dictionary<string, bool>();
|
||
_hasCacheFor = _p;
|
||
}
|
||
if (_hasCache.TryGetValue(_block, out bool has))
|
||
{
|
||
return has;
|
||
}
|
||
int type = Block.GetBlockValue(_block).type;
|
||
for (int y = 0; y < _p.size.y && !has; y++)
|
||
for (int z = 0; z < _p.size.z && !has; z++)
|
||
for (int x = 0; x < _p.size.x && !has; x++)
|
||
{
|
||
has = _p.GetBlock(x, y, z).type == type;
|
||
}
|
||
_hasCache[_block] = has;
|
||
return has;
|
||
}
|
||
|
||
static Dictionary<string, bool> _hasCache;
|
||
static Prefab _hasCacheFor;
|
||
|
||
/// <summary>Что должно стоять в клетке префаба после постановки (с заменами). False - клетку
|
||
/// не трогаем (дочерняя часть многоблочного предмета).</summary>
|
||
static bool Expected(Prefab _p, int _x, int _y, int _z, out BlockValue _bv, out sbyte _d)
|
||
{
|
||
_bv = _p.GetBlock(_x, _y, _z);
|
||
_d = _p.GetDensity(_x, _y, _z);
|
||
if (_bv.ischild)
|
||
{
|
||
return false;
|
||
}
|
||
Vector3i lp = new Vector3i(_x, _y, _z);
|
||
// Пол (y 0) - НЕ bedrock: в префабе он же пол комнат, а bedrock - блок-земля, её сглаженная
|
||
// поверхность выпирала из пола в комнаты каменными наплывами (тест 03.10: «в помещении
|
||
// блоки камня»). Защита снизу - отдельный слой bedrock под префабом (FloorShield).
|
||
bool outer = _x == 0 || _z == 0 || _x == _p.size.x - 1 || _z == _p.size.z - 1 || _y == _p.size.y - 1;
|
||
if (outer && !_bv.isair && _bv.Block.GetBlockName() == ShellFrom)
|
||
{
|
||
_bv = Block.GetBlockValue(ShellTo);
|
||
_d = MarchingCubes.DensityTerrain;
|
||
}
|
||
else if (lp == BookAt && !PrefabHas(_p, BookBlock))
|
||
{
|
||
BlockValue b = Block.GetBlockValue(BookBlock);
|
||
b.rotation = _bv.rotation;
|
||
_bv = b;
|
||
_d = MarchingCubes.DensityAir;
|
||
}
|
||
else if (lp == KnifeAt && !PrefabHas(_p, KnifeBlock))
|
||
{
|
||
BlockValue k = Block.GetBlockValue(KnifeBlock);
|
||
k.rotation = (byte)KnifeRotation;
|
||
_bv = k;
|
||
_d = MarchingCubes.DensityAir;
|
||
}
|
||
else if (!_bv.isair && _bv.Block is BlockSleepingBag)
|
||
{
|
||
// Кровать префаба - просто мебель (тест 03.10: синий камень портала отправил в чертоги).
|
||
// Чертоги ставятся SetBlocksRPC без игрока, а GameManager.ChangeBlocks тогда считает
|
||
// ставившим локального игрока и любую SleepingBag-кровать делает его точкой возрождения.
|
||
BlockValue bed = Block.GetBlockValue(BedBlock);
|
||
bed.rotation = _bv.rotation;
|
||
_bv = bed;
|
||
_d = MarchingCubes.DensityAir;
|
||
}
|
||
else if (!_bv.isair && _bv.Block.GetBlockName() == DoorTagBlock)
|
||
{
|
||
// Табличка номера двери - только для редактора.
|
||
_bv = BlockValue.Air;
|
||
_d = MarchingCubes.DensityAir;
|
||
}
|
||
else if (_bv.isair || !_bv.Block.shape.IsTerrain())
|
||
{
|
||
_d = MarchingCubes.DensityAir;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/// <summary>Диагностика (03.10, «нож пропал со стола»): что стоит в клетке и есть ли у блока модель.</summary>
|
||
static void LogCell(World _world, Vector3i _wp, string _what)
|
||
{
|
||
BlockValue bv = _world.GetBlock(_wp);
|
||
string s = "[NecromancerTome] MindPalace: " + _what + " " + _wp + " - " + bv.Block.GetBlockName() + " пов. " + bv.rotation;
|
||
Chunk c = _world.GetChunkFromWorldPos(_wp) as Chunk;
|
||
BlockEntityData be = c?.GetBlockEntity(_wp);
|
||
if (be == null)
|
||
{
|
||
s += ", модели нет (нет BlockEntity)";
|
||
}
|
||
else if (!be.bHasTransform || be.transform == null)
|
||
{
|
||
s += ", модель ещё не создана";
|
||
}
|
||
else
|
||
{
|
||
Renderer[] rs = be.transform.GetComponentsInChildren<Renderer>(true);
|
||
int on = 0;
|
||
foreach (Renderer r in rs) if (r.enabled && r.gameObject.activeInHierarchy) on++;
|
||
s += ", модель " + be.transform.name + " в " + (be.transform.position + Origin.position) + ", активна " + be.transform.gameObject.activeInHierarchy + ", рендеров " + on + "/" + rs.Length;
|
||
}
|
||
Debug.Log(s);
|
||
}
|
||
|
||
/// <summary>Сколько клеток в мире не совпадает с префабом по типу блока (0 - чертоги целы).</summary>
|
||
public static int Mismatches(World _world, Vector3i _o, List<string> _report = null)
|
||
{
|
||
Prefab p = LoadPrefab();
|
||
if (p == null)
|
||
{
|
||
return -1;
|
||
}
|
||
int n = 0;
|
||
int shieldType = Block.GetBlockValue(ShellTo).type;
|
||
for (int z = 0; z < p.size.z; z++)
|
||
for (int x = 0; x < p.size.x; x++)
|
||
{
|
||
if (_world.GetBlock(_o + new Vector3i(x, -1, z)).type != shieldType)
|
||
{
|
||
n++;
|
||
}
|
||
}
|
||
for (int y = 0; y < p.size.y; y++)
|
||
for (int z = 0; z < p.size.z; z++)
|
||
for (int x = 0; x < p.size.x; x++)
|
||
{
|
||
Vector3i wp = _o + new Vector3i(x, y, z);
|
||
if (!Expected(p, x, y, z, out BlockValue want, out sbyte wantD))
|
||
{
|
||
// Дочерняя клетка: там не должно быть земли/камня.
|
||
BlockValue c = _world.GetBlock(wp);
|
||
if (c.Block.shape.IsTerrain() || _world.GetDensity(wp.x, wp.y, wp.z) < 0)
|
||
{
|
||
n++;
|
||
if (_report != null && _report.Count < 40)
|
||
{
|
||
_report.Add("(" + x + "," + y + "," + z + ") дочерняя клетка, стоит " + c.Block.GetBlockName() + " плотн. " + _world.GetDensity(wp.x, wp.y, wp.z));
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
BlockValue have = _world.GetBlock(wp);
|
||
// Плотность тоже: тип блока - воздух, а плотность земли - и клетка рисуется камнем
|
||
// (тест 03.10: «камень под столом, в дверном проёме, под потолком»).
|
||
sbyte haveD = _world.GetDensity(wp.x, wp.y, wp.z);
|
||
bool densityBad = (wantD < 0) != (haveD < 0);
|
||
// И покраска (указание 03.10: «покраску обязательно перенеси из префаба»).
|
||
bool paintBad = !want.isair && want.type == p.GetBlock(x, y, z).type &&
|
||
_world.GetTextureFullArray(wp.x, wp.y, wp.z) != p.GetTexture(x, y, z);
|
||
if (have.type != want.type || densityBad || paintBad)
|
||
{
|
||
n++;
|
||
if (_report != null && _report.Count < 40)
|
||
{
|
||
_report.Add("(" + x + "," + y + "," + z + ") ждали " + want.Block.GetBlockName() + " плотн. " + wantD +
|
||
", стоит " + have.Block.GetBlockName() + " плотн. " + haveD + (paintBad ? ", не та покраска" : ""));
|
||
}
|
||
}
|
||
}
|
||
return n;
|
||
}
|
||
|
||
public static bool IsBuilt(World _world, Vector3i _o)
|
||
{
|
||
return Mismatches(_world, _o) == 0;
|
||
}
|
||
|
||
/// <summary>Номера дверей из табличек префаба: локальная клетка таблички -> номер.</summary>
|
||
static Dictionary<Vector3i, int> DoorTags(Prefab _p)
|
||
{
|
||
var tags = new Dictionary<Vector3i, int>();
|
||
foreach (var kv in _p.tileEntities)
|
||
{
|
||
Vector3i lp = kv.Key;
|
||
if (_p.GetBlock(lp.x, lp.y, lp.z).Block?.GetBlockName() != DoorTagBlock ||
|
||
!(kv.Value is TileEntityComposite tec) || !tec.TryGetSelfOrFeature(out TEFeatureSignable sign))
|
||
{
|
||
continue;
|
||
}
|
||
string text = (sign.GetAuthoredText()?.Text ?? "").Trim();
|
||
if (int.TryParse(text, out int n))
|
||
{
|
||
tags[lp] = n;
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] MindPalace: табличка двери " + lp + " без номера: «" + text + "»");
|
||
}
|
||
}
|
||
return tags;
|
||
}
|
||
|
||
/// <summary>Двери с номерами: номер -> локальная клетка двери (ближайшая к табличке дверь в пределах DoorTagReach).</summary>
|
||
public static Dictionary<int, Vector3i> NumberedDoors(World _world, Vector3i _o)
|
||
{
|
||
var doors = new Dictionary<int, Vector3i>();
|
||
Prefab p = LoadPrefab();
|
||
if (p == null)
|
||
{
|
||
return doors;
|
||
}
|
||
foreach (var kv in DoorTags(p))
|
||
{
|
||
Vector3i best = default;
|
||
int bestD = int.MaxValue;
|
||
for (int dy = -DoorTagReach; dy <= DoorTagReach; dy++)
|
||
for (int dz = -DoorTagReach; dz <= DoorTagReach; dz++)
|
||
for (int dx = -DoorTagReach; dx <= DoorTagReach; dx++)
|
||
{
|
||
Vector3i lp = kv.Key + new Vector3i(dx, dy, dz);
|
||
int d = dx * dx + dy * dy + dz * dz;
|
||
if (d < bestD && _world.GetTileEntity(_o + lp) is TileEntityComposite tec && tec.GetFeature<TEFeatureDoor>() != null)
|
||
{
|
||
best = lp;
|
||
bestD = d;
|
||
}
|
||
}
|
||
if (bestD == int.MaxValue)
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] MindPalace: у таблички " + kv.Key + " (дверь " + kv.Value + ") рядом нет двери");
|
||
}
|
||
else if (doors.ContainsKey(kv.Value))
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] MindPalace: номер двери " + kv.Value + " повторяется (" + doors[kv.Value] + " и " + best + ")");
|
||
}
|
||
else
|
||
{
|
||
doors[kv.Value] = best;
|
||
}
|
||
}
|
||
return doors;
|
||
}
|
||
|
||
/// <summary>Состояние дверей чертогов при каждом входе. Дверь с номером (табличка DoorTagBlock) открыта, только
|
||
/// если событие её открыло (SetDoorOpen). Межкомнатные двери без номера (PassageDoor) открыты (в редакторе
|
||
/// сохранились закрытыми, а дух дверей не открывает), входная - закрыта. Прочие двери без номера не трогаем.
|
||
/// Состояние двери - в её TEFeatureDoor.</summary>
|
||
public static void OpenDoors(World _world, Vector3i _o)
|
||
{
|
||
var numbered = new Dictionary<Vector3i, int>();
|
||
foreach (var kv in NumberedDoors(_world, _o))
|
||
{
|
||
numbered[kv.Value] = kv.Key;
|
||
}
|
||
HashSet<int> opened = OpenedDoors(_world.GetPrimaryPlayer());
|
||
Vector3i s = Size;
|
||
int switched = 0;
|
||
for (int y = 0; y < s.y; y++)
|
||
for (int z = 0; z < s.z; z++)
|
||
for (int x = 0; x < s.x; x++)
|
||
{
|
||
Vector3i lp = new Vector3i(x, y, z);
|
||
if (!(_world.GetTileEntity(_o + lp) is TileEntityComposite tec) || !(tec.GetFeature<TEFeatureDoor>() is TEFeatureDoor door))
|
||
{
|
||
continue;
|
||
}
|
||
bool wantOpen;
|
||
if (numbered.TryGetValue(lp, out int n))
|
||
{
|
||
// В файле: n - открыта событием, -n - закрыта событием (для открытых с начала).
|
||
wantOpen = opened.Contains(n) || DefaultOpenDoors.Contains(n) && !opened.Contains(-n);
|
||
}
|
||
else if (_world.GetBlock(_o + lp).Block.GetBlockName() == PassageDoor)
|
||
{
|
||
// Входная - закрыть (могла остаться открытой от прежних входов), остальные - открыть.
|
||
wantOpen = lp != EntranceDoorAt;
|
||
}
|
||
else
|
||
{
|
||
continue;
|
||
}
|
||
if (door.IsOpen() != wantOpen)
|
||
{
|
||
door.SetOpen(wantOpen);
|
||
switched++;
|
||
}
|
||
}
|
||
if (switched > 0)
|
||
{
|
||
Debug.Log("[NecromancerTome] MindPalace: дверей переключено " + switched + ", с номерами " + numbered.Count + ", открыты по событиям: " + string.Join(",", opened));
|
||
}
|
||
}
|
||
|
||
// Открытые событиями двери - по игрокам, в файле в папке сохранения мира: «игрок номер номер ...».
|
||
static string DoorsFile => Path.Combine(GameIO.GetSaveGameDir(), "NecromancerTome_palaceDoors.txt");
|
||
static readonly Dictionary<string, HashSet<int>> _openedDoors = new Dictionary<string, HashSet<int>>();
|
||
static string _openedFor;
|
||
|
||
static HashSet<int> OpenedDoors(EntityPlayer _player)
|
||
{
|
||
string dir = GameIO.GetSaveGameDir();
|
||
if (_openedFor != dir)
|
||
{
|
||
_openedFor = dir;
|
||
_openedDoors.Clear();
|
||
try
|
||
{
|
||
if (File.Exists(DoorsFile))
|
||
{
|
||
foreach (string line in File.ReadAllLines(DoorsFile))
|
||
{
|
||
string[] parts = line.Split(' ');
|
||
var set = new HashSet<int>();
|
||
for (int i = 1; i < parts.Length; i++)
|
||
{
|
||
if (int.TryParse(parts[i], out int n))
|
||
{
|
||
set.Add(n);
|
||
}
|
||
}
|
||
_openedDoors[parts[0]] = set;
|
||
}
|
||
}
|
||
}
|
||
catch (System.Exception ex)
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] MindPalace: не прочитать двери - " + ex.Message);
|
||
}
|
||
}
|
||
string key = _player != null ? PlayerKey(_player) : "local";
|
||
if (!_openedDoors.TryGetValue(key, out HashSet<int> opened))
|
||
{
|
||
opened = new HashSet<int>();
|
||
_openedDoors[key] = opened;
|
||
}
|
||
return opened;
|
||
}
|
||
|
||
/// <summary>Открыть/закрыть дверь чертогов с номером (для событий). Запоминается в сейве; если игрок сейчас
|
||
/// в чертогах - дверь переключается сразу.</summary>
|
||
public static void SetDoorOpen(EntityPlayer _player, int _number, bool _open)
|
||
{
|
||
HashSet<int> opened = OpenedDoors(_player);
|
||
bool changed = _open ? opened.Remove(-_number) | opened.Add(_number) : opened.Remove(_number) | DefaultOpenDoors.Contains(_number) && opened.Add(-_number);
|
||
if (!changed)
|
||
{
|
||
return;
|
||
}
|
||
try
|
||
{
|
||
var lines = new List<string>();
|
||
foreach (var kv in _openedDoors)
|
||
{
|
||
lines.Add(kv.Key + (kv.Value.Count > 0 ? " " + string.Join(" ", kv.Value) : ""));
|
||
}
|
||
File.WriteAllLines(DoorsFile, lines);
|
||
}
|
||
catch (System.Exception ex)
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] MindPalace: не записать двери - " + ex.Message);
|
||
}
|
||
World world = GameManager.Instance.World;
|
||
if (_stage == Stage.Ready && world != null && _player == world.GetPrimaryPlayer())
|
||
{
|
||
OpenDoors(world, _origin);
|
||
}
|
||
}
|
||
|
||
/// <summary>Надписи на досках чертогов (указание 09.10). Лор: некроманта бросили в этот мир умирать - раздели,
|
||
/// лишили артефактов (и вживлённых тоже) и ментальной магией стёрли память, «закрыв знания, опасные для общества».
|
||
/// Доска показывает информацию из подсознания, которую герой пока не помнит (указание 09.10); строки прибавляются по событиям игры. Пока событие -
|
||
/// только день: (с какого дня, ключ локализации). Остальные строки - позднее.</summary>
|
||
public static readonly (int day, string key)[] BoardLines =
|
||
{
|
||
(1, "necroPalaceBoardRevenge"),
|
||
(5, "necroPalaceBoardTide"), // было «излучение красной луны»; 10.10 - кровавый прилив
|
||
};
|
||
|
||
/// <summary>При каждом входе: на каждой доске (necroChalkboard) чертогов - все наступившие строки, на языке игрока.
|
||
/// Текст из префаба в чертоги не идёт (Stamp ставит только блоки) - пишет только мод.</summary>
|
||
public static void UpdateBoards(World _world, Vector3i _o)
|
||
{
|
||
int day = GameUtils.WorldTimeToDays(_world.worldTime);
|
||
var lines = new List<string>();
|
||
foreach (var l in BoardLines)
|
||
{
|
||
if (day >= l.day)
|
||
{
|
||
lines.Add(Localization.Get(l.key));
|
||
}
|
||
}
|
||
string text = string.Join("\n", lines);
|
||
Vector3i s = Size;
|
||
int n = 0;
|
||
for (int y = 0; y < s.y; y++)
|
||
for (int z = 0; z < s.z; z++)
|
||
for (int x = 0; x < s.x; x++)
|
||
{
|
||
Vector3i wp = _o + new Vector3i(x, y, z);
|
||
if (_world.GetBlock(wp).Block?.GetBlockName() != Chalkboard.BlockName ||
|
||
!(_world.GetTileEntity(wp) is TileEntityComposite tec) || !tec.TryGetSelfOrFeature(out TEFeatureSignable sign))
|
||
{
|
||
continue;
|
||
}
|
||
if ((sign.GetAuthoredText()?.Text ?? "") != text)
|
||
{
|
||
sign.SetText(text, true);
|
||
n++;
|
||
}
|
||
}
|
||
if (n > 0)
|
||
{
|
||
Debug.Log("[NecromancerTome] MindPalace: надпись на досках (день " + day + ", строк " + lines.Count + "), досок " + n);
|
||
}
|
||
}
|
||
|
||
public static void Stamp(World _world, Vector3i _o)
|
||
{
|
||
Prefab p = LoadPrefab();
|
||
if (p == null)
|
||
{
|
||
return;
|
||
}
|
||
// ПОРЯДОК ВАЖЕН (тест 03.10: при очистке всей коробки разом на миг открывалась пустота
|
||
// 17x8x17, земля над ней и края теряли опору - обвал, падающие блоки ушли под мир):
|
||
// 1) слой bedrock под чертогами;
|
||
// 2) оболочка - пол (y 0), стены (края по x/z) и крыша - сразу на место земли, без очистки;
|
||
// 3) очистка внутренности (её уже держит оболочка) - в том числе дочерних клеток стола,
|
||
// дверей, шкафов, где иначе оставался камень;
|
||
// 4) всё внутреннее по префабу.
|
||
var list = new List<BlockChangeInfo>();
|
||
// 0) Рамка LooseMargin блоков вокруг чертогов, со всех сторон и сверху, - сплошь камнем
|
||
// (указание 03.10: «заполни сначала камнем, на 3 блока во все стороны»): земля, песок,
|
||
// гравий (осыпаются без опоры) и пустоты пещер -> terrStone. Не земля и не воздух
|
||
// (чужие постройки) - не трогаем.
|
||
BlockValue stone = Block.GetBlockValue("terrStone");
|
||
int loose = 0;
|
||
for (int y = -1 - LooseMargin; y < p.size.y + LooseMargin; y++)
|
||
for (int z = -LooseMargin; z < p.size.z + LooseMargin; z++)
|
||
for (int x = -LooseMargin; x < p.size.x + LooseMargin; x++)
|
||
{
|
||
bool inside = y >= -1 && y < p.size.y && x >= 0 && x < p.size.x && z >= 0 && z < p.size.z;
|
||
if (inside)
|
||
{
|
||
continue;
|
||
}
|
||
Vector3i wp = _o + new Vector3i(x, y, z);
|
||
if (wp.y < 1)
|
||
{
|
||
continue; // ниже 1 игра блоки не ставит (дно мира)
|
||
}
|
||
BlockValue have = _world.GetBlock(wp);
|
||
Block b = have.Block;
|
||
if (have.type != stone.type && (have.isair || b != null && b.shape.IsTerrain()))
|
||
{
|
||
list.Add(new BlockChangeInfo(wp, stone, MarchingCubes.DensityTerrain));
|
||
loose++;
|
||
}
|
||
}
|
||
if (loose > 0)
|
||
{
|
||
Debug.Log("[NecromancerTome] MindPalace: рамка вокруг залита камнем - " + loose + " клеток");
|
||
}
|
||
BlockValue shield = Block.GetBlockValue(ShellTo);
|
||
for (int z = 0; z < p.size.z; z++)
|
||
for (int x = 0; x < p.size.x; x++)
|
||
{
|
||
list.Add(new BlockChangeInfo(_o + new Vector3i(x, -1, z), shield, MarchingCubes.DensityTerrain));
|
||
}
|
||
for (int pass = 0; pass < 3; pass++)
|
||
for (int yi = 0; yi < p.size.y; yi++)
|
||
for (int z = 0; z < p.size.z; z++)
|
||
for (int x = 0; x < p.size.x; x++)
|
||
{
|
||
// Очистка (проход 1) - СВЕРХУ ВНИЗ: снизу вверх убранный слой оставлял над собой
|
||
// песок/гравий без опоры, тот падал и рушил уже поставленные стены (тест 03.10,
|
||
// necropalace test: 30 расхождений сразу, 309 - при следующем входе). Постановка -
|
||
// снизу вверх, на опору.
|
||
int y = pass == 1 ? p.size.y - 1 - yi : yi;
|
||
bool shellCell = y == 0 || y == p.size.y - 1 || x == 0 || z == 0 || x == p.size.x - 1 || z == p.size.z - 1;
|
||
Vector3i wp = _o + new Vector3i(x, y, z);
|
||
if (pass == 0 && shellCell || pass == 2 && !shellCell)
|
||
{
|
||
if (Expected(p, x, y, z, out BlockValue bv, out sbyte d))
|
||
{
|
||
// С покраской из префаба (указание 03.10).
|
||
list.Add(new BlockChangeInfo(wp, bv, d, p.GetTexture(x, y, z)));
|
||
}
|
||
}
|
||
else if (pass == 1 && !shellCell)
|
||
{
|
||
list.Add(new BlockChangeInfo(wp, BlockValue.Air, MarchingCubes.DensityAir));
|
||
}
|
||
}
|
||
_world.SetBlocksRPC(list);
|
||
Debug.Log("[NecromancerTome] MindPalace: чертоги поставлены в " + _o + ", блоков " + list.Count);
|
||
}
|
||
|
||
static Vector3i Size => _prefab != null ? _prefab.size : new Vector3i(17, 8, 17);
|
||
|
||
/// <summary>Проверка постановки в нетронутую землю (указание 03.10): залить камнем коробку на
|
||
/// 2 блока шире чертогов во все стороны (включая слой bedrock снизу), подождать, поставить
|
||
/// чертоги заново, выставить двери. Сразу заливкой, без предварительной очистки - очистка и
|
||
/// есть та пустота без опоры, из-за которой был обвал.</summary>
|
||
public static System.Collections.IEnumerator StressTest(World _world, Vector3i _o)
|
||
{
|
||
Vector3i s = Size;
|
||
// Гравием - худший случай: сыпучее, падает без опоры (указание 03.10).
|
||
BlockValue stone = Block.GetBlockValue("terrGravel");
|
||
var list = new List<BlockChangeInfo>();
|
||
for (int y = -3; y < s.y + 2; y++)
|
||
for (int z = -2; z < s.z + 2; z++)
|
||
for (int x = -2; x < s.x + 2; x++)
|
||
{
|
||
list.Add(new BlockChangeInfo(_o + new Vector3i(x, y, z), stone, MarchingCubes.DensityTerrain));
|
||
}
|
||
_world.SetBlocksRPC(list);
|
||
Debug.Log("[NecromancerTome] necropalace test: залито гравием " + list.Count + " клеток, ждём 2 с");
|
||
yield return new WaitForSeconds(2f);
|
||
Stamp(_world, _o);
|
||
yield return new WaitForSeconds(1f);
|
||
OpenDoors(_world, _o);
|
||
Debug.Log("[NecromancerTome] necropalace test: готово, расхождений с префабом " + Mismatches(_world, _o));
|
||
}
|
||
|
||
public static bool InAnyPalace(Vector3i _pos, int _margin = 0)
|
||
{
|
||
if (_slots.Count == 0)
|
||
{
|
||
return false;
|
||
}
|
||
Vector3i s = Size;
|
||
foreach (Vector3i o in _slots.Values)
|
||
{
|
||
if (_pos.x >= o.x - _margin && _pos.x < o.x + s.x + _margin &&
|
||
_pos.y >= o.y - 1 - _margin && _pos.y < o.y + s.y + _margin &&
|
||
_pos.z >= o.z - _margin && _pos.z < o.z + s.z + _margin)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// ---------------------------------------------------------------- вход и выход
|
||
|
||
public static void Enter(EntityPlayerLocal _player)
|
||
{
|
||
if (Active || SnakeMode.Active || _player == null || _player.IsDead() || _player.AttachedToEntity != null)
|
||
{
|
||
return;
|
||
}
|
||
if (!SnakeMode.IsNight())
|
||
{
|
||
Debug.Log("[NecromancerTome] MindPalace: днём чертоги недоступны");
|
||
return;
|
||
}
|
||
if (SnakeMode.IsBloodTide())
|
||
{
|
||
SnakeMode.BloodTideHint(_player);
|
||
Debug.Log("[NecromancerTome] MindPalace: кровавый прилив - дух не покидает тело");
|
||
return;
|
||
}
|
||
World world = GameManager.Instance.World;
|
||
if (world.IsRemote())
|
||
{
|
||
Debug.Log("[NecromancerTome] MindPalace: в мультиплеере у клиента пока недоступно");
|
||
return;
|
||
}
|
||
if (LoadPrefab() == null)
|
||
{
|
||
return;
|
||
}
|
||
_origin = GetSlot(world, _player);
|
||
Vector3 center = _origin.ToVector3() + new Vector3(Size.x * 0.5f, 0f, Size.z * 0.5f);
|
||
// Дальность как у змеи: при радиусе 2 игра не строила коллизии участка (тест 03.10:
|
||
// «чертоги не построились вовремя» - ждали коллизий 30 с).
|
||
_observer = GameManager.Instance.AddChunkObserver(center, true, GameUtils.GetViewDistance(), -1);
|
||
ShrinkBody(_player);
|
||
_stage = Stage.Loading;
|
||
_deadline = Time.time + 30f;
|
||
Active = true;
|
||
_player.ScreenEffectManager?.SetScreenEffect("FadeToBlack", 1f, 0.6f);
|
||
_player.SetFirstPersonView(false, true);
|
||
_ragdollAt = Time.time + SnakeMode.RagdollDelay;
|
||
_waitStandUp = false;
|
||
Debug.Log("[NecromancerTome] MindPalace: вход, чертоги в " + _origin);
|
||
}
|
||
|
||
public static void Exit(EntityPlayerLocal _player)
|
||
{
|
||
if (!Active)
|
||
{
|
||
return;
|
||
}
|
||
Active = false;
|
||
_stage = Stage.None;
|
||
MindControl.End(_player);
|
||
RemoveObserver();
|
||
_player?.ScreenEffectManager?.SetScreenEffect("FadeToBlack", 0f, 1f);
|
||
if (_player == null || _player.IsDead())
|
||
{
|
||
return;
|
||
}
|
||
_ragdollAt = 0f;
|
||
_player.emodel.ragdollTime = _player.emodel.ragdollDuration;
|
||
_waitStandUp = true;
|
||
_standUpDeadline = Time.time + 5f;
|
||
Debug.Log("[NecromancerTome] MindPalace: выход");
|
||
}
|
||
|
||
static void RemoveObserver()
|
||
{
|
||
if (_observer != null && GameManager.Instance?.World != null)
|
||
{
|
||
GameManager.Instance.RemoveChunkObserver(_observer);
|
||
}
|
||
_observer = null;
|
||
EntityPlayerLocal p = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||
if (_bodyViewDim > 0 && p?.ChunkObserver != null)
|
||
{
|
||
p.ChunkObserver.viewDim = _bodyViewDim;
|
||
}
|
||
_bodyViewDim = -1;
|
||
}
|
||
|
||
/// <summary>Область мира у тела до входа (-1 - не сжата). Фризы в чертогах (жалоба 10.10): игра держала две
|
||
/// полные области - у чертогов на краю карты и у лежащего тела; подгрузка и пересборка участков - мусор
|
||
/// и паузы его сборки. Тело сжимаем до Possession.BodyViewDim, как у змеи и одержимого.</summary>
|
||
static int _bodyViewDim = -1;
|
||
|
||
static void ShrinkBody(EntityPlayerLocal _player)
|
||
{
|
||
ChunkManager.ChunkObserver body = _player?.ChunkObserver;
|
||
if (body != null && _bodyViewDim < 0 && body.viewDim > Possession.BodyViewDim)
|
||
{
|
||
_bodyViewDim = body.viewDim;
|
||
body.viewDim = Possession.BodyViewDim;
|
||
}
|
||
}
|
||
|
||
static void OnUnityUpdate(ref ModEvents.SUnityUpdateData _data)
|
||
{
|
||
World world = GameManager.Instance?.World;
|
||
if (world == null)
|
||
{
|
||
Active = false;
|
||
_stage = Stage.None;
|
||
_observer = null;
|
||
MindControl.End(null);
|
||
_slotsFor = null;
|
||
_bodyViewDim = -1;
|
||
return;
|
||
}
|
||
EntityPlayerLocal player = world.GetPrimaryPlayer();
|
||
Guard(world);
|
||
if (!Active)
|
||
{
|
||
if (_waitStandUp && player != null && (!player.emodel.IsRagdollActive || Time.time > _standUpDeadline))
|
||
{
|
||
_waitStandUp = false;
|
||
player.SetFirstPersonView(true, true);
|
||
}
|
||
return;
|
||
}
|
||
if (player == null)
|
||
{
|
||
return;
|
||
}
|
||
if (player.IsDead())
|
||
{
|
||
Debug.Log("[NecromancerTome] MindPalace: тело убито");
|
||
Exit(player);
|
||
return;
|
||
}
|
||
if (!SnakeMode.IsNight())
|
||
{
|
||
Debug.Log("[NecromancerTome] MindPalace: наступило утро - обратно в тело");
|
||
Exit(player);
|
||
return;
|
||
}
|
||
if (SnakeMode.IsBloodTide())
|
||
{
|
||
SnakeMode.BloodTideHint(player);
|
||
Debug.Log("[NecromancerTome] MindPalace: кровавый прилив - дух выброшен в тело");
|
||
Exit(player);
|
||
return;
|
||
}
|
||
Body(player);
|
||
switch (_stage)
|
||
{
|
||
case Stage.Loading:
|
||
if (ChunksLoaded(world, _origin, Size))
|
||
{
|
||
var report = new List<string>();
|
||
int bad = Mismatches(world, _origin, report);
|
||
if (bad != 0)
|
||
{
|
||
Debug.Log("[NecromancerTome] MindPalace: расхождений с префабом " + bad + " - ставим заново");
|
||
for (int i = 0; i < report.Count && i < 10; i++)
|
||
{
|
||
Debug.Log("[NecromancerTome] MindPalace: " + report[i]);
|
||
}
|
||
Stamp(world, _origin);
|
||
}
|
||
_stage = Stage.Settling;
|
||
_settleUntil = Time.time + 1.5f;
|
||
}
|
||
else if (Time.time > _deadline)
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] MindPalace: участки чертогов не загрузились");
|
||
Exit(player);
|
||
}
|
||
break;
|
||
case Stage.Settling:
|
||
Vector3 spawn = _origin.ToVector3() + SpawnAt;
|
||
// Коллизии готовы - или прошло 5 с: тогда всё равно пускаем, от провала страхует
|
||
// MindControl (возврат на точку появления).
|
||
if (Time.time > _settleUntil && (world.IsChunkAreaCollidersLoaded(spawn) || Time.time > _settleUntil + 5f))
|
||
{
|
||
Debug.Log("[NecromancerTome] MindPalace: коллизии " + (world.IsChunkAreaCollidersLoaded(spawn) ? "готовы" : "НЕ готовы, пускаем по таймеру"));
|
||
_stage = Stage.Ready;
|
||
OpenDoors(world, _origin);
|
||
UpdateBoards(world, _origin);
|
||
LogCell(world, _origin + KnifeAt, "нож");
|
||
LogCell(world, _origin + BookAt, "книга");
|
||
MindControl.Begin(player, spawn, SpawnYaw);
|
||
player.ScreenEffectManager?.SetScreenEffect("FadeToBlack", 0f, 1.5f);
|
||
}
|
||
else if (Time.time > _deadline)
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] MindPalace: чертоги не построились вовремя");
|
||
Exit(player);
|
||
}
|
||
break;
|
||
case Stage.Ready:
|
||
MindControl.Update(player);
|
||
break;
|
||
}
|
||
// Выход по R - когда тело уже лежит и не открыто окно (как у змеи).
|
||
PlayerActionsLocal input = player.playerInput;
|
||
bool gui = player.PlayerUI != null && player.PlayerUI.windowManager.IsModalWindowOpen();
|
||
if (_ragdollAt <= 0f && player.emodel.IsRagdollActive && input != null && !gui &&
|
||
(input.Reload.WasPressed || input.PermanentActions.Reload.WasPressed))
|
||
{
|
||
Exit(player);
|
||
}
|
||
}
|
||
|
||
/// <summary>Тело падает и лежит - как в режиме змеи (SnakeMode.OnUnityUpdate).</summary>
|
||
static void Body(EntityPlayerLocal _player)
|
||
{
|
||
if (_ragdollAt > 0f)
|
||
{
|
||
if (Time.time >= _ragdollAt)
|
||
{
|
||
_ragdollAt = 0f;
|
||
_player.emodel.DoRagdoll(EModelBase.RagdollMode.Default, 999999f, EnumBodyPartHit.None, Vector3.zero, Vector3.zero, false);
|
||
}
|
||
}
|
||
else if (!_player.emodel.IsRagdollActive)
|
||
{
|
||
_player.emodel.DoRagdoll(EModelBase.RagdollMode.Default, 999999f, EnumBodyPartHit.None, Vector3.zero, Vector3.zero, false);
|
||
}
|
||
}
|
||
|
||
/// <summary>Раз в секунду: всё живое в коробке чертогов - вон. Дух сущности не имеет.</summary>
|
||
static void Guard(World _world)
|
||
{
|
||
_guardCheck -= Time.deltaTime;
|
||
if (_guardCheck > 0f || _world.IsRemote())
|
||
{
|
||
return;
|
||
}
|
||
_guardCheck = 1f;
|
||
LoadSlots();
|
||
if (_slots.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
// Игроков в EntityAlives нет (World.SpawnEntityInWorld кладёт туда только не-игроков) - их
|
||
// отдельно, по Players.list (тест 03.10: камень портала привёл в чертоги, охрана молчала).
|
||
for (int i = _world.Players.list.Count - 1; i >= 0; i--)
|
||
{
|
||
EntityPlayer pl = _world.Players.list[i];
|
||
if (pl == null || pl.IsDead() || !InAnyPalace(World.worldToBlockPos(pl.position)))
|
||
{
|
||
continue;
|
||
}
|
||
Vector3i b = World.worldToBlockPos(pl.position);
|
||
Vector3 up = new Vector3(pl.position.x, _world.GetTerrainHeight(b.x, b.z) + 2f, pl.position.z);
|
||
Debug.Log("[NecromancerTome] MindPalace: игрок " + pl.entityId + " в чертогах - выброшен на " + up);
|
||
if (pl is EntityPlayerLocal)
|
||
{
|
||
// Как камень портала (PortalStonePatch.TeleportToBedroll).
|
||
NetPackageManager.GetPackage<NetPackageTeleportPlayer>().Setup(up, null).ProcessPackage(_world, GameManager.Instance);
|
||
}
|
||
else
|
||
{
|
||
pl.Teleport(up);
|
||
}
|
||
}
|
||
for (int i = _world.EntityAlives.Count - 1; i >= 0; i--)
|
||
{
|
||
EntityAlive e = _world.EntityAlives[i];
|
||
if (e == null || e.IsDead() || !InAnyPalace(World.worldToBlockPos(e.position)))
|
||
{
|
||
continue;
|
||
}
|
||
EntityAlive snake = SnakeMode.Snake(_world);
|
||
if (snake != null && e == snake)
|
||
{
|
||
Debug.Log("[NecromancerTome] MindPalace: змея в чертогах - развеяна");
|
||
SnakeMode.Exit(_world.GetPrimaryPlayer());
|
||
}
|
||
else
|
||
{
|
||
Debug.Log("[NecromancerTome] MindPalace: " + e.EntityName + " в чертогах - удалён");
|
||
_world.RemoveEntity(e.entityId, EnumRemoveEntityReason.Despawned);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>В чертогах и в рамке камня вокруг ничего не падает (указание 03.10: «почему не
|
||
/// отключить просчёт физики в той области»): World.AddFallingBlock - единственная очередь
|
||
/// падающих блоков (туда же игра сама не пускает блоки со StabilityIgnore). Тест гравием
|
||
/// 03.10: после постановки нижний/верхний ряд внутренней стены всё равно обрушивался.</summary>
|
||
[HarmonyPatch(typeof(World), nameof(World.AddFallingBlock))]
|
||
public static class Patch_World_AddFallingBlock_MindPalace
|
||
{
|
||
public static bool Prefix(Vector3i _blockPos)
|
||
{
|
||
return !MindPalace.InAnyPalace(_blockPos, MindPalace.LooseMargin);
|
||
}
|
||
}
|
||
|
||
/// <summary>Чертоги - «зона торговца»: блоки не ломаются ударом и взрывом, строить нельзя
|
||
/// (ItemActionAttack, Explosion, Block.DamageBlock проверяют IsWithinTraderArea).</summary>
|
||
[HarmonyPatch(typeof(World), nameof(World.IsWithinTraderArea), new[] { typeof(Vector3i) })]
|
||
public static class Patch_World_IsWithinTraderArea_MindPalace
|
||
{
|
||
public static void Postfix(Vector3i _worldBlockPos, ref bool __result)
|
||
{
|
||
if (!__result && MindPalace.InAnyPalace(_worldBlockPos))
|
||
{
|
||
__result = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// necropalace - отладка чертогов: where (где мои), rebuild (поставить заново, если участки
|
||
/// загружены), knife N (поворот ножа 0-23 и переставить его).
|
||
/// </summary>
|
||
[Preserve]
|
||
public class ConsoleCmdNecroPalace : ConsoleCmdAbstract
|
||
{
|
||
public override string[] getCommands() => new[] { "necropalace" };
|
||
public override string getDescription() => "NecromancerTome: чертоги разума - where | scan | rebuild | test | knife N | door [N open|close]";
|
||
|
||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||
{
|
||
World world = GameManager.Instance.World;
|
||
EntityPlayerLocal player = world?.GetPrimaryPlayer();
|
||
if (player == null)
|
||
{
|
||
return;
|
||
}
|
||
Vector3i o = Traverse.Create(typeof(MindPalace)).Method("GetSlot", world, (EntityPlayer)player).GetValue<Vector3i>();
|
||
string cmd = _params.Count > 0 ? _params[0] : "where";
|
||
if (cmd == "knife" && _params.Count > 1 && int.TryParse(_params[1], out int r))
|
||
{
|
||
MindPalace.KnifeRotation = Mathf.Clamp(r, 0, 3);
|
||
BlockValue k = Block.GetBlockValue(MindPalace.KnifeBlock);
|
||
k.rotation = (byte)MindPalace.KnifeRotation;
|
||
world.SetBlockRPC(o + MindPalace.KnifeAt, k, MarchingCubes.DensityAir);
|
||
SdtdConsole.Instance.Output("нож: поворот " + MindPalace.KnifeRotation);
|
||
}
|
||
else if (cmd == "scan")
|
||
{
|
||
var report = new List<string>();
|
||
int n = MindPalace.Mismatches(world, o, report);
|
||
SdtdConsole.Instance.Output("расхождений с префабом: " + n);
|
||
foreach (string line in report)
|
||
{
|
||
SdtdConsole.Instance.Output(line);
|
||
Debug.Log("[NecromancerTome] necropalace scan: " + line);
|
||
}
|
||
}
|
||
else if (cmd == "test")
|
||
{
|
||
GameManager.Instance.StartCoroutine(MindPalace.StressTest(world, o));
|
||
SdtdConsole.Instance.Output("проверка: заливка гравием, через 2 с - постановка (итог - в логе)");
|
||
}
|
||
else if (cmd == "door" && _params.Count > 2 && int.TryParse(_params[1], out int dn))
|
||
{
|
||
MindPalace.SetDoorOpen(player, dn, _params[2] == "open");
|
||
SdtdConsole.Instance.Output("дверь " + dn + ": " + (_params[2] == "open" ? "открыта" : "закрыта"));
|
||
}
|
||
else if (cmd == "door")
|
||
{
|
||
var doors = MindPalace.NumberedDoors(world, o);
|
||
SdtdConsole.Instance.Output("дверей с номерами: " + doors.Count);
|
||
foreach (var kv in doors)
|
||
{
|
||
SdtdConsole.Instance.Output(" " + kv.Key + " - " + world.GetBlock(o + kv.Value).Block.GetBlockName() + " " + kv.Value);
|
||
}
|
||
}
|
||
else if (cmd == "rebuild")
|
||
{
|
||
MindPalace.Stamp(world, o);
|
||
SdtdConsole.Instance.Output("чертоги поставлены заново в " + o);
|
||
}
|
||
else
|
||
{
|
||
SdtdConsole.Instance.Output("чертоги: " + o + ", построены: " + MindPalace.IsBuilt(world, o));
|
||
}
|
||
}
|
||
}
|
||
}
|