Книга: главы ножа и Могильной крысы; плакат ножа, грифельная доска, дверь-шкаф; чертоги 25x8x25; плата силовой ножа 20/40

- Книга некроманта: главы «Нож некроманта» и «Могильная крыса» - лист-чертёж сверху (Textures/,
  три вида из Blender, подписи на эсперанто), управление и лор; окно книги непрозрачное, текст с контуром
- «Дух крысы» переименован в «Могильную крысу» на 13 языках (ключи прежние)
- Блоки (Dev): плакат-синька ножа (necroPosterKnife, 3:2 под полотно рамки), грифельная доска
  necroChalkboard (надписи - TEFeatureSignable, писать только в редакторе/творческом; шрифт Neucha OFL,
  свой шейдер текста с тестом глубины), дверь-книжный шкаф necroBookDoor (своя модель с аниматором,
  хитбокс створки через RootTransformRefParent)
- Чертоги: префаб 25x8x25 (промежуточный); открываются только межкомнатные двери; надписи на досках
  по дням игры: «Отомсти архимагу!», с 5-го дня «Берегись излучения красной луны»
- Нож: силовая атака стоит 20 здоровья, с «Мёртвой бурей» 40; не хватает - itembreak, «Недостаточно
  здоровья» и обычный удар; описания на 13 языках
- Консольная команда necromodel - устройство модели блока в лог

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-09 18:26:17 +03:00
co-authored by Claude Opus 5.5
parent e94e967b8e
commit 4e14a0562b
23 changed files with 609 additions and 41 deletions
+25 -11
View File
File diff suppressed because one or more lines are too long
+7 -5
View File
@@ -122,14 +122,16 @@
чертогах (BlockNecroMemoryBook.Open) и консольная команда necrotome. --> чертогах (BlockNecroMemoryBook.Open) и консольная команда necrotome. -->
<append xpath="/windows"> <append xpath="/windows">
<window name="necroTome" anchor="Center" pos="-650,420" width="1300" height="840" cursor_area="true" depth="1001" lock_navigation="true" controller="NecromancerTome.NecroTome, NecromancerHarmony"> <window name="necroTome" anchor="Center" pos="-650,420" width="1300" height="840" cursor_area="true" depth="1001" lock_navigation="true" controller="NecromancerTome.NecroTome, NecromancerHarmony">
<sprite depth="0" name="bg" sprite="menu_empty" color="14,10,10,250" type="sliced" /> <sprite depth="0" name="bg" sprite="menu_empty" color="14,10,10,250" type="sliced" globalopacitymod="0" />
<sprite depth="1" name="frame" sprite="menu_empty2px" color="120,18,14,255" type="sliced" fillcenter="false" /> <sprite depth="1" name="frame" sprite="menu_empty2px" color="120,18,14,255" type="sliced" fillcenter="false" globalopacitymod="0" />
<label depth="2" name="lblTitle" pos="0,-22" width="1300" height="50" justify="center" font_size="38" color="205,40,30,255" effect="outline" upper_case="false" /> <label depth="2" name="lblTitle" pos="0,-22" width="1300" height="50" justify="center" font_size="38" color="205,40,30,255" effect="outline" upper_case="false" />
<sprite depth="1" name="rule" pos="200,-82" width="900" height="2" sprite="menu_empty" color="120,18,14,255" /> <sprite depth="1" name="rule" pos="200,-82" width="900" height="2" sprite="menu_empty" color="120,18,14,255" globalopacitymod="0" />
<label depth="2" name="lblText" pos="60,-104" width="1180" height="640" justify="left" font_size="27" color="215,205,190,255" overflow="shrinkcontent" upper_case="false" /> <label depth="2" name="lblText" pos="60,-104" width="1180" height="640" justify="left" font_size="27" color="215,205,190,255" overflow="shrinkcontent" upper_case="false" effect="outline" effect_color="0,0,0,255" />
<!-- Иллюстрация страницы справа (Chapter.Image / Chapter.Video в NecroTome.cs); при ней <!-- Иллюстрация страницы справа (Chapter.Image / Chapter.Video в NecroTome.cs); при ней
текст сужается до левой половины. Видео - тот же XUiV_Video, что фон главного меню. --> текст сужается до левой половины. Видео - тот же XUiV_Video, что фон главного меню. -->
<texture depth="2" name="imgPage" pos="680,-104" width="560" height="640" visible="false" /> <!-- globalopacitymod="0" (09.10): иначе фон и иллюстрация берут настройку прозрачности интерфейса и
просвечивают обоями чертогов - красные заголовки сливались, картинка была полупрозрачной. -->
<texture depth="2" name="imgPage" pos="680,-104" width="560" height="640" visible="false" globalopacitymod="0" />
<video depth="2" name="vidPage" pos="680,-104" width="560" height="640" videoaspect="FitInside" loop="true" visible="false" /> <video depth="2" name="vidPage" pos="680,-104" width="560" height="640" videoaspect="FitInside" loop="true" visible="false" />
<simplebutton name="btnCh1" depth="3" pos="350,-130" width="600" height="48" font_size="30" visible="false" /> <simplebutton name="btnCh1" depth="3" pos="350,-130" width="600" height="48" font_size="30" visible="false" />
<simplebutton name="btnCh2" depth="3" pos="350,-190" width="600" height="48" font_size="30" visible="false" /> <simplebutton name="btnCh2" depth="3" pos="350,-190" width="600" height="48" font_size="30" visible="false" />
+51
View File
@@ -283,6 +283,57 @@
necroposters (на каждый плакат свой префаб с тем же багетом). Исходник рамки: necroposters (на каждый плакат свой префаб с тем же багетом). Исходник рамки:
BlenderWork/necroSchemeFramePrefab/build_frame.py. --> BlenderWork/necroSchemeFramePrefab/build_frame.py. -->
<append xpath="/blocks"> <append xpath="/blocks">
<block name="necroBookDoor">
<!-- Дверь-книжный шкаф 1x2 (09.10.2026): «замаскирована под книжную полку, закрыта, откроется только по событию».
Механика, звуки и прочность - ванильной межкомнатной двери; модель своя (бандл necroposters, собирает
ItemsUnityProject Assets/Editor/NecroBookDoor.cs): створка 0.98x1.98x0.12 с полками на обеих сторонах, петля
слева, Animator IsOpen/OpenTrigger, состояния Open/Close (как ждёт TEFeatureDoor). Чертоги её не открывают
(MindPalace.OpenDoors трогает только exteriorHouseDoorWhite). -->
<property name="Extends" value="interiorHouseDoorWhite" param1="TintColor,CustomIconTint,MeshDamage"/>
<!-- MeshDamage не наследуем: он ищет в модели Door/DMG0..DMG3 (у ванильной двери они есть, у нашей нет) - тест 09.10:
NullReferenceException в BlockShapeModelEntity.UpdateDamageState при постановке. -->
<property name="CreativeMode" value="Dev"/>
<property name="CustomIcon" value="necroBookDoor"/>
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necroposters?necroBookDoorPrefab.prefab"/>
<property name="ModelOffset" value="0,0,0"/>
<property name="MultiBlockDim" value="1,2,1"/>
</block>
<block name="necroChalkboard">
<!-- Грифельная доска (09.10.2026): «как рамка, только тёмная винтажная доска; вписаться в 2x2; надписи как
на ящиках - для сюжета». Модель - бандл necroposters (собирает ItemsUnityProject Assets/Editor/NecroChalkboard.cs):
рама 1.90x1.40 м, полочка для мела, дочерний TextMesh (шрифт Neucha, OFL). Надпись - TEFeatureSignable,
писать можно только в редакторе префабов / творческом режиме / отладке (ChalkboardPatch.cs).
Размер букв: em = FontSize x 0.01 / 10 м; 110 -> ~9 строк по ~30 знаков. -->
<property name="Extends" value="pictureFrame_01a"/>
<property name="CreativeMode" value="Dev"/>
<property name="CustomIcon" value="necroChalkboard"/>
<property name="Material" value="Mwood_weak"/>
<property name="Class" value="CompositeTileEntity"/>
<property class="CompositeFeatures">
<property class="TEFeatureSignable">
<property name="FontSize" value="110"/>
<property name="LineCount" value="9"/>
<property name="LineWidth" value="1.68"/>
<property name="LineSpacing" value="1"/>
</property>
</property>
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necroposters?necroPosterChalkboardPrefab.prefab"/>
<property name="ModelOffset" value="0,0,-0.5"/>
<property name="MultiBlockDim" value="2,2,1"/>
<property name="OnlySimpleRotations" value="true"/>
<property name="CanPickup" value="false"/>
</block>
<block name="necroPosterKnife">
<!-- Схема ножа некроманта (09.10.2026): синька в стиле ванильного чертежа пистолета, три вида из Blender
(BlenderWork/necroTomePets/make_knife.py poster). Тот же багет; пользователь повесит сам в чертогах. -->
<property name="Extends" value="pictureFrame_01a"/>
<property name="CreativeMode" value="Dev"/>
<property name="CustomIcon" value="necroPosterKnife"/>
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necroposters?necroPosterKnifePrefab.prefab"/>
<property name="ModelOffset" value="0,0,-0.5"/>
<property name="MultiBlockDim" value="2,1,1"/>
<property name="CanPickup" value="false"/>
</block>
<block name="necroPosterAnatomy"> <block name="necroPosterAnatomy">
<property name="Extends" value="pictureFrame_01a"/> <property name="Extends" value="pictureFrame_01a"/>
<property name="CreativeMode" value="Dev"/> <property name="CreativeMode" value="Dev"/>
+3 -2
View File
@@ -274,8 +274,9 @@
<!-- Вдвое дороже по стамине, только силовая. --> <!-- Вдвое дороже по стамине, только силовая. -->
<passive_effect name="StaminaLoss" operation="perc_add" value="1" tags="secondary"/> <passive_effect name="StaminaLoss" operation="perc_add" value="1" tags="secondary"/>
<!-- Вторая половина платы: базовые 5 + эти 5 = 10 HP. --> <!-- Вторая половина платы: базовые 20 + эти 20 = 40 HP (было 5 + 5; указание 09.10). Не хватает 40 -
<triggered_effect trigger="onSelfSecondaryActionRayHit" action="ModifyStats" stat="Health" operation="subtract" value="5"> KnifePowerCostPatch.cs меняет силовую на обычный удар. -->
<triggered_effect trigger="onSelfSecondaryActionRayHit" action="ModifyStats" stat="Health" operation="subtract" value="20">
<requirement name="EntityTagCompare" target="other" tags="zombie"/> <requirement name="EntityTagCompare" target="other" tags="zombie"/>
<!-- "Health GT 0" на цели - тот же гейт на трупы, что у самого ножа, чтобы <!-- "Health GT 0" на цели - тот же гейт на трупы, что у самого ножа, чтобы
силовой удар по трупу не брал 5 HP впустую; подробный разбор в items.xml силовой удар по трупу не брал 5 HP впустую; подробный разбор в items.xml
+4 -2
View File
@@ -1543,10 +1543,12 @@
<requirement name="EntityTagCompare" target="other" tags="zombie"/> <requirement name="EntityTagCompare" target="other" tags="zombie"/>
<requirement name="StatCompareCurrent" target="other" stat="Health" operation="GT" value="0"/> <requirement name="StatCompareCurrent" target="other" stat="Health" operation="GT" value="0"/>
</triggered_effect> </triggered_effect>
<triggered_effect trigger="onSelfSecondaryActionRayHit" action="ModifyStats" stat="Health" operation="subtract" value="5"> <!-- Плата 20 (было 5; указание 09.10: лечение удара - убийства/20, прежние 5 окупались одним ударом).
Не хватает здоровья - KnifePowerCostPatch.cs меняет силовую на обычный удар ещё до замаха. -->
<triggered_effect trigger="onSelfSecondaryActionRayHit" action="ModifyStats" stat="Health" operation="subtract" value="20">
<requirement name="EntityTagCompare" target="other" tags="zombie"/> <requirement name="EntityTagCompare" target="other" tags="zombie"/>
<requirement name="StatCompareCurrent" target="other" stat="Health" operation="GT" value="0"/> <requirement name="StatCompareCurrent" target="other" stat="Health" operation="GT" value="0"/>
<requirement name="StatCompareCurrent" stat="Health" operation="GT" value="5"/> <requirement name="StatCompareCurrent" stat="Health" operation="GT" value="20"/>
</triggered_effect> </triggered_effect>
</effect_group> </effect_group>
</item> </item>
+106
View File
@@ -0,0 +1,106 @@
using System;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Грифельная доска (necroChalkboard, 09.10.2026): надписи мелом - как на табличках (TEFeatureSignable), но
/// «игрок этого делать не будет, а нам для сюжета пригодится». Поэтому команда «написать» у доски есть
/// только в редакторе префабов, в творческом режиме и в режиме отладки (так мы и подписываем её - текст
/// сохраняется в префабе локации, IFeatureSavedInPrefab). В обычной игре доску только читают: без команды
/// нет и подсказки «E - использовать».
/// </summary>
public static class Chalkboard
{
public const string BlockName = "necroChalkboard";
public static bool Is(TEFeatureSignable _sign)
{
return _sign?.Parent?.TeData?.Block != null && _sign.Parent.TeData.Block.GetBlockName() == BlockName;
}
/// <summary>Кто может писать: редактор префабов, творческий режим, отладка.</summary>
public static bool CanWrite()
{
return GameManager.Instance != null && GameManager.Instance.IsEditMode() ||
GameStats.GetBool(EnumGameStats.IsCreativeMenuEnabled) ||
GamePrefs.GetBool(EnumGamePrefs.DebugMenuEnabled);
}
/// <summary>Материал надписи - из бандла, свой шейдер NecroTome/ChalkText с ZTest LEqual (тест 09.10: надпись
/// просвечивала сквозь стены - встроенный GUI/Text Shader берёт ZTest из глобального unity_GUIZTestMode, который
/// интерфейс держит «Always», и переопределение в материале не помогло). Шрифт динамический: его текстура
/// растёт и пересоздаётся при новых буквах - держим её в материале надписи (один материал на все доски).</summary>
static Material _mat;
static Font _font;
static bool _hooked;
public static void BindText(TextMesh _tm, MeshRenderer _r)
{
_mat = _r.sharedMaterial;
_font = _tm.font;
if (_mat == null || _font == null)
{
return;
}
_mat.mainTexture = _font.material.mainTexture;
if (!_hooked)
{
_hooked = true;
Font.textureRebuilt += _f =>
{
if (_mat != null && _f == _font)
{
_mat.mainTexture = _f.material.mainTexture;
}
};
}
}
}
[HarmonyPatch(typeof(TEFeatureSignable), nameof(TEFeatureSignable.SetBlockEntityData))]
public static class Patch_TEFeatureSignable_SetBlockEntityData_Chalkboard
{
public static void Postfix(TEFeatureSignable __instance, BlockEntityData _blockEntityData)
{
if (_blockEntityData == null || !_blockEntityData.bHasTransform || !Chalkboard.Is(__instance))
{
return;
}
foreach (TextMesh tm in _blockEntityData.transform.GetComponentsInChildren<TextMesh>())
{
MeshRenderer r = tm.GetComponent<MeshRenderer>();
if (r != null && tm.font != null)
{
Chalkboard.BindText(tm, r);
}
}
}
}
[HarmonyPatch(typeof(TEFeatureSignable), nameof(TEFeatureSignable.AllowBlockActivationCommand))]
public static class Patch_TEFeatureSignable_AllowBlockActivationCommand_Chalkboard
{
public static void Postfix(TEFeatureSignable __instance, ITileEntityFeature _module, ReadOnlySpan<char> _commandName, ref bool __result)
{
if (__result && __instance.Equals(_module) && _commandName.SequenceEqual("edit".AsSpan()) &&
Chalkboard.Is(__instance) && !Chalkboard.CanWrite())
{
__result = false;
}
}
}
[HarmonyPatch(typeof(TEFeatureSignable), nameof(TEFeatureSignable.GetActivationText))]
public static class Patch_TEFeatureSignable_GetActivationText_Chalkboard
{
public static void Postfix(TEFeatureSignable __instance, ref string __result)
{
if (Chalkboard.Is(__instance) && !Chalkboard.CanWrite())
{
__result = null;
}
}
}
}
+16 -1
View File
@@ -20,7 +20,7 @@ namespace NecromancerTome
static bool Ours(string _name) static bool Ours(string _name)
{ {
return _name == "necroBookMindPrefab" || _name == "necroKnifeDecorPrefab" || _name == "necroMortarPrefab" || _name.StartsWith("necroBloodRose") || _name.StartsWith("necroPoster"); return _name == "necroBookMindPrefab" || _name == "necroKnifeDecorPrefab" || _name == "necroMortarPrefab" || _name.StartsWith("necroBloodRose") || _name.StartsWith("necroPoster") || _name == "necroBookDoorPrefab";
} }
public static void Postfix(Transform __result) public static void Postfix(Transform __result)
@@ -37,6 +37,21 @@ namespace NecromancerTome
} }
__result.tag = vanilla.tag; __result.tag = vanilla.tag;
__result.gameObject.layer = vanilla.gameObject.layer; __result.gameObject.layer = vanilla.gameObject.layer;
// Дверь-шкаф (09.10): коллайдер на створке (ходит с петлёй), а не на корне - тег и ей.
foreach (Collider c in __result.GetComponentsInChildren<Collider>(true))
{
c.gameObject.tag = vanilla.tag;
c.gameObject.layer = vanilla.gameObject.layer;
}
// По попаданию в дочерний коллайдер игра ищет блок через RootTransformRefParent.FindRoot (Voxel ->
// GameUtils.FindMasterBlockForEntityModelBlock): без него луч по створке «не находил» блок - двери не было
// хитбокса (тест 09.10). Как у ванильной двери - компонент на корне со ссылкой на корень; при копировании
// модели Unity переводит ссылку на корень копии.
if (__result.GetComponentInChildren<Collider>(true) != null && __result.GetComponent<Collider>() == null)
{
RootTransformRefParent rr = __result.GetComponent<RootTransformRefParent>() ?? __result.gameObject.AddComponent<RootTransformRefParent>();
rr.RootTransform = __result;
}
Debug.Log("[NecromancerTome] DecorCollider: '" + __result.name + "' тег " + vanilla.tag + ", слой " + vanilla.gameObject.layer + Debug.Log("[NecromancerTome] DecorCollider: '" + __result.name + "' тег " + vanilla.tag + ", слой " + vanilla.gameObject.layer +
", коллайдер " + (__result.GetComponent<Collider>() != null ? "есть" : "НЕТ")); ", коллайдер " + (__result.GetComponent<Collider>() != null ? "есть" : "НЕТ"));
} }
+61
View File
@@ -0,0 +1,61 @@
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Плата здоровьем за силовую атаку Ножа некроманта (указание 09.10.2026: «обычная силовая - 20 хп, с модификацией
/// 40; если здоровья на штраф не хватает - звук сломавшегося инвентаря, а силовая заменится обычной»).
/// Саму плату снимает XML (items.xml - 20 при попадании силовой, item_modifiers.xml «Мёртвая буря» - ещё 20).
/// Здесь - только проверка до удара: ItemClass.ExecuteAction запускает и обычный удар (действие 0), и силовую
/// (действие 1); не хватает здоровья - номер действия меняется на 0 (тот же удар с той же анимацией обычного),
/// а при нажатии звучит ванильный itembreak и надпись «Недостаточно здоровья».
/// Почему был нужен штраф побольше: лечение обычным ударом - убийства/20, и уже со 100 убийств один удар
/// окупал прежнюю плату в 5 здоровья.
/// </summary>
[HarmonyPatch(typeof(ItemClass), nameof(ItemClass.ExecuteAction))]
public static class Patch_ItemClass_ExecuteAction_KnifePowerCost
{
public const string KnifeName = "necroWpnBladeNecroKnife";
public const string StormMod = "necroModKnifeDeadStorm";
/// <summary>Должно совпадать с items.xml и item_modifiers.xml.</summary>
public const int Cost = 20, StormCost = 40;
public static void Prefix(ItemClass __instance, ref int _actionIdx, ItemInventoryData _data, bool _bReleased)
{
if (_actionIdx != 1 || __instance.Name != KnifeName || !(_data?.holdingEntity is EntityPlayerLocal player))
{
return;
}
int cost = HasStorm(_data.itemValue) ? StormCost : Cost;
if (player.Health > cost)
{
return;
}
_actionIdx = 0;
if (!_bReleased)
{
Audio.Manager.PlayInsidePlayerHead("itembreak");
// Надпись (указание 09.10) - сразу, мимо очереди подсказок, чтобы при частых нажатиях не копилась.
GameManager.ShowTooltip(player, Localization.Get("necroKnifeNoHealth"), true);
Debug.Log("[NecromancerTome] KnifePowerCost: здоровья " + player.Health + ", плата " + cost + " - вместо силовой обычный удар");
}
}
static bool HasStorm(ItemValue _iv)
{
if (_iv?.modifications == null)
{
return false;
}
foreach (ItemValue m in _iv.modifications)
{
if (m != null && !m.IsEmpty() && m.ItemClass?.Name == StormMod)
{
return true;
}
}
return false;
}
}
}
+61 -3
View File
@@ -27,7 +27,9 @@ namespace NecromancerTome
/// (woodShapes:cube) -&gt; terrBedrock (в меню творчества его нет, рисовали деревом), /// (woodShapes:cube) -&gt; terrBedrock (в меню творчества его нет, рисовали деревом),
/// одиночная книга на столе -&gt; necroMemoryBook, на стол с реактивами - нож-декорация, /// одиночная книга на столе -&gt; necroMemoryBook, на стол с реактивами - нож-декорация,
/// кровать (SleepingBag) -&gt; necroPalaceBed: иначе игра делает её точкой возрождения игрока. /// кровать (SleepingBag) -&gt; necroPalaceBed: иначе игра делает её точкой возрождения игрока.
/// Построены ли - по книге: нет её - ставим заново (самопочинка). /// Построены ли - сверка с префабом каждой клетки (Mismatches: тип, плотность, покраска) при каждом входе:
/// расхождение - ставим заново (самопочинка; заодно новый, более широкий префаб сам перестраивает старые
/// чертоги в сейвах). Надписи на досках из префаба НЕ переносятся (указание 09.10: «текст будет добавлять мод по событиям»).
/// ///
/// ОХРАНА. Внутри чертогов игра считает место «зоной торговца» (патч IsWithinTraderArea): /// ОХРАНА. Внутри чертогов игра считает место «зоной торговца» (патч IsWithinTraderArea):
/// блоки не ломаются ни ударом, ни взрывом, строить нельзя. Чужие сущности в коробке чертогов /// блоки не ломаются ни ударом, ни взрывом, строить нельзя. Чужие сущности в коробке чертогов
@@ -45,6 +47,8 @@ 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>
public const string PassageDoor = "exteriorHouseDoorWhite";
/// <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>
@@ -408,8 +412,9 @@ namespace NecromancerTome
return Mismatches(_world, _o) == 0; return Mismatches(_world, _o) == 0;
} }
/// <summary>Все двери чертогов - открыты (в редакторе сохранились закрытыми, а дух дверей не /// <summary>Межкомнатные двери чертогов (PassageDoor) - открыты (в редакторе сохранились закрытыми, а дух
/// открывает). Состояние двери - в её TEFeatureDoor, не в блоке.</summary> /// дверей не открывает), входная - закрыта. Прочие двери (шкаф-тайник, стальная - указание 09.10: «закрыты
/// и откроются только по событию») не трогаем: их открывает событие. Состояние двери - в её TEFeatureDoor.</summary>
public static void OpenDoors(World _world, Vector3i _o) public static void OpenDoors(World _world, Vector3i _o)
{ {
Vector3i s = Size; Vector3i s = Size;
@@ -421,6 +426,10 @@ namespace NecromancerTome
if (_world.GetTileEntity(_o + new Vector3i(x, y, z)) is TileEntityComposite tec) if (_world.GetTileEntity(_o + new Vector3i(x, y, z)) is TileEntityComposite tec)
{ {
TEFeatureDoor door = tec.GetFeature<TEFeatureDoor>(); 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; bool wantOpen = new Vector3i(x, y, z) != EntranceDoorAt;
if (door != null && door.IsOpen() != wantOpen) if (door != null && door.IsOpen() != wantOpen)
@@ -436,6 +445,54 @@ namespace NecromancerTome
} }
} }
/// <summary>Надписи на досках чертогов (указание 09.10). Лор: некроманта бросили в этот мир умирать - раздели,
/// лишили артефактов (и вживлённых тоже) и ментальной магией стёрли память, «закрыв знания, опасные для общества».
/// Доска - то, что он ещё помнит или успел себе написать; строки прибавляются по событиям игры. Пока событие -
/// только день: (с какого дня, ключ локализации). Остальные строки - позднее.</summary>
public static readonly (int day, string key)[] BoardLines =
{
(1, "necroPalaceBoardRevenge"),
(5, "necroPalaceBoardRedMoon"),
};
/// <summary>При каждом входе: на каждой доске (necroChalkboard) чертогов - все наступившие строки, на языке игрока.
/// Текст из префаба в чертоги не идёт (Stamp ставит только блоки) - пишет только мод.</summary>
public static void UpdateBoards(World _world, Vector3i _o)
{
int day = GameUtils.WorldTimeToDays(_world.worldTime);
var lines = new List<string>();
foreach (var l in BoardLines)
{
if (day >= l.day)
{
lines.Add(Localization.Get(l.key));
}
}
string text = string.Join("\n", lines);
Vector3i s = Size;
int n = 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++)
{
Vector3i wp = _o + new Vector3i(x, y, z);
if (_world.GetBlock(wp).Block?.GetBlockName() != Chalkboard.BlockName ||
!(_world.GetTileEntity(wp) is TileEntityComposite tec) || !tec.TryGetSelfOrFeature(out TEFeatureSignable sign))
{
continue;
}
if ((sign.GetAuthoredText()?.Text ?? "") != text)
{
sign.SetText(text, true);
n++;
}
}
if (n > 0)
{
Debug.Log("[NecromancerTome] MindPalace: надпись на досках (день " + day + ", строк " + lines.Count + "), досок " + n);
}
}
public static void Stamp(World _world, Vector3i _o) public static void Stamp(World _world, Vector3i _o)
{ {
Prefab p = LoadPrefab(); Prefab p = LoadPrefab();
@@ -707,6 +764,7 @@ namespace NecromancerTome
Debug.Log("[NecromancerTome] MindPalace: коллизии " + (world.IsChunkAreaCollidersLoaded(spawn) ? "готовы" : "НЕ готовы, пускаем по таймеру")); Debug.Log("[NecromancerTome] MindPalace: коллизии " + (world.IsChunkAreaCollidersLoaded(spawn) ? "готовы" : "НЕ готовы, пускаем по таймеру"));
_stage = Stage.Ready; _stage = Stage.Ready;
OpenDoors(world, _origin); OpenDoors(world, _origin);
UpdateBoards(world, _origin);
LogCell(world, _origin + KnifeAt, "нож"); LogCell(world, _origin + KnifeAt, "нож");
LogCell(world, _origin + BookAt, "книга"); LogCell(world, _origin + BookAt, "книга");
MindControl.Begin(player, spawn, SpawnYaw); MindControl.Begin(player, spawn, SpawnYaw);
+128
View File
@@ -0,0 +1,128 @@
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// necromodel &lt;блок&gt; - выписать в лог устройство модели блока (09.10.2026, для своей двери-книжного
/// шкафа: где у ванильной двери петля, куда она открывается, какие коллайдеры). Иерархия с местом, поворотом,
/// масштабом, мешами и коллайдерами; у аниматоров - параметры и клипы, а створка в состояниях «Open» и
/// «Close» - пробой клипа на копии префаба. Копия создаётся и тут же уничтожается.
/// </summary>
[Preserve]
public class ConsoleCmdNecroModel : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necromodel" };
public override string getDescription() => "NecromancerTome: устройство модели блока в лог (necromodel <имя блока>)";
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{
if (_params.Count < 1)
{
SdtdConsole.Instance.Output("necromodel <имя блока>");
return;
}
Block block = Block.GetBlockByName(_params[0]);
string model = block?.Properties.GetString("Model");
if (string.IsNullOrEmpty(model))
{
SdtdConsole.Instance.Output("Нет блока или у него нет Model: " + _params[0]);
return;
}
if (!model.StartsWith("@") && !model.StartsWith("#"))
{
model = "@:" + model;
}
GameObject prefab = DataLoader.LoadAsset<GameObject>(model);
if (prefab == null)
{
SdtdConsole.Instance.Output("Модель не загрузилась: " + model);
return;
}
var sb = new StringBuilder();
sb.AppendLine("[NecromancerTome] necromodel " + _params[0] + " = " + model + ", ModelOffset " +
block.Properties.GetString("ModelOffset") + ", MultiBlockDim " + block.Properties.GetString("MultiBlockDim"));
Dump(prefab.transform, 0, sb);
GameObject copy = Object.Instantiate(prefab);
try
{
foreach (Animator a in copy.GetComponentsInChildren<Animator>(true))
{
RuntimeAnimatorController rc = a.runtimeAnimatorController;
sb.AppendLine("Animator на " + a.name + ": " + (rc != null ? rc.name : "нет контроллера"));
foreach (AnimatorControllerParameter p in a.parameters)
{
sb.AppendLine(" параметр " + p.name + " " + p.type);
}
if (rc == null)
{
continue;
}
foreach (AnimationClip clip in rc.animationClips)
{
sb.AppendLine(" клип " + clip.name + ", " + clip.length.ToString("F2") + " с");
clip.SampleAnimation(copy, clip.length);
foreach (Transform t in copy.GetComponentsInChildren<Transform>(true))
{
sb.AppendLine(" в конце: " + Path(t, copy.transform) + " pos " + t.localPosition.ToString("F3") +
" rot " + t.localEulerAngles.ToString("F1"));
}
}
}
}
finally
{
Object.Destroy(copy);
}
Debug.Log(sb.ToString());
SdtdConsole.Instance.Output("necromodel: выписано в лог");
}
static void Dump(Transform _t, int _depth, StringBuilder _sb)
{
var line = new StringBuilder(new string(' ', _depth * 2));
line.Append(_t.name).Append(" pos ").Append(_t.localPosition.ToString("F3")).Append(" rot ")
.Append(_t.localEulerAngles.ToString("F1")).Append(" scale ").Append(_t.localScale.ToString("F2"))
.Append(" tag ").Append(_t.tag).Append(" layer ").Append(_t.gameObject.layer);
MeshFilter mf = _t.GetComponent<MeshFilter>();
if (mf != null && mf.sharedMesh != null)
{
line.Append(" mesh ").Append(mf.sharedMesh.name).Append(" bounds c").Append(mf.sharedMesh.bounds.center.ToString("F3"))
.Append(" size").Append(mf.sharedMesh.bounds.size.ToString("F3"));
}
foreach (Collider c in _t.GetComponents<Collider>())
{
line.Append(" | ").Append(c.GetType().Name).Append(c.isTrigger ? "(trigger)" : "");
if (c is BoxCollider b)
{
line.Append(" c").Append(b.center.ToString("F3")).Append(" s").Append(b.size.ToString("F3"));
}
}
foreach (Component comp in _t.GetComponents<Component>())
{
if (!(comp is Transform) && !(comp is MeshFilter) && !(comp is MeshRenderer) && !(comp is Collider))
{
line.Append(" [").Append(comp != null ? comp.GetType().Name : "null").Append("]");
}
}
_sb.AppendLine(line.ToString());
foreach (Transform ch in _t)
{
Dump(ch, _depth + 1, _sb);
}
}
static string Path(Transform _t, Transform _root)
{
string p = _t.name;
while (_t.parent != null && _t != _root)
{
_t = _t.parent;
p = _t.name + "/" + p;
}
return p;
}
}
}
+42 -4
View File
@@ -29,6 +29,9 @@ namespace NecromancerTome
/// "@modfolder(NecromancerTome):Textures/x.png") или зацикленное видео без звука. Ни того, /// "@modfolder(NecromancerTome):Textures/x.png") или зацикленное видео без звука. Ни того,
/// ни другого - текст на всю ширину.</summary> /// ни другого - текст на всю ширину.</summary>
public string Image, Video; public string Image, Video;
/// <summary>Картинка полосой сверху во всю ширину, текст под ней (главы питомцев, указание 09.10:
/// «под картинкой - управление схематично, под ним - описание питомца»).</summary>
public bool ImageTop;
} }
/// <summary>cvar «письмо архимага прочитано» - ставит записка noteDuke01 (Config/items.xml).</summary> /// <summary>cvar «письмо архимага прочитано» - ставит записка noteDuke01 (Config/items.xml).</summary>
@@ -54,6 +57,26 @@ namespace NecromancerTome
PageKeys = new[] { "necroTomeCh1P1", "necroTomeCh1P2", "necroTomeCh1P3", "necroTomeCh1P4" }, PageKeys = new[] { "necroTomeCh1P1", "necroTomeCh1P2", "necroTomeCh1P3", "necroTomeCh1P4" },
Unlocked = _p => true Unlocked = _p => true
}, },
// Нож некроманта (указание 09.10): лист-чертёж ножа (тот же рецепт, что у крысы), управление ножом и духом
// змеи, описание. Перед питомцами - «основной инструмент», с него всё начинается.
new Chapter
{
TitleKey = "necroTomeKnifeTitle",
PageKeys = new[] { "necroTomeKnifeP1", "necroTomeKnifeP2", "necroTomeKnifeP3" },
Image = "@modfolder(NecromancerTome):Textures/necroTomeKnife.png",
ImageTop = true,
Unlocked = _p => true
},
// Главы питомцев (указание 09.10): лист-чертёж модели (три вида из Blender, подписи на эсперанто) сверху, на первой
// странице - управление, дальше - описание. Крыса - учебная модель, глава открыта сразу.
new Chapter
{
TitleKey = "necroTomeRatTitle",
PageKeys = new[] { "necroTomeRatP1", "necroTomeRatP2", "necroTomeRatP3" },
Image = "@modfolder(NecromancerTome):Textures/necroTomeRat.png",
ImageTop = true,
Unlocked = _p => true
},
}; };
public static void Open(EntityPlayerLocal _player) public static void Open(EntityPlayerLocal _player)
@@ -73,6 +96,11 @@ namespace NecromancerTome
XUiV_Video _vid; XUiV_Video _vid;
/// <summary>Ширина текста без иллюстрации и с ней (windows.xml: lblText 1180, картинка с x 680).</summary> /// <summary>Ширина текста без иллюстрации и с ней (windows.xml: lblText 1180, картинка с x 680).</summary>
const int TextWidthFull = 1180, TextWidthHalf = 590; const int TextWidthFull = 1180, TextWidthHalf = 590;
/// <summary>Раскладка «картинка сверху»: лист-чертёж 1180x400 (как Textures/necroTomeRat.png), текст ниже.</summary>
static readonly Vector2i ImgSidePos = new Vector2i(680, -104), ImgSideSize = new Vector2i(560, 640);
static readonly Vector2i ImgTopPos = new Vector2i(60, -104), ImgTopSize = new Vector2i(1180, 400);
static readonly Vector2i TextPos = new Vector2i(60, -104), TextTopPos = new Vector2i(60, -518);
const int TextHeightFull = 640, TextHeightTop = 245;
XUiC_SimpleButton _toc, _prev, _next, _close; XUiC_SimpleButton _toc, _prev, _next, _close;
readonly XUiC_SimpleButton[] _chapterButtons = new XUiC_SimpleButton[MaxButtons]; readonly XUiC_SimpleButton[] _chapterButtons = new XUiC_SimpleButton[MaxButtons];
/// <summary>Глава по кнопке оглавления (только открытые главы, по порядку).</summary> /// <summary>Глава по кнопке оглавления (только открытые главы, по порядку).</summary>
@@ -164,7 +192,7 @@ namespace NecromancerTome
SetLabel(_title, Localization.Get(ch.TitleKey)); SetLabel(_title, Localization.Get(ch.TitleKey));
SetLabel(_text, Localization.Get(ch.PageKeys[_pageIndex])); SetLabel(_text, Localization.Get(ch.PageKeys[_pageIndex]));
SetLabel(_page, (_pageIndex + 1) + " / " + ch.PageKeys.Length); SetLabel(_page, (_pageIndex + 1) + " / " + ch.PageKeys.Length);
SetMedia(ch.Image, ch.Video); SetMedia(ch.Image, ch.Video, ch.ImageTop);
SetVisible(_toc, true); SetVisible(_toc, true);
SetVisible(_prev, true); SetVisible(_prev, true);
SetVisible(_next, true); SetVisible(_next, true);
@@ -181,12 +209,15 @@ namespace NecromancerTome
/// <summary>Показать картинку или видео справа (или убрать обе). Видео задаётся только здесь, /// <summary>Показать картинку или видео справа (или убрать обе). Видео задаётся только здесь,
/// а не в вёрстке: XUiV_Video сам запускает ролик при открытии окна, и в оглавлении он играл /// а не в вёрстке: XUiV_Video сам запускает ролик при открытии окна, и в оглавлении он играл
/// бы невидимым. Пустой uri - XUiV_Video.startVideo сам освобождает свою текстуру.</summary> /// бы невидимым. Пустой uri - XUiV_Video.startVideo сам освобождает свою текстуру.</summary>
void SetMedia(string _image, string _video) void SetMedia(string _image, string _video, bool _top = false)
{ {
bool img = !string.IsNullOrEmpty(_image), vid = !img && !string.IsNullOrEmpty(_video); bool img = !string.IsNullOrEmpty(_image), vid = !img && !string.IsNullOrEmpty(_video);
if (_img != null) if (_img != null)
{ {
if (img && _img.Path != _image) // Без чтения _img.Path: у пустой картинки (в вёрстке texture не задан) его геттер берёт
// textureUris[0] из пустого массива - IndexOutOfRange (тест 09.10). Сеттер сам сравнивает
// с текущим путём и не перезагружает тот же файл.
if (img)
{ {
_img.Path = _image; _img.Path = _image;
} }
@@ -212,9 +243,16 @@ namespace NecromancerTome
} }
_vid.IsVisible = vid; _vid.IsVisible = vid;
} }
if (_img != null)
{
_img.Position = _top ? ImgTopPos : ImgSidePos;
_img.Size = _top ? ImgTopSize : ImgSideSize;
}
if (_text != null) if (_text != null)
{ {
_text.Size = new Vector2i(img || vid ? TextWidthHalf : TextWidthFull, _text.Size.y); bool top = _top && img;
_text.Position = top ? TextTopPos : TextPos;
_text.Size = new Vector2i(top || !(img || vid) ? TextWidthFull : TextWidthHalf, top ? TextHeightTop : TextHeightFull);
} }
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+12 -13
View File
@@ -8,20 +8,19 @@
<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="17, 8, 17" /> <property name="PrefabSize" value="25, 8, 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="POILight"> <property class="POILight">
<property name="0" value="11, 3, 9" /> <property name="0" value="11, 3, 9" />
<property name="1" value="12, 3, 9" /> <property name="1" value="12, 3, 9" />
<property name="2" value="8, 4, 10" /> <property name="2" value="9, 3, 13" />
<property name="3" value="12, 4, 10" /> <property name="3" value="8, 4, 10" />
<property name="4" value="12, 4, 12" /> <property name="4" value="11, 3, 10" />
<property name="5" value="9, 3, 13" />
</property> </property>
<property class="challenge_sinks"> <property class="challenge_sinks">
<property name="0" value="11, 1, 3" /> <property name="0" value="13, 1, 3" />
<property name="1" value="13, 1, 4" /> <property name="1" value="13, 1, 4" />
</property> </property>
<property class="TraderOnOff"> <property class="TraderOnOff">
@@ -36,16 +35,16 @@
</property> </property>
</property> </property>
<property class="Stats"> <property class="Stats">
<property name="LightsVolume" value="5516.637" /> <property name="LightsVolume" value="9206.96" />
<property name="TotalVertices" value="91813" /> <property name="TotalVertices" value="108077" />
<property name="TotalTriangles" value="61958" /> <property name="TotalTriangles" value="69295" />
<property class="BlockEntities"> <property class="BlockEntities">
<property name="Vertices" value="73400" /> <property name="Vertices" value="76900" />
<property name="Triangles" value="52089" /> <property name="Triangles" value="53018" />
</property> </property>
<property class="ChunkMeshes0"> <property class="ChunkMeshes0">
<property name="Vertices" value="18121" /> <property name="Vertices" value="30885" />
<property name="Triangles" value="9645" /> <property name="Triangles" value="16053" />
</property> </property>
<property class="ChunkMeshes1"> <property class="ChunkMeshes1">
<property name="Vertices" value="0" /> <property name="Vertices" value="0" />
+93
View File
@@ -0,0 +1,93 @@
Copyright (c) 2008-2010 by Jovanny Lemonad (http://www.jovanny.ru)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB