Режим змеи для Ножа некроманта и чёрный разрыв

Ночью по R с ножом - дух змеи: тело падает (-95% HP), игрок управляет
призрачной змеёй от первого лица. Свободный ход по миру (свой наблюдатель
чанков, отрисовка, звук, спавн и спящие считаются от змеи), прыжок,
атака на ЛКМ - рывок сквозь препятствия и чёрный разрыв, ПКМ - выбраться
из текстур. Тёмное чутьё, тревога у тела, отсчёт в радиации края карты,
пурпурный фильтр зрения (necrovision). Утром змея развеивается.

Чёрный разрыв (NecroBurst.cs): клон частиц confetti в чёрном, команда
necroburst. Проверено в игре 28.09.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-09-28 15:31:37 +03:00
co-authored by Claude Opus 5.5
parent 4cdd315b98
commit 6ff76d174e
13 changed files with 2289 additions and 1 deletions
+113
View File
@@ -318,6 +318,11 @@ namespace NecromancerTome
{
return false;
}
// Призрачная змея режима змеи (SnakeMode.cs, 2026-09-28): «текстуры как у торговцев».
if (entityClass.entityClassName == SnakeMode.SnakeClassName)
{
return true;
}
return Patch_ItemActionSpawnEntity_Spawn_PetLimit.LimitedPets.ContainsKey(entityClass.entityClassName);
}
@@ -429,7 +434,15 @@ namespace NecromancerTome
// Before anything is touched, because the body's replacement shader is found on the
// trader's OWN hair and the hair is not guaranteed to come first in this array.
// Змея не должна становиться источником BlendShader: он кэшируется на всю сессию, и
// торговцы после неё перекрашивались бы шейдером змеи, а не своих волос.
bool isSnake = _trader.EntityClass.entityClassName == SnakeMode.SnakeClassName;
Shader keptBlendShader = BlendShader;
FindBlendShader(renderers);
if (isSnake)
{
BlendShader = keptBlendShader;
}
int converted = 0;
int leversBefore = TintedMaterials.Count;
@@ -456,6 +469,17 @@ namespace NecromancerTome
}
_body.Renderers = taken.ToArray();
// Змея режима змеи идёт по ветке «блендим на месте» (Autodesk Interactive), и альфа там
// не видна даже с полной шестёркой настроек (_Mode 2, _ZWrite 0, ключевые слова, очередь)
// - проверено 28.09 дважды. Вероятная причина: копия шейдера из сборки змеи собрана без
// варианта _ALPHABLEND_ON (Unity выкидывает неиспользуемые варианты при сборке). Поэтому
// материалы змеи пересобираются на шейдер, который этот вариант ТОЧНО имеет - его берём
// у любого загруженного материала с включённым _ALPHABLEND_ON.
if (isSnake)
{
MakeSnakeFade(taken);
}
// The lever count is the half that answers "will the console command reach him":
// desaturation and opacity come from different properties, and the body had the first
// without the second until 2026-09-14. Fewer levers than converted renderers means
@@ -466,6 +490,95 @@ namespace NecromancerTome
return true;
}
static Shader _fadeShader;
/// <summary>Шейдер, у которого точно есть прозрачный вариант: берётся с любого загруженного
/// материала, где _ALPHABLEND_ON включён. Предпочтение - тем же шейдерам, что у змеи.</summary>
public static Shader FindFadeShader()
{
if (_fadeShader != null)
{
return _fadeShader;
}
Shader best = null;
int bestRank = int.MaxValue;
HashSet<string> seen = new HashSet<string>();
foreach (Material m in Resources.FindObjectsOfTypeAll<Material>())
{
if (m == null || m.shader == null || !m.IsKeywordEnabled("_ALPHABLEND_ON") ||
!m.HasProperty("_MainTex") || !m.HasProperty("_Color") || !m.HasProperty("_SrcBlend"))
{
continue;
}
string name = m.shader.name;
seen.Add(name + " (" + m.name + ")");
int rank = name == "Autodesk Interactive" ? 0 : name == "Standard" ? 1 : 2;
if (rank < bestRank)
{
best = m.shader;
bestRank = rank;
}
}
Debug.Log("[NecromancerTome] GhostTraderPatch: fade shaders loaded: " +
(seen.Count == 0 ? "<none>" : string.Join(", ", seen)) +
" -> " + (best != null ? best.name : "NONE"));
_fadeShader = best;
return best;
}
/// <summary>Пересобирает материалы змеи на шейдер из FindFadeShader: серая текстура и карта
/// нормалей переносятся, прозрачность - полной шестёркой.</summary>
public static void MakeSnakeFade(List<Renderer> _renderers)
{
Shader fade = FindFadeShader();
if (fade == null)
{
return;
}
int done = 0;
foreach (Renderer renderer in _renderers)
{
Material[] current = renderer.sharedMaterials;
Material[] rebuilt = new Material[current.Length];
for (int i = 0; i < current.Length; i++)
{
Material src = current[i];
if (src == null)
{
continue;
}
Material m = new Material(fade);
string albedo = FindAlbedoProperty(src);
if (albedo != null)
{
m.SetTexture("_MainTex", src.GetTexture(albedo));
}
Texture normal = FindTexture(src, NormalNameHints);
if (normal != null && m.HasProperty("_BumpMap"))
{
m.SetTexture("_BumpMap", normal);
m.EnableKeyword("_NORMALMAP");
}
MakeMatte(m);
if (m.HasProperty("_Mode"))
{
m.SetFloat("_Mode", 2f);
}
if (m.HasProperty("_ZWrite"))
{
m.SetFloat("_ZWrite", 0f);
}
m.DisableKeyword("_ALPHATEST_ON");
m.DisableKeyword("_ALPHAPREMULTIPLY_ON");
ApplyTransparency(m);
rebuilt[i] = m;
done++;
}
renderer.materials = rebuilt;
}
Debug.Log("[NecromancerTome] GhostTraderPatch: snake re-shaded onto '" + fade.name + "', " + done + " material(s)");
}
/// <summary>Builds and installs this renderer's ghost materials from the ORIGINALS it was
/// handed. True when anything was actually changed - a renderer nothing could be done to
/// keeps the materials it has rather than being handed a half-built array.</summary>