Книга: главы собаки, стервятника, волка, медведя; гримуары; чертоги 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:
co-authored by
Claude Opus 5.5
parent
32065dacac
commit
bdca1bdba5
+26
-9
File diff suppressed because one or more lines are too long
@@ -298,6 +298,13 @@
|
||||
<property name="ModelOffset" value="0,0,0"/>
|
||||
<property name="MultiBlockDim" value="1,2,1"/>
|
||||
</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">
|
||||
<!-- Грифельная доска (09.10.2026): «как рамка, только тёмная винтажная доска; вписаться в 2x2; надписи как
|
||||
на ящиках - для сюжета». Модель - бандл necroposters (собирает ItemsUnityProject Assets/Editor/NecroChalkboard.cs):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
@@ -33,6 +34,10 @@ namespace NecromancerTome
|
||||
static Vector3 _feet;
|
||||
static Vector3 _spawn;
|
||||
static float _yaw, _pitch, _vy;
|
||||
/// <summary>Высота камеры (ног) со сглаживанием: ступенька поднимает ноги рывком, камера догоняет
|
||||
/// плавно (тест 09.10 на лестнице чертогов: «двигается рывками»).</summary>
|
||||
static float _camY;
|
||||
public const float CamSmooth = 18f;
|
||||
static bool _grounded;
|
||||
public static bool HasFocus;
|
||||
static EntityPlayerLocal _player;
|
||||
@@ -44,6 +49,7 @@ namespace NecromancerTome
|
||||
_player = _p;
|
||||
_feet = _at;
|
||||
_spawn = _at;
|
||||
_camY = _at.y;
|
||||
_yaw = _atYaw;
|
||||
_pitch = 0f;
|
||||
_vy = 0f;
|
||||
@@ -86,6 +92,27 @@ namespace NecromancerTome
|
||||
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)
|
||||
{
|
||||
Vector3 c = _feet + _d;
|
||||
@@ -94,12 +121,27 @@ namespace NecromancerTome
|
||||
_feet = c;
|
||||
return true;
|
||||
}
|
||||
// Ступенька: на полблока выше свободно - шагаем вверх.
|
||||
if (_grounded && !Blocked(c + Vector3.up * StepUp))
|
||||
// Ступенька: шагаем вверх на наименьшую высоту (до StepUp), где свободно, ноги - на верх ступеньки
|
||||
// (не выше: иначе на следующем кадре проседали - рывок). Сразу на StepUp нельзя: в проёме с порогом
|
||||
// и низкой притолокой (сейфовая дверь, тест 09.10: «не получается пройти в проём») голова упиралась.
|
||||
if (_grounded)
|
||||
{
|
||||
_feet = c + Vector3.up * StepUp;
|
||||
return true;
|
||||
for (float h = StepUp / 4f; h <= StepUp + 0.001f; h += StepUp / 4f)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -161,7 +203,9 @@ namespace NecromancerTome
|
||||
_vy = 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;
|
||||
_vy = 0f;
|
||||
@@ -184,7 +228,9 @@ namespace NecromancerTome
|
||||
/// <summary>Из патчей камеры режима змеи: глаза духа и поиск книги под прицелом.</summary>
|
||||
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);
|
||||
_camera.SetPositionAndRotation(eye, rot);
|
||||
HasFocus = FindBook(_camera.position + Origin.position, _camera.forward);
|
||||
|
||||
+208
-20
@@ -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);
|
||||
|
||||
@@ -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 <префаб> <имя> - выгрузить модель префаба в OBJ (09.10.2026, для чертежей питомцев в книге:
|
||||
/// медведь, волк, собака, стервятник - ванильные модели). Скелетные меши «запекаются» в позе префаба (BakeMesh),
|
||||
/// у LODGroup - только LOD0. Координаты - от корня префаба, оси Unity переведены в правые (x -> -x, обход граней
|
||||
/// развёрнут). Файл - %APPDATA%\7DaysToDie\NecroMailbox\obj\<имя>.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,38 @@ namespace NecromancerTome
|
||||
ImageTop = 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)
|
||||
|
||||
@@ -550,7 +550,10 @@ namespace NecromancerTome
|
||||
_flyMove = move;
|
||||
_flyRun = run;
|
||||
_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;
|
||||
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
@@ -8,24 +8,39 @@
|
||||
<property name="DifficultyTier" value="0" />
|
||||
<property name="ShowQuestClearCount" value="1" />
|
||||
<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="Zoning" value="none" />
|
||||
<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 name="0" value="11, 3, 9" />
|
||||
<property name="1" value="12, 3, 9" />
|
||||
<property name="2" value="9, 3, 13" />
|
||||
<property name="3" value="8, 4, 10" />
|
||||
<property name="4" value="11, 3, 10" />
|
||||
<property name="0" value="14, 9, 12" />
|
||||
<property name="1" value="16, 4, 12" />
|
||||
<property name="2" value="18, 4, 12" />
|
||||
<property name="3" value="20, 4, 12" />
|
||||
<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 class="challenge_sinks">
|
||||
<property name="0" value="13, 1, 3" />
|
||||
<property name="1" value="13, 1, 4" />
|
||||
</property>
|
||||
<property class="TraderOnOff">
|
||||
<property name="0" value="8, 1, 8" />
|
||||
<property name="1" value="10, 1, 11" />
|
||||
<property name="0" value="15, 1, 18" />
|
||||
<property name="1" value="15, 1, 21" />
|
||||
</property>
|
||||
<property class="challenge_gore">
|
||||
<property name="0" value="3, 2, 9" />
|
||||
@@ -33,18 +48,28 @@
|
||||
<property class="challenge_mech_parts">
|
||||
<property name="0" value="6, 1, 13" />
|
||||
</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 class="Stats">
|
||||
<property name="LightsVolume" value="9206.96" />
|
||||
<property name="TotalVertices" value="108077" />
|
||||
<property name="TotalTriangles" value="69295" />
|
||||
<property name="LightsVolume" value="42763.35" />
|
||||
<property name="TotalVertices" value="218641" />
|
||||
<property name="TotalTriangles" value="114274" />
|
||||
<property class="BlockEntities">
|
||||
<property name="Vertices" value="76900" />
|
||||
<property name="Triangles" value="53018" />
|
||||
<property name="Vertices" value="169120" />
|
||||
<property name="Triangles" value="88691" />
|
||||
</property>
|
||||
<property class="ChunkMeshes0">
|
||||
<property name="Vertices" value="30885" />
|
||||
<property name="Triangles" value="16053" />
|
||||
<property name="Vertices" value="49229" />
|
||||
<property name="Triangles" value="25359" />
|
||||
</property>
|
||||
<property class="ChunkMeshes1">
|
||||
<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 |
Reference in New Issue
Block a user