using System.Collections.Generic; using HarmonyLib; using System.Globalization; using UnityEngine; using UnityEngine.Scripting; namespace NecromancerTome { /// /// Руки одержимого в виде от первого лица (указание 13.10: «делаем так, как у игрока»). У персонажа игры две /// модели: полное тело (его видят другие и он сам со стороны) и отдельные руки для первого лица (baseRigFP, /// SDCSUtils.CreateVizFP) - рисуются только у себя. Здесь собираем такие же руки по тем же шагам игры: /// скелет baseRigFPPrefab с аниматором FPPlayerController, голая кожа (basePartsFP), без одежды и волос /// (указание: «одежду пока не показываем»). Пол - по зомби (IsMale): BaseMale или BaseFemale из /// archetypes.xml. Кожа серая (указание: «руки вообще серые - типа зомби их так видит») - цвет _Tint у /// материала кожи, как игра красит тон кожи. /// /// Руки - под UFPSRoot камеры игрока, как у персонажа; камеру одержимости мы ставим в голову зомби, руки /// едут вместе с ней. Слой HoldingItem - его камера игрока рисует только при ShowHoldingItemLayer(true). /// /// Только гуманоиды обычной комплекции (Allows), указание 13.10. /// /// ПАМЯТЬ. Одна модель рук на время вида от первого лица; Hide выгружает её (SDCSUtils.UnloadViz - материалы /// и ссылки на ассеты освобождаются) - на переключении вида и на выходе из одержимости. /// public static class FpArms { /// Начала имён классов, у кого вид от первого лица (варианты Feral/Radiated/Charged/Infernal - тоже). 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); /// Цветная текстура кожи (_Albedo): false - убрана, кожа ровно-серая по _Tint, рельеф - от карты нормалей /// (указание 13.10: «руки вообще серые»; _Tint только умножает, смуглая кожа от него темнела, а не серела). public static bool Albedo; /// Сигнал аниматора рук на удар (подбор necroarms punch; у пустых рук игры - триггер атаки). public static string PunchTrigger = "WeaponFire"; /// Параметры аниматора рук, выставленные necroarms set (подбор «пустых рук»), - ставятся и при пересборке. static readonly Dictionary _animParams = new Dictionary { // Подобрано в игре 13.10: без них аниматор стоял в позе «держит мешок» (WeaponHoldType 45). 0 - пустые руки // (HoldType у meleeHandPlayer), клип FP_Unarmed_Hold. { "IsAlive", 1f }, { "IsFPV", 1f }, { "WeaponHoldType", 0f }, }; /// Сдвиг рук от камеры (подбор necroarms pos). У персонажа игра опускает руки на 1.7 м: начало их /// скелета - у ног (vp_FPWeapon PositionOffset 0,-1.7,0.02 для SDCS). public static Vector3 Offset = new Vector3(0f, -1.7f, 0.02f); /// Угол обзора, с которым игра рисует руки персонажа (vp_FPWeapon RenderingFieldOfView 45 у SDCS). Наши руки /// рисует общая камера с широким углом - удар уходил выше и правее прицела (тест 13.10). Тот же вид даёт сжатие /// рук по экранным осям: масштаб (k, k, 1) от камеры, k = tg(FOV камеры / 2) / tg(ArmsFov / 2) - каждый кадр /// (FOV меняется при приближении). public static float ArmsFov = 45f; /// Положение рук в покое (подобрано 13.10, без сжатия): «руки подними как были»; на удар - Offset со /// сжатием под ArmsFov, «анимацию удара занизь как сейчас». Переход - за BlendTime. public static Vector3 IdleOffset = new Vector3(0f, -1.55f, 0.05f); public const float BlendTime = 0.12f; /// Минимальный промежуток между ударами рук. public const float SwingTime = 0.6f; static bool _inAttack; static float _impactAt, _blend; /// Родитель: true - сама камера, false - её UFPSRoot (как у персонажа). public static bool ParentCamera = true; static GameObject _rig; static SDCSUtils.TransformCatalog _bones; static bool _layerWasShown; public static bool Active => _rig != null; /// Пол зомби. IsMale у зомби в entityclasses.xml не задан (все «мужчины» по умолчанию, тест 13.10: /// Арлин получила мужские руки) - женщину выдают звуки zombiefemale*. 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(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(); if (anim != null) { anim.enabled = true; anim.cullingMode = AnimatorCullingMode.AlwaysAnimate; anim.Rebind(); foreach (KeyValuePair 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(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(true)) { c.enabled = false; } foreach (HingeJoint j in _rig.GetComponentsInChildren(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); } /// Состояние рук для necroarms: где они относительно камеры, видны ли, кто рисует их слой. 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(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() : 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; } /// necroarms set: параметр аниматора (запоминается для пересборки; у триггера - только срабатывание). 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); } /// Удар одержимого - руки бьют кулаком (указание 13.10: хват пустых рук, «там как раз анимация ударов /// кулаками»). public static void Punch() { Animator a = Anim; if (a != null && !string.IsNullOrEmpty(PunchTrigger)) { a.SetTrigger(PunchTrigger); } } /// Анимация удара рук вошла в состояние удара (AnimatorMeleeAttackState у наших рук): через RaycastTime - /// миг удара, как у персонажа (impactStart). Руки на это время уходят в нижнее положение (под прицел). public static void OnSwingEnter(float _raycastTime) { _inAttack = true; _impactAt = Time.time + Mathf.Max(0.01f, _raycastTime); } public static void OnSwingExit() { _inAttack = false; } /// Урон зомби прошёл - вынужденный миг удара снят. public static void OnZombieHit() { Possession.ForceImpact = false; } /// Каждый кадр вида от первого лица (Possession.PlaceCamera): отложенный удар и сжатие под угол рук. public static void Tick(Camera _cam) { if (_rig == null) { return; } if (_impactAt > 0f && Time.time >= _impactAt) { _impactAt = 0f; Possession.FpImpact(); } float k = 1f; if (_cam != null && ArmsFov > 1f) { k = Mathf.Tan(_cam.fieldOfView * 0.5f * Mathf.Deg2Rad) / Mathf.Tan(ArmsFov * 0.5f * Mathf.Deg2Rad); } _blend = Mathf.MoveTowards(_blend, _inAttack ? 1f : 0f, Time.deltaTime / BlendTime); float kk = Mathf.Lerp(1f, k, _blend); _rig.transform.localScale = new Vector3(kk, kk, 1f); _rig.transform.localPosition = Vector3.Lerp(IdleOffset, new Vector3(Offset.x * k, Offset.y * k, Offset.z), _blend); } public static string Trigger(string _name) { Animator a = Anim; if (a == null) { return "рук нет"; } a.SetTrigger(_name); return "триггер " + _name; } /// Аниматор рук: вкл, контроллер, текущие клипы по слоям и все параметры со значениями. 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; } /// Материалы рук: шейдер и его цветовые/текстурные свойства (подбор серого). public static string DumpMat() { if (_rig == null) { return "рук нет"; } string s = ""; foreach (SkinnedMeshRenderer smr in _rig.GetComponentsInChildren(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; } /// Пересобрать руки на месте (после necroarms pos/parent). 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: руки убраны"); } } /// /// necroarms - руки одержимого от первого лица (13.10): без параметров - состояние; pos x y z - сдвиг от камеры; /// parent cam|ufps - к чему крепить. Сдвиг и родитель применяются сразу (руки пересобираются). /// [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 | trigger | tint r g b | albedo on|off | punch | fov ]"; void Out(string _s) { SdtdConsole.Instance.Output(_s); Debug.Log("[NecromancerTome] necroarms: " + _s); } public override void Execute(List _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 == "punch" && _params.Count >= 2) { FpArms.PunchTrigger = _params[1]; FpArms.Punch(); Out("удар - " + FpArms.PunchTrigger); return; } else if (cmd == "fov" && _params.Count >= 2 && float.TryParse(_params[1].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out float fov)) { FpArms.ArmsFov = fov; } 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()); } } /// Скрипт состояния удара в аниматоре рук (AnimatorMeleeAttackState) берёт персонажа через /// AnimationEventBridge и бьёт предметом в его руке. У рук одержимого моста нет (FpArms снимает скрипты) - без этой /// заплатки NullReferenceException на каждом ударе (тест 13.10), а с мостом руки зомби били бы предметом тела. /// У рук персонажа мост есть всегда - их не трогаем. [HarmonyPatch(typeof(AnimatorMeleeAttackState))] public static class Patch_AnimatorMeleeAttackState_FpArms { static bool Ours(Animator _a) => _a != null && _a.GetComponent() == null; [HarmonyPrefix, HarmonyPatch(nameof(AnimatorMeleeAttackState.OnStateEnter))] public static bool Enter(AnimatorMeleeAttackState __instance, Animator animator) { if (!Ours(animator)) { return true; } FpArms.OnSwingEnter(__instance.RaycastTime); // миг удара - как у персонажа (13.10) return false; } [HarmonyPrefix, HarmonyPatch(nameof(AnimatorMeleeAttackState.OnStateUpdate))] public static bool Update(Animator animator) => !Ours(animator); [HarmonyPrefix, HarmonyPatch(nameof(AnimatorMeleeAttackState.OnStateExit))] public static bool Exit(Animator animator) { if (!Ours(animator)) { return true; } FpArms.OnSwingExit(); return false; } } /// Миг урона одержимого зомби (ItemActionMelee.OnHoldingUpdate снимает bAttackStarted, когда замах дошёл до /// точки удара) - FpArms мерит по нему задержку кулака. [HarmonyPatch(typeof(ItemActionMelee), nameof(ItemActionMelee.OnHoldingUpdate))] public static class Patch_ItemActionMelee_OnHoldingUpdate_FpArms { public static void Prefix(ItemActionData _actionData, out bool __state) { __state = _actionData is ItemActionMelee.InventoryDataMelee d && d.bAttackStarted && Possession.IsPossessed(_actionData.invData?.holdingEntity); } public static void Postfix(ItemActionData _actionData, bool __state) { if (__state && _actionData is ItemActionMelee.InventoryDataMelee d && !d.bAttackStarted) { FpArms.OnZombieHit(); } } } /// Миг удара одержимого от первого лица задают руки (Possession.FpImpact): пока ForceImpact - «замах дошёл». [HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.IsAttackImpact))] public static class Patch_EntityAlive_IsAttackImpact_FpArms { public static void Postfix(EntityAlive __instance, ref bool __result) { if (Possession.ForceImpact && Possession.IsPossessed(__instance)) { __result = true; } } } }