- Книга некроманта: главы «Нож некроманта» и «Могильная крыса» - лист-чертёж сверху (Textures/, три вида из Blender, подписи на эсперанто), управление и лор; окно книги непрозрачное, текст с контуром - «Дух крысы» переименован в «Могильную крысу» на 13 языках (ключи прежние) - Блоки (Dev): плакат-синька ножа (necroPosterKnife, 3:2 под полотно рамки), грифельная доска necroChalkboard (надписи - TEFeatureSignable, писать только в редакторе/творческом; шрифт Neucha OFL, свой шейдер текста с тестом глубины), дверь-книжный шкаф necroBookDoor (своя модель с аниматором, хитбокс створки через RootTransformRefParent) - Чертоги: префаб 25x8x25 (промежуточный); открываются только межкомнатные двери; надписи на досках по дням игры: «Отомсти архимагу!», с 5-го дня «Берегись излучения красной луны» - Нож: силовая атака стоит 20 здоровья, с «Мёртвой бурей» 40; не хватает - itembreak, «Недостаточно здоровья» и обычный удар; описания на 13 языках - Консольная команда necromodel - устройство модели блока в лог Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
951 lines
40 KiB
C#
951 lines
40 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>Одиночная книга на обеденном столе (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.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>Межкомнатные двери чертогов (PassageDoor) - открыты (в редакторе сохранились закрытыми, а дух
|
||
/// дверей не открывает), входная - закрыта. Прочие двери (шкаф-тайник, стальная - указание 09.10: «закрыты
|
||
/// и откроются только по событию») не трогаем: их открывает событие. Состояние двери - в её TEFeatureDoor.</summary>
|
||
public static void OpenDoors(World _world, Vector3i _o)
|
||
{
|
||
Vector3i s = Size;
|
||
int opened = 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++)
|
||
{
|
||
if (_world.GetTileEntity(_o + new Vector3i(x, y, z)) is TileEntityComposite tec)
|
||
{
|
||
TEFeatureDoor door = tec.GetFeature<TEFeatureDoor>();
|
||
if (door == null || _world.GetBlock(_o + new Vector3i(x, y, z)).Block.GetBlockName() != PassageDoor)
|
||
{
|
||
continue;
|
||
}
|
||
// Входная - закрыть (могла остаться открытой от прежних входов), остальные - открыть.
|
||
bool wantOpen = new Vector3i(x, y, z) != EntranceDoorAt;
|
||
if (door != null && door.IsOpen() != wantOpen)
|
||
{
|
||
door.SetOpen(wantOpen);
|
||
opened++;
|
||
}
|
||
}
|
||
}
|
||
if (opened > 0)
|
||
{
|
||
Debug.Log("[NecromancerTome] MindPalace: дверей переключено " + opened);
|
||
}
|
||
}
|
||
|
||
/// <summary>Надписи на досках чертогов (указание 09.10). Лор: некроманта бросили в этот мир умирать - раздели,
|
||
/// лишили артефактов (и вживлённых тоже) и ментальной магией стёрли память, «закрыв знания, опасные для общества».
|
||
/// Доска - то, что он ещё помнит или успел себе написать; строки прибавляются по событиям игры. Пока событие -
|
||
/// только день: (с какого дня, ключ локализации). Остальные строки - позднее.</summary>
|
||
public static readonly (int day, string key)[] BoardLines =
|
||
{
|
||
(1, "necroPalaceBoardRevenge"),
|
||
(5, "necroPalaceBoardRedMoon"),
|
||
};
|
||
|
||
/// <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;
|
||
}
|
||
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);
|
||
_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;
|
||
}
|
||
|
||
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;
|
||
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;
|
||
}
|
||
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";
|
||
|
||
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 == "rebuild")
|
||
{
|
||
MindPalace.Stamp(world, o);
|
||
SdtdConsole.Instance.Output("чертоги поставлены заново в " + o);
|
||
}
|
||
else
|
||
{
|
||
SdtdConsole.Instance.Output("чертоги: " + o + ", построены: " + MindPalace.IsBuilt(world, o));
|
||
}
|
||
}
|
||
}
|
||
}
|