Книга: главы собаки, стервятника, волка, медведя; гримуары; чертоги 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
File diff suppressed because one or more lines are too long
+7
View File
@@ -298,6 +298,13 @@
<property name="ModelOffset" value="0,0,0"/> <property name="ModelOffset" value="0,0,0"/>
<property name="MultiBlockDim" value="1,2,1"/> <property name="MultiBlockDim" value="1,2,1"/>
</block> </block>
<block name="necroDoorNumber">
<!-- Табличка с номером двери чертогов (09.10.2026): ставится в редакторе префабов рядом с дверью или над ней,
в ней пишется номер. В чертоги не ставится - вместо неё воздух (MindPalace.Expected), видна только в
редакторе. Дверь с номером закрыта, пока событие не откроет её (MindPalace.SetDoorOpen, necropalace door). -->
<property name="Extends" value="playerSignWood1x1"/>
<property name="CreativeMode" value="Dev"/>
</block>
<block name="necroChalkboard"> <block name="necroChalkboard">
<!-- Грифельная доска (09.10.2026): «как рамка, только тёмная винтажная доска; вписаться в 2x2; надписи как <!-- Грифельная доска (09.10.2026): «как рамка, только тёмная винтажная доска; вписаться в 2x2; надписи как
на ящиках - для сюжета». Модель - бандл necroposters (собирает ItemsUnityProject Assets/Editor/NecroChalkboard.cs): на ящиках - для сюжета». Модель - бандл necroposters (собирает ItemsUnityProject Assets/Editor/NecroChalkboard.cs):
+52 -6
View File
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
namespace NecromancerTome namespace NecromancerTome
@@ -33,6 +34,10 @@ namespace NecromancerTome
static Vector3 _feet; static Vector3 _feet;
static Vector3 _spawn; static Vector3 _spawn;
static float _yaw, _pitch, _vy; static float _yaw, _pitch, _vy;
/// <summary>Высота камеры (ног) со сглаживанием: ступенька поднимает ноги рывком, камера догоняет
/// плавно (тест 09.10 на лестнице чертогов: «двигается рывками»).</summary>
static float _camY;
public const float CamSmooth = 18f;
static bool _grounded; static bool _grounded;
public static bool HasFocus; public static bool HasFocus;
static EntityPlayerLocal _player; static EntityPlayerLocal _player;
@@ -44,6 +49,7 @@ namespace NecromancerTome
_player = _p; _player = _p;
_feet = _at; _feet = _at;
_spawn = _at; _spawn = _at;
_camY = _at.y;
_yaw = _atYaw; _yaw = _atYaw;
_pitch = 0f; _pitch = 0f;
_vy = 0f; _vy = 0f;
@@ -86,6 +92,27 @@ namespace NecromancerTome
return Physics.CheckCapsule(a, b, Radius, SnakeControl.WallMask, QueryTriggerInteraction.Ignore); return Physics.CheckCapsule(a, b, Radius, SnakeControl.WallMask, QueryTriggerInteraction.Ignore);
} }
static float _blockLogAt;
/// <summary>Диагностика (09.10): во что упёрся дух - раз в 2 с, имена коллайдеров.</summary>
static void LogBlocker(Vector3 _p)
{
if (Time.time < _blockLogAt)
{
return;
}
_blockLogAt = Time.time + 2f;
Vector3 a = _p + Vector3.up * (Radius + 0.05f) - Origin.position;
Vector3 b = _p + Vector3.up * (Height - Radius) - Origin.position;
var names = new List<string>();
foreach (Collider col in Physics.OverlapCapsule(a, b, Radius, SnakeControl.WallMask, QueryTriggerInteraction.Ignore))
{
Transform r = col.transform.root;
names.Add(col.name + "@" + (r != null ? r.name : "?") + " " + col.bounds.min.y.ToString("0.00") + ".." + col.bounds.max.y.ToString("0.00"));
}
Debug.Log("[NecromancerTome] MindControl: упёрся у " + _p + " (ноги " + (_p.y - Origin.position.y).ToString("0.00") + "): " + string.Join(", ", names));
}
static bool TryMove(Vector3 _d) static bool TryMove(Vector3 _d)
{ {
Vector3 c = _feet + _d; Vector3 c = _feet + _d;
@@ -94,12 +121,27 @@ namespace NecromancerTome
_feet = c; _feet = c;
return true; return true;
} }
// Ступенька: на полблока выше свободно - шагаем вверх. // Ступенька: шагаем вверх на наименьшую высоту (до StepUp), где свободно, ноги - на верх ступеньки
if (_grounded && !Blocked(c + Vector3.up * StepUp)) // (не выше: иначе на следующем кадре проседали - рывок). Сразу на StepUp нельзя: в проёме с порогом
// и низкой притолокой (сейфовая дверь, тест 09.10: «не получается пройти в проём») голова упиралась.
if (_grounded)
{ {
_feet = c + Vector3.up * StepUp; for (float h = StepUp / 4f; h <= StepUp + 0.001f; h += StepUp / 4f)
return true; {
if (Blocked(c + Vector3.up * h))
{
continue;
}
Vector3 up = c + Vector3.up * h;
if (Physics.Raycast(up + Vector3.up * 0.05f - Origin.position, Vector3.down, out RaycastHit hit, h + 0.05f, SnakeControl.TerrainMask, QueryTriggerInteraction.Ignore))
{
up.y = hit.point.y + Origin.position.y;
}
_feet = up;
return true;
}
} }
LogBlocker(c);
return false; return false;
} }
@@ -161,7 +203,9 @@ namespace NecromancerTome
_vy = 0f; _vy = 0f;
dy = 0f; dy = 0f;
} }
if (Physics.Raycast(_feet + Vector3.up * 0.5f - Origin.position, Vector3.down, out RaycastHit hit, 0.5f - dy + 0.05f, SnakeControl.TerrainMask, QueryTriggerInteraction.Ignore) && dy <= 0f) // Стоял на полу и сошёл со ступеньки - прилипаем к нижней (до StepUp), а не падаем с неё скачками.
float snap = _grounded && _vy <= 0f ? StepUp : 0f;
if (Physics.Raycast(_feet + Vector3.up * 0.5f - Origin.position, Vector3.down, out RaycastHit hit, 0.5f - dy + 0.05f + snap, SnakeControl.TerrainMask, QueryTriggerInteraction.Ignore) && dy <= 0f)
{ {
_feet.y = hit.point.y + Origin.position.y; _feet.y = hit.point.y + Origin.position.y;
_vy = 0f; _vy = 0f;
@@ -184,7 +228,9 @@ namespace NecromancerTome
/// <summary>Из патчей камеры режима змеи: глаза духа и поиск книги под прицелом.</summary> /// <summary>Из патчей камеры режима змеи: глаза духа и поиск книги под прицелом.</summary>
public static void PlaceCamera(Transform _camera) public static void PlaceCamera(Transform _camera)
{ {
Vector3 eye = _feet + Vector3.up * EyeHeight - Origin.position; // Камера догоняет ноги плавно; большой перепад (падение, возврат на точку появления) - сразу.
_camY = Mathf.Abs(_feet.y - _camY) > 1.2f ? _feet.y : Mathf.Lerp(_camY, _feet.y, 1f - Mathf.Exp(-CamSmooth * Time.deltaTime));
Vector3 eye = new Vector3(_feet.x, _camY, _feet.z) + Vector3.up * EyeHeight - Origin.position;
Quaternion rot = Quaternion.Euler(-_pitch, _yaw, 0f); Quaternion rot = Quaternion.Euler(-_pitch, _yaw, 0f);
_camera.SetPositionAndRotation(eye, rot); _camera.SetPositionAndRotation(eye, rot);
HasFocus = FindBook(_camera.position + Origin.position, _camera.forward); HasFocus = FindBook(_camera.position + Origin.position, _camera.forward);
+208 -20
View File
@@ -47,8 +47,18 @@ namespace NecromancerTome
public const string BedBlock = "necroPalaceBed"; public const string BedBlock = "necroPalaceBed";
public const string ShellFrom = "woodShapes:cube"; public const string ShellFrom = "woodShapes:cube";
public const string ShellTo = "terrBedrock"; public const string ShellTo = "terrBedrock";
/// <summary>Межкомнатные двери - их чертоги открывают сами при входе (кроме входной).</summary> /// <summary>Межкомнатные двери - их чертоги открывают сами при входе (кроме входной и дверей с номером).</summary>
public const string PassageDoor = "exteriorHouseDoorWhite"; 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> /// <summary>Одиночная книга на обеденном столе (cntBookPile09 в префабе).</summary>
public static readonly Vector3i BookAt = new Vector3i(12, 2, 9); public static readonly Vector3i BookAt = new Vector3i(12, 2, 9);
/// <summary>Место ножа на столе с реактивами (указание пользователя).</summary> /// <summary>Место ножа на столе с реактивами (указание пользователя).</summary>
@@ -317,6 +327,12 @@ namespace NecromancerTome
_bv = bed; _bv = bed;
_d = MarchingCubes.DensityAir; _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()) else if (_bv.isair || !_bv.Block.shape.IsTerrain())
{ {
_d = MarchingCubes.DensityAir; _d = MarchingCubes.DensityAir;
@@ -412,36 +428,194 @@ namespace NecromancerTome
return Mismatches(_world, _o) == 0; return Mismatches(_world, _o) == 0;
} }
/// <summary>Межкомнатные двери чертогов (PassageDoor) - открыты (в редакторе сохранились закрытыми, а дух /// <summary>Номера дверей из табличек префаба: локальная клетка таблички -> номер.</summary>
/// дверей не открывает), входная - закрыта. Прочие двери (шкаф-тайник, стальная - указание 09.10: «закрыты static Dictionary<Vector3i, int> DoorTags(Prefab _p)
/// и откроются только по событию») не трогаем: их открывает событие. Состояние двери - в её TEFeatureDoor.</summary> {
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) 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; Vector3i s = Size;
int opened = 0; int switched = 0;
for (int y = 0; y < s.y; y++) for (int y = 0; y < s.y; y++)
for (int z = 0; z < s.z; z++) for (int z = 0; z < s.z; z++)
for (int x = 0; x < s.x; x++) 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; wantOpen = lp != EntranceDoorAt;
if (door != null && door.IsOpen() != wantOpen) }
{ else
door.SetOpen(wantOpen); {
opened++; 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 class ConsoleCmdNecroPalace : ConsoleCmdAbstract
{ {
public override string[] getCommands() => new[] { "necropalace" }; 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) public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{ {
@@ -936,6 +1110,20 @@ namespace NecromancerTome
GameManager.Instance.StartCoroutine(MindPalace.StressTest(world, o)); GameManager.Instance.StartCoroutine(MindPalace.StressTest(world, o));
SdtdConsole.Instance.Output("проверка: заливка гравием, через 2 с - постановка (итог - в логе)"); 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") else if (cmd == "rebuild")
{ {
MindPalace.Stamp(world, o); MindPalace.Stamp(world, o);
+144
View File
@@ -0,0 +1,144 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// necroobj &lt;префаб&gt; &lt;имя&gt; - выгрузить модель префаба в OBJ (09.10.2026, для чертежей питомцев в книге:
/// медведь, волк, собака, стервятник - ванильные модели). Скелетные меши «запекаются» в позе префаба (BakeMesh),
/// у LODGroup - только LOD0. Координаты - от корня префаба, оси Unity переведены в правые (x -&gt; -x, обход граней
/// развёрнут). Файл - %APPDATA%\7DaysToDie\NecroMailbox\obj\&lt;имя&gt;.obj, группы «o» по рендерерам, «usemtl» -
/// имя материала. Это сырьё для рендера в Blender: в мод и в репозиторий модели игры не идут (гигиена ассетов).
/// </summary>
[Preserve]
public class ConsoleCmdNecroObj : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necroobj" };
public override string getDescription() => "NecromancerTome: модель префаба в OBJ (necroobj <префаб> <имя>)";
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{
if (_params.Count < 2)
{
SdtdConsole.Instance.Output("necroobj <префаб, например @:Entities/Animals/Bear/animalBearZombiePrefab.prefab> <имя>");
return;
}
GameObject prefab = DataLoader.LoadAsset<GameObject>(_params[0]);
if (prefab == null)
{
SdtdConsole.Instance.Output("Префаб не загрузился: " + _params[0]);
return;
}
GameObject copy = Object.Instantiate(prefab);
copy.transform.position = Vector3.zero;
copy.transform.rotation = Quaternion.identity;
try
{
var renderers = new List<Renderer>();
var lod = copy.GetComponentInChildren<LODGroup>(true);
if (lod != null && lod.GetLODs().Length > 0)
{
renderers.AddRange(lod.GetLODs()[0].renderers);
}
if (renderers.Count == 0)
{
foreach (Renderer r in copy.GetComponentsInChildren<Renderer>(true))
{
if (r is SkinnedMeshRenderer || r is MeshRenderer)
{
renderers.Add(r);
}
}
}
var sb = new StringBuilder();
var ci = CultureInfo.InvariantCulture;
int vBase = 1, tris = 0;
Matrix4x4 toRoot = copy.transform.worldToLocalMatrix;
foreach (Renderer r in renderers)
{
if (r == null)
{
continue;
}
Mesh mesh;
Matrix4x4 m;
if (r is SkinnedMeshRenderer smr)
{
mesh = new Mesh();
smr.BakeMesh(mesh, true);
// Запечённый меш - в пространстве рендерера без его масштаба (useScale учтён в вершинах).
m = toRoot * Matrix4x4.TRS(smr.transform.position, smr.transform.rotation, Vector3.one);
}
else
{
MeshFilter mf = r.GetComponent<MeshFilter>();
if (mf == null || mf.sharedMesh == null || !mf.sharedMesh.isReadable)
{
continue;
}
mesh = mf.sharedMesh;
m = toRoot * r.transform.localToWorldMatrix;
}
if (!mesh.isReadable && !(r is SkinnedMeshRenderer))
{
continue;
}
Vector3[] v = mesh.vertices;
Vector2[] uv = mesh.uv;
sb.Append("o ").Append(r.name).Append('\n');
foreach (Vector3 p0 in v)
{
Vector3 p = m.MultiplyPoint3x4(p0);
sb.Append("v ").Append((-p.x).ToString("F5", ci)).Append(' ').Append(p.y.ToString("F5", ci)).Append(' ')
.Append(p.z.ToString("F5", ci)).Append('\n');
}
bool hasUv = uv != null && uv.Length == v.Length;
if (hasUv)
{
foreach (Vector2 t in uv)
{
sb.Append("vt ").Append(t.x.ToString("F5", ci)).Append(' ').Append(t.y.ToString("F5", ci)).Append('\n');
}
}
Material[] mats = r.sharedMaterials;
for (int s = 0; s < mesh.subMeshCount; s++)
{
sb.Append("usemtl ").Append(s < mats.Length && mats[s] != null ? mats[s].name.Replace(' ', '_') : "none").Append('\n');
int[] idx = mesh.GetTriangles(s);
for (int i = 0; i + 2 < idx.Length; i += 3)
{
// обход развёрнут из-за смены руки осей
int a = idx[i] + vBase, b = idx[i + 2] + vBase, c = idx[i + 1] + vBase;
if (hasUv)
{
sb.Append("f ").Append(a).Append('/').Append(a).Append(' ').Append(b).Append('/').Append(b).Append(' ')
.Append(c).Append('/').Append(c).Append('\n');
}
else
{
sb.Append("f ").Append(a).Append(' ').Append(b).Append(' ').Append(c).Append('\n');
}
tris++;
}
}
vBase += v.Length;
}
string dir = Path.Combine(GameIO.GetUserGameDataDir(), "NecroMailbox", "obj");
Directory.CreateDirectory(dir);
string path = Path.Combine(dir, _params[1] + ".obj");
File.WriteAllText(path, sb.ToString());
string msg = "necroobj: " + _params[1] + " - рендереров " + renderers.Count + ", вершин " + (vBase - 1) + ", треуг. " + tris + " -> " + path;
Debug.Log("[NecromancerTome] " + msg);
SdtdConsole.Instance.Output(msg);
}
finally
{
Object.Destroy(copy);
}
}
}
}
+32
View File
@@ -77,6 +77,38 @@ namespace NecromancerTome
ImageTop = true, ImageTop = true,
Unlocked = _p => true Unlocked = _p => true
}, },
new Chapter
{
TitleKey = "necroTomeDogTitle",
PageKeys = new[] { "necroTomeDogP1", "necroTomeDogP2", "necroTomeDogP3" },
Image = "@modfolder(NecromancerTome):Textures/necroTomeDog.png",
ImageTop = true,
Unlocked = _p => true
},
new Chapter
{
TitleKey = "necroTomeVultureTitle",
PageKeys = new[] { "necroTomeVultureP1", "necroTomeVultureP2", "necroTomeVultureP3" },
Image = "@modfolder(NecromancerTome):Textures/necroTomeVulture.png",
ImageTop = true,
Unlocked = _p => true
},
new Chapter
{
TitleKey = "necroTomeWolfTitle",
PageKeys = new[] { "necroTomeWolfP1", "necroTomeWolfP2", "necroTomeWolfP3" },
Image = "@modfolder(NecromancerTome):Textures/necroTomeWolf.png",
ImageTop = true,
Unlocked = _p => true
},
new Chapter
{
TitleKey = "necroTomeBearTitle",
PageKeys = new[] { "necroTomeBearP1", "necroTomeBearP2", "necroTomeBearP3" },
Image = "@modfolder(NecromancerTome):Textures/necroTomeBear.png",
ImageTop = true,
Unlocked = _p => true
},
}; };
public static void Open(EntityPlayerLocal _player) public static void Open(EntityPlayerLocal _player)
+4 -1
View File
@@ -550,7 +550,10 @@ namespace NecromancerTome
_flyMove = move; _flyMove = move;
_flyRun = run; _flyRun = run;
_flyUp = input != null && !gui && input.Jump.IsPressed; _flyUp = input != null && !gui && input.Jump.IsPressed;
_flyDown = input != null && !gui && (input.Crouch.IsPressed || _crouchToggled); // Только C (пока держишь): Ctrl-переключатель у стервятника не используется (указание 09.10:
// «снижается, пока не прожмёшь Ctrl снова», а индикатора, как у приседания, нет).
_flyDown = input != null && !gui && input.Crouch.IsPressed;
_crouchToggled = false;
Crouching = false; Crouching = false;
if (attack && _carry != null) if (attack && _carry != null)
{ {
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+44 -19
View File
@@ -8,24 +8,39 @@
<property name="DifficultyTier" value="0" /> <property name="DifficultyTier" value="0" />
<property name="ShowQuestClearCount" value="1" /> <property name="ShowQuestClearCount" value="1" />
<property name="TraderArea" value="False" /> <property name="TraderArea" value="False" />
<property name="PrefabSize" value="25, 8, 25" /> <property name="PrefabSize" value="25, 13, 25" />
<property name="RotationToFaceNorth" value="2" /> <property name="RotationToFaceNorth" value="2" />
<property name="Zoning" value="none" /> <property name="Zoning" value="none" />
<property class="IndexedBlockOffsets"> <property class="IndexedBlockOffsets">
<property class="TraderOnOff">
<property name="0" value="12, 1, 13" />
<property name="1" value="18, 6, 13" />
<property name="2" value="14, 1, 16" />
<property name="3" value="18, 6, 18" />
<property name="4" value="14, 1, 19" />
</property>
<property class="POILight"> <property class="POILight">
<property name="0" value="11, 3, 9" /> <property name="0" value="14, 9, 12" />
<property name="1" value="12, 3, 9" /> <property name="1" value="16, 4, 12" />
<property name="2" value="9, 3, 13" /> <property name="2" value="18, 4, 12" />
<property name="3" value="8, 4, 10" /> <property name="3" value="20, 4, 12" />
<property name="4" value="11, 3, 10" /> <property name="4" value="18, 9, 13" />
<property name="5" value="20, 9, 13" />
<property name="6" value="16, 4, 14" />
<property name="7" value="18, 4, 14" />
<property name="8" value="20, 4, 14" />
<property name="9" value="16, 4, 16" />
<property name="10" value="18, 4, 16" />
<property name="11" value="20, 4, 16" />
<property name="12" value="15, 9, 18" />
<property name="13" value="18, 9, 18" />
<property name="14" value="20, 9, 18" />
<property name="15" value="15, 4, 19" />
<property name="16" value="15, 9, 19" />
</property> </property>
<property class="challenge_sinks"> <property class="challenge_sinks">
<property name="0" value="13, 1, 3" /> <property name="0" value="15, 1, 18" />
<property name="1" value="13, 1, 4" /> <property name="1" value="15, 1, 21" />
</property>
<property class="TraderOnOff">
<property name="0" value="8, 1, 8" />
<property name="1" value="10, 1, 11" />
</property> </property>
<property class="challenge_gore"> <property class="challenge_gore">
<property name="0" value="3, 2, 9" /> <property name="0" value="3, 2, 9" />
@@ -33,18 +48,28 @@
<property class="challenge_mech_parts"> <property class="challenge_mech_parts">
<property name="0" value="6, 1, 13" /> <property name="0" value="6, 1, 13" />
</property> </property>
<property class="challenge_polymers">
<property name="0" value="17, 1, 19" />
<property name="1" value="18, 1, 19" />
<property name="2" value="20, 1, 19" />
<property name="3" value="21, 1, 19" />
<property name="4" value="17, 1, 20" />
<property name="5" value="18, 1, 20" />
<property name="6" value="20, 1, 20" />
<property name="7" value="21, 1, 20" />
</property>
</property> </property>
<property class="Stats"> <property class="Stats">
<property name="LightsVolume" value="9206.96" /> <property name="LightsVolume" value="42763.35" />
<property name="TotalVertices" value="108077" /> <property name="TotalVertices" value="218641" />
<property name="TotalTriangles" value="69295" /> <property name="TotalTriangles" value="114274" />
<property class="BlockEntities"> <property class="BlockEntities">
<property name="Vertices" value="76900" /> <property name="Vertices" value="169120" />
<property name="Triangles" value="53018" /> <property name="Triangles" value="88691" />
</property> </property>
<property class="ChunkMeshes0"> <property class="ChunkMeshes0">
<property name="Vertices" value="30885" /> <property name="Vertices" value="49229" />
<property name="Triangles" value="16053" /> <property name="Triangles" value="25359" />
</property> </property>
<property class="ChunkMeshes1"> <property class="ChunkMeshes1">
<property name="Vertices" value="0" /> <property name="Vertices" value="0" />
Binary file not shown.

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB