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;
}
}
}