Книга: главы собаки, стервятника, волка, медведя; гримуары; чертоги 25x13x25 с нумерованными дверями

- Главы питомцев с чертежами (Fig. III-VI), команда necroobj для выгрузки моделей
- Книги призыва питомцев переименованы в гримуары (13 языков)
- Чертоги: второй этаж; таблички necroDoorNumber (только в редакторе) нумеруют двери,
  дверь с номером закрыта до события (SetDoorOpen, necropalace door N open|close),
  с начала открыты 2, 4, 5, 6
- Дух: плавный шаг по ступенькам, проход в проёмы с порогом
- Стервятник: снижение только C, без Ctrl-переключателя

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-09 21:43:25 +03:00
co-authored by Claude Opus 5.5
parent 32065dacac
commit bdca1bdba5
17 changed files with 517 additions and 55 deletions
+208 -20
View File
@@ -47,8 +47,18 @@ namespace NecromancerTome
public const string BedBlock = "necroPalaceBed";
public const string ShellFrom = "woodShapes:cube";
public const string ShellTo = "terrBedrock";
/// <summary>Межкомнатные двери - их чертоги открывают сами при входе (кроме входной).</summary>
/// <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>
@@ -317,6 +327,12 @@ namespace NecromancerTome
_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;
@@ -412,36 +428,194 @@ namespace NecromancerTome
return Mismatches(_world, _o) == 0;
}
/// <summary>Межкомнатные двери чертогов (PassageDoor) - открыты (в редакторе сохранились закрытыми, а дух
/// дверей не открывает), входная - закрыта. Прочие двери (шкаф-тайник, стальная - указание 09.10: «закрыты
/// и откроются только по событию») не трогаем: их открывает событие. Состояние двери - в её TEFeatureDoor.</summary>
/// <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 opened = 0;
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++)
{
if (_world.GetTileEntity(_o + new Vector3i(x, y, z)) is TileEntityComposite tec)
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)
{
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++;
}
wantOpen = lp != EntranceDoorAt;
}
else
{
continue;
}
if (door.IsOpen() != wantOpen)
{
door.SetOpen(wantOpen);
switched++;
}
}
if (opened > 0)
if (switched > 0)
{
Debug.Log("[NecromancerTome] MindPalace: дверей переключено " + opened);
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);
}
}
@@ -900,7 +1074,7 @@ namespace NecromancerTome
public class ConsoleCmdNecroPalace : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necropalace" };
public override string getDescription() => "NecromancerTome: чертоги разума - where | scan | rebuild | test | knife N";
public override string getDescription() => "NecromancerTome: чертоги разума - where | scan | rebuild | test | knife N | door [N open|close]";
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{
@@ -936,6 +1110,20 @@ namespace NecromancerTome
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);