- Аниматор: IsAlive, IsFPV, WeaponHoldType 0 (FP_Unarmed_Hold) по умолчанию; сдвиг 0,-1.55,0.05. - Кожа: _Albedo убран, _Tint 0.6 серый (necroarms albedo on|off - сравнить). - Пояс персонажа скрыт в одержимости от первого лица. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
432 lines
18 KiB
C#
432 lines
18 KiB
C#
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using UnityEngine;
|
||
using UnityEngine.Scripting;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// Руки одержимого в виде от первого лица (указание 13.10: «делаем так, как у игрока»). У персонажа игры две
|
||
/// модели: полное тело (его видят другие и он сам со стороны) и отдельные руки для первого лица (baseRigFP,
|
||
/// SDCSUtils.CreateVizFP) - рисуются только у себя. Здесь собираем такие же руки по тем же шагам игры:
|
||
/// скелет baseRigFPPrefab с аниматором FPPlayerController, голая кожа (basePartsFP), без одежды и волос
|
||
/// (указание: «одежду пока не показываем»). Пол - по зомби (IsMale): BaseMale или BaseFemale из
|
||
/// archetypes.xml. Кожа серая (указание: «руки вообще серые - типа зомби их так видит») - цвет _Tint у
|
||
/// материала кожи, как игра красит тон кожи.
|
||
///
|
||
/// Руки - под UFPSRoot камеры игрока, как у персонажа; камеру одержимости мы ставим в голову зомби, руки
|
||
/// едут вместе с ней. Слой HoldingItem - его камера игрока рисует только при ShowHoldingItemLayer(true).
|
||
///
|
||
/// Только гуманоиды обычной комплекции (Allows), указание 13.10.
|
||
///
|
||
/// ПАМЯТЬ. Одна модель рук на время вида от первого лица; Hide выгружает её (SDCSUtils.UnloadViz - материалы
|
||
/// и ссылки на ассеты освобождаются) - на переключении вида и на выходе из одержимости.
|
||
/// </summary>
|
||
public static class FpArms
|
||
{
|
||
/// <summary>Начала имён классов, у кого вид от первого лица (варианты Feral/Radiated/Charged/Infernal - тоже).</summary>
|
||
static readonly string[] Allowed =
|
||
{
|
||
"zombieArlene", "zombieBiker", "zombieBoe", "zombieBowler", "zombieBurnt", "zombieBusinessMan", "zombieDarlene",
|
||
"zombieInmate", "zombieJanitor", "zombieJoe", "zombieLab", "zombieLumberjack", "zombieMaleHazmat", "zombieMarlene",
|
||
"zombieMoe", "zombieNurse", "zombiePartyGirl", "zombieSkateboarder", "zombieSoldier", "zombieSteve", "zombieTomClark",
|
||
"zombieUtilityWorker", "zombieYo",
|
||
};
|
||
|
||
public static Color Tint = new Color(0.6f, 0.6f, 0.6f, 1f);
|
||
/// <summary>Цветная текстура кожи (_Albedo): false - убрана, кожа ровно-серая по _Tint, рельеф - от карты нормалей
|
||
/// (указание 13.10: «руки вообще серые»; _Tint только умножает, смуглая кожа от него темнела, а не серела).</summary>
|
||
public static bool Albedo;
|
||
/// <summary>Параметры аниматора рук, выставленные necroarms set (подбор «пустых рук»), - ставятся и при пересборке.</summary>
|
||
static readonly Dictionary<string, float> _animParams = new Dictionary<string, float>
|
||
{
|
||
// Подобрано в игре 13.10: без них аниматор стоял в позе «держит мешок» (WeaponHoldType 45). 0 - пустые руки
|
||
// (HoldType у meleeHandPlayer), клип FP_Unarmed_Hold.
|
||
{ "IsAlive", 1f }, { "IsFPV", 1f }, { "WeaponHoldType", 0f },
|
||
};
|
||
/// <summary>Сдвиг рук от камеры (подбор necroarms pos). У персонажа игра опускает руки на 1.7 м: начало их
|
||
/// скелета - у ног (vp_FPWeapon PositionOffset 0,-1.7,0.02 для SDCS).</summary>
|
||
public static Vector3 Offset = new Vector3(0f, -1.55f, 0.05f); // подобрано в игре 13.10 (у персонажа -1.7, но руки рисуются с FOV 45)
|
||
/// <summary>Родитель: true - сама камера, false - её UFPSRoot (как у персонажа).</summary>
|
||
public static bool ParentCamera = true;
|
||
|
||
static GameObject _rig;
|
||
static SDCSUtils.TransformCatalog _bones;
|
||
static bool _layerWasShown;
|
||
|
||
public static bool Active => _rig != null;
|
||
|
||
/// <summary>Пол зомби. IsMale у зомби в entityclasses.xml не задан (все «мужчины» по умолчанию, тест 13.10:
|
||
/// Арлин получила мужские руки) - женщину выдают звуки zombiefemale*.</summary>
|
||
public static bool IsFemale(EntityAlive _z)
|
||
{
|
||
if (_z == null || !EntityClass.list.TryGetValue(_z.entityClass, out EntityClass cls) || cls?.Properties == null)
|
||
{
|
||
return false;
|
||
}
|
||
string s = cls.Properties.GetString("SoundAlert");
|
||
return s != null && s.ToLowerInvariant().Contains("female");
|
||
}
|
||
|
||
public static bool Allows(EntityAlive _z)
|
||
{
|
||
if (_z == null || !EntityClass.list.TryGetValue(_z.entityClass, out EntityClass cls) || cls?.entityClassName == null)
|
||
{
|
||
return false;
|
||
}
|
||
string name = cls.entityClassName;
|
||
if (name.Contains("Crawler"))
|
||
{
|
||
return false;
|
||
}
|
||
foreach (string a in Allowed)
|
||
{
|
||
if (name.StartsWith(a))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
public static void Show(EntityPlayerLocal _player, EntityAlive _z)
|
||
{
|
||
if (_rig != null || _player == null || _z == null)
|
||
{
|
||
return;
|
||
}
|
||
Archetype arch = Archetype.GetArchetype(IsFemale(_z) ? "BaseFemale" : "BaseMale");
|
||
Transform cam = _player.cameraTransform;
|
||
if (arch == null || cam == null)
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] FpArms: нет архетипа или камеры - рук не будет");
|
||
return;
|
||
}
|
||
Transform parent = ParentCamera ? cam : cam.FindInChildren("UFPSRoot") ?? cam;
|
||
Archetype wasArch = SDCSUtils.tmpArchetype;
|
||
SDCSUtils.AssetRefs wasOwner = SDCSUtils.tmpAssetsOwner;
|
||
try
|
||
{
|
||
SDCSUtils.tmpArchetype = arch;
|
||
SDCSUtils.setupRig(ref _rig, ref _bones, SDCSUtils.baseRigFPPrefab, parent, SDCSUtils.FPAnimController);
|
||
SDCSUtils.tmpAssetsOwner = SDCSUtils.GetAssetOwnerForRig(_rig);
|
||
SDCSUtils.setupBase(_rig, _bones, SDCSUtils.basePartsFP, true);
|
||
}
|
||
finally
|
||
{
|
||
SDCSUtils.tmpArchetype = wasArch;
|
||
SDCSUtils.tmpAssetsOwner = wasOwner;
|
||
}
|
||
_rig.transform.SetLocalPositionAndRotation(Offset, Quaternion.identity);
|
||
// Скрипты префаба (vp_FPWeapon, риг IK и др.) рассчитаны на руки персонажа: включённая копия с ними повесила
|
||
// игру (тест 13.10: «прожал ~ и всё зависло», лог - vp_FPWeapon «Could not find CharacterController»).
|
||
// Рукам хватает скелета, аниматора и сетки - скрипты снимаем до включения.
|
||
// С конца и в два прохода: скрипт, от которого зависит другой ([RequireComponent]), снимется вторым.
|
||
int stripped = 0;
|
||
for (int pass = 0; pass < 2; pass++)
|
||
{
|
||
MonoBehaviour[] mbs = _rig.GetComponentsInChildren<MonoBehaviour>(true);
|
||
for (int i = mbs.Length - 1; i >= 0; i--)
|
||
{
|
||
if (mbs[i] != null)
|
||
{
|
||
Object.DestroyImmediate(mbs[i]);
|
||
stripped++;
|
||
}
|
||
}
|
||
}
|
||
// Префаб рук выключен: у персонажа его включает BodyAnimator (State = Visible) - тест 13.10: «активны False».
|
||
_rig.SetActive(true);
|
||
// Аниматор у персонажа включает тот же BodyAnimator (тест 13.10: «рука одна и без анимации»).
|
||
Animator anim = _rig.GetComponent<Animator>();
|
||
if (anim != null)
|
||
{
|
||
anim.enabled = true;
|
||
anim.cullingMode = AnimatorCullingMode.AlwaysAnimate;
|
||
anim.Rebind();
|
||
foreach (KeyValuePair<string, float> kv in _animParams)
|
||
{
|
||
SetParam(anim, kv.Key, kv.Value);
|
||
}
|
||
}
|
||
Debug.Log("[NecromancerTome] FpArms: снято скриптов " + stripped + ", аниматор " + (anim != null ? anim.runtimeAnimatorController?.name : "нет"));
|
||
_rig.name = "necroZombieArmsFP";
|
||
int layer = LayerMask.NameToLayer("HoldingItem");
|
||
int tinted = 0;
|
||
foreach (SkinnedMeshRenderer smr in _rig.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
||
{
|
||
smr.gameObject.layer = layer;
|
||
// Материалы с _Tint игра уже размножила для этой модели (SDCSUtils.Stitch) - красим свои копии.
|
||
foreach (Material m in smr.sharedMaterials)
|
||
{
|
||
if (m != null && m.HasColor("_Tint"))
|
||
{
|
||
m.SetColor("_Tint", Tint);
|
||
if (!Albedo && m.HasProperty("_Albedo"))
|
||
{
|
||
m.SetTexture("_Albedo", null);
|
||
}
|
||
tinted++;
|
||
}
|
||
}
|
||
}
|
||
// Коллайдеры рук (у женского скелета игра вешает капсулу на таз) - выключены: руки в голове зомби ловили бы
|
||
// луч прицела и толкали бы предметы.
|
||
foreach (Collider c in _rig.GetComponentsInChildren<Collider>(true))
|
||
{
|
||
c.enabled = false;
|
||
}
|
||
foreach (HingeJoint j in _rig.GetComponentsInChildren<HingeJoint>(true))
|
||
{
|
||
if (j.connectedBody == null)
|
||
{
|
||
j.gameObject.SetActive(false);
|
||
}
|
||
}
|
||
_layerWasShown = _player.playerCamera != null && (_player.playerCamera.cullingMask & (1 << layer)) != 0;
|
||
_player.ShowHoldingItemLayer(true);
|
||
Debug.Log("[NecromancerTome] FpArms: руки " + arch.Name + " для " + _z.EntityName + ", серых материалов " + tinted +
|
||
", родитель " + parent.name);
|
||
}
|
||
|
||
/// <summary>Состояние рук для necroarms: где они относительно камеры, видны ли, кто рисует их слой.</summary>
|
||
public static string Dump()
|
||
{
|
||
if (_rig == null)
|
||
{
|
||
return "рук нет (вид от третьего лица или зомби не из списка)";
|
||
}
|
||
Camera main = Camera.main;
|
||
Transform par = _rig.transform.parent;
|
||
string s = "активны " + _rig.activeInHierarchy + ", родитель " + (par != null ? par.name + (par.gameObject.activeInHierarchy ? "" : " (ВЫКЛЮЧЕН)") : "нет") +
|
||
", сдвиг " + _rig.transform.localPosition.ToString("F2");
|
||
foreach (SkinnedMeshRenderer smr in _rig.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
||
{
|
||
Vector3 c = main != null ? main.transform.InverseTransformPoint(smr.bounds.center) : smr.bounds.center;
|
||
s += "\n " + smr.name + ": вкл " + smr.enabled + "/" + smr.gameObject.activeInHierarchy + ", виден " + smr.isVisible +
|
||
", слой " + LayerMask.LayerToName(smr.gameObject.layer) + ", центр от камеры " + c.ToString("F2") + ", размер " + smr.bounds.size.ToString("F2");
|
||
}
|
||
foreach (Camera c in Camera.allCameras)
|
||
{
|
||
s += "\n камера " + c.name + (c == main ? " (main)" : "") + ": слой HoldingItem " +
|
||
((c.cullingMask & (1 << LayerMask.NameToLayer("HoldingItem"))) != 0 ? "рисует" : "нет") + ", near " + c.nearClipPlane.ToString("F3");
|
||
}
|
||
return s;
|
||
}
|
||
|
||
static Animator Anim => _rig != null ? _rig.GetComponent<Animator>() : null;
|
||
|
||
static bool SetParam(Animator _a, string _name, float _v)
|
||
{
|
||
foreach (AnimatorControllerParameter p in _a.parameters)
|
||
{
|
||
if (p.name != _name)
|
||
{
|
||
continue;
|
||
}
|
||
switch (p.type)
|
||
{
|
||
case AnimatorControllerParameterType.Float: _a.SetFloat(_name, _v); break;
|
||
case AnimatorControllerParameterType.Int: _a.SetInteger(_name, (int)_v); break;
|
||
case AnimatorControllerParameterType.Bool: _a.SetBool(_name, _v != 0f); break;
|
||
default: _a.SetTrigger(_name); break;
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// <summary>necroarms set: параметр аниматора (запоминается для пересборки; у триггера - только срабатывание).</summary>
|
||
public static string Set(string _name, float _v)
|
||
{
|
||
Animator a = Anim;
|
||
if (a == null)
|
||
{
|
||
return "рук нет";
|
||
}
|
||
if (!SetParam(a, _name, _v))
|
||
{
|
||
return "нет параметра " + _name;
|
||
}
|
||
_animParams[_name] = _v;
|
||
return _name + " = " + _v.ToString(CultureInfo.InvariantCulture);
|
||
}
|
||
|
||
public static string Trigger(string _name)
|
||
{
|
||
Animator a = Anim;
|
||
if (a == null)
|
||
{
|
||
return "рук нет";
|
||
}
|
||
a.SetTrigger(_name);
|
||
return "триггер " + _name;
|
||
}
|
||
|
||
/// <summary>Аниматор рук: вкл, контроллер, текущие клипы по слоям и все параметры со значениями.</summary>
|
||
public static string DumpAnim()
|
||
{
|
||
Animator a = Anim;
|
||
if (a == null)
|
||
{
|
||
return "рук нет";
|
||
}
|
||
string s = "аниматор вкл " + a.enabled + ", контроллер " + a.runtimeAnimatorController?.name + ", слоёв " + a.layerCount;
|
||
for (int l = 0; l < a.layerCount; l++)
|
||
{
|
||
string clips = "";
|
||
foreach (AnimatorClipInfo ci in a.GetCurrentAnimatorClipInfo(l))
|
||
{
|
||
clips += " " + ci.clip?.name + "(" + ci.weight.ToString("F2") + ")";
|
||
}
|
||
s += "\n слой " + l + " " + a.GetLayerName(l) + " вес " + a.GetLayerWeight(l).ToString("F2") + ":" + clips;
|
||
}
|
||
foreach (AnimatorControllerParameter p in a.parameters)
|
||
{
|
||
string v = p.type == AnimatorControllerParameterType.Float ? a.GetFloat(p.name).ToString("F2", CultureInfo.InvariantCulture)
|
||
: p.type == AnimatorControllerParameterType.Int ? a.GetInteger(p.name).ToString()
|
||
: p.type == AnimatorControllerParameterType.Bool ? a.GetBool(p.name).ToString() : "trigger";
|
||
s += "\n " + p.name + " = " + v;
|
||
}
|
||
return s;
|
||
}
|
||
|
||
/// <summary>Материалы рук: шейдер и его цветовые/текстурные свойства (подбор серого).</summary>
|
||
public static string DumpMat()
|
||
{
|
||
if (_rig == null)
|
||
{
|
||
return "рук нет";
|
||
}
|
||
string s = "";
|
||
foreach (SkinnedMeshRenderer smr in _rig.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
||
{
|
||
foreach (Material m in smr.sharedMaterials)
|
||
{
|
||
if (m == null)
|
||
{
|
||
continue;
|
||
}
|
||
s += "\n " + smr.name + " / " + m.name + " / " + m.shader.name + ":";
|
||
for (int i = 0; i < m.shader.GetPropertyCount(); i++)
|
||
{
|
||
UnityEngine.Rendering.ShaderPropertyType t = m.shader.GetPropertyType(i);
|
||
string n = m.shader.GetPropertyName(i);
|
||
if (t == UnityEngine.Rendering.ShaderPropertyType.Color)
|
||
{
|
||
s += " " + n + "=" + m.GetColor(n);
|
||
}
|
||
else if (t == UnityEngine.Rendering.ShaderPropertyType.Texture)
|
||
{
|
||
s += " " + n + "[" + (m.GetTexture(n) != null ? m.GetTexture(n).name : "-") + "]";
|
||
}
|
||
else
|
||
{
|
||
s += " " + n + "=" + m.GetFloat(n).ToString("F2", CultureInfo.InvariantCulture);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return s;
|
||
}
|
||
|
||
/// <summary>Пересобрать руки на месте (после necroarms pos/parent).</summary>
|
||
public static void Rebuild()
|
||
{
|
||
if (_rig == null || !Possession.Active || Possession.Zombie == null)
|
||
{
|
||
return;
|
||
}
|
||
EntityPlayerLocal p = Possession.Player;
|
||
Hide(p);
|
||
Show(p, Possession.Zombie);
|
||
}
|
||
|
||
public static void Hide(EntityPlayerLocal _player)
|
||
{
|
||
if (_rig == null)
|
||
{
|
||
return;
|
||
}
|
||
if (!_layerWasShown)
|
||
{
|
||
_player?.ShowHoldingItemLayer(false);
|
||
}
|
||
SDCSUtils.UnloadViz(_rig);
|
||
_rig = null;
|
||
_bones = null;
|
||
Debug.Log("[NecromancerTome] FpArms: руки убраны");
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// necroarms - руки одержимого от первого лица (13.10): без параметров - состояние; pos x y z - сдвиг от камеры;
|
||
/// parent cam|ufps - к чему крепить. Сдвиг и родитель применяются сразу (руки пересобираются).
|
||
/// </summary>
|
||
[Preserve]
|
||
public class ConsoleCmdNecroArms : ConsoleCmdAbstract
|
||
{
|
||
public override string[] getCommands() => new[] { "necroarms" };
|
||
public override string getDescription() => "NecromancerTome: руки одержимого от первого лица - [pos x y z | parent cam|ufps | anim | mat | set <param> <value> | trigger <name> | tint r g b | albedo on|off]";
|
||
|
||
void Out(string _s)
|
||
{
|
||
SdtdConsole.Instance.Output(_s);
|
||
Debug.Log("[NecromancerTome] necroarms: " + _s);
|
||
}
|
||
|
||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||
{
|
||
string cmd = _params.Count > 0 ? _params[0] : "";
|
||
if (cmd == "pos" && _params.Count >= 4 &&
|
||
float.TryParse(_params[1].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out float x) &&
|
||
float.TryParse(_params[2].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out float y) &&
|
||
float.TryParse(_params[3].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out float z))
|
||
{
|
||
FpArms.Offset = new Vector3(x, y, z);
|
||
FpArms.Rebuild();
|
||
}
|
||
else if (cmd == "parent" && _params.Count >= 2)
|
||
{
|
||
FpArms.ParentCamera = _params[1] != "ufps";
|
||
FpArms.Rebuild();
|
||
}
|
||
else if (cmd == "albedo" && _params.Count >= 2)
|
||
{
|
||
FpArms.Albedo = _params[1] == "on";
|
||
FpArms.Rebuild();
|
||
}
|
||
else if (cmd == "anim")
|
||
{
|
||
Out(FpArms.DumpAnim());
|
||
return;
|
||
}
|
||
else if (cmd == "mat")
|
||
{
|
||
Out(FpArms.DumpMat());
|
||
return;
|
||
}
|
||
else if (cmd == "set" && _params.Count >= 3 && float.TryParse(_params[2].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out float v))
|
||
{
|
||
Out(FpArms.Set(_params[1], v));
|
||
return;
|
||
}
|
||
else if (cmd == "trigger" && _params.Count >= 2)
|
||
{
|
||
Out(FpArms.Trigger(_params[1]));
|
||
return;
|
||
}
|
||
else if (cmd == "tint" && _params.Count >= 4 &&
|
||
float.TryParse(_params[1].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out float r) &&
|
||
float.TryParse(_params[2].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out float g) &&
|
||
float.TryParse(_params[3].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out float b))
|
||
{
|
||
FpArms.Tint = new Color(r, g, b, 1f);
|
||
FpArms.Rebuild();
|
||
}
|
||
Out(FpArms.Dump());
|
||
}
|
||
}
|
||
}
|