Commit Graph
30 Commits
Author SHA1 Message Date
AlexCubeandClaude Opus 5 156e046cc2 Череп вместо зомби на скилле, вкладке и двух баффах
Вкладка испытаний подтверждена в игре ("вкладка видна, нож сработал"), лог
чистый - ERR за весь запуск ноль, challenges загрузились за 0.08. Заодно
подтвердилось, что осознанный риск скелета сыграл: у группы нет reward_event и
reward_text_key, и игра не сказала ни слова - умолчания корня challenges на
группы распространяются.

По указанию пользователя ui_game_symbol_zombie заменён на ui_game_symbol_skull
во всех четырёх живых местах мода:

    progression.xml:90   crafting_skill craftingNecroNecromancy  - сам скилл
    challenges.xml:41    challenge_category NecroNecromancy     - вкладка
    buffs.xml:14         buffNecroZombieKillTrackerDisplay      - счётчик убийств
    buffs.xml:175        buffNecroDarkSense                     - Тёмное чутьё

Причина пользователя: "Значок зомби - протокольная рожа, как у уголовника. Череп
будет более некромантским."

Вкладка и скилл меняются ТОЛЬКО ПАРОЙ - они обязаны читаться как одно целое. Это
записано комментарием в challenges.xml, чтобы позже их не развели по одному.

СОЗНАТЕЛЬНО НЕ ТРОНУТО: комментарий buffs.xml:150 упоминает ui_game_symbol_zombie,
но описывает ВАНИЛЬНЫЙ класс nav_object="zombie" и его собственную иконку на
карте и компасе. Это не наша иконка, текст не устарел.

Чистый XML, пересборка не нужна.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 22:45:00 +03:00
AlexCubeandClaude Opus 5 90d8fe331b Вкладка испытаний Некромантии: скелет
Новый Config/challenges.xml - мод его раньше не имел. Категория (вкладка),
группа и ОДНО испытание "Первым делом - нож".

Одно, а не два заказанных, намеренно: файл трогается впервые, сначала надо
увидеть в игре, что вкладка вообще появляется. Нож крафтится сразу и без
верстака, то есть проверка занимает минуту. "Безумный донор" добавится одной
записью, когда скелет подтвердится.

РАЗРОЗНЕННО, НЕ ЦЕПОЧКОЙ (по указанию): у группы нет ни link_challenges, ни
hidden_by. Побочная польза - снялось противоречие порядка: Кровь некроманта
нельзя скрафтить без ножа, так что "донор" первым в цепочке был бы невыполним.

ИКОНКИ ТОЛЬКО ИЗ СИМВОЛЬНОГО АТЛАСА ИГРЫ. В templates.xml оба места, где они
рисуются (challenge_entry и category_icon), объявлены БЕЗ атрибута atlas, то есть
читают атлас по умолчанию; в ванильном challenges.xml все 215 иконок имеют вид
ui_game_symbol_* без исключений. Свой атлас мода (NecroFinal) не поможет -
обратиться к нему можно только явным atlas= на спрайте, а дописать его в шаблон
нельзя: шаблон один на все испытания, ванильные иконки пропадут.

Из 359 собранных символов нашлись некромантские, череп не понадобился. Вкладка -
ui_game_symbol_zombie (тот же символ, что у скилла в progression.xml, чтобы
вкладка и скилл читались как одно целое), нож - ui_game_symbol_knife. Для
"Безумного донора" отложен ui_game_symbol_siphoning_strikes.

Локализация: 5 ключей x 13 языков, ключей стало 140. Название вкладки взято слово
в слово из craftingNecroNecromancyName. Счётчик в README и SITE_DESCRIPTION
обновлён 135 -> 140.

ОСОЗНАННЫЙ РИСК: у группы нет reward_text_key и reward_event, хотя у всех 20
ванильных они есть. Ставка на умолчания корня challenges (default_reward,
default_reward_text_key). Если не сработает - будет в логе, и это дешевле, чем
заводить событие награды вслепую.

НЕ ПРОВЕРЕНО В ИГРЕ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 22:29:46 +03:00
AlexCubeandClaude Opus 5 03b4b8f164 README: пометка, что сборка проверена не полностью
По указанию пользователя перед пушем. Текст коммитов про непроверенное видит
только тот, кто читает историю; README видно сразу.

В обеих половинах (RU и EN) раздел "Статус" теперь перечисляет, что подтверждено
в игре, и отдельной врезкой - что нет: Кровавый камень (нужен уровень 2000 и
химстанция) и новый порог Кровавой сферы (переехала с уровня 1 на 20). Указано,
что это ожидаемое место для сюрприза.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 22:19:03 +03:00
AlexCubeandClaude Opus 5 c832f734d7 Кровавая сфера открывается вместе с браслетом, а не с уровня 1
Сфера вставляется в Пространственный браслет, но открывалась с уровня 1,
тогда как сам браслет - с 20. Девятнадцать уровней её можно было крафтить в
пустоту. По записанному правилу мода "пороги - по нужде, а не по силе" порог
ставится туда, где предмет впервые нужен.

progression.xml: resourceBloodSphere убрана из unlock_entry с unlock_tier="1" и
дописана в строку браслета (unlock_tier="2").
recipes.xml: теги рецепта packMuleCrafting -> learnable,packMuleCrafting,
necroNecromancyLvl20. learnable как у всех гейтованных рецептов мода, чтобы
рецепт не светился в меню до открытия.

Два числа обязаны совпадать и совпадают: RecipeTagUnlocked для
necroNecromancyLvl20 стоит на level="20,5000", а unlock_tier="2" в группе с
unlock_level="1,20,30,60,100,300" - это те же 20. Разойдись они, замок на панели
скилла разошёлся бы с реальной доступностью рецепта.

Чистый XML, пересборка не нужна. Комментарии в обоих файлах переписаны - там
стояло "ЗАМЕЧЕНО, НЕ ИСПРАВЛЕНО".

НЕ ПРОВЕРЕНО В ИГРЕ: на свежем персонаже рецепта быть не должно, после 20
убийств должны открыться браслет и сфера одной строкой.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 22:17:31 +03:00
AlexCubeandClaude Opus 5 8c5636b523 Версия 1.2.0: заряд браслета и починенный счёт убийств
Проверка в игре прошла, версия поднята с 1.1.0 до 1.2.0, описания доведены до
текущего состояния мода.

ПОЧЕМУ МИНОРНАЯ, А НЕ ПАТЧ. Тег v1.1.0 указывает на 7172681. Заряд браслета
(Кровавая сфера и Кровавый камень), починка счёта убийств, кряк боли при крафте
крови и фикс призрачности торговца появились ПОСЛЕ него и в выпущенную 1.1.0 не
входили. Это новая функциональность, а не исправления к выпущенному.

ЧТО ПОДТВЕРЖДЕНО В ИГРЕ:
  - Кровавая сфера исчезает при обнулении прочности, звук разбивания играет;
  - кряк боли при создании Крови некроманта играет;
  - торговец остаётся призраком (на одном торговце; на трёх будет проверено
    отдельно);
  - зомбопёс засчитывается в скилл - причина 1 (пять зомби-зверей вне
    zombieTemplateMale) закрыта, фильтр по тегу zombie работает;
  - зомби, умерший от кровотечения, засчитывается - причина 2 (DamageSource
    вместо буквального убийцы) закрыта;
  - зомби, сгоревшие от Пирамиды духов, засчитываются. Этого случая не было ни в
    указании, ни в плане - он достался бесплатно, потому что хук стоит на
    EntityPlayer.AddKillXP, то есть следует решению ванили, а не перечисляет
    причины смерти по одной.

ПИТОМЦЫ: ВОПРОС ЗАКРЫТ ОТРИЦАТЕЛЬНО. Зомби под контролем Камня духов, убивая
другого зомби, в скилл не идёт - это и есть случай "убил питомец", и ваниль
владельцу такое убийство не зачисляет. Патч следует её решению, как и написано в
его комментарии. Пункт исходного указания про призванных существ требует
отдельной работы (проставить владельца в DamageSource питомца); пользователь
решил оставить как есть.

ОПИСАНИЯ (README.md, RU и EN):
  - шапка версии в обеих половинах;
  - новый абзац 1.2.0 в "Статусе", 1.1.0 и 1.0.1 сдвинуты в историю. Заодно
    исправлена фактическая ошибка: раздел приписывал сферу и камень версии 1.1.0,
    хотя их там не было;
  - описание скилла теперь говорит, что засчитывается любое убийство, за которое
    игра даёт опыт - ловушка, огонь, кровотечение, зомби-звери наравне с
    человекоподобными. Раньше было просто "счётчик упокоенных зомби", и это
    вводило в заблуждение ровно в тех случаях, что чинились;
  - снята оговорка про непроверенную сферу, вместо неё честная оговорка про
    непроверенный Кровавый камень.

SITE_DESCRIPTION.html и NEXUS_DESCRIPTION.bbcode номера версии не содержат, сферу
и камень получили раньше - правки не потребовалось.

НЕ ПРОВЕРЕН В ИГРЕ Кровавый камень: собран и на месте, но требует уровня
Некромантии 2000 и химстанции. В README это сказано прямым текстом.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 22:11:41 +03:00
AlexCubeandClaude Opus 5 229b436420 Счёт убийств Некромантии переехал из XML в код
Чинит баг: скилл не засчитывал часть убийств зомби. Причин было две, и обе
закрываются одним ходом.

ПРИЧИНА 1: ОДИН КЛАСС - НЕ ВСЕ ЗОМБИ. Счёт висел append-ом на zombieTemplateMale.
Человекоподобные покрыты (effect_group у entity_class наследуется через extends, и
все шаблоны зомби сходятся к Male), но пять зомби-ЗВЕРЕЙ наследуют животную ветку
и до него не доходят вовсе:

    animalZombieBear   extends animalBear
    animalZombieBoar   extends animalBoar
    animalZombieDog    extends animalWolf
    animalZombieVulture extends animalTemplateHostile
    animalZombieVultureRadiated extends animalZombieVulture

Убийство зомбопса, зомбомедведя, зомбокабана и зомбоворона не считалось никак.
Вороны попадаются постоянно - это и была бОльшая часть "не всегда засчитывает".

ПРИЧИНА 2: target="other" - БУКВАЛЬНЫЙ УБИЙЦА. Триггер onOtherKilledSelf с
требованием EntityTagCompare tags="player" начислял только за убийство своей
рукой. Робомолот, кровотечение и питомцы требование не проходят, хотя опыт игрок
за них получает: ваниль определяет получателя не по убийце, а по DamageSource - в
EntityAlive.AwardKillXPServer, где для этого есть обращение к BuffClass (DoT) и
отдельный флаг bTrapKillXP (ловушки).

Требование было ОДНО на оба эффекта, поэтому недосчитывался и
necroZombieKillsCVar, а это урон Ножа некроманта (items.xml: Damage = CVar / 10).
Баг тихо занижал ещё и нож.

РЕШЕНИЕ: Postfix на EntityPlayer.AddKillXP. Этот метод вызывается ровно из одного
места во всей сборке - из AwardKillXPServer, то есть уже ПОСЛЕ того, как ваниль
разобрала DamageSource и решила, чей это фраг (проверено сканированием IL).
Мы не повторяем её логику и не угадываем владельца турели или автора
кровотечения - забираем готовый ответ. Наш счёт совпадает с опытом на экране по
построению, включая случаи, о которых мы не подумали.

ФИЛЬТР ПО ТЕГУ zombie, А НЕ ПО КЛАССУ. Проверено по данным: zombieBiker,
zombieArlene, zombieBoe, zombieSpider несут "entity,zombie,...", зомби-звери -
"entity,animal,zombie,zombieAnimal,...". Тег есть у всех. Работает это благодаря
тому, что Tags у entity_class НЕ наследуется через extends: каждый реально
спавнящийся зомби выписывает теги сам, а безтеговые шаблоны не спавнятся. Тег
переживёт и новых зомби из патчей игры, и чужие моды.

Повышение уровня воспроизводит MinEventActionAddProgressionLevel.Execute шаг в
шаг по его IL: GetProgressionValue, Level+1, кламп по MaxLevel, для крафтового
скилла AddCraftingSkillNotification и HandleCheckCrafting, затем
bProgressionStatsChanged и bPlayerStatsChanged под !isEntityRemote.
HandleCheckCrafting легко выбросить и дорого потерять - без него рецепты рискуют
не заметить, что открылись. Уведомление с _bAddOnlyIfNotExisting=true, чтобы в
орду не всплывал тост на каждый труп.

Config/entityclasses.xml: append снят ЦЕЛИКОМ, на его месте комментарий, почему
возвращать нельзя - XML-триггер рядом с патчем засчитает убийство своей рукой
ДВАЖДЫ. Это единственная ловушка переезда.

Сборка: 0 ошибок (4 прежних MSB3277). Проверено рефлексией по собранной DLL:
атрибут нацелен верно, перегрузка AddKillXP ровно одна, имя параметра killedEntity
совпадает с ванильным (Harmony инжектит по имени - опечатка дала бы молчаливо
неработающий патч), PatchAll подхватывает файл сам.

НЕ ПРОВЕРЕНО В ИГРЕ. Отдельно: питомцы НЕ гарантированы - патч следует решению
ванили, а не принимает его. Если ваниль не зачисляет владельцу убийство
питомцем, не зачислит и он; это отдельная работа, а не ошибка здесь.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 21:21:11 +03:00
AlexCubeandClaude Opus 5 e890999391 Кряк боли при создании Крови некроманта
Крафт крови и так снимает 90% текущего ХП, но делал это молча. Теперь на том же
месте играет штатный звук боли игрока - тот, что слышно при накалывании на кол
или на колючую проволоку.

ИМЯ КЛИПА СПРАШИВАЕТСЯ У СУЩНОСТИ, А НЕ ВПИСАНО СТРОКОЙ. Звук боли зависит от
пола персонажа (Data/Config/entityclasses.xml):

    playerMale     SoundHurt="player1painlg"   SoundHurtSmall="player1painsm"
    playerFemale   SoundHurt="player2painlg"   SoundHurtSmall="player2painsm"

playerFemale extends playerMale и переопределяет обе строки, так что
захардкоженный "player1painlg" выдал бы женскому персонажу мужской кряк. Вместо
этого зовутся GetSoundHurt() и GetSoundHurtSmall() - публичные методы
EntityAlive, каждый из которых целиком есть чтение поля (проверено по IL:
"ldarg.0; ldfld soundHurt|soundHurtSmall; ret", токены разрешены через
Module.ResolveField).

ПОРОГ ВЗЯТ У САМОЙ ИГРЫ. EntityPlayer переопределяет
GetSoundHurt(DamageSource, int) - тот самый метод, через который проходят колья
и колючая проволока. Его IL разобран целиком, и правило такое: урон СТРОГО
больше 15 -> большой кряк (soundHurt), 15 и меньше -> малый (soundHurtSmall),
плюс фолбэк на большой, если малый не объявлен. Воспроизведено один в один,
включая строгое "больше" и фолбэк; константа названа PainSoundBigDamage и несёт
этот разбор в комментарии.

Сам GetSoundHurt(DamageSource, int) не вызывается: ему нужен DamageSource, а
здесь урона от источника нет - ХП снимается через AddHealth, осознанно (см.
комментарий на месте вызова). Правило выбора - три строки, поэтому оно повторено,
а не подделано синтетическим DamageSource.

PLAYONESHOT ВЫЗВАН РОВНО КАК В ВАНИЛИ. Единственный вызывающий -
EntityAlive.OnUpdateEntity, IL 348: там PlayOneShot получает
sound_in_head:false, serverSignalOnly:false, isUnique:false, _animEvent:null,
volumeScale:1f. Значения по умолчанию у Entity.PlayOneShot - те же самые
(сверено через RawDefaultValue), так что вызов одним аргументом поведенчески
идентичен ванильному: кряк идёт ОТ персонажа, а не "в голове". Оттуда же взят
null-guard - ваниль пропускает вызов при пустом имени клипа, а не отдаёт его
PlayOneShot.

Вызов стоит ПОСЛЕ AddHealth(-amount), а не до: крафт, который свалился бы выше,
не должен издавать звук, которому игрок не найдёт объяснения. При цене 90% это
практически всегда большой кряк - малый включится только на остатке около 17 ХП,
там же, где его сделала бы малым ваниль.

ЗАОДНО: устаревшая шапка NecromancerBloodPatch.cs. Комментарий утверждал, что
кровь живёт в item_modifiers.xml, "not in items.xml, since 2026-09-15". Откат
15.09 это отменил, но тогда правились только комментарии в items.xml, а .cs
пропустили. Переписано: кровь в items.xml, и рядом коротко, почему её оттуда
нельзя двигать.

Сборка: 0 ошибок (4 прежних MSB3277 про версии System.Runtime, к правке
отношения не имеют). Скомпилированный PlayPainSound проверен рефлексией по
готовой DLL - вызовы идут в ожидаемом порядке.

НЕ ПРОВЕРЕНО В ИГРЕ. Проверять обоими полами персонажа - ради этого имя клипа и
спрашивается у сущности.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 20:35:01 +03:00
AlexCubeandClaude Opus 5 5a512260cc Кровавый камень: бесконечный заряд браслета
Эндгейм-версия расходника Пространственного браслета: то же место в слоте, что у
Кровавой сферы, но не тратится вовсе. Кода менять не пришлось - только конфиги.

ЗАОДНО ЭТО ПОЧИНКА УЖЕ ОТГРУЖЕННОГО ТЕКСТА. Сообщение о пустом слоте
(braceletSpatialVaultNoMod) и описание браслета обещают игроку "кровавую сферу
ИЛИ кровавый камень" на всех 13 языках с 15.09. Пока камня не было, оба текста
отправляли за предметом, которого в игре не существует.

СОВМЕСТИМОСТЬ СЕЙВОВ - ПЕРВОЕ, ЧТО ПРОВЕРЕНО. resourceBloodStone - НОВОЕ имя, в
существующих сейвах его нет, значит нет и ни одного стака, который читался бы по
другой байтовой раскладке. Это ровно то условие, нарушение которого в e362c62
убило персонажа: ItemValue.Read/Write гейтят блок модификаций одним условием
!(itemClass is ItemClassModifier), и класс предмета определяет раскладку каждого
стака в сейве. Камень заведён новым предметом, а не переводом существующего -
тем же решением, что и Кровавая сфера. Добавление нового <item_modifier>
раскладку уже записанных стаков не меняет: айди новых предметов дописываются в
name->id мэппинг сейва, байты старых стаков остаются как были.

БЕСКОНЕЧНАЯ ПРОЧНОСТЬ СДЕЛАНА ОТСУТСТВИЕМ СТРОК, А НЕ БОЛЬШИМ ЧИСЛОМ. У камня
сознательно нет ни ShowQuality, ни DegradationBreaksAfter, ни effect_group с
DegradationMax - сравнить со сферой, у которой все три есть. SpendCharge
(SpatialVaultPickupPatch.cs) списывает прочность только у модификации, чьё имя
равно ChargeItemName ("resourceBloodSphere"); камень под условие не попадает,
цикл делает continue, его UseTimes не трогает никто. Большой DegradationMax был
бы хуже: неподвижная полоска прочности бесполезна, а при MaxUseTimes == 0 она
рисуется ПОЛНОЙ, то есть забытая пассивка выглядела бы как "всё работает".

Три места в коде сверены на то, что камень пройдёт весь путь:

  1. Гейт пустого слота в Begin - это !_bracelet.HasMods(), то есть "в слоте
     что-нибудь есть", а НЕ "есть сфера по имени". Камень гейт проходит. Это был
     главный риск правки: искал бы гейт сферу по имени, камень не работал бы.
  2. SpendCharge вызывается ПОСЛЕ SetBlockRPC(..., Air) и bag.AddItem, а его
     результат только логируется - ветки "не заплатил -> откажи" нет. Блок с
     камнем уходит в хранилище, в логе "took ... into the vault for 0 of charge".
  3. Ветка max <= 0 в SpendCharge камня не касается: до неё доходят только
     предметы, прошедшие проверку имени.

ФАЙЛЫ

Config/item_modifiers.xml - новый item_modifier resourceBloodStone,
installable_tags="necroBracelet", свой modifier_tags="necroBraceletStone".
Extends="modGeneralMaster" param1="CustomIcon" - НЕ resourceRockSmall: с ним
приехали бы Action0 ThrowAway и ThrowableDecoy, и камень можно было бы метать
как отвлекающий (на сфере эта ошибка уже была и была исправлена). Вид в руке -
HoldType 40 плюс все три меша rock_smallPrefab плюс TintColor "220, 30, 45", как
у сферы и трёх камней мода. CustomIcon="BloodStone", без CustomIconTint:
иконка своя, рисованная, лежит в обеих папках атласа с 15.09.

Config/recipes.xml - рецепт на химстанции, craft_area="chemistryStation",
tags="learnable,chemStationCrafting,necroNecromancyApprentice": 50 праха зомби,
20 костей, 4 обычной крови, 1 кровь некроманта. Все четыре имени проверены по
файлам. Цена выше, чем читается по списку: крафт самой крови некроманта снимает
90% текущего ХП.

Config/progression.xml - камень добавлен в unlock_entry третьего display_entry
(unlock_level="2000", unlock_tier="1"). Это только строка в панели скилла;
открытие рецепта делает тег. 2000 здесь и 2000 у RecipeTagUnlocked обязаны
совпадать, иначе замок на панели разойдётся с доступностью рецепта.

Config/Localization.csv - два ключа, resourceBloodStone и
resourceBloodStoneDesc, 13 языков; ключей стало 135. Названия камня взяты из уже
переведённого braceletSpatialVaultNoMod, так что сообщение об отказе и имя в
инвентаре совпадают слово в слово. braceletSpatialVaultDesc дополнено на всех 13
языках: камень не тратится. Файл переписан через csv-модуль, round-trip без
правок даёт побайтово тот же файл - кавычки и CRLF остальных строк не поехали.

ОПИСАНИЯ. README.md, SITE_DESCRIPTION.html и NEXUS_DESCRIPTION.bbcode доведены
до текущего состояния мода: сфера и камень добавлены в таблицы тиров и в список
арсенала, у браслета сказано, что забор блоков питается зарядом в слоте,
счётчик ключей 130 -> 135. Заодно снято утверждение "весь контент проходит тесты
в игре" - оно перестало быть верным: расход сферы в игре подтверждён, камень в
игру ещё не грузился.

НЕ ПРОВЕРЕНО В ИГРЕ: камень целиком. Всё выше - сверка по файлам и по
декомпиляции. Сценарий проверки в BACKLOG.md; главное, на что смотреть, -
отсутствие полоски прочности у камня в слоте и "for 0 of charge" в логе.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 20:24:22 +03:00
AlexCubeandClaude Opus 5 896501dcd8 Откат крови и Кровавая сфера вместо неё
Чинит поломку сейвов, внесённую предыдущим коммитом e362c62, и заменяет её
механику на безопасную.

ЧТО БЫЛО СЛОМАНО. e362c62 перенёс resourceNecromancerBlood из items.xml в
item_modifiers.xml, чтобы кровь вставлялась в Пространственный браслет. Класс
предмета определяет БАЙТОВУЮ РАСКЛАДКУ каждого его стака в сейве:

    ItemValue.Read  строка 1094:  if ((version > 4 || HasQuality) && !(itemClass is ItemClassModifier))
    ItemValue.Write строка 1228:  if (!(ItemClass is ItemClassModifier))

Обычный предмет пишет байт числа модификаций и байт числа косметических слотов;
ItemClassModifier не пишет ни того, ни другого. Сейв, записанный до переноса,
после переноса читается со сдвигом - поток съезжает на первом же стаке крови, и
PlayerDataFile.Load падает. Бэкап .ttp.bak умирает вместе с основным файлом: он
того же формата. В тестовом мире персонаж потерян вместе с бэкапом, игра
откатилась на Respawning: NewGame.

В коммите e362c62 написано "сейв цел" со ссылкой на assignIdsFromMapping. Айди
действительно берутся из сохранённого name->id мэппинга - но ломается не айди, а
раскладка байтов, и к мэппингу это отношения не имеет. Была проверена не та вещь.

ОТКАТ. Кровь вернулась в items.xml обычным <item>. Набор её свойств сверен с
7172681 и совпадает посимвольно: убраны и Stacknumber=1, и прочность
(ShowQuality + DegradationBreaksAfter + effect_group DegradationMax). Обе правки
были безвредны для сейва, но существовали ради отменённой механики - по указанию
пользователя откат доведён до "как было", а не до "как было плюс безвредное".
Стак снова 15, наследуется от medicalBloodBag.

В комментарии у предмета оставлено предупреждение с номерами строк Read/Write -
единственное, что вынесено из аварии, и единственное, ради чего стоит читать этот
комментарий целиком.

КРОВАВАЯ СФЕРА. Расходник браслета теперь отдельный предмет, и это ровно то, что
делает правку безопасной: resourceBloodSphere - НОВОЕ имя, в старых сейвах его
нет, значит нет и ни одного стака, который читался бы по другой раскладке. Общее
правило, выведенное из аварии: предмет, который уже мог попасть в чужой
инвентарь, нельзя переводить между ItemClass и ItemClassModifier ни в какую
сторону - нужна модификация, заводи новый предмет.

Продиктовано: доступна на первом грейде, без станка, 1 кровь некроманта + 5 праха
зомби дают две сферы, прочность 500. В руке - камень с алым тинтом.

  - item_modifiers.xml: installable_tags="necroBracelet", свой modifier_tags,
    type="attachment", DegradationMax 500 в tiered="false" группе.
    Extends=modGeneralMaster, а НЕ resourceRockSmall: вместе с камнем приезжали бы
    Action0 ThrowAway и ThrowableDecoy, то есть сферу можно было бы метать.
    Меш берётся строкой Meshfile, наследовать ради него весь предмет не нужно.
  - Вид в руке: HoldType 40 и все три меша (Meshfile/HandMeshfile/DropMeshfile) на
    rock_smallPrefab плюс TintColor "220, 30, 45". Путь проверенный - на этом же
    меше с таким же тинтом в моде живут Камень духов и оба портальных камня.
  - recipes.xml: count=2, без craft_area и без тега разблокировки (группа 1
    открыта с уровня 1, рецепт без тега доступен всегда - как у Камня духов).
  - progression.xml: сфера в unlock_entry группы 1, unlock_tier="1".
  - SpatialVaultPickupPatch: SpendBlood -> SpendCharge, имя из новой константы
    ChargeItemName. Гейт пустого слота в Begin снова включён - после отката он был
    временно закомментирован, потому что вставлять было нечего.

ИКОНКИ. Свои, от пользователя: BloodSphere.png и BloodStone.png, 160x160 RGBA, в
ItemIconAtlas и ItemIconAtlasGreyscale. Серая копия обязательна - без неё у
заблокированной записи в скиллах не будет картинки вообще. Способ её получения
подобран сверкой с существующими файлами и совпал ПОБИТОВО: convert('L') на RGB
без альфы, альфа приклеивается обратно отдельным каналом. Записано в BACKLOG.md,
раньше это нигде не было зафиксировано. Иконка камня положена заранее - самого
предмета ещё нет, он запланирован.

ПРОВЕРЕНО, ЧТО НИЧЕГО БОЛЬШЕ НЕ ЕДЕТ. Сверка с 7172681: набор <item> в items.xml
не изменился, в item_modifiers.xml единственное добавление - resourceBloodSphere,
и ни у одного существующего предмета не менялись Tags, ShowQuality и Stacknumber.
То есть ни один предмет не сменил класс и не сменил раскладку.

В игре: мир грузится без ошибок, сфера крафтится и тратится - в логе
"sphere in slot 0 now 12/500 used" за 12-секундный канал.

---

Revert the blood, and a Blood Sphere in its place

Fixes the save corruption introduced by e362c62 and replaces the mechanic behind
it with a safe one.

WHAT WAS BROKEN. e362c62 moved resourceNecromancerBlood from items.xml into
item_modifiers.xml so it could be installed in the Spatial Bracelet. An item's
CLASS decides the BYTE LAYOUT of every stack of it in a save:

    ItemValue.Read  line 1094:  if ((version > 4 || HasQuality) && !(itemClass is ItemClassModifier))
    ItemValue.Write line 1228:  if (!(ItemClass is ItemClassModifier))

A plain item writes a modifications count and a cosmetic-slot count; an
ItemClassModifier writes neither. A save written before the move reads out of
step after it - the stream slips on the first blood stack and PlayerDataFile.Load
throws. The .ttp.bak backup dies with the main file, being the same format. In the
test world the character was lost along with its backup and the game fell back to
Respawning: NewGame.

e362c62's message claimed "saves are safe", citing assignIdsFromMapping. Item ids
really do come from the stored name->id mapping - but what breaks is not the id,
it is the byte layout, and the mapping has nothing to do with it. The wrong thing
was verified.

THE REVERT. The blood is a plain <item> in items.xml again. Its property set was
diffed against 7172681 and matches character for character: both Stacknumber=1 and
the durability (ShowQuality + DegradationBreaksAfter + the DegradationMax
effect_group) are gone. Both were harmless to the save format, but both existed
only to serve the cancelled mechanic - on the user's instruction the revert goes
back to "as it was", not "as it was plus whatever I judged harmless". The stack is
15 again, inherited from medicalBloodBag.

A warning carrying the Read/Write line numbers stays in the item's comment - the
one thing worth keeping out of this accident.

THE BLOOD SPHERE. The bracelet's charge is its own item now, and that is precisely
what makes this safe: resourceBloodSphere is a NEW name, absent from every
existing save, so no stack of it can be read under the wrong layout. The general
rule the accident produced: an item that may already sit in someone's inventory
must never be moved between ItemClass and ItemClassModifier in either direction -
if a modifier is wanted, make a new item.

Dictated: available at the first grade, no workstation, 1 Necromancer's Blood +
5 zombie ash makes two spheres, durability 500. Held, it is a stone with a
scarlet tint.

  - item_modifiers.xml: installable_tags="necroBracelet", its own modifier_tags,
    type="attachment", DegradationMax 500 in a tiered="false" group.
    Extends=modGeneralMaster, NOT resourceRockSmall: the rock would have brought
    Action0 ThrowAway and ThrowableDecoy with it, making the sphere throwable.
    The mesh comes from the Meshfile line; inheriting a whole item for it is not
    needed.
  - Held look: HoldType 40 and all three meshes (Meshfile/HandMeshfile/
    DropMeshfile) on rock_smallPrefab, plus TintColor "220, 30, 45". A proven
    path - the Spirit Stone and both portal stones already live on that mesh with
    that same kind of tint.
  - recipes.xml: count=2, no craft_area and no unlock tag (group 1 opens at level
    1, and a recipe with no tag is simply always available, as with the Spirit
    Stone).
  - progression.xml: the sphere joins group 1's unlock_entry at unlock_tier="1".
  - SpatialVaultPickupPatch: SpendBlood -> SpendCharge, the name coming from a new
    ChargeItemName constant. The empty-slot gate in Begin is switched back on - it
    was commented out during the revert because nothing could be installed.

ICONS. The user's own art: BloodSphere.png and BloodStone.png, 160x160 RGBA, in
both ItemIconAtlas and ItemIconAtlasGreyscale. The greyscale copy is mandatory -
without it a locked skill entry has no picture at all. How those copies are made
was worked out by diffing against the existing files and matched BIT FOR BIT:
convert('L') over RGB without the alpha, with the alpha merged back as its own
channel. Written up in BACKLOG.md; it had never been recorded anywhere. The
stone's icon is filed ahead of the item, which is still only planned.

VERIFIED THAT NOTHING ELSE SHIFTS. Diffed against 7172681: the set of <item>
entries in items.xml is unchanged, the only addition to item_modifiers.xml is
resourceBloodSphere, and no existing item had its Tags, ShowQuality or Stacknumber
changed. No item changed class, and no item changed layout.

In game: the world loads clean, and the sphere crafts and drains - the log shows
"sphere in slot 0 now 12/500 used" for a 12-second channel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MnwP2Dt1vk8bUPJ452EoVL
2026-09-15 23:50:37 +03:00
AlexCubeandClaude Opus 5 e362c627e7 Кровь некроманта - топливо Пространственного браслета
Шесть указаний одного захода, которые сложились в одну механику: поглощение блока
больше не бесплатно. Браслет требует модификацию в слоте, кровь ею стала, и она
на это тратится.

СТАК ПО ОДНОЙ БАНКЕ. Наследуемый medicalBloodBag даёт Stacknumber 15, и это надо
перебивать явно - Extends копирует свойство целиком, а не "если не задано иначе".
Дорого и так задумано: Чёрный портал просит десять банок, то есть десять ячеек.

ПРОЧНОСТЬ 1000. Свойства с именем вроде Durability в игре нет; ручек две, и обе
обязательны. Число - пассивный эффект DegradationMax
(ItemValue.MaxUseTimesBase -> EffectManager.GetValue), полоска - отдельное
свойство ShowQuality (XUiC_ItemStack.ShowDurability -> ItemClass.ShowQualityBar).
Без первой прочность равна нулю, а полоска при MaxUseTimes == 0 рисуется ПОЛНОЙ,
то есть забытый эффект выглядит как "всё работает". tiered="false" обязателен:
ItemClass.HasQuality читается как Effects.IsOwnerTiered(), и тированная группа
превратила бы банку в предмет с качеством, с тирами и рамкой.

КРОВЬ ПЕРЕЕХАЛА В item_modifiers.xml. Это не настройка, а смена класса предмета:
XUiC_ItemPartStack.CanSwap открывается строкой
`if (!(stack.itemValue.ItemClass is ItemClassModifier ...)) return false;` - слот
модификации не смотрит ни на теги, ни на свойства, пока предмет не
ItemClassModifier, а этот класс создаётся только из <item_modifier>. Свойства
вида CanBeInstalled не существует; остаться ресурсом в items.xml и вставляться в
браслет физически нельзя. На старом месте оставлен комментарий-указатель.

Что при этом проверено, а не понадеялось: рецепты резолвятся (ItemClassModifier
наследует ItemClass, имена лежат в общем ItemClass.nameToItem, Recipe ищет через
GetItemClass по тому же словарю); сейв цел (ItemClass.assignIdsFromMapping берёт
айди из сохранённого name->id мэппинга, перестановка в конфигах предмет не
подменит); Harmony-патч и ключи локализации ходят по имени, имя не менялось.

ЛОВУШКА ПЕРЕЕЗДА: у модификации effect_group применяется к предмету, В КОТОРЫЙ её
вставили - прочность 1000 начала бы выдаваться БРАСЛЕТУ. Пассивка гейтована
tags="necroBloodFlask", тег добавлен в Tags флакона: MaxUseTimesBase зовёт
GetValue с ItemTags того предмета, для которого считает.

СЛОТОВ У БРАСЛЕТА 1, было 4. Слот из набора улучшений стал выбором.

ПУСТОЙ СЛОТ ОТКАЗЫВАЕТ. Проверка стоит первой строкой Begin, впереди всех
остальных отказов: прочие про ЦЕЛЬ (нет блока, не тот блок, хранилище полно), эта
про ИНСТРУМЕНТ, и сказать "здесь нет блока", когда пуст браслет, значит отправить
игрока искать не там. Тест - ItemValue.HasMods(), игровой собственный: обходит
только Modifications, пропуская null и IsEmpty, и не считает CosmeticMods, иначе
краска читалась бы как "браслет заряжен". Звук отказа достался бесплатно -
Deny() в этом файле уже играет ванильный ui_denied.

РАСХОД. Цена пула считается ОДИН раз, при старте, и едет в PickupJob.ChannelSeconds
вместе с самим браслетом. Не потому, что так короче: ChannelSecondsFor меряет луч
игрока, а за десять секунд игрок успевает отвернуться - второй вызов насчитал бы
цену за другой блок, а не за тот, который забрали. Браслет хранится экземпляром по
той же причине: моды живут на ItemValue, а колесо прокручивается.

Списывается в SpendBlood, ПОСЛЕ SetBlockRPC и после того, как предмет лёг в
хранилище: все отказы выходят раньше через return, так что кровь за отменённое
поглощение невозможна по построению. Имя предмета берётся из
NecromancerBloodPatch.BloodItemName, а не вторым литералом, чтобы не разъехались.
Мод, который не кровь, не платит ничего и поглощению не мешает - слот задуман под
другие вещи.

КОНЧИЛАСЬ - РАЗБИВАЕТСЯ. Правило именно "прочность 0 или меньше", а не "не хватило
на пул", и разница не косметическая: по второй формулировке флакон, которому
хватило впритык, остался бы в слоте с нулём, HasMods() видел бы "что-то вставлено",
и браслет работал бы бесплатно до конца света. Поэтому зажим по MaxUseTimes убран,
а слот обнуляется через ItemValue.None - это type 0, ровно то, что проверяет
IsEmpty(). Последнее поглощение проходит всегда, флакон его просто не переживает.
Звук - ванильный itembreak, тот же, что играет ItemAction.HandleItemBreak.

Защита: если MaxUseTimes окажется 0 (снесли passive_effect или тег), флакон НЕ
удаляется, а в лог идёт предупреждение с указанием, где чинить. Без этой ветки
ошибка в XML съедала бы игроку предмет на первом же поглощении, и выглядело бы это
багом механики.

Локализация: новый ключ braceletSpatialVaultNoMod в 13 языках, плюс описания
флакона и браслета - механика стала условной и платной, и оба текста без этого
стали бы враньём.

Известное и намеренное: кровавого камня, который обещает сообщение о пустом слоте,
ещё нет - он запланирован, разбор в BACKLOG.md. В игре ничего из этого не
проверено.

---

Necromancer's Blood is the Spatial Bracelet's fuel

Six instructions from one session that add up to one mechanic: pulling a block
into the vault is no longer free. The bracelet needs a mod in its slot, the blood
became that mod, and it is spent doing the work.

ONE JAR PER STACK. The inherited medicalBloodBag sets Stacknumber 15 and it has to
be overridden explicitly - Extends copies a property wholesale, not "unless set".
Expensive on purpose: the Black Portal asks for ten jars, so ten slots.

DURABILITY 1000. There is no property called anything like Durability; there are
two knobs and both are required. The number is a DegradationMax passive effect
(ItemValue.MaxUseTimesBase -> EffectManager.GetValue); the bar is a separate
ShowQuality property (XUiC_ItemStack.ShowDurability -> ItemClass.ShowQualityBar).
Without the first, durability is zero - and the bar at MaxUseTimes == 0 draws
FULL, so a forgotten effect looks exactly like success. tiered="false" is
mandatory: ItemClass.HasQuality is Effects.IsOwnerTiered(), and a tiered group
would have turned the jar into a quality item with tiers and a frame.

THE BLOOD MOVED TO item_modifiers.xml. Not a setting but a change of item class:
XUiC_ItemPartStack.CanSwap opens with
`if (!(stack.itemValue.ItemClass is ItemClassModifier ...)) return false;` - a mod
slot looks at neither tags nor properties until the item is an ItemClassModifier,
and that class is only created from <item_modifier>. No CanBeInstalled property
exists; staying a resource in items.xml and going into the bracelet is impossible.
A pointer comment was left where it used to live.

Checked rather than hoped: recipes still resolve (ItemClassModifier extends
ItemClass, names live in the shared ItemClass.nameToItem, Recipe looks them up
through GetItemClass); saves are safe (ItemClass.assignIdsFromMapping takes ids
from the stored name->id mapping, so shuffling configs cannot swap the item); the
Harmony patch and the localization keys go by name, and the name did not change.

THE TRAP IN MOVING IT: a modifier's effect_group applies to the item it is
INSTALLED IN - the 1000 durability would have been granted to the BRACELET. The
passive is gated with tags="necroBloodFlask" and the tag added to the flask's own
Tags: MaxUseTimesBase calls GetValue with the ItemTags of whatever it is
computing for.

THE BRACELET HAS 1 MOD SLOT, down from 4. The slot stopped being a set of
upgrades and became a choice.

AN EMPTY SLOT REFUSES. The check is the first line of Begin, ahead of every other
refusal: the others are about the TARGET (no block, wrong block, vault full), this
one is about the TOOL, and saying "no block there" when the real problem is an
empty bracelet sends the player looking in the wrong place. The test is
ItemValue.HasMods(), the game's own: it walks Modifications only, skipping nulls
and IsEmpty, and does not count CosmeticMods - a dye would otherwise have read as
"loaded". The refusal sound came free: Deny() in this file already plays vanilla's
ui_denied.

THE COST. The price of a pull is computed ONCE, at the start, and carried in
PickupJob.ChannelSeconds along with the bracelet itself. Not for brevity:
ChannelSecondsFor measures the player's ray, and ten seconds is long enough to
turn away - a second call would charge for a different block than the one taken.
The bracelet is kept as an instance for the same reason: mods live on the
ItemValue and the hotbar scrolls.

It is charged in SpendBlood, AFTER SetBlockRPC and after the item is in the vault:
every refusal returns earlier, so blood charged for a cancelled pull is impossible
by construction. The item name comes from NecromancerBloodPatch.BloodItemName
rather than a second literal, so the two cannot drift. A mod that is not blood
pays nothing and does not block the pull - the slot is meant for other things.

RUNS OUT, SHATTERS. The rule is "durability 0 or less", not "could not cover the
pull", and the difference is not cosmetic: under the second wording a flask with
exactly enough left would sit in the slot at zero, HasMods() would see "something
installed", and the bracelet would work for free forever. So the MaxUseTimes clamp
is gone and the slot is cleared with ItemValue.None - type 0, exactly what
IsEmpty() tests. The last pull always completes; the flask simply does not survive
it. The sound is vanilla's itembreak, the same cue ItemAction.HandleItemBreak
plays.

A guard: if MaxUseTimes comes out 0 (the passive effect or the tag removed), the
flask is NOT deleted and a warning naming the fix goes to the log. Without that
branch a config error would eat the player's item on the first pull and look like
a bug in the mechanic.

Localization: a new braceletSpatialVaultNoMod key in 13 languages, plus the flask
and bracelet descriptions - the mechanic became conditional and paid, and both
texts would have been lies without it.

Known and deliberate: the Blood Stone the empty-slot message promises does not
exist yet - it is planned, written up in BACKLOG.md. None of this is tested in
game.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MnwP2Dt1vk8bUPJ452EoVL
2026-09-15 22:32:59 +03:00
AlexCubeandClaude Opus 5 20af2bbe6c Торговец снова становится призраком после выгрузки чанка
Баг со стрима: торговец, ставший чёрно-белым и полупрозрачным, наутро снова
обычный человек. Догадка была про обновление ассортимента - не подтвердилась, и
это стоит записать, потому что по часам лавки модель не трогает ВООБЩЕ ничего:
TraderData при сбросе переписывает только PrimaryInventory и lastInventoryUpdate,
а TraderArea.SetClosed ходит по блокам TraderOnOff - двери, замки, BlockLight,
динамик - и до сущности торговца не дотягивается.

Настоящая причина в EntityFactory.CreateEntityOperation.CompleteEntity:
`entity.entityId = ecd.id`, то есть сохранённый айди ВОССТАНАВЛИВАЕТСЯ. Торговцы
стримятся на подходе и точно так же выгружаются, когда игрок уходит на ночь.
Возвращается он - торговец собран заново: новый GameObject, новые рендереры,
ванильные материалы, ТОТ ЖЕ entityId. А в патче стоял HashSet<int> Ghosted -
"этот айди я уже обработал". Айди в наборе есть, свип проходит мимо, торговец
остаётся живым человеком до конца сессии. Отсюда и "на следующее утро": симптом
идёт не за часами лавки, а за чанком, в котором лавка стоит.

Ghosted стал Dictionary<int, GhostBody>, где GhostBody держит массив рендереров,
которые патч взял себе. IsIntact проверяет их: у Unity уничтоженный объект
сравнивается с null, так что подмена модели видна прямо, и та же проверка
бесплатно закрывает любую другую пересборку, не только выгрузку чанка.

Prune чистит Converted и TintedMaterials от уничтоженных объектов. Без него оба
списка росли бы на одного торговца за каждую пересборку, а Retint/Reapply ходили
бы по обломкам. Материал, выданный через renderer.materials, принадлежит
рендереру и умирает вместе с ним - одного прохода хватает на оба списка.

В лог добавлена строка "entity <id> came back with a new model" - ровно та,
которой не хватало, чтобы найти это за один заход вместо разбора в декомпиляторе.

Счётчик в necroghost переименован: со словарём он означает "торговцев под
присмотром сейчас", а не "id, которые когда-либо видели" - он теперь падает и
растёт.

Не проверено в игре: нужен заход к торговцу, выгрузка лавки и возвращение.

---

Traders go back to being ghosts after a chunk unload

Reported from the stream: a trader who had gone black-and-white and translucent
was an ordinary person again the next morning. The guess was the restock - it was
wrong, and that is worth recording, because nothing on the shop's clock touches
the model at all: TraderData's reset rewrites PrimaryInventory and
lastInventoryUpdate only, and TraderArea.SetClosed walks TraderOnOff blocks -
doors, locks, BlockLight, speaker - and never reaches the trader entity.

The real cause is in EntityFactory.CreateEntityOperation.CompleteEntity:
`entity.entityId = ecd.id`, so the saved id is RESTORED. Traders are streamed in
on approach and streamed out the same way when the player leaves for the night.
On return the trader is rebuilt from scratch - new GameObject, new renderers, the
game's own materials - carrying THE SAME entityId. The patch held a
HashSet<int> Ghosted, meaning "this id is done". The id was still in the set, the
sweep skipped him, and he stayed an ordinary person for the rest of the session.
Hence "the next morning": the symptom follows the chunk the shop sits in, not the
shop's clock.

Ghosted is now a Dictionary<int, GhostBody>, the value holding the renderers the
patch took over. IsIntact tests them: Unity's destroyed objects compare equal to
null, so a swapped model is directly visible, and the same check covers any other
rebuild for free.

Prune drops destroyed entries from Converted and TintedMaterials. Without it both
lists would grow by one trader's worth per rebuild and Retint/Reapply would be
walking the wreckage. A material assigned through renderer.materials is owned by
that renderer and dies with it, so one pass settles both.

A log line was added - "entity <id> came back with a new model" - the exact line
that was missing to find this in one visit rather than in a decompiler.

The necroghost counter was reworded: with a dictionary it means "traders held as
ghosts right now", not "ids ever seen" - it now falls as well as rises.

Not tested in game: needs a visit, a shop unload and a return.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MnwP2Dt1vk8bUPJ452EoVL
2026-09-15 22:31:41 +03:00
AlexCubeandClaude Opus 5 7172681353 Версия 1.1.0: описания под забор блоков и торговцев
Номер по публичной линии Nexus: после 1.0.1 идёт 1.1.0, а не 1.0.2 - вышла не
починка, а две новые механики.

Описания приведены в соответствие с модом во всех четырёх местах, где они живут:
обе половины README, обе половины SITE_DESCRIPTION.html и NEXUS_DESCRIPTION.bbcode.

- Забор блоков дописан в пункт про Пространственный браслет: десять секунд
  вплотную плюс секунда за блок расстояния, тот же круглый индикатор, что у
  разбора верстака, обесцвечивание мира на время ожидания, перечень того, что не
  поддаётся, и прерывание силовой атакой. До этой правки там стояло "обычная
  атака пока ничего не делает".
- Торговцы - отдельным пунктом в "По мелочи", а на Nexus своим абзацем в тон
  остальному тексту: чёрно-белые, полупрозрачные и матовые, торговля не меняется.
- Счётчик строк 123 -> 130 в трёх местах. Он врал и до сегодня: ключей было 124.

ЗАОДНО ЭТО ЧИСЛО СТАЛО ЧЕСТНЫМ. Рядом со счётчиком все описания утверждают "пустых
ячеек нет", и это было неправдой: у txName_NecroAsh пустовали семь языков
(японский, корейский, польский, бразильский, турецкий и оба китайских).
Заполнены - теперь в файле 130 ключей и ноль пустых ячеек перевода.

Раздел "Статус" переписан под 1.1.0, прежний текст про 1.0.1 сохранён абзацем
ниже: он не устарел, он просто стал предысторией.

NEXUS_FILE_DESCRIPTION_1.0.1.bbcode намеренно не тронут - он описывает конкретную
уже залитую загрузку. Новой версии понадобится свой файл.

---

Version 1.1.0: descriptions updated for the block pull and the traders

The number follows the public Nexus line: after 1.0.1 comes 1.1.0 rather than
1.0.2, because what shipped is two new mechanics, not a fix.

The descriptions now match the mod in all four places they live: both halves of
the README, both halves of SITE_DESCRIPTION.html, and NEXUS_DESCRIPTION.bbcode.

- The block pull was added to the Spatial Bracelet entry: ten seconds up close
  plus one per block of distance, the same circular indicator a workbench pickup
  uses, the world draining to black and white for the wait, what refuses to be
  taken, and the power attack as the interrupt. Until this change the text still
  said the regular attack does nothing.
- The traders got their own bullet under "Small things", and on Nexus their own
  paragraph in the tone of the rest: black and white, half-transparent and matte,
  with the trading itself unchanged.
- The string count went 123 -> 130 in three places. It was already wrong before
  today: there were 124 keys.

AND THAT NUMBER IS NOW HONEST. Next to the count, every description claims there
are no empty cells, which was untrue: txName_NecroAsh was missing seven languages
(Japanese, Korean, Polish, Brazilian Portuguese, Turkish and both Chinese). They
are filled in - the file now has 130 keys and zero empty translation cells.

The "Status" section is rewritten for 1.1.0, with the old 1.0.1 text kept as a
paragraph below it: it did not become wrong, it became backstory.

NEXUS_FILE_DESCRIPTION_1.0.1.bbcode is deliberately untouched - it describes one
specific upload that already happened. The new version will need its own file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XN8J75vnum2qAVrtRUMKf7
v1.1.0
2026-09-14 16:32:46 +03:00
AlexCubeandClaude Opus 5 a6e19f9b97 Браслет утаскивает блоки в хранилище; обесцвечивание на время каналов
Обычная атака Пространственного браслета до сих пор ничего не делала. Теперь
она наводится на блок, показывает тот же круглый индикатор, что и демонтаж
верстака, и по его заполнении блок исчезает из мира и появляется в хранилище.

ВЕСЬ РЕЦЕПТ ВАНИЛЬНЫЙ. Block.TakeItemWithTimer и TakeItemWithTimerDone - это и
есть демонтаж верстака; взяты как есть, с двумя заменами: длительность и Bag
хранилища вместо рюкзака. Сообщения об отказе тоже ванильные
(ttRepairBeforePickup, ttBlockMissingPickup, ttCantPickupInUse,
ttWorkstationNotEmpty) - уже переведены на все языки игры, и игрок, хоть раз
забиравший верстак, знает, что они значат. Повреждённый блок отсекается первой
строкой, до открытия таймера: сообщение есть, индикатора нет.

Все проверки делаются ДВАЖДЫ, на старте и на финише: за десять секунд блок
можно расстрелять, выкопать, подменить, кто-то может открыть контейнер. Порядок
в финале принципиален - предмет кладётся в хранилище ПЕРВЫМ, и блок сносится,
только если он туда лёг; обратный порядок удалял бы блок из мира, когда
хранилище успело заполниться.

Цель - любой блок под прицелом, а не только то, что ваниль и так разрешает
поднимать. Отсюда две вещи, которых у узкого варианта не было бы: мультиблок
(дверь, кровать) приводится к родительской клетке через
multiBlockPos.GetParentPos, иначе половина модели осталась бы стоять; блоки без
предметной формы (ToItemValue пуст) отсекаются, иначе блок исчезал бы в обмен
на ничто.

СОДЕРЖИМОЕ КОНТЕЙНЕРОВ ПЕРЕЕХАТЬ НЕ МОЖЕТ: ItemStack в этой игре некуда
положить чужой инвентарь. Ваниль решает отказом, тем же и здесь, расширенным на
сундуки - в этой версии игры они моделируются композитным tile entity со
storage-фичей, поэтому вопрос задаётся фиче через
TryGetSelfOrFeature<ITileEntityLootable>.

ОТМЕНА СИЛОВОЙ АТАКОЙ, потому что десять секунд неподвижности после случайного
клика - долго, а ванильные выходы оба плохие: урон игрок не выбирает, а кнопка
активации не та, на которой уже лежит рука. Патч на XUiC_Timer.Update, как у
Синего портала, и вместе с его уроком: семантической PlayerActionsLocal.Secondary
недостаточно (модальное окно таймера держит фокус ввода - это выяснилось
багрепортом 29.08), поэтому рядом стоит сырой Input.GetMouseButtonDown(1).
Отдельная страховка от двойного открытия: отмена ловит НАЖАТИЕ, а обычная
силовая атака - ОТПУСКАНИЕ, и это одно нажатие.

ТЕРРИТОРИЯ ТОРГОВЦА И ДНО МИРА. Оба случая выглядят изнутри игры одинаково
("здесь ничего не ломается") и устроены совершенно по-разному. У торговца блоки
обычные, защищена ТЕРРИТОРИЯ: ваниль просто не зовёт DamageBlock внутри неё,
поэтому кирка не берёт, а браслет брал - он спрашивал про блок, а спрашивать
надо про место. Условие скопировано целиком, вместе с песочничной половиной
(World.SandboxUseTraderArea != Default || !IsWithinTraderArea): защита торговца
- серверная настройка, и сервер, который её выключил, не должен обнаружить, что
мод навязывает её сам. Дно мира - обратный случай: у бедрока CanDestroy=false на
МАТЕРИАЛЕ, и это спрошено как вопрос о материале, а не по имени блока.

Оба отказа с сообщением, хотя ваниль молчит: кирка, которая ничего не делает,
объясняет сама себя, а индикатор, который не появляется, выглядит как поломка
мода.

ДЛИТЕЛЬНОСТЬ РАСТЁТ С РАССТОЯНИЕМ - десять секунд вплотную, плюс секунда за
каждый полный блок. Расстояние не вычисляется заново: HitInfoDetails.distanceSq
- это квадрат длины ТОГО САМОГО луча, которым блок и выбран, а вычислять между
позициями значило бы выбрать точку в игроке (ноги? глаза?) и точку в блоке
(центр? грань?) и ошибиться хотя бы в одной. Пол, а не округление: только так
сходятся обе заданные точки - вплотную ровно 10, в пяти блоках ровно 15.

МИР ОБЕСЦВЕЧИВАЕТСЯ НА ВРЕМЯ ЛЮБОГО КАНАЛА - и утаскивания блока, и обоих
порталов (HarmonySrc/ChannelVision.cs, общий на оба, чтобы вид и время жили в
одном месте). Это штатный ScreenEffects игры: SetScreenEffect(name, intensity,
fadeTime), и плавность досталась даром - три секунды туда и три обратно это
третий аргумент. Эффект "Greyscale" выбран по тому, с кем НЕ придётся драться:
в него пишут только twitch_buffMonochrome и sandbox_blackandwhite, которых в
обычной сессии не бывает. "Dying"/"Dead" - те самые эффекты смерти, но их пишет
EntityPlayerLocal.Update из здоровья игрока при каждом изменении, и любой урон
посреди канала перехватил бы эффект. "Dark" дал бы затемнение, но принадлежит
buffCrouching и срабатывает на каждое приседание - поэтому затемнения нет
сознательно. Возврат красок вызывается на КАЖДОМ пути выхода, а в завершении
утаскивания - первой строкой, до всех проверок: десять секунд кончились и когда
блок забрали, и когда отказали.

Локализация: шесть новых ключей на 13 языков. Описание браслета переписано
(дважды за правку, вслед за механикой) - оно утверждало сначала "обычная атака
ничего не делает", потом "через десять секунд".

В игре проверено: забор блока работает. Отмена, обесцвечивание и рост
длительности - ещё нет.

---

The bracelet pulls blocks into the vault; colour drains during channels

The Spatial Bracelet's regular attack did nothing until now. It aims at a block,
shows the same circular indicator a workbench pickup does, and when it fills the
block leaves the world and appears in the vault.

THE WHOLE RECIPE IS VANILLA'S. Block.TakeItemWithTimer and TakeItemWithTimerDone
are the workbench pickup; taken as they are, with two substitutions - the
duration, and the vault's Bag instead of the backpack. The refusal messages are
vanilla's own keys too (ttRepairBeforePickup, ttBlockMissingPickup,
ttCantPickupInUse, ttWorkstationNotEmpty), already translated into every language
the game ships, and a player who has taken a workbench already knows them. A
damaged block is refused on the first line, before the timer opens: a message,
and no indicator.

Every guard is checked TWICE, once to open and once to finish: in ten seconds a
block can be shot, mined, replaced, or opened by someone else. The order at the
end matters - the item goes into the vault FIRST and the block is only removed if
it got there; the other way round deletes a block out of the world in exchange
for nothing when the vault filled up meanwhile.

The target is any block under the crosshair, not only what vanilla already lets
you take. Two things follow that the narrow version would never have faced: a
multiblock (a door, a bed) is resolved to its parent cell through
multiBlockPos.GetParentPos, or half the model would be left standing; and blocks
with no item form (ToItemValue comes back empty) are refused, or a block would
vanish in exchange for nothing.

CONTENTS CANNOT TRAVEL: an ItemStack in this game has nowhere to put another
container's inventory. Vanilla solves this by refusing, and so does this,
extended to chests - this version of the game models them as a composite tile
entity with a storage feature, so the question is asked of the feature through
TryGetSelfOrFeature<ITileEntityLootable>.

THE POWER ATTACK CANCELS, because ten seconds of standing still after a misclick
is long and vanilla's two escapes are both poor here: getting hit is not a
choice, and the activate key is not the button a hand is already on. The patch
sits on XUiC_Timer.Update like the Blue Portal Stone's, and carries its lesson:
the semantic PlayerActionsLocal.Secondary is not enough, because the modal timer
window holds input focus (found by a bug report on 29 Aug), so a raw
Input.GetMouseButtonDown(1) sits next to it. A separate guard stops one press
opening the vault twice: the cancel catches the button going DOWN, the ordinary
power attack catches it coming UP, and that is one press.

A TRADER'S GROUND AND THE WORLD'S FLOOR look identical from inside the game
("nothing breaks here") and are nothing alike underneath. A trader's blocks are
ordinary; it is the AREA that is protected - vanilla simply skips DamageBlock
inside it, which is why a pickaxe does nothing while the bracelet did not: it was
asking about the block when it had to ask about the place. The condition is
copied whole, sandbox half included (World.SandboxUseTraderArea != Default ||
!IsWithinTraderArea): trader protection is a server setting, and a server that
turned it off should not find this mod enforcing it anyway. The world's floor is
the opposite case - bedrock carries CanDestroy=false on its MATERIAL, and that is
asked as a question about the material rather than by block name.

Both refusals speak, where vanilla stays silent: a pickaxe that does nothing
explains itself, an indicator that never appears looks like this mod is broken.

THE CHANNEL GROWS WITH REACH - ten seconds up close, one more per full block.
The distance is not recomputed: HitInfoDetails.distanceSq is the squared length
of the very ray that chose this block, while measuring between positions would
mean picking a point in the player (feet? eyes?) and a point in the block
(centre? face?) and being wrong about one. Floor rather than round, because only
that makes both given anchors come out right - exactly 10 up close, exactly 15 at
five blocks.

COLOUR DRAINS OUT DURING ANY CHANNEL - the block pull and both portals
(HarmonySrc/ChannelVision.cs, shared so the look and the timing live in one
place). This is the game's own ScreenEffects: SetScreenEffect(name, intensity,
fadeTime), and the smoothness came free - three seconds each way is that third
argument. "Greyscale" was chosen by who else writes to it: only
twitch_buffMonochrome and sandbox_blackandwhite, neither of which happens in an
ordinary session. "Dying"/"Dead" are the death visuals being imitated, but
EntityPlayerLocal.Update writes "Dying" from the player's health on every change,
so any damage mid-channel would take it over. "Dark" would have supplied the
darkening half, but it belongs to buffCrouching and fires on every crouch - so
the darkening is deliberately absent. The colour is restored on EVERY exit path,
and in the pull's completion on the first line, before any check: the ten seconds
are over whether the block was taken or refused.

Localization: six new keys in 13 languages. The bracelet's description was
rewritten (twice in this change, following the mechanics) - it claimed first that
the regular attack does nothing, then that the pull takes ten seconds.

Confirmed in game: taking a block works. The cancel, the desaturation and the
distance scaling are not tested yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XN8J75vnum2qAVrtRUMKf7
2026-09-14 16:22:55 +03:00
AlexCubeandClaude Opus 5 275a739646 README: английская версия описания рядом с русской
На заглавной странице репозитория был только русский текст: английская
версия существовала, но лежала в SITE_DESCRIPTION.html - это исходник
страницы мода на сайте, в Gitea он не выводится никуда. Человек, пришедший
из Nexus, где описание как раз английское, попадал на русскую страницу.

Обе версии теперь в одном файле, английская после разделителя - той же
схемой, что уже применена в SITE_DESCRIPTION.html. Отдельный README.en.md
не заводился намеренно: Gitea показывает на заглавной ровно один файл, и
второй язык за ссылкой увидел бы только тот, кто догадается кликнуть.

Перевод сделан с README, а не скопирован из SITE_DESCRIPTION.html: тексты
не совпадают. В README подробнее арсенал (числа по модам ножа), есть ссылка
на ключи necroFinal* в Localization.csv и раздел "Статус", которых на сайте
нет вовсе. Все девять разделов зеркалят русские.

Под заголовком добавлена строка-указатель, чтобы англоязычный читатель не
листал вслепую длинную русскую половину, гадая, есть ли там что-то для него.

---

README: the English description alongside the Russian one

The repository's front page carried Russian only. An English version did
exist, but it lived in SITE_DESCRIPTION.html - the source of the mod's page
on the website, which Gitea renders nowhere. Anyone arriving from Nexus,
where the description is in English, landed on a Russian page.

Both versions now sit in one file, the English half after a separator - the
same shape already used in SITE_DESCRIPTION.html. A separate README.en.md
was deliberately not created: Gitea shows exactly one file on the front
page, and a second language behind a link is seen only by whoever thinks to
click it.

The translation is made from the README rather than copied out of
SITE_DESCRIPTION.html, because the two texts differ. The README goes into
more detail on the arsenal (the numbers on the knife mods), points at the
necroFinal* keys in Localization.csv, and has a "Status" section the website
text does not have at all. All nine sections mirror the Russian ones.

A pointer line under the title keeps an English reader from scrolling
blindly through the long Russian half wondering whether anything there is
for them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XN8J75vnum2qAVrtRUMKf7
2026-09-14 15:14:01 +03:00
AlexCubeandClaude Opus 5 7768541f12 Торговцы-призраки: настоящая прозрачность тела, матовость, консоль
Прозрачность у торговцев управляла только бородой. Причина была напечатана
зондом ещё при первом заходе: у шейдера тела (Game/Character) нет НИ цвета с
альфой, НИ режима смешивания - оба рычага ApplyTransparency существуют только
у шейдера волос (Game/Autodesk). Никакое значение альфы тело бы не тронуло.

Сначала добавлен третий рычаг - _Fade, собственный рычаг игры:
EntityModel.SetFade пишет ровно это свойство и отбирает материалы проверкой
HasProperty("_Fade") && shader.name.Contains("Game/Character"), а стоящий рядом
SetVisible(renderFade > 0.01f) закрывает направление: 1 = сплошной, 0 = исчез.
В игре выяснилось, что шейдер реализует его screen-door дизерингом - "тело
гаснет, но идёт мелкой сеточкой". Рычаг рабочий, но пиксели не смешиваются, а
выбрасываются, и никакое число этого не исправит. Оставлен как режим dither.

РАБОЧЕЕ РЕШЕНИЕ - перешивка тела на шейдер волос, у которого есть прозрачный
проход. Доказательство лежало на той же модели в том же кадре: борода всё это
время гасла плавно. Шейдер берётся С МОДЕЛИ - первый материал, умеющий
смешивать (есть цвет с альфой И режим смешивания); Shader.Find оставлен
запасным путём, потому что мод уже дважды получал null/негатив от семейства
Unlit. Решение принимается по способностям материала, имя шейдера нигде не
зашито. Переносятся только альбедо и нормаль: _RMOE - упакованная
roughness/metallic/occlusion/emission, а _MetallicGlossMap ждёт металл в R и
гладкость в A, каналы не совпадают, и связывание "на авось" даёт мокрый пластик
или негатив. Цена названа прямо: тело теряет собственную световую модель
шейдера персонажа и получает стандартную.

МАТОВОСТЬ по просьбе пользователя - три рычага, ломаются по-разному:
_Glossiness в 0 (чистая математика шейдера, работает всегда - несущая
половина); _SpecularHighlights и _GlossyReflections в 0 ВМЕСТЕ с ключевыми
словами _SPECULARHIGHLIGHTS_OFF / _GLOSSYREFLECTIONS_OFF, потому что это
[ToggleOff]-пара и запись одного флоата не делает ничего; карты глянца
очищаются, если непусты, - карта побеждает флоат. Оба keyword'а -
shader_feature, Unity вырезает такие на сборке, если их не выставляет ни один
материал игры, поэтому шершавость сделана основным рычагом, а не запасным.
Применяется ко всем призрачным материалам: волосы нарисованы тем же шейдером и
сохраняли свой блеск, а матовое тело под глянцевой бородой читалось бы хуже.

КОНСОЛЬНАЯ КОМАНДА necroghost (алиас necrotrader): проценты прозрачности,
blend/dither, reset. Балансировать рендер можно только глядя на него, а шаг без
команды стоил пересборки, перезапуска и ~4 минут пешком до торговца. Проценты,
а не альфа: это единица, в которой просьба формулировалась, и они идут в разные
стороны. Регистрации нет и не требуется - SdtdConsole.RegisterCommands ищет
через ReflectionHelpers.FindTypesImplementingBase, который обходит
ModManager.GetLoadedAssemblies(), а LoadMods() стоит на три строки раньше
RegisterCommands(). IsExecuteOnClient = true: команда меняет пиксели.

Две мины, обе реальные. Повторное применение не должно умножать: материалы
кэшируются, и sharedMaterials после первого прохода возвращает наши же клоны,
так что наивный повторный свип дал бы 0.9, потом 0.8 = 0.72; запоминается
базовое значение, живое всегда base * GhostAlpha. Запятая: StringParsers не
зависит от локали, но читает "," как разделитель ТЫСЯЧ, и "necroghost 12,5"
молча стало бы 125 - запятая заменяется на точку до парсинга.

DefaultGhostAlpha 0.9 -> 0.3: 70% прозрачности, найденные в игре. Путь был
1% -> 10% -> 70%, и последний скачок не смена вкуса - на 10% тело ещё
дизерилось, а дизеринг терпим лишь пока слаб. Поэтому же переписана заметка
"ниже ~0.7 силуэт разваливается": предел принадлежал технике, а не глазу.

---

Ghost traders: the body really blends, matte finish, console command

The transparency only ever reached the beard. The probe had already printed
why: the body's shader (Game/Character) has NEITHER a colour with an alpha NOR
a blend mode - both of ApplyTransparency's levers exist only on the hair's
shader (Game/Autodesk). No value of the alpha was ever going to touch it.

A third lever was added first - _Fade, the game's own: EntityModel.SetFade
writes exactly that property and guards it with HasProperty("_Fade") &&
shader.name.Contains("Game/Character"), and the SetVisible(renderFade > 0.01f)
sitting next to it settles the direction: 1 = solid, 0 = gone. In game the
shader turned out to implement it as screen-door dithering - "the body fades,
but goes to a fine grid". The lever works, but pixels are thrown away rather
than blended, and no number fixes that. It is kept as the dither mode.

WHAT ACTUALLY WORKS is re-shading the body onto the hair's shader, which does
have a transparent pass. The existence proof was on the same model in the same
frame: the beard had been fading smoothly all along. The shader is taken OFF
THE MODEL - the first material that can blend (a colour with an alpha AND a
blend mode); Shader.Find is kept only as a fallback, because this mod has twice
been handed null or a negative by the Unlit family. The decision is made on
what a material can do; no shader name is hard-coded. Only albedo and normal
are carried over: _RMOE is a packed roughness/metallic/occlusion/emission map
while _MetallicGlossMap wants metallic in R and smoothness in A - the channels
do not line up, and wiring them by hope is how a character ends up looking like
wet plastic or a negative. The trade is stated plainly: the body loses the
character shader's own lighting response and gets standard lighting instead.

MATTE, as requested - three levers that fail differently: _Glossiness to 0
(plain shader maths, always works - the load-bearing half); _SpecularHighlights
and _GlossyReflections to 0 TOGETHER WITH the _SPECULARHIGHLIGHTS_OFF /
_GLOSSYREFLECTIONS_OFF keywords, because they are a [ToggleOff] pair and
setting the float alone does nothing; and the gloss maps cleared if anything is
in them, since a map beats the float. Both keywords are shader_feature, which
Unity strips at build time if no material in the game sets them - which is why
roughness is the main lever and not the fallback. Applied to every ghost
material: the hair uses the same shader and kept its own shine, and a matte
body under a glossy beard would have read worse than either.

CONSOLE COMMAND necroghost (alias necrotrader): transparency in percent,
blend/dither, reset. A rendering balance can only be judged by looking at it,
and without the command each step cost a rebuild, a restart and a four-minute
walk to a trader. Percent rather than alpha: percent is the unit the request
was made in, and the two run in opposite directions. No registration is needed
- SdtdConsole.RegisterCommands goes through
ReflectionHelpers.FindTypesImplementingBase, which walks
ModManager.GetLoadedAssemblies(), and LoadMods() runs three lines before
RegisterCommands(). IsExecuteOnClient = true: the command changes pixels.

Two real traps. Re-applying must not compound: materials are cached and
sharedMaterials hands back our own clones after the first pass, so a naive
second sweep would give 0.9, then 0.8 = 0.72; the base value is remembered and
the live one is always base * GhostAlpha. The comma: StringParsers is
culture-independent but reads "," as a THOUSANDS separator, so "necroghost
12,5" would silently have become 125 - the comma is turned into a point first.

DefaultGhostAlpha 0.9 -> 0.3: the 70% transparency settled on in game. The road
was 1% -> 10% -> 70%, and the last jump was not a change of taste - at 10% the
body was still dithering, and a dither is bearable only while it is faint. For
the same reason the old "below ~0.7 the silhouette falls apart" note was
rewritten: that limit belonged to the technique, not to the eye.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XN8J75vnum2qAVrtRUMKf7
bugfix1
2026-09-14 15:05:49 +03:00
AlexCubeandClaude Opus 5 9ac575075a Описание файла 1.0.1 для Nexus
Описание КОНКРЕТНОГО файла в разделе Files, отдельно от описания страницы
мода в NEXUS_DESCRIPTION.bbcode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEXvXg1FSAQJHrvYbeAKqq
2026-09-13 20:42:06 +03:00
AlexCubeandClaude Opus 5 4801341676 Версия 1.0.1
Номер выбран под то, что видят игроки: на Nexus опубликована 1.0, а 2.0.0
в ModInfo проставлялась для внутренней работы и наружу не выходила.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEXvXg1FSAQJHrvYbeAKqq
v1.0.1
2026-09-13 19:55:50 +03:00
AlexCubeandClaude Opus 5 29431990f6 Хранилище браслета переживает перезапуск; торговцы чёрно-белые
Исправляет первый баг-репорт мода на Nexus (youkia96581, 11.09.2026):
"Items stored in the space bracelet will disappear after leaving the game
and going online again". Причина была записана в коде как нерешённая:
PlayerVaults - обычный статический Dictionary, save/load не существовало.

ХРАНИЛИЩЕ ТЕПЕРЬ ЖИВЁТ В PlayerDataFile, рядом с рюкзаком игрока. Так
решено после вопроса пользователя "почему не сделать принцип как у ящика?":
ящик хранит вещи тем, что они лежат в чанке (у TileEntity единственный
конструктор TileEntity(Chunk)), а браслету нужен был дом в чём-то, что
движок и так сохраняет. Четыре постфикса - FromPlayer/Write/Read/ToPlayer,
блоб с магией "NECROVLT" и явной длиной дописывается после всего
ванильного. Байтовая часть - в сателлитной сборке: PooledBinaryWriter.Write
не резолвится из основного проекта (CS7069), как и у PyramidWardWriteHelper.

Два дефекта, найденные и убитые по дороге живыми тестами:

1. ModEvents.WorldShuttingDown приходит ПЕРЕД финальным сохранением игрока
   (GameManager.SaveAndCleanupWorld: событие на IL_0026, SaveLocalPlayerData
   на IL_00c4). Обработчик, чистивший там кэш, затирал хранилище на каждом
   корректном выходе. Обработчик убран; свежесть решает авторитетность
   ToPlayer, а не таймер.
2. Пустой сессионный кэш трактовался как "хранилища нет" и записывался
   поверх настоящего. Путь восстановления имеет право не сработать, удалять
   он права не имеет - добавлена страховка LastLoadedVault.

Проверено в игре: положил -> вышел -> запустил заново -> вещи на месте,
блоб на 54 байта сверен в .ttp побайтово.

ТОРГОВЦЫ (npcTraderJoel/Rekt/Bob/Hugh/Jen) - чёрно-белые. Шейдер НЕ
подменяется: материал клонируется со своим шейдером, меняется только
текстура альбедо на обесцвеченную копию, так что свет, нормали и скиннинг
остаются движковыми. Альбедо ищется обходом свойств шейдера, а не по имени:
тело - Game/Character/_Albedo, волосы - Game/Autodesk/_MainTex. Плюс 1%
прозрачности с сохранением _ZWrite. Опрос раз в 2 с, потому что торговцы
стримятся на подходе, а Джен собирается в рантайме.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEXvXg1FSAQJHrvYbeAKqq
2026-09-13 19:54:48 +03:00
AlexCubeandClaude Opus 5 a5f8592903 Пространственный браслет: 4 слота под модификации
Сами моды будут позже. Схема списана с Ножа некроманта: тег noMods
отсекает все ванильные моды, тег necroBracelet зарезервирован под будущие
свои. canHaveCosmetic намеренно не добавлен - именно он создаёт слот под
краски.

У effect_group НЕТ tiered="false", и это главное: ItemClass.HasQuality -
это Effects.IsOwnerTiered(), а ItemValue.FireEvent выходит по
if (!HasQuality) return; ДО обхода Modifications[]. На нетированном
предмете слоты появились бы, моды вставлялись бы, и ни один
triggered_effect внутри них не сработал бы. Качество при этом не
показывается: ShowQuality по умолчанию false и намеренно не задан.

Старый браслет из сейва слотов не получит - размер Modifications[]
фиксируется при создании предмета.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEXvXg1FSAQJHrvYbeAKqq
2026-09-13 19:54:22 +03:00
AlexCubeandClaude Opus 5 ccf58a2ec8 "Могильный покой": термозащита 5 -> 50
Прямое указание пользователя. Единица здесь - градусы сдвига уличной
температуры к комфортным 70, а не проценты, и в PlayerEntityStats стоит
min/max-ограничение: с 50 любая температура в пределах 50 градусов от 70
подтягивается к 70 целиком. То есть примерно от 20 до 120 по шкале игры
это полный иммунитет, а не "сильная защита". Прежние 5 были уровнем одной
детали брони с T3-подкладкой.

Описание в Localization.csv править не потребовалось: там "strong
protection from both freezing and heat" без конкретных цифр.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEXvXg1FSAQJHrvYbeAKqq
2026-09-13 19:54:22 +03:00
AlexCubeandClaude Opus 5 a645c53ff3 Пространственный браслет: в руке пусто, блочный хват
Свёрток-«коробочка на бечёвке» (parcelGenericPrefab + HoldType=31,
унаследованные от Петли вора) для браслета выглядел нелепо. Теперь в
руке не видно ничего, только кулак, повёрнутый вниз.

Связка списана с ванильного vehicleMinibikePlaceable (items.xml:13385),
где те же две строки стоят подряд: HoldType="7" + HoldingItemHidden="true".

Найдено декомпиляцией Assembly-CSharp 3.2.0 через Mono.Cecil, который
лежит прямо в игре (Mods/0_TFP_Harmony/Mono.Cecil.dll):

- HoldType=7 - это и есть блочный хват: его ставит ItemClassBlock..ctor,
  а в blocks.xml свойство HoldType не встречается ни разу, то есть каждый
  блок в игре держится именно этим значением. Костет - это 70, запасной
  вариант не понадобился.
- HoldingItemHidden - штатное свойство ItemClass (PropHoldingItemHidden в
  .cctor, ParseBool в Init), а Inventory.setHoldingItemTransform в конце
  делает SetActive(!HoldingItemHidden). Гаснет только модель в руке -
  иконка инвентаря и предмет на земле не трогаются.
- Пустой меш поставить нельзя: ItemClass.CloneModel подставляет заглушку
  leather.fbx, если ассет не загрузился. Поэтому Meshfile оставлен
  затычкой, а DropMeshfile - ванильный мешок, как у минибайка.
- Действия предмета не задеты: ItemActionEat читает HoldType только ради
  AnimationDelay[HoldType].RayCast, который равен 0f и у 31, и у 7;
  ExecuteAction, за которую держится SpatialVaultPatch, его не читает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEXvXg1FSAQJHrvYbeAKqq
2026-09-13 17:21:20 +03:00
AlexCubeandClaude Opus 5 ab1b3eadaf Версия 2.0: свои модели ножа и крови, своя краска блоков через патч атласа
Нож некроманта и Кровь некроманта получили собственные модели, а Пирамида
духов - собственную поверхность. Три разных способа, каждый выбран по тому,
как устроен сам предмет.

НОЖ И КРОВЬ - свои префабы в бандлах мода.
Форма записи "#@modfolder(...)?prefab" подтверждена живым примером; геометрия
у обоих ванильная, меняются материал и текстура. Текстуры генерируются
скриптами (см. _private/tools), а не рисуются: правка вида сводится к правке
констант и одному запуску.

Нож: состаренная кость, почти чёрная обмотка, пурпур во впадинах, плюс слой
под ручную роспись рун - он подмешивается в альбедо и в эмиссию, поэтому руны
светятся. Слой пользовательский, генератор его никогда не перезаписывает.
Положение росписи посчитано по геометрии: развёртка выгружена из меша, и
подобрано смещение, при котором вся роспись ложится на одну плоскую грань.
Раньше она перегибалась через кромку, обращённую к игроку.

Кровь: банка вместо мешка. Заодно чинится расхождение, жившее с самого начала -
описание предмета говорило "Банка, наполненная кровью", а наследуемый
medicalBloodBag показывал sackPrefab, обычный мешок. Жидкость перерисована по
маскам мешей, а не перекрашена тинтом: тинт предмета на этот меш не действует
вовсе, у шейдера Game_EntityTintMaskSSS выигрывает _Color материала.

ПИРАМИДА - своя краска в атласе блоков через собственный Harmony-патч.
Ваниль своих текстур блоков не умеет: Texture у блока это индекс в готовом
атласе, а запись краски несёт только TextureId/PaintCost/Group/SortIndex и
никогда путь к картинке. Поэтому CustomBlockPaintPatch дописывает наш слой в
массивы атласа на лету.

Путь через свою модель (Shape="ModelEntity") пробовался и отложен: там
остались нерешёнными столкновения и маджента на дальних экземплярах. Краска
лучше тем, что блок остаётся обычным Shape="New" - со всеми работающими
столкновениями, наведением по E и правильной посадкой, - а поверхность у него
своя. Побочно краска доступна кисточкой под именем "Некротический прах".

Параметры атласа не угаданы, а замерены в игре (BlockAtlasProbePatch): 512x512,
DXT1 для альбедо и DXT5 для нормалей со specular, 10 мип-уровней, массивы
нечитаемые - отсюда GPU-копирование и явные настройки импорта текстур.
Зонд оставлен намеренно: номер краски в blocks.xml (608) это длина ванильного
uvMapping, и после обновления игры он может сдвинуться - зонд печатает
фактические числа при каждом запуске.

Патчи обёрнуты целиком: они работают внутри загрузки игры, где вылетевшее
исключение срывает шаг загрузки.

Resources/necropyramid в коммит не идёт - на него никто не ссылается.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5F3AVwsusZHMqPBcSQcVJ
2026-09-10 16:28:59 +03:00
AlexCubeandClaude Opus 5 026e006c9c Убран мёртвый код проигрывания видео; заготовка своей модели ножа
PlayBlackPortalVideoLegacy и BlackPortalVideoPath ниоткуда не вызывались
с 2026-09-09, когда концовку заменили слайдами. Разборы Pause/PlayVideo и
синтаксиса @modfolder перенесены в NoteFlashbackPatch - единственное место,
где эти API ещё вызываются. Перекрёстные ссылки в FinalSlides поправлены,
устаревшее упоминание StayVideoPath/ReturnVideoPath убрано.

В items.xml у necroWpnBladeNecroKnife добавлена закомментированная строка
Meshfile под будущий бандл Resources/necroknife - включать вместе с самим
бандлом, не раньше. Там же снято старое сомнение про TintColor: ваниль сама
тинтит тот же boneShivPrefab, значит механизм на оружии работает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3pwyLpTrwz41qBjyjzNSA
2026-09-10 02:07:31 +03:00
Alex CubeandClaude Opus 5 6e1f42f053 Добавлена ссылка на страницу Nexus Mods
Мод опубликован: nexusmods.com/7daystodie/mods/12547. Ссылка вписана в README
и в оба языковых блока описания для сайта, первой строкой в списке ссылок -
для читателя это главная точка загрузки.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX
2026-09-09 21:49:38 +03:00
Alex CubeandClaude Opus 5 239aaeda3e Удалены три webm - копии ванильного TFP_Intro.webm
BlackPortal.webm, FinalStay.webm и FinalReturn.webm были побайтово одним и тем
же файлом: копией студийного ролика The Fun Pimps, положенной заглушкой для
тестов конвейера диалог-пауза-видео. В релиз чужой ассет игры не отдаём.

Ни один из трёх в игре не проигрывался: финал показывает текстовые эпилоги,
вызов проигрывателя лежит в неиспользуемом PlayBlackPortalVideoLegacy.
Комментарии в FinalSlides.cs и PortalStonePatch.cs обновлены. Свой
DukeNoteFlashback.mp4 остался - он играет по стартовой записке.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX
v1.0.0
2026-09-09 21:45:46 +03:00
Alex CubeandClaude Opus 5 89aafb8ae2 Nexus: добавлено раскрытие про ИИ-ассеты
Иконки предметов и картинки финала сгенерированы ИИ - сказано в описании
прямо, тег AI-Generated Content на странице уже стоит.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX
2026-09-09 21:35:15 +03:00
Alex CubeandClaude Opus 5 6879f2ea22 Добавлено описание для страницы Nexus Mods (BBCode)
Пять разделов по шаблону Nexus: Description, Installation instructions,
Main features, Requirements, Shout outs. Таблица прогрессии переложена в
список - Nexus BBCode таблиц не поддерживает. Финал описан без спойлеров.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX
2026-09-09 21:29:17 +03:00
Alex CubeandClaude Opus 5 642b0db7d9 Описание для сайта: убраны теги p и разметка блоков WordPress
Абзацы разделены пустой строкой - wpautop обернёт их сам. Остались только
теги, которые редактор не восстановит: strong, a, code, h2/h3, ul/li, table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX
2026-09-09 21:20:06 +03:00
Alex CubeandClaude Opus 5 1db415d4d4 Описание для сайта переведено в разметку блоков WordPress
SITE_DESCRIPTION.md заменён на SITE_DESCRIPTION.html: блоки wp:paragraph,
wp:heading, wp:list, wp:table, wp:separator - вставляется в редактор кода
страницы целиком и разбирается на настоящие блоки.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX
2026-09-09 21:19:01 +03:00
Alex CubeandClaude Opus 5 e8f064f5ec Книга некроманта 1.0 — первая публичная версия
Мод для 7 Days to Die 3.2: навык «Некромантия», растущий от счётчика убитых
зомби, тёмное оружие с шестью собственными модами, призывная нежить, пирамида
духов и сюжетный финал через Чёрный портал. Локализация на 13 языках.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX
2026-09-09 21:13:03 +03:00