using System.Collections.Generic; using UnityEngine; namespace NecromancerTome { /// /// Оружие на поясе одержимого зомби (#37, решения пользователя 25.10). Пояс - две ячейки нижнего ряда сумки зомби /// (ZombieBag.Belt), туда попадает только то, что зомби нашёл сам или что положили в сумку. Чем зомби владеет - /// по его типу («зомби строитель сможет пользоваться простыми инструментами: топоры, молотки и т.п. Медсестра - /// ножами»; список утверждён 25.10, карточка #37). Неумелое или сломанное в выбранной ячейке - кулаки и подсказка. /// /// Только ближний бой (ItemActionDynamic - всё оружие и инструменты ближнего боя персонажа). Бензопила и бур в игре /// стрелковые (Ranged, топливо), гвоздомёт - метатель: они придут вместе с огнестрелом (#80). /// /// Удар - от первого лица, в миг удара рук (FpArms -> Possession.FpImpact): луч из глаз зомби на дальность оружия и /// ItemActionAttack.Hit, как у персонажа (урон по существам и блокам, кровь, звук, отрубание, износ). Урон - самого /// предмета с его модами, без перков и баффов зомби (holdingEntity = null): «урон от оружия». /// public static class ZombieWeapons { /// Выключатель (necroarms weapon on|off): выкл - все бьют кулаками, как до #37. public static bool Enabled = true; // Группы предметов: имена групп (не путать с концовками имён предметов, их тоже можно писать в умениях). const string Knife = "Knife", SmallKnife = "SmallKnife", Club = "Club", Baton = "Baton", Knuckles = "Knuckles", Spear = "Spear", Sledge = "Sledge", Axe = "Axe", Hammer = "Hammer", Pick = "Pick", Shovel = "Shovel", Wrench = "Wrench", Light = "Light"; static readonly HashSet GroupNames = new HashSet { Knife, SmallKnife, Club, Baton, Knuckles, Spear, Sledge, Axe, Hammer, Pick, Shovel, Wrench, Light }; /// Умения по началу имени класса зомби (как FpArms.Allows; Feral/Radiated/Charged/Infernal - как базовый). /// Строка - группа или концовка имени предмета игры (StoneAxe подходит и к TazasStoneAxe). static readonly Dictionary Skills = new Dictionary { { "zombieUtilityWorker", new[] { Hammer, Pick, Shovel, Axe, Sledge, Wrench, "ImpactDriver", "Auger", "Nailgun" } }, { "zombieLumberjack", new[] { Axe, "Chainsaw", Sledge } }, { "zombieTomClark", new[] { Shovel, Axe, Spear } }, { "zombieJanitor", new[] { Wrench, Shovel, Light, Club } }, { "zombieNurse", new[] { Knife } }, { "zombieLab", new[] { Knife, "StunBaton", "PlasmaBaton" } }, { "zombieMaleHazmat", new[] { Shovel, Light, Wrench } }, { "zombieSoldier", new[] { Knife, Baton, Spear } }, { "zombieBiker", new[] { Knuckles, Club, "PipeBaton", Wrench } }, { "zombieInmate", new[] { "BoneKnife", Knuckles, "PipeBaton" } }, { "zombieYo", new[] { Knuckles, "BaseballBat", "PipeBaton" } }, { "zombieSkateboarder", new[] { "BaseballBat", "PipeBaton" } }, { "zombieBowler", new[] { Club, Sledge } }, { "zombieBoe", new[] { Club, Knuckles, "StoneAxe", "StoneShovel" } }, { "zombieJoe", new[] { Club, Knuckles, "StoneAxe", "StoneShovel" } }, { "zombieMoe", new[] { Sledge, Club } }, { "zombieSteve", new[] { Club, "StoneAxe", "StoneShovel" } }, { "zombieBusinessMan", new[] { "BaseballBat", SmallKnife } }, { "zombieBurnt", new[] { "Torch", Club } }, { "zombieDarlene", new[] { Knife, "WoodenClub" } }, { "zombieArlene", new[] { Knife, "BaseballBat" } }, { "zombieMarlene", new[] { Knife, "BaseballBat" } }, { "zombiePartyGirl", new[] { "CandyKnife", "CandyClub", "BaseballBat" } }, }; static readonly Dictionary> _groups = new Dictionary>(); static bool Tag(ItemClass _ic, string _tag) => _ic.ItemTags.Test_AnySet(FastTags.Parse(_tag)); /// Группы предмета: у предметов игры - по имени, у предметов других модов - по тегам (как у игры). static HashSet Groups(ItemClass _ic) { if (_groups.TryGetValue(_ic.Id, out HashSet g)) { return g; } g = new HashSet(); string n = _ic.Name ?? ""; bool motor = Tag(_ic, "motorTool") || Tag(_ic, "nailgun"); if (n.StartsWith("meleeWpnBlade") || Tag(_ic, "knife") || Tag(_ic, "machete")) { g.Add(Knife); if (!n.Contains("Machete") && !Tag(_ic, "machete")) { g.Add(SmallKnife); } } if (n.StartsWith("meleeWpnClub") || Tag(_ic, "club")) { g.Add(Club); } if (n.StartsWith("meleeWpnBaton") || Tag(_ic, "batonJunkie") || Tag(_ic, "stunBaton")) { g.Add(Baton); } if (n.StartsWith("meleeWpnKnuckles") || Tag(_ic, "knuckleSkill")) { g.Add(Knuckles); } if (n.StartsWith("meleeWpnSpear") || Tag(_ic, "spearSkill")) { g.Add(Spear); } if (n.StartsWith("meleeWpnSledge") || Tag(_ic, "sledge")) { g.Add(Sledge); } if (!motor && (n.StartsWith("meleeToolAxe") || Tag(_ic, "axe"))) { g.Add(Axe); } if (n.Contains("ClawHammer") || !motor && Tag(_ic, "repairTool") && !Tag(_ic, "axe")) { g.Add(Hammer); } if (n.StartsWith("meleeToolShovel") || Tag(_ic, "shovel")) { g.Add(Shovel); } else if (!motor && (n.StartsWith("meleeToolPick") || Tag(_ic, "miningTool") && !Tag(_ic, "axe"))) { g.Add(Pick); } if (!n.Contains("ImpactDriver") && (n.StartsWith("meleeToolSalvage") || Tag(_ic, "salvageTool"))) { g.Add(Wrench); } if (n.Contains("Flashlight") || n.Contains("Torch") || Tag(_ic, "heldTorch")) { g.Add(Light); } _groups[_ic.Id] = g; return g; } static string[] SkillsOf(EntityAlive _z) { if (_z == null || !EntityClass.list.TryGetValue(_z.entityClass, out EntityClass cls) || cls?.entityClassName == null) { return null; } foreach (KeyValuePair kv in Skills) { if (cls.entityClassName.StartsWith(kv.Key)) { return kv.Value; } } return null; } /// Зомби этого типа владеет предметом. public static bool Knows(EntityAlive _z, ItemClass _ic) { string[] skills = SkillsOf(_z); if (skills == null || _ic == null) { return false; } HashSet g = Groups(_ic); string n = _ic.Name ?? ""; foreach (string s in skills) { if (GroupNames.Contains(s) ? g.Contains(s) : n.EndsWith(s)) { return true; } } return false; } public enum Fit { Fists, Ok, Unknown, Broken } /// Чем бьёт зомби с этим предметом в выбранной ячейке. public static Fit Check(EntityAlive _z, ItemValue _iv) { if (_iv == null || _iv.IsEmpty() || _iv.ItemClass == null) { return Fit.Fists; } ItemClass ic = _iv.ItemClass; if (!(ic.Actions != null && ic.Actions.Length > 0 && ic.Actions[0] is ItemActionDynamic) || !Knows(_z, ic)) { return Fit.Unknown; } if (_iv.MaxUseTimes > 0 && _iv.UseTimes >= _iv.MaxUseTimes) { return Fit.Broken; } return Fit.Ok; } /// Оружие в руке одержимого сейчас: выбранная ячейка пояса, если зомби им владеет и оно цело; иначе null. public static ItemValue Current(EntityAlive _z) { if (!Enabled || !ZombieBag.Active) { return null; } ItemStack s = ZombieBag.Belt(Possession.BeltSlot); return s != null && Check(_z, s.itemValue) == Fit.Ok ? s.itemValue : null; } /// Пауза между ударами этим оружием - как у персонажа (удары в минуту). public static float SwingTime(ItemValue _iv) { float apm = EffectManager.GetValue(PassiveEffects.AttacksPerMinute, _iv, 60f, null, null, _iv.ItemClass.ItemTags); return apm > 1f ? 60f / apm : 1f; } /// Удар оружием в миг удара рук: луч из глаз зомби (камера одержимости) на дальность оружия. Мимо - false /// (звука нет, как у персонажа - замах в воздух). public static bool Strike(World _world, EntityAlive _z, Ray _ray) { ItemStack stack = ZombieBag.Belt(Possession.BeltSlot); ItemValue iv = stack?.itemValue; if (iv == null || Check(_z, iv) != Fit.Ok || !(iv.ItemClass.Actions[0] is ItemActionDynamic act)) { return false; } FastTags tags = ItemActionAttack.PrimaryTag | iv.ItemClass.ItemTags; float range = EffectManager.GetValue(PassiveEffects.MaxRange, iv, act.RangeDefault, null, null, tags); float blockRange = EffectManager.GetValue(PassiveEffects.BlockRange, iv, range, null, null, tags); float sphere = act.SphereRadius; float dist = Mathf.Max(range, blockRange) + sphere; _ray.origin -= sphere * _ray.direction; // Свои коллайдеры зомби - мимо луча (как GetExecuteActionTarget у игры). Камера одержимости - в голове зомби: // если луч всё же задел его самого (тест 25.10: все удары «мимо» вплотную), - продолжить луч за ним. int layer = _z.GetModelLayer(); _z.SetModelLayer(2); WorldRayHitInfo hit = null; float passed = 0f; try { for (int i = 0; i < 4 && hit == null; i++) { Ray ray = new Ray(_ray.origin + _ray.direction * passed, _ray.direction); EntityAlive target = null; bool any = Voxel.Raycast(_world, ray, dist - passed, -538767389, 128, sphere); if (any) { target = ItemActionAttack.GetEntityFromHit(Voxel.voxelRayHitInfo) as EntityAlive; } if (target == null) { any = Voxel.Raycast(_world, ray, dist - passed, -538488845, 128, sphere); target = any ? ItemActionAttack.GetEntityFromHit(Voxel.voxelRayHitInfo) as EntityAlive : null; } if (!any) { break; } if (target == _z) { passed += Mathf.Sqrt(Voxel.voxelRayHitInfo.hit.distanceSq) + 0.05f; continue; } hit = Voxel.voxelRayHitInfo.Clone(); } } finally { _z.SetModelLayer(layer); } if (hit?.tag == null) { string phys = "ничего"; if (Physics.Raycast(_ray.origin - Origin.position, _ray.direction, out RaycastHit ph, 5f, -5, QueryTriggerInteraction.Ignore)) { phys = ph.transform.name + " слой " + LayerMask.LayerToName(ph.transform.gameObject.layer) + " в " + ph.distance.ToString("F2"); } Debug.Log("[NecromancerTome] ZombieWeapons: луч ничего не задел (дальность " + dist.ToString("F1") + (passed > 0f ? ", сквозь себя " + passed.ToString("F1") : "") + "), луч " + _ray.origin.ToString("F2") + " -> " + _ray.direction.ToString("F2") + ", голова " + _z.getHeadPosition().ToString("F2") + ", физика: " + phys); return false; } bool entity = hit.tag.StartsWith("E_"); float d = Vector3.Distance(_ray.origin, hit.hit.pos); if (entity ? d > range + sphere : d > blockRange + sphere) { Debug.Log("[NecromancerTome] ZombieWeapons: далеко - " + hit.tag + " в " + d.ToString("F1") + " м, достаёт " + (entity ? range : blockRange).ToString("F1")); return false; } if (entity && ItemActionAttack.GetEntityFromHit(hit) is EntityAlive e && (e == _z || e is EntityPlayer || SnakeControl.IsNecroEntity(e))) { Debug.Log("[NecromancerTome] ZombieWeapons: под ударом свой - " + e.EntityName); return false; // хозяина и его нежить одержимый не бьёт - как кулаками (MeleeAim) } BlockValue bv = ItemActionAttack.GetBlockHit(_world, hit); float blockDamage = entity || bv.isair ? 0f : act.GetDamageBlock(iv, bv, null, 0); float entityDamage = act.GetDamageEntity(iv, null, 0); string tagHit = hit.tag; FastTags dTags = tags; if (tagHit == "E_BP_Head") { dTags |= FastTags.Parse("head"); } float dismember = EffectManager.GetValue(PassiveEffects.DismemberChance, iv, 0f, null, null, dTags); ItemActionAttack.AttackHitInfo details = new ItemActionAttack.AttackHitInfo { WeaponTypeTag = ItemActionAttack.MeleeTag }; ItemActionAttack.Hit(hit, _z.entityId, act.DamageType, blockDamage, entityDamage, 1f, 1f, 0f, dismember, iv.ItemClass.MadeOfMaterial.SurfaceCategory, new DamageMultiplier(), new List(), details, 1, 0, 0f, null, act.ToolBonuses, ItemActionAttack.EnumAttackMode.RealNoHarvesting, act.HitSoundOverrides, -1, iv); // Износ - как у персонажа (ItemActionDynamic.hitTarget); сломалось - звук, дальше кулаки. if (iv.MaxUseTimes > 0) { iv.UseTimes += EffectManager.GetValue(PassiveEffects.DegradationPerUse, iv, 1f, null, null, tags) * ItemAction.ItemDegradationModifier; if (iv.UseTimes >= iv.MaxUseTimes) { Audio.Manager.BroadcastPlay(_z, "itembreak"); Debug.Log("[NecromancerTome] ZombieWeapons: " + iv.ItemClass.Name + " сломалось"); } ZombieBag.BeltChanged(); } Debug.Log("[NecromancerTome] ZombieWeapons: удар " + iv.ItemClass.Name + " по " + (entity ? tagHit : bv.Block?.GetBlockName()) + ", урон " + (entity ? entityDamage : blockDamage).ToString("F0") + (details.bKilled ? ", убит" : "")); return true; } /// Подсказка при смене выбранного предмета: неумелое или сломанное - кулаки. public static void Hint(EntityPlayerLocal _player, EntityAlive _z, ItemValue _iv) { Fit f = Enabled ? Check(_z, _iv) : Fit.Fists; if (f == Fit.Unknown) { GameManager.ShowTooltip(_player, string.Format(Localization.Get("necroZombieWeaponUnknown"), _iv.ItemClass.GetLocalizedItemName())); } else if (f == Fit.Broken) { GameManager.ShowTooltip(_player, string.Format(Localization.Get("necroZombieWeaponBroken"), _iv.ItemClass.GetLocalizedItemName())); } } /// necroarms weapon: что умеет текущий одержимый и что в его поясе. public static string Dump(EntityAlive _z) { string[] sk = SkillsOf(_z); string s = "оружие " + (Enabled ? "вкл" : "ВЫКЛ") + ", умения: " + (sk != null ? string.Join(", ", sk) : "нет"); for (int i = 0; i < Possession.BeltSlots; i++) { ItemStack st = ZombieBag.Belt(i); s += "\n ячейка " + (i + 1) + (i == Possession.BeltSlot ? " (выбрана)" : "") + ": " + (st == null || st.IsEmpty() ? "пусто" : st.itemValue.ItemClass.Name + " [" + string.Join(",", Groups(st.itemValue.ItemClass)) + "] - " + Check(_z, st.itemValue)); } return s; } } }