Files
necromants-tome-7d2d-3-2/HarmonySrc/ZombieWeapons.cs
T
AlexCubeandClaude Opus 5.5 cf61d98bb7 Одержимый зомби: оружие на поясе по ремеслу (#37), карта по M (#36)
- Пояс - нижний ряд сумки зомби; оружие ближнего боя в руку, если этот тип зомби
  им владеет (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>
2026-10-25 14:26:55 +03:00

340 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}
}