- Пояс - нижний ряд сумки зомби; оружие ближнего боя в руку, если этот тип зомби им владеет (ZombieWeapons: таблица умений, утверждена 25.10). Удар как у персонажа: ItemActionAttack.Hit, урон предмета без перков зомби, износ, темп оружия. Модель на кость RightWeapon, хват WeaponHoldType; иконки и износ на поясе HUD. Неумелое или сломанное - кулаки и подсказка (13 языков). Выключатель necroarms weapon on|off; dev-команда necropossess для проверок без мыши. - M в зомби и волке открывал сумку вместо карты (перехват windowpaging) - исправлено; туман карты и карта на одержимом - у любого одержимого зомби, как у стервятника. - Глава «Одержимость» (EN/RU): пояс - оружие, M - карта. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
340 lines
15 KiB
C#
340 lines
15 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// Оружие на поясе одержимого зомби (#37, решения пользователя 25.10). Пояс - две ячейки нижнего ряда сумки зомби
|
||
/// (ZombieBag.Belt), туда попадает только то, что зомби нашёл сам или что положили в сумку. Чем зомби владеет -
|
||
/// по его типу («зомби строитель сможет пользоваться простыми инструментами: топоры, молотки и т.п. Медсестра -
|
||
/// ножами»; список утверждён 25.10, карточка #37). Неумелое или сломанное в выбранной ячейке - кулаки и подсказка.
|
||
///
|
||
/// Только ближний бой (ItemActionDynamic - всё оружие и инструменты ближнего боя персонажа). Бензопила и бур в игре
|
||
/// стрелковые (Ranged, топливо), гвоздомёт - метатель: они придут вместе с огнестрелом (#80).
|
||
///
|
||
/// Удар - от первого лица, в миг удара рук (FpArms -> Possession.FpImpact): луч из глаз зомби на дальность оружия и
|
||
/// ItemActionAttack.Hit, как у персонажа (урон по существам и блокам, кровь, звук, отрубание, износ). Урон - самого
|
||
/// предмета с его модами, без перков и баффов зомби (holdingEntity = null): «урон от оружия».
|
||
/// </summary>
|
||
public static class ZombieWeapons
|
||
{
|
||
/// <summary>Выключатель (necroarms weapon on|off): выкл - все бьют кулаками, как до #37.</summary>
|
||
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<string> GroupNames = new HashSet<string>
|
||
{ Knife, SmallKnife, Club, Baton, Knuckles, Spear, Sledge, Axe, Hammer, Pick, Shovel, Wrench, Light };
|
||
|
||
/// <summary>Умения по началу имени класса зомби (как FpArms.Allows; Feral/Radiated/Charged/Infernal - как базовый).
|
||
/// Строка - группа или концовка имени предмета игры (StoneAxe подходит и к TazasStoneAxe).</summary>
|
||
static readonly Dictionary<string, string[]> Skills = new Dictionary<string, string[]>
|
||
{
|
||
{ "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<int, HashSet<string>> _groups = new Dictionary<int, HashSet<string>>();
|
||
|
||
static bool Tag(ItemClass _ic, string _tag) => _ic.ItemTags.Test_AnySet(FastTags<TagGroup.Global>.Parse(_tag));
|
||
|
||
/// <summary>Группы предмета: у предметов игры - по имени, у предметов других модов - по тегам (как у игры).</summary>
|
||
static HashSet<string> Groups(ItemClass _ic)
|
||
{
|
||
if (_groups.TryGetValue(_ic.Id, out HashSet<string> g))
|
||
{
|
||
return g;
|
||
}
|
||
g = new HashSet<string>();
|
||
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<string, string[]> kv in Skills)
|
||
{
|
||
if (cls.entityClassName.StartsWith(kv.Key))
|
||
{
|
||
return kv.Value;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// <summary>Зомби этого типа владеет предметом.</summary>
|
||
public static bool Knows(EntityAlive _z, ItemClass _ic)
|
||
{
|
||
string[] skills = SkillsOf(_z);
|
||
if (skills == null || _ic == null)
|
||
{
|
||
return false;
|
||
}
|
||
HashSet<string> 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 }
|
||
|
||
/// <summary>Чем бьёт зомби с этим предметом в выбранной ячейке.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>Оружие в руке одержимого сейчас: выбранная ячейка пояса, если зомби им владеет и оно цело; иначе null.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>Пауза между ударами этим оружием - как у персонажа (удары в минуту).</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>Удар оружием в миг удара рук: луч из глаз зомби (камера одержимости) на дальность оружия. Мимо - false
|
||
/// (звука нет, как у персонажа - замах в воздух).</summary>
|
||
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<TagGroup.Global> 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<TagGroup.Global> dTags = tags;
|
||
if (tagHit == "E_BP_Head")
|
||
{
|
||
dTags |= FastTags<TagGroup.Global>.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<string>(), 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;
|
||
}
|
||
|
||
/// <summary>Подсказка при смене выбранного предмета: неумелое или сломанное - кулаки.</summary>
|
||
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()));
|
||
}
|
||
}
|
||
|
||
/// <summary>necroarms weapon: что умеет текущий одержимый и что в его поясе.</summary>
|
||
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;
|
||
}
|
||
}
|
||
}
|