Ремонтник и лутер: зона из 3x3 квадратов, лишние выключаются (#84)

Сетка «Где работать» в «Содержимом» урны, квадраты очерчены линиями при
показе границ; выключенные квадраты обход пропускает. Сохраняется пятым
числом v6 (старая DLL его пропустит). Консоль: necrourn x y z sectors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-28 10:21:39 +03:00
co-authored by Claude Opus 5.5
parent 9a567b6ac0
commit 96d930e5bb
9 changed files with 302 additions and 17 deletions
+74 -11
View File
@@ -264,8 +264,9 @@ namespace NecromancerTome
AppendVia(via, l);
}
CinerariumWriteHelper.WriteInts(_bw, via.ToArray());
// v6: ремонтник/лутер - сторона зоны, идёт ли проход, где курсор обхода, кто нажал кнопку.
CinerariumWriteHelper.WriteInts(_bw, new[] { ZoneSide, ZoneRunning ? 1 : 0, ZoneCursor, ZonePressedBy });
// v6: ремонтник/лутер - сторона зоны, идёт ли проход, где курсор обхода, кто нажал кнопку; пятое (28.10, #84) -
// выключенные квадраты зоны. Числа идут со счётчиком - старая DLL пятое просто пропустит, версия та же.
CinerariumWriteHelper.WriteInts(_bw, new[] { ZoneSide, ZoneRunning ? 1 : 0, ZoneCursor, ZonePressedBy, ZoneOff });
}
static void AppendVia(List<int> _dst, List<Vector3i> _l)
@@ -411,6 +412,7 @@ namespace NecromancerTome
ZoneRunning = false;
ZoneCursor = 0;
ZonePressedBy = -1;
ZoneOff = 0;
if (version >= 6)
{
int n = _br.ReadInt32();
@@ -423,6 +425,7 @@ namespace NecromancerTome
ZoneRunning = n > 1 && v[1] != 0;
ZoneCursor = n > 2 ? v[2] : 0;
ZonePressedBy = n > 3 ? v[3] : -1;
ZoneOff = n > 4 ? v[4] : 0;
}
_threadsDirty = true;
if (version > SaveVersion)
@@ -1099,19 +1102,11 @@ namespace NecromancerTome
}
_bounds = new GameObject("NecroCinerariumBounds");
_bounds.transform.SetParent(t, false);
LineRenderer lr = _bounds.AddComponent<LineRenderer>();
lr.useWorldSpace = false;
lr.widthMultiplier = 0.06f;
if (_boundsMat == null)
{
_boundsMat = new Material(Shader.Find("Sprites/Default")) { name = "NecroCinerariumBounds" };
}
lr.sharedMaterial = _boundsMat;
Color col = new Color(0.55f, 0.9f, 0.75f, 0.85f);
lr.startColor = col;
lr.endColor = col;
lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
lr.receiveShadows = false;
LineRenderer lr = BoundsLine(_bounds, BoundsOn);
// Углы по граням блоков: x и z от -2.5 до +2.5 от центра урны, y от низа блока под ней до верха +4. Большая зона
// (ремонтник, лутер) - сторона ZoneSideNow, по высоте весь столб мира.
@@ -1129,6 +1124,74 @@ namespace NecromancerTome
{
lr.SetPosition(i, t.InverseTransformPoint(path[i]));
}
if (IsBigZone)
{
SpawnSectorLines(t, c, x0, z0, y0, y1);
}
}
static readonly Color BoundsOn = new Color(0.55f, 0.9f, 0.75f, 0.85f);
static readonly Color BoundsOff = new Color(0.95f, 0.3f, 0.25f, 0.85f);
/// <summary>Ломаная границ - дочерний объект _parent (уходит с ним в RemoveBounds), материал общий.</summary>
static LineRenderer BoundsLine(GameObject _parent, Color _col)
{
GameObject go = _parent;
if (go.GetComponent<LineRenderer>() != null)
{
go = new GameObject("NecroCinerariumBoundsLine");
go.transform.SetParent(_parent.transform, false);
}
LineRenderer lr = go.AddComponent<LineRenderer>();
lr.useWorldSpace = false;
lr.widthMultiplier = 0.06f;
lr.sharedMaterial = _boundsMat;
lr.startColor = _col;
lr.endColor = _col;
lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
lr.receiveShadows = false;
return lr;
}
/// <summary>Квадраты большой зоны (#84): на уровне пола урны - рамка каждого квадрата (включён - зелёная, выключен - красная
/// с крестом), чуть отступив внутрь, чтобы соседние не сливались; на границах квадратов - вертикали на всю высоту зоны.</summary>
void SpawnSectorLines(Transform _t, Vector3 _c, float _x0, float _z0, float _y0, float _y1)
{
int side = ZoneSideNow;
float y = _c.y + 0.05f, inset = 0.2f;
float Edge(float _from, int _k) => _from + BandStart(_k, side);
for (int sz = 0; sz < ZoneGrid; sz++)
{
for (int sx = 0; sx < ZoneGrid; sx++)
{
bool on = SectorOn(sx + sz * ZoneGrid);
float ax = Edge(_x0, sx) + inset, bx = Edge(_x0, sx + 1) - inset;
float az = Edge(_z0, sz) + inset, bz = Edge(_z0, sz + 1) - inset;
Vector3 p0 = new Vector3(ax, y, az), p1 = new Vector3(bx, y, az), p2 = new Vector3(bx, y, bz), p3 = new Vector3(ax, y, bz);
Vector3[] path = on ? new[] { p0, p1, p2, p3, p0 } : new[] { p0, p1, p2, p3, p0, p2, p1, p3 };
LineRenderer lr = BoundsLine(_bounds, on ? BoundsOn : BoundsOff);
lr.positionCount = path.Length;
for (int i = 0; i < path.Length; i++)
{
lr.SetPosition(i, _t.InverseTransformPoint(path[i]));
}
}
}
// Вертикали - во всех узлах сетки, кроме углов зоны (их уже рисует рамка).
for (int kz = 0; kz <= ZoneGrid; kz++)
{
for (int kx = 0; kx <= ZoneGrid; kx++)
{
if ((kx == 0 || kx == ZoneGrid) && (kz == 0 || kz == ZoneGrid))
{
continue;
}
LineRenderer lr = BoundsLine(_bounds, BoundsOn);
lr.positionCount = 2;
lr.SetPosition(0, _t.InverseTransformPoint(new Vector3(Edge(_x0, kx), _y0, Edge(_z0, kz))));
lr.SetPosition(1, _t.InverseTransformPoint(new Vector3(Edge(_x0, kx), _y1, Edge(_z0, kz))));
}
}
}
public void RemoveBounds()
+98 -2
View File
@@ -8,7 +8,8 @@ namespace NecromancerTome
/// Урны с большой зоной - ремонтник и лутер (решения 17.10 и 18.10, BACKLOG «Урна ремонтника», «Урна лутера»).
///
/// Общее. Зона - квадрат со стороной блока притязания (LandClaimSize сервера, по умолчанию 41) или меньше (31/21/11, пункт
/// меню «Размер зоны»), по высоте - весь столб мира; зоны таких урн могут пересекаться. Работа - только по кнопке (17.10: «А
/// меню «Размер зоны»), по высоте - весь столб мира; делится на 3x3 квадрата, любой можно выключить (#84, ZoneOff); зоны
/// таких урн могут пересекаться. Работа - только по кнопке (17.10: «А
/// если сделать рубильник или кнопку. Включил, ремонтник начал работу»): «Запустить» - один проход по зоне, конец прохода -
/// снова ожидание; до нажатия урна ничего не считает. Кнопки - на самой урне с четырёх сторон («как у дверного звонка, но с
/// черепом»), их цвет - состояние (CinerariumButtons): не горят - нет черепа; жёлтая ровно - ожидание; жёлтая мигает - не
@@ -123,6 +124,88 @@ namespace NecromancerTome
}
}
/// <summary>Квадраты зоны (28.10, #84, пользователь: «разделить уже имеющуюся зону на 9 квадратов, с цинерарием посередине...
/// по умолчанию все 9 были выбраны, но можно было какой-нибудь отключить. Иначе он начинает ремонтировать близстоящие
/// здания»): сетка ZoneGrid x ZoneGrid, урна в среднем квадрате. Номер квадрата - столбец + строка * ZoneGrid, столбец 0 -
/// запад (меньший x), строка 0 - юг (меньший z). Бит в ZoneOff - квадрат выключен; 0 - все включены. Сохраняется (v6,
/// пятое число). Выключать - в «Содержимом» (XUiC_NecroUrnZone) или «necrourn x y z sectors».</summary>
public int ZoneOff;
public const int ZoneGrid = 3;
public const int ZoneSectors = ZoneGrid * ZoneGrid;
public bool SectorOn(int _s) => (ZoneOff & (1 << _s)) == 0;
/// <summary>Полоса сетки клетки _i (0..side-1, от западного/южного края зоны).</summary>
static int Band(int _i, int _side) => _i * ZoneGrid / _side;
/// <summary>Первая клетка полосы _k от края зоны; _k = ZoneGrid - сторона зоны.</summary>
public static int BandStart(int _k, int _side) => (_k * _side + ZoneGrid - 1) / ZoneGrid;
/// <summary>Работает ли урна в столбе мира (x, z): в зоне и квадрат включён.</summary>
public bool InZone(int _x, int _z)
{
int side = ZoneSideNow, half = side / 2;
Vector3i c = ToWorldPos();
int ix = _x - c.x + half, iz = _z - c.z + half;
if (ix < 0 || iz < 0 || ix >= side || iz >= side)
{
return false;
}
return SectorOn(Band(ix, side) + Band(iz, side) * ZoneGrid);
}
/// <summary>Включить/выключить квадрат. Блоки выключенного квадрата, уже взятые в очередь, отпускаются; линии перерисовать.</summary>
public void SetZoneOff(int _mask)
{
ZoneOff = _mask & ((1 << ZoneSectors) - 1);
DropOutside(_queue);
DropOutside(_lacking);
if (_zonePending && IsRepairer && !InZone(_zoneTarget.x, _zoneTarget.z))
{
_zonePending = false;
Claimed.Remove(_zoneTarget);
}
if (ZoneShown)
{
RemoveBounds();
SpawnBounds();
}
SetModified();
Debug.Log("[NecromancerTome] Cinerarium: урна " + ToWorldPos() + " - квадраты зоны " + SectorsText());
}
public void ToggleSector(int _s) => SetZoneOff(ZoneOff ^ (1 << _s));
/// <summary>Квадраты строками с севера: «+» включён, «-» выключен (для лога и консоли).</summary>
public string SectorsText()
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
for (int r = ZoneGrid - 1; r >= 0; r--)
{
for (int col = 0; col < ZoneGrid; col++)
{
sb.Append(SectorOn(col + r * ZoneGrid) ? '+' : '-');
}
if (r > 0)
{
sb.Append('/');
}
}
return sb.ToString();
}
void DropOutside(List<Vector3i> _l)
{
for (int i = _l.Count - 1; i >= 0; i--)
{
if (!InZone(_l[i].x, _l[i].z))
{
Claimed.Remove(_l[i]);
_l.RemoveAt(i);
}
}
}
public enum ButtonState
{
Off,
@@ -317,6 +400,10 @@ namespace NecromancerTome
{
int x = c.x - half + ZoneCursor % side, z = c.z - half + ZoneCursor / side;
ZoneCursor++;
if (ZoneOff != 0 && !InZone(x, z))
{
continue; // квадрат выключен (#84)
}
if (!(_world.GetChunkFromWorldPos(x, 0, z) is Chunk ch))
{
continue; // не загружен - пропустить столб
@@ -585,7 +672,16 @@ namespace NecromancerTome
foreach (Entity e in _ents)
{
int rank = LootRank(e);
if (rank < bestRank && !_lootDone.Contains(e.entityId) && !LockManager.Instance.IsLockedServer(e, 0))
if (rank >= bestRank)
{
continue;
}
Vector3i ep = World.worldToBlockPos(e.GetPosition());
if (ZoneOff != 0 && !InZone(ep.x, ep.z))
{
continue; // квадрат выключен (#84)
}
if (!_lootDone.Contains(e.entityId) && !LockManager.Instance.IsLockedServer(e, 0))
{
best = e;
bestRank = rank;
+79
View File
@@ -0,0 +1,79 @@
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Квадраты зоны ремонтника и лутера (28.10.2026, #84): сетка 3x3 в окне «Содержимое» - окно necroUrnZone в группе окон
/// игры bagStorage (Config/XUi_InGame), как фильтр логиста. Север сверху, в среднем квадрате - урна. Щелчок включает /
/// выключает квадрат (TEFeatureCinerarium.ToggleSector); выключенный - тёмный с красной рамкой. Видна только у урн с
/// большой зоной; у прочих урн и сумок транспорта и волка скрыта. s0..s8 в XML - номера квадратов (столбец + строка * 3,
/// строка 0 - юг), f0..f8 - их рамки.
/// </summary>
[Preserve]
public class XUiC_NecroUrnZone : XUiController
{
readonly XUiController[] _cells = new XUiController[TEFeatureCinerarium.ZoneSectors];
readonly XUiController[] _frames = new XUiController[TEFeatureCinerarium.ZoneSectors];
static readonly Color On = new Color(0.3f, 0.6f, 0.45f, 0.9f);
static readonly Color Off = new Color(0.12f, 0.12f, 0.12f, 0.9f);
static readonly Color FrameOn = new Color(0.55f, 0.9f, 0.75f, 1f);
static readonly Color FrameOff = new Color(0.95f, 0.3f, 0.25f, 1f);
public override void Init()
{
base.Init();
for (int i = 0; i < _cells.Length; i++)
{
int s = i;
_cells[i] = GetChildById("s" + i);
_frames[i] = GetChildById("f" + i);
if (_cells[i] != null)
{
_cells[i].OnPress += (_x, _m) => Toggle(s);
}
}
}
public override void OnOpen()
{
base.OnOpen();
Refresh();
}
void Toggle(int _s)
{
TEFeatureCinerarium urn = CinerariumContents.Urn;
if (urn == null)
{
return;
}
urn.ToggleSector(_s);
Refresh();
}
void Refresh()
{
TEFeatureCinerarium urn = CinerariumContents.Urn;
bool show = urn != null && urn.IsBigZone;
ViewComponent.IsVisible = show;
if (!show)
{
return;
}
for (int i = 0; i < _cells.Length; i++)
{
bool on = urn.SectorOn(i);
if (_cells[i]?.ViewComponent is XUiV_Sprite sp)
{
sp.Color = on ? On : Off;
sp.ToolTip = Localization.Get(on ? "necroZoneSectorOn" : "necroZoneSectorOff");
}
if (_frames[i]?.ViewComponent is XUiV_Sprite fr)
{
fr.Color = on ? FrameOn : FrameOff;
}
}
}
}
}
+12 -2
View File
@@ -17,7 +17,7 @@ namespace NecromancerTome
/// necroslot [N] - какая ячейка пояса в руке и что в ячейках; N - взять в руку ячейку N (23.10: проверка Archer's Eye без мыши)
/// necrofire press|release - ЛКМ предметом в руке: нажать / отпустить (лук: натянуть / выстрелить; 23.10, ошибка в Hit от стрелы)
/// necrobox x y z list|clear|add предмет число - содержимое хранилища (ящик, сумка урны)
/// necrourn x y z info|skull 0|1|from|to|out bx by bz|clear|sample предмет число|group все|нет - урна
/// necrourn x y z info|skull 0|1|from|to|out bx by bz|clear|sample предмет число|group все|нет|sectors all|N|-маска - урна
/// necroclick 0|1 - отпущенная ЛКМ/ПКМ ножом для нитей по блоку под прицелом
/// necropossess near [класс]|fp|hit|look id / x y z|slot N|belt N предмет [качество]|info|exit - одержимость без мыши (#37, 25.10)
/// </summary>
@@ -552,6 +552,16 @@ namespace NecromancerTome
case "run": u.ZoneCommand("zurn_run", GameManager.Instance.World.GetPrimaryPlayer()); break;
case "stop": u.ZoneCommand("zurn_stop", null); break;
case "size": u.ZoneSide = int.Parse(_p[4]); u.SetModified(); break;
// necrourn x y z sectors all|N|-N - квадраты зоны (#84): all - все включить; N - переключить квадрат N (0..8, столбец +
// строка * 3, 0 - юго-запад); -N - маска выключенных числом.
case "sectors":
if (_p.Count > 4)
{
if (_p[4] == "all" || _p[4] == "все") u.SetZoneOff(0);
else if (_p[4].StartsWith("-")) u.SetZoneOff(int.Parse(_p[4].Substring(1)));
else u.ToggleSector(int.Parse(_p[4]));
}
break;
case "group":
u.GroupMask = _p[4] == "нет" || _p[4] == "none" ? 0u : CinerariumGroups.All;
u.SetModified();
@@ -567,7 +577,7 @@ namespace NecromancerTome
}
Out("necrourn: " + pos + " " + _w.GetBlock(pos).Block?.GetBlockName() + ", череп " + u.HasSkull + ", застрял " + u.Stuck + ", мигание " + blink +
", откуда " + u.ThreadFrom.Count + ", куда " + (u.HasThreadTo ? u.ThreadTo.ToString() : "нет") + ", от урны " + u.ThreadOut.Count +
", зона " + (u.IsBigZone ? u.ZoneSideNow + (u.ZoneRunning ? " идёт" : " ждёт") + (u.ZoneLacking ? " нехватка" : "") + " курсор " + u.ZoneCursor : "-") + ", образец " + (u.Sample == null || u.Sample.IsEmpty() ? "нет" : u.Sample.itemValue.ItemClass?.GetItemName() + "x" + u.Sample.count) + "; " + u.WorkState());
", зона " + (u.IsBigZone ? u.ZoneSideNow + (u.ZoneRunning ? " идёт" : " ждёт") + (u.ZoneLacking ? " нехватка" : "") + " курсор " + u.ZoneCursor + " квадраты " + u.SectorsText() : "-") + ", образец " + (u.Sample == null || u.Sample.IsEmpty() ? "нет" : u.Sample.itemValue.ItemClass?.GetItemName() + "x" + u.Sample.count) + "; " + u.WorkState());
}
}