Книга: главы собаки, стервятника, волка, медведя; гримуары; чертоги 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
+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);
}
}
}
}