Одержимость: прицел-перекрестие с покраснением, кнопки E/F, лут мешков и багажник волка, медведь на дыбы
- Прицел всех одержимых - всегда перекрестие (свой guiDrawCrosshair: точка бралась из проверки камеры от 3-го лица у тела); краснеет на враге: у стервятника - цель рывка, у наземных - враг на луче прицела до 30 м, не за стеной - Падаль: отхил вдвое меньше (EatHealFrac 0.125); ест на F стервятник, медведь, собака, волк и одержимые зомби - E - действие: стервятник - клич (был F), крыса/волк - обыск, медведь - на дыбы (враги до 3.5 м сбиты с ног и отброшены, откат 6 с) - Рёв медведя: надписи «на медведя идут N» и откат, мимо очереди подсказок - Крыса и волк обыскивают мешки с зомби (EntityLootContainer), волк берёт в сумку - Багажник волка: окно сумки как у браслета, без рюкзака; выброс - у волка - Отзыв волка: мешок на месте и метка «Сумка волка» на карте, снимается с мешком - Тексты на 13 языках Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
31aa99860c
commit
e94e967b8e
@@ -10,7 +10,9 @@ namespace NecromancerTome
|
||||
/// увести толпу от тела или базы. Та же провокация, что каждую секунду делает одержимость
|
||||
/// (Possession.Taunt), только шире, без «видит ли» и на 20 с;
|
||||
/// - удар лапой по блоку: ЛКМ по блоку под прицелом бьёт его на SmashDamage (дерево и двери за 2-3
|
||||
/// удара, бетон дольше).
|
||||
/// удара, бетон дольше);
|
||||
/// - на дыбы на E (указание 09.10: «на E - действие»): враги ближе RearRange сбиты с ног на RearStun с
|
||||
/// и отброшены от медведя - передышка в толпе. Откат RearCooldown.
|
||||
/// </summary>
|
||||
public static class BearControl
|
||||
{
|
||||
@@ -25,6 +27,13 @@ namespace NecromancerTome
|
||||
public const float SmashReach = 2.6f;
|
||||
public const float SkinFactor = 0.5f;
|
||||
static float _nextRoar;
|
||||
public const float RearRange = 3.5f;
|
||||
public const float RearStun = 2f;
|
||||
public const float RearCooldown = 6f;
|
||||
/// <summary>Толчок от медведя, м за тик (как motion существ; гаснет сам).</summary>
|
||||
public const float RearPush = 0.5f;
|
||||
public const string RearSound = "bearattack";
|
||||
static float _nextRear;
|
||||
|
||||
public static bool Is(EntityAlive _e)
|
||||
{
|
||||
@@ -36,6 +45,8 @@ namespace NecromancerTome
|
||||
{
|
||||
if (Time.time < _nextRoar)
|
||||
{
|
||||
// Рёв в толпе не отличить от удара (тест 09.10) - пишем, сколько ждать и сколько пошло на медведя.
|
||||
Tip(string.Format(Localization.Get("necroBearRoarCooldown"), Mathf.CeilToInt(_nextRoar - Time.time)));
|
||||
return;
|
||||
}
|
||||
_nextRoar = Time.time + RoarCooldown;
|
||||
@@ -59,6 +70,53 @@ namespace NecromancerTome
|
||||
n++;
|
||||
}
|
||||
Debug.Log("[NecromancerTome] BearControl: рёв - на медведя идут " + n);
|
||||
Tip(string.Format(Localization.Get("necroBearRoarResult"), n));
|
||||
}
|
||||
|
||||
/// <summary>Сразу, мимо очереди подсказок: очередь игры (XUiC_PopupToolTip) отсеивает только одинаковый текст,
|
||||
/// и «через 7 с», «через 6 с»... при частых ПКМ копились бы и всплывали с опозданием.</summary>
|
||||
static void Tip(string _text)
|
||||
{
|
||||
if (Possession.Player != null)
|
||||
{
|
||||
GameManager.ShowTooltip(Possession.Player, _text, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>E: на дыбы - враги рядом падают (тот же приём, что у броска волка: BeginStun Prone) и
|
||||
/// отлетают от медведя.</summary>
|
||||
public static void RearUp(World _world, EntityAlive _bear)
|
||||
{
|
||||
if (Time.time < _nextRear)
|
||||
{
|
||||
Tip(string.Format(Localization.Get("necroBearRearCooldown"), Mathf.CeilToInt(_nextRear - Time.time)));
|
||||
return;
|
||||
}
|
||||
_nextRear = Time.time + RearCooldown;
|
||||
_bear.PlayOneShot(RearSound);
|
||||
_bear.emodel?.avatarController?.StartAnimationAttack();
|
||||
int n = 0;
|
||||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||||
{
|
||||
EntityAlive e = _world.EntityAlives[i];
|
||||
if (!SnakeControl.IsFoe(e, _bear) || (e.position - _bear.position).sqrMagnitude > RearRange * RearRange)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Vector3 away = e.position - _bear.position;
|
||||
away.y = 0f;
|
||||
away = away.sqrMagnitude > 0.01f ? away.normalized : _bear.GetForwardVector();
|
||||
e.motion += away * RearPush + Vector3.up * 0.15f;
|
||||
if (e.bodyDamage.CurrentStun == EnumEntityStunType.None && e.emodel?.avatarController != null)
|
||||
{
|
||||
e.emodel.avatarController.BeginStun(EnumEntityStunType.Prone, EnumBodyPartHit.Torso, Utils.EnumHitDirection.Back, false, 0.5f);
|
||||
e.SetStun(EnumEntityStunType.Prone);
|
||||
e.bodyDamage.StunDuration = RearStun;
|
||||
}
|
||||
n++;
|
||||
}
|
||||
Tip(string.Format(Localization.Get("necroBearRearResult"), n));
|
||||
Debug.Log("[NecromancerTome] BearControl: на дыбы - сбиты с ног " + n);
|
||||
}
|
||||
|
||||
/// <summary>ЛКМ: блок под прицелом в досягаемости лапы (не существо) - урон SmashDamage.</summary>
|
||||
|
||||
@@ -317,6 +317,11 @@ namespace NecromancerTome
|
||||
public static void Prefix(EntityPlayerLocal __instance, out Color? __state)
|
||||
{
|
||||
__state = null;
|
||||
// В одержимости прицел красит Possession.AimTint по цели одержимого, а не хват тела.
|
||||
if (Possession.Active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int hold = HandHolds.Current(__instance);
|
||||
EntityAlive target = hold == HandHolds.None ? null : HandHolds.Target(__instance);
|
||||
// «Пиф-паф» берёт только зомби-гуманоидов - на зверях-зомби прицел не краснеет.
|
||||
|
||||
+132
-27
@@ -122,14 +122,14 @@ namespace NecromancerTome
|
||||
/// <summary>Толчок вверх при взлёте с земли на пробел, м за тик.</summary>
|
||||
public const float FlyTakeOff = 0.3f;
|
||||
static bool _flySitting;
|
||||
/// <summary>Питание падалью (указание 06.10): сидя у трупа, F - EatTime секунд клюёт его и лечится на
|
||||
/// <summary>Питание падалью (указание 06.10; у медведя и собаки - 09.10): сидя или стоя у трупа, E - EatTime секунд клюёт его и лечится на
|
||||
/// EatHealFrac здоровья. У трупа запас мяса - его полное здоровье при жизни; поедание отнимает
|
||||
/// столько же, сколько лечит (и на полном здоровье тоже). Запас кончился - труп исчезает (указание
|
||||
/// 06.10), вместе с содержимым, если это красный труп-добыча. Сдвинулся, взлетел, ударил - еда
|
||||
/// прервана, вылеченное и съеденное остаются; к трупу можно вернуться.</summary>
|
||||
public const float EatRange = 2.5f;
|
||||
public const float EatTime = 3f;
|
||||
public const float EatHealFrac = 0.25f;
|
||||
public const float EatHealFrac = 0.125f; // было 0.25 - указание 09.10: «лечится слишком быстро, вдвое»
|
||||
static EntityAlive _eatCorpse;
|
||||
static float _eatLeft, _eatHeal, _eatBite;
|
||||
static int _eatCount;
|
||||
@@ -325,6 +325,7 @@ namespace NecromancerTome
|
||||
}
|
||||
Active = false;
|
||||
RatLoot.CloseIfViewing();
|
||||
WolfBag.CloseWindow();
|
||||
RatPhase.Stop(GameManager.Instance?.World, Zombie, "выход из одержимости");
|
||||
DogSense.Clear();
|
||||
WolfControl.Clear();
|
||||
@@ -588,20 +589,23 @@ namespace NecromancerTome
|
||||
_nextAttack = Time.time + FlyAttackDelay;
|
||||
_meleeLookUntil = Time.time + FlyAttackDelay;
|
||||
}
|
||||
// F - клич-приманка (указание 06.10; кнопка - 08.10, была ПКМ): крик крикуньи, только голосом
|
||||
// паникующей курицы - зомби в ScreamRange идут на него; с воздуха уводит толпу от базы или от тела.
|
||||
// F у игрока - фонарик, но его читает только выключенный PlayerMoveController.Update.
|
||||
bool scream = input != null && !gui && (input.PermanentActions.ToggleFlashlight.WasPressed || input.ToggleFlashlight.WasPressed);
|
||||
// E - клич-приманка (указание 06.10; кнопка - 08.10 F, была ПКМ; 09.10 - E: «на E действие, у стервятника крик»):
|
||||
// крик крикуньи, только голосом паникующей курицы - зомби в ScreamRange идут на него; с воздуха уводит
|
||||
// толпу от базы или от тела. detonate - это и есть нажатие E.
|
||||
bool scream = detonate;
|
||||
if (scream)
|
||||
{
|
||||
Scream(world, z);
|
||||
}
|
||||
// ПКМ (держать) - «зоркий глаз» (указание 08.10: «приближение как у прицела»), в PlaceCamera.
|
||||
_flyZoomHeld = rangedHeld;
|
||||
// E - питаться падалью (указание 06.10, кнопка - 08.10: «пусть ест по кнопке E» - у игрока E это
|
||||
// взаимодействие, лут; было F). detonate - это и есть нажатие E.
|
||||
bool eat = detonate;
|
||||
Eat(world, z, eat, attack || scream);
|
||||
// F - питаться падалью (указание 06.10; кнопка 08.10 - E, 09.10 - F у всех одержимых питомцев).
|
||||
// F у игрока - фонарик, но его читает только выключенный PlayerMoveController.Update.
|
||||
bool eat = FKey(input, gui);
|
||||
// Есть можно сидя или просто остановившись рядом (зависнув над падалью): без ввода и почти без скорости.
|
||||
bool still = _flySitting || _flyMove.sqrMagnitude < 0.01f && !_flyUp &&
|
||||
new Vector2(z.motion.x, z.motion.z).sqrMagnitude < FlySitSpeed * FlySitSpeed;
|
||||
Eat(world, z, eat, attack || scream || _flyMove.sqrMagnitude > 0.01f || _flyUp || !_flySitting, still);
|
||||
if (_meleeLookUntil > 0f && Time.time >= _meleeLookUntil)
|
||||
{
|
||||
_meleeLookUntil = 0f;
|
||||
@@ -643,16 +647,29 @@ namespace NecromancerTome
|
||||
{
|
||||
// ПКМ - бросок на цель, след крови раненых (решено 09.10).
|
||||
WolfControl.Tick(world, _player, z, ranged, ref _bodyYaw);
|
||||
// E: у тела - выгрузить сумку в рюкзак, у контейнера - лутать в сумку (3 ячейки).
|
||||
if (!(detonate && WolfBag.TryUnload(_player, z)))
|
||||
// E: на контейнер или мешок с зомби - лутать в сумку (3 ячейки); у тела - выгрузить сумку в рюкзак;
|
||||
// иначе - «багажник», окно сумки (указание 09.10).
|
||||
bool looted = RatLoot.Tick(world, _player, z, detonate, true);
|
||||
if (detonate && !looted && !RatLoot.Viewing && !WolfBag.TryUnload(_player, z))
|
||||
{
|
||||
RatLoot.Tick(world, _player, z, detonate, true);
|
||||
WolfBag.OpenWindow(_player, z);
|
||||
}
|
||||
}
|
||||
if (ranged && IsPet && BearControl.Is(z))
|
||||
{
|
||||
BearControl.Roar(world, z); // ПКМ - рёв-провокация (решено 09.10)
|
||||
}
|
||||
if (detonate && IsPet && BearControl.Is(z))
|
||||
{
|
||||
BearControl.RearUp(world, z); // E - на дыбы, враги рядом падают (указание 09.10)
|
||||
}
|
||||
if (EatsCarrion(z))
|
||||
{
|
||||
// F - есть падаль, как стервятник (указание 09.10: «поедание падали на F одержимым питомцам, на E -
|
||||
// действие»; и одержимым зомби - их ИИ в игре и так ест трупы), стоя на месте рядом с трупом.
|
||||
Eat(world, z, FKey(input, gui), moving || jump || attack || ranged, !moving && z.onGround);
|
||||
}
|
||||
AimTint(GroundAimTarget(world, z) != null);
|
||||
if (IsPet && DogSense.Is(z))
|
||||
{
|
||||
// Нюх (указание 09.10): все существа рядом с собакой - на компасе и карте.
|
||||
@@ -758,11 +775,20 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
static void Eat(World _world, EntityAlive _v, bool _pressed, bool _acted)
|
||||
/// <summary>Едят падаль на F, кроме стервятника (указание 09.10): чужие одержимые зомби и питомцы медведь,
|
||||
/// собака, волк. Крыса - дух, не ест.</summary>
|
||||
static bool EatsCarrion(EntityAlive _z) => !IsPet || BearControl.Is(_z) || DogSense.Is(_z) || WolfControl.Is(_z);
|
||||
|
||||
/// <summary>F на клавиатуре или геймпаде (у игрока - фонарик).</summary>
|
||||
static bool FKey(PlayerActionsLocal _input, bool _gui) =>
|
||||
_input != null && !_gui && (_input.PermanentActions.ToggleFlashlight.WasPressed || _input.ToggleFlashlight.WasPressed);
|
||||
|
||||
/// <summary>_moved - сдвинулся или что-то сделал (еда прерывается), _still - стоит/сидит, можно начать.</summary>
|
||||
static void Eat(World _world, EntityAlive _v, bool _pressed, bool _moved, bool _still)
|
||||
{
|
||||
if (_eating)
|
||||
{
|
||||
bool moved = _flyMove.sqrMagnitude > 0.01f || _flyUp || _acted || !_flySitting;
|
||||
bool moved = _moved;
|
||||
bool gone = _eatCorpse != null ? _eatCorpse.IsMarkedForUnload() : _world.GetBlock(_eatBlock).type != _eatBlockType;
|
||||
if (moved || gone)
|
||||
{
|
||||
@@ -822,17 +848,14 @@ namespace NecromancerTome
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Есть можно сидя или просто остановившись рядом (зависнув над падалью): без ввода и почти без скорости.
|
||||
bool still = _flySitting || _flyMove.sqrMagnitude < 0.01f && !_flyUp &&
|
||||
new Vector2(_v.motion.x, _v.motion.z).sqrMagnitude < FlySitSpeed * FlySitSpeed;
|
||||
if (!still || !FindCarrion(_world, _v))
|
||||
if (!_still || !FindCarrion(_world, _v))
|
||||
{
|
||||
GameManager.ShowTooltip(_player, Localization.Get("necroPossessNoCarrion"));
|
||||
Debug.Log("[NecromancerTome] Possession: падали нет" + (still ? "" : " - стервятник не остановился"));
|
||||
GameManager.ShowTooltip(_player, Localization.Get(Flying ? "necroPossessNoCarrion" : "necroPossessNoCarrionGround"));
|
||||
Debug.Log("[NecromancerTome] Possession: падали нет" + (_still ? "" : " - " + _v.EntityName + " не остановился"));
|
||||
return;
|
||||
}
|
||||
_eating = true;
|
||||
if (!_flySitting)
|
||||
if (Flying && !_flySitting)
|
||||
{
|
||||
// Сел за еду сразу, не дожидаясь тика полёта: иначе проверка «не сидит» тут же прервала бы еду.
|
||||
_flySitting = true;
|
||||
@@ -845,7 +868,7 @@ namespace NecromancerTome
|
||||
_eatCount = 0;
|
||||
_v.SetLookPosition(_eatCorpse != null ? _eatCorpse.getChestPosition() : _eatBlock.ToVector3() + new Vector3(0.5f, 0.3f, 0.5f));
|
||||
_v.IsEating = true; // как у зомби: анимация поедания и синхронизация по сети
|
||||
Debug.Log("[NecromancerTome] Possession: стервятник ест " + EatName() + ", здоровье " + _v.Health + "/" + _v.GetMaxHealth() +
|
||||
Debug.Log("[NecromancerTome] Possession: " + _v.EntityName + " ест " + EatName() + ", здоровье " + _v.Health + "/" + _v.GetMaxHealth() +
|
||||
", мяса у падали " + Mathf.CeilToInt(EatMeat(0f)));
|
||||
}
|
||||
|
||||
@@ -1011,7 +1034,6 @@ namespace NecromancerTome
|
||||
colors[_tintIndex] = _tintSaved;
|
||||
_tintIndex = -1;
|
||||
}
|
||||
Debug.Log("[NecromancerTome] Possession: прицел " + (_on ? "на цели - красный" : "без цели"));
|
||||
}
|
||||
|
||||
static readonly Color AimOnTarget = new Color(1f, 0.15f, 0.1f, 1f);
|
||||
@@ -1219,6 +1241,47 @@ namespace NecromancerTome
|
||||
static EntityAlive _aimMissNear;
|
||||
static float _aimMissDist, _aimMissOff;
|
||||
|
||||
/// <summary>Враг под прицелом наземного одержимого (указание 09.10: прицел краснеет на противнике у
|
||||
/// всех одержимых): тело врага - отрезок от ног до головы - не дальше GroundAimRadius от луча
|
||||
/// камеры и не дальше GroundAimRange от одержимого, и не за стеной - не дальше первого попадания
|
||||
/// луча (AimPoint).</summary>
|
||||
static EntityAlive GroundAimTarget(World _world, EntityAlive _z)
|
||||
{
|
||||
Transform cam = _player?.cameraTransform;
|
||||
if (cam == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Vector3 from = cam.position + Origin.position;
|
||||
Vector3 dir = cam.forward;
|
||||
float wall = Vector3.Dot(AimPoint() - from, dir) + 0.5f;
|
||||
EntityAlive best = null;
|
||||
float bestAlong = float.MaxValue;
|
||||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||||
{
|
||||
EntityAlive e = _world.EntityAlives[i];
|
||||
if (!SnakeControl.IsFoe(e, _z) || (e.position - _z.position).sqrMagnitude > GroundAimRange * GroundAimRange)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Vector3 feet = e.position, head = e.getHeadPosition();
|
||||
for (int k = 0; k <= 4; k++)
|
||||
{
|
||||
Vector3 p = Vector3.Lerp(feet, head, k / 4f);
|
||||
float a = Vector3.Dot(p - from, dir);
|
||||
if (a > 0f && a < wall && a < bestAlong && Vector3.Cross(p - from, dir).magnitude < GroundAimRadius)
|
||||
{
|
||||
best = e;
|
||||
bestAlong = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
public const float GroundAimRange = 30f; // зрение зомби, как TauntRange
|
||||
public const float GroundAimRadius = 0.6f;
|
||||
|
||||
/// <summary>Раз в полсекунды: есть ли живой враг ближе BodyDangerRange к телу (как у змеи).</summary>
|
||||
static void CheckBody(World _world, EntityAlive _z, float _dt)
|
||||
{
|
||||
@@ -1288,11 +1351,11 @@ namespace NecromancerTome
|
||||
}
|
||||
if (Flying)
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintZoom") + ", " + Localization.Get("necroPossessHintScreamF");
|
||||
text += ", " + Localization.Get("necroPossessHintZoom") + ", " + Localization.Get("necroPossessHintScreamE");
|
||||
}
|
||||
if (Flying)
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintEat");
|
||||
text += ", " + Localization.Get("necroPossessHintEatF");
|
||||
}
|
||||
if (IsPet && RatModel.Is(_z))
|
||||
{
|
||||
@@ -1309,9 +1372,14 @@ namespace NecromancerTome
|
||||
}
|
||||
if (IsPet && BearControl.Is(_z))
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintRoar") + ", " + Localization.Get("necroPossessHintSmash") + ", " +
|
||||
text += ", " + Localization.Get("necroPossessHintRoar") + ", " + Localization.Get("necroPossessHintRear") + ", " +
|
||||
Localization.Get("necroPossessHintSmash") + ", " +
|
||||
Localization.Get("necroPossessHintSkin");
|
||||
}
|
||||
if (EatsCarrion(_z))
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintEatGroundF");
|
||||
}
|
||||
if (_z.jumpMaxDistance >= 5f)
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintLeap");
|
||||
@@ -1765,6 +1833,43 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Прицел одержимого - всегда перекрестие (указание 09.10: «то перекрестие, то точка»). Вид прицела
|
||||
/// игра берёт у предмета в руке тела, а у книги призыва и пустой руки он зависит от проверки камеры от
|
||||
/// третьего лица (ItemAction.GetCrosshairType - EntityPlayerLocal.CharacterCameraAngleValid): камера дальше
|
||||
/// 20° от того, куда повёрнуто тело, - прицела нет, и игра рисует точку. В одержимости камера - глаза
|
||||
/// одержимого, тело стоит, так что вид менялся от поворота мыши. Рисуем сами то же, что ветка Plus
|
||||
/// guiDrawCrosshair; цвет - из таблицы игры, его красит Possession.AimTint.</summary>
|
||||
[HarmonyPatch(typeof(EntityPlayerLocal), nameof(EntityPlayerLocal.guiDrawCrosshair))]
|
||||
public static class Patch_EntityPlayerLocal_guiDrawCrosshair_Possession
|
||||
{
|
||||
public static bool Prefix(EntityPlayerLocal __instance, NGuiWdwInGameHUD _guiInGame, bool bModalWindowOpen)
|
||||
{
|
||||
if (!Possession.Active || __instance != Possession.Player)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!_guiInGame.showCrosshair || Event.current.type != EventType.Repaint || __instance.IsDead() ||
|
||||
!GamePrefs.GetBool(EnumGamePrefs.OptionsCrosshairEnabled) || bModalWindowOpen || _guiInGame.CrosshairTexture == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
int i = GamePrefs.GetInt(EnumGamePrefs.OptionsCrosshairColor);
|
||||
if (i < 0 || i >= EntityPlayerLocal.crosshairColors.Count)
|
||||
{
|
||||
i = 0;
|
||||
}
|
||||
Color c = EntityPlayerLocal.crosshairColors[i];
|
||||
Vector2 center = __instance.GetCrosshairPosition2D();
|
||||
center.y = Screen.height - center.y;
|
||||
float size = Screen.height * 0.059f * GamePrefs.GetFloat(EnumGamePrefs.OptionsCrosshairScale);
|
||||
Color was = GUI.color;
|
||||
GUI.color = new Color(c.r, c.g, c.b, GamePrefs.GetFloat(EnumGamePrefs.OptionsCrosshairOpacity));
|
||||
GUI.DrawTexture(new Rect(center.x - size / 2f, center.y - size / 2f, size, size), _guiInGame.CrosshairTexture, ScaleMode.StretchToFill);
|
||||
GUI.color = was;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Тело оцепенело: ввод игрока не идёт (как у тела в рэгдолле - stopMoving и выход).</summary>
|
||||
[HarmonyPatch(typeof(PlayerMoveController), nameof(PlayerMoveController.Update))]
|
||||
public static class Patch_PlayerMoveController_Update_Possession
|
||||
|
||||
+176
-16
@@ -17,6 +17,11 @@ namespace NecromancerTome
|
||||
/// переполнены»): звук ui_denied и xuiInventoryFullForPickup;
|
||||
/// - окно не закрывается из-за дальности до ТЕЛА: XUiC_LootWindow.Update меряет её от персонажа.
|
||||
/// Подсказку «E: обыскать» рисует управление игрока, а оно в одержимости выключено - выводим сами.
|
||||
///
|
||||
/// МЕШКИ (указание 09.10: «лутать жёлтые мешочки, и синие, и красные, падающие с зомби»). Мешок - сущность
|
||||
/// EntityLootContainer, открывается той же цепочкой замка (LockRequestLocal -> Entity.OnLockResponseLocal), но
|
||||
/// окно другое - XUiC_BagStorageWindowGroup; для него те же отличия: рюкзак закрыт, ячейки заперты, дальность
|
||||
/// от крысы. Рюкзак погибшего игрока (EntityBackpack) - не мешок, его не трогаем.
|
||||
/// </summary>
|
||||
public static class RatLoot
|
||||
{
|
||||
@@ -29,6 +34,41 @@ namespace NecromancerTome
|
||||
static float _spareUntil;
|
||||
/// <summary>Досягаемость контейнера от крысы, м.</summary>
|
||||
const float Reach = 2.5f;
|
||||
/// <summary>Мешок - не дальше стольких градусов от луча камеры (мешок мал, точно навести трудно).</summary>
|
||||
const float BagAimDeg = 25f;
|
||||
/// <summary>Открытый на просмотр мешок - для щелчков по ячейкам и «Взять всё».</summary>
|
||||
static EntityLootContainer _viewBag;
|
||||
|
||||
/// <summary>Кто осматривает - для лога (у волка тот же обыск).</summary>
|
||||
static string Who => Possession.Zombie != null && WolfControl.Is(Possession.Zombie) ? "волк" : "крыса";
|
||||
|
||||
/// <summary>Мешок перед крысой: ближе Reach к ней и ближе всех к лучу камеры, не дальше BagAimDeg.</summary>
|
||||
public static EntityLootContainer FindBag(World _world, EntityPlayerLocal _player, EntityAlive _rat)
|
||||
{
|
||||
Transform cam = _player.cameraTransform;
|
||||
if (cam == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Vector3 from = cam.position + Origin.position;
|
||||
EntityLootContainer best = null;
|
||||
float bestDeg = BagAimDeg;
|
||||
for (int i = 0; i < _world.Entities.list.Count; i++)
|
||||
{
|
||||
if (!(_world.Entities.list[i] is EntityLootContainer bag) || bag.IsMarkedForUnload() || bag.bag == null ||
|
||||
(bag.position - _rat.position).sqrMagnitude > Reach * Reach)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
float deg = Vector3.Angle(cam.forward, bag.position + Vector3.up * 0.2f - from);
|
||||
if (deg < bestDeg)
|
||||
{
|
||||
bestDeg = deg;
|
||||
best = bag;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/// <summary>Контейнер перед крысой по лучу камеры: блок с хранилищем ближе Reach к крысе; иначе null.</summary>
|
||||
public static TEFeatureStorage Find(World _world, EntityPlayerLocal _player, EntityAlive _rat, out Vector3i _pos, out BlockValue _bv)
|
||||
@@ -76,21 +116,39 @@ namespace NecromancerTome
|
||||
return st;
|
||||
}
|
||||
|
||||
/// <summary>Каждый кадр одержимости крысой: подсказка и E - открыть.</summary>
|
||||
public static void Tick(World _world, EntityPlayerLocal _player, EntityAlive _rat, bool _activate, bool _take = false)
|
||||
/// <summary>Каждый кадр одержимости крысой: подсказка и E - открыть. true - нажатие E ушло на контейнер или
|
||||
/// мешок (волку тогда не открывать свою сумку).</summary>
|
||||
public static bool Tick(World _world, EntityPlayerLocal _player, EntityAlive _rat, bool _activate, bool _take = false)
|
||||
{
|
||||
LocalPlayerUI ui = _player.PlayerUI;
|
||||
if (ui == null || Viewing || ui.windowManager.IsModalWindowOpen())
|
||||
{
|
||||
return;
|
||||
}
|
||||
TEFeatureStorage st = Find(_world, _player, _rat, out Vector3i pos, out BlockValue bv);
|
||||
if (st == null)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
PlayerActionsLocal input = _player.playerInput;
|
||||
string key = input.Activate.GetBindingXuiMarkupString() + input.PermanentActions.Activate.GetBindingXuiMarkupString();
|
||||
TEFeatureStorage st = Find(_world, _player, _rat, out Vector3i pos, out BlockValue bv);
|
||||
if (st == null)
|
||||
{
|
||||
EntityLootContainer bag = FindBag(_world, _player, _rat);
|
||||
if (bag == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
XUiC_InteractionPrompt.SetText(ui, string.Format(Localization.Get("necroPossessSearchBag"), key, bag.LocalizedEntityName));
|
||||
if (!_activate)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Viewing = true;
|
||||
Taking = _take;
|
||||
_viewBag = bag;
|
||||
XUiC_InteractionPrompt.SetText(ui, null);
|
||||
_lockRequest = _lockRequest ?? AccessTools.Method(typeof(LockManager), nameof(LockManager.LockRequestLocal), new[] { typeof(ILockTarget), typeof(ushort) });
|
||||
_lockRequest.Invoke(LockManager.Instance, new object[] { bag, (ushort)0 });
|
||||
Debug.Log("[NecromancerTome] RatLoot: " + Who + " осматривает мешок " + EntityClass.GetEntityClassName(bag.entityClass) + " (" + bag.entityId + ")");
|
||||
return true;
|
||||
}
|
||||
string text = st.GetActivationText(_world, pos, bv, _rat, key, bv.Block.GetLocalizedBlockName());
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
@@ -98,7 +156,7 @@ namespace NecromancerTome
|
||||
}
|
||||
if (!_activate)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
bool locked = st.lockpickFeature != null && st.lockpickFeature.NeedsLockpicking() ||
|
||||
st.lockFeature != null && st.lockFeature.IsLocked() && !st.lockFeature.IsUserAllowed(Platform.PlatformManager.InternalLocalUserIdentifier) ||
|
||||
@@ -106,7 +164,7 @@ namespace NecromancerTome
|
||||
if (locked)
|
||||
{
|
||||
Audio.Manager.BroadcastPlayByLocalPlayer(pos.ToVector3() + Vector3.one * 0.5f, "Misc/locked");
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
Viewing = true;
|
||||
Taking = _take;
|
||||
@@ -116,7 +174,8 @@ namespace NecromancerTome
|
||||
// Через отражение: у LockRequestLocal есть перегрузка с ReadOnlySpan, которой нет в справочных сборках проекта.
|
||||
_lockRequest = _lockRequest ?? AccessTools.Method(typeof(LockManager), nameof(LockManager.LockRequestLocal), new[] { typeof(ILockTarget), typeof(ushort) });
|
||||
_lockRequest.Invoke(LockManager.Instance, new object[] { st, (ushort)0 });
|
||||
Debug.Log("[NecromancerTome] RatLoot: крыса осматривает " + bv.Block.GetBlockName() + " " + pos);
|
||||
Debug.Log("[NecromancerTome] RatLoot: " + Who + " осматривает " + bv.Block.GetBlockName() + " " + pos);
|
||||
return true;
|
||||
}
|
||||
|
||||
static System.Reflection.MethodInfo _lockRequest;
|
||||
@@ -141,12 +200,17 @@ namespace NecromancerTome
|
||||
LocalPlayerUI ui = LocalPlayerUI.GetUIForPrimaryPlayer();
|
||||
ui?.windowManager.CloseAllOpenModalWindows();
|
||||
Viewing = false;
|
||||
_viewBag = null;
|
||||
_spareUntil = Time.time + 5f;
|
||||
}
|
||||
|
||||
static void LockSlots(XUiC_LootWindow _w, bool _lock)
|
||||
{
|
||||
XUiC_LootContainer grid = _w?.lootContainer;
|
||||
LockSlots(_w?.lootContainer, _lock);
|
||||
}
|
||||
|
||||
static void LockSlots(XUiC_ItemStackGrid grid, bool _lock)
|
||||
{
|
||||
if (grid?.itemControllers == null)
|
||||
{
|
||||
return;
|
||||
@@ -263,6 +327,90 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Окно мешка открылось на просмотр - рюкзак закрыть, ячейки запереть (как у ящика).</summary>
|
||||
[HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.OnOpen))]
|
||||
public static class Patch_Bag_OnOpen
|
||||
{
|
||||
public static void Postfix(XUiC_BagStorageWindowGroup __instance)
|
||||
{
|
||||
if (!Viewing || _viewBag == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
__instance.xui.playerUI.windowManager.Close("backpack");
|
||||
LockSlots(__instance.containerWindow, true);
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.OnClose))]
|
||||
public static class Patch_Bag_OnClose
|
||||
{
|
||||
public static void Prefix(XUiC_BagStorageWindowGroup __instance)
|
||||
{
|
||||
LockSlots(__instance.containerWindow, false); // всегда: запертые ячейки не должны достаться персонажу
|
||||
if (Viewing && _viewBag != null)
|
||||
{
|
||||
Viewing = false;
|
||||
_viewBag = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Таймер обыска мешка прерван - просмотра нет.</summary>
|
||||
[HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.CancelOpen))]
|
||||
public static class Patch_Bag_CancelOpen
|
||||
{
|
||||
public static void Postfix()
|
||||
{
|
||||
if (Viewing && _viewBag != null)
|
||||
{
|
||||
Viewing = false;
|
||||
_viewBag = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Замок мешка не дан (занят другим) - просмотра нет.</summary>
|
||||
[HarmonyPatch(typeof(Entity), nameof(Entity.OnLockResponseLocal))]
|
||||
public static class Patch_Entity_OnLockResponseLocal
|
||||
{
|
||||
public static void Postfix(Entity __instance, bool __result)
|
||||
{
|
||||
if (!__result && Viewing && _viewBag != null && __instance == _viewBag)
|
||||
{
|
||||
Viewing = false;
|
||||
_viewBag = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Дальность окна мешка - от крысы: проверка окна меряет её до entityPlayer, поэтому на время Update
|
||||
/// персонаж «стоит» на месте крысы, как у окна ящика.</summary>
|
||||
[HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.Update))]
|
||||
public static class Patch_Bag_Update
|
||||
{
|
||||
public static void Prefix(XUiC_BagStorageWindowGroup __instance, out Vector3? __state)
|
||||
{
|
||||
__state = null;
|
||||
EntityPlayerLocal p = __instance.xui.playerUI.entityPlayer;
|
||||
EntityAlive rat = Possession.Zombie;
|
||||
if (!Viewing || _viewBag == null || p == null || rat == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
__state = p.position;
|
||||
p.position = rat.position;
|
||||
}
|
||||
|
||||
public static void Finalizer(XUiC_BagStorageWindowGroup __instance, Vector3? __state)
|
||||
{
|
||||
if (__state.HasValue)
|
||||
{
|
||||
__instance.xui.playerUI.entityPlayer.position = __state.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveAll))]
|
||||
public static class Patch_MoveAll
|
||||
{
|
||||
@@ -277,6 +425,10 @@ namespace NecromancerTome
|
||||
{
|
||||
WolfBag.TakeAll(te);
|
||||
}
|
||||
else if (Taking && _viewBag != null)
|
||||
{
|
||||
WolfBag.TakeAll(_viewBag);
|
||||
}
|
||||
else
|
||||
{
|
||||
Deny();
|
||||
@@ -300,21 +452,29 @@ namespace NecromancerTome
|
||||
{
|
||||
public static void Postfix(XUiC_ItemStack __instance)
|
||||
{
|
||||
if (!Viewing || !__instance.isOver || __instance.ItemStack == null || __instance.ItemStack.IsEmpty() ||
|
||||
__instance.GetParentByType<XUiC_LootContainer>() == null)
|
||||
if (!Viewing || !__instance.isOver || __instance.ItemStack == null || __instance.ItemStack.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Ячейка контейнера-блока (XUiC_LootContainer) или мешка (XUiC_BagContainer), не рюкзака.
|
||||
XUiC_ItemStackGrid grid = (XUiC_ItemStackGrid)__instance.GetParentByType<XUiC_LootContainer>() ?? __instance.GetParentByType<XUiC_BagContainer>();
|
||||
if (grid == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CursorControllerAbs cur = __instance.xui.playerUI.CursorController;
|
||||
if (cur.GetMouseButtonDown(UICamera.MouseButton.LeftButton) || cur.GetMouseButtonDown(UICamera.MouseButton.RightButton))
|
||||
{
|
||||
XUiC_LootContainer grid = __instance.GetParentByType<XUiC_LootContainer>();
|
||||
TEFeatureStorage te = __instance.GetParentByType<XUiC_LootWindow>()?.te;
|
||||
int idx = grid?.itemControllers != null ? System.Array.IndexOf(grid.itemControllers, __instance) : -1;
|
||||
int idx = grid.itemControllers != null ? System.Array.IndexOf(grid.itemControllers, __instance) : -1;
|
||||
if (Taking && te != null && idx >= 0)
|
||||
{
|
||||
WolfBag.TakeFrom(te, idx);
|
||||
}
|
||||
else if (Taking && _viewBag != null && grid is XUiC_BagContainer && idx >= 0)
|
||||
{
|
||||
WolfBag.TakeFrom(_viewBag, idx);
|
||||
}
|
||||
else
|
||||
{
|
||||
Deny();
|
||||
|
||||
+187
-8
@@ -11,7 +11,12 @@ namespace NecromancerTome
|
||||
/// но щелчок по предмету (и «Взять всё») кладёт его в сумку волка; места нет - «Сумка полна»;
|
||||
/// - выгрузка - принести к телу: рядом с телом E перекладывает сумку в рюкзак персонажа, что не
|
||||
/// влезло - мешком на землю рядом;
|
||||
/// - волк погиб или отозван - сумка выпадает мешком на месте.
|
||||
/// - волк погиб или отозван - сумка выпадает мешком на месте; при отзыве (указание 09.10) на карте метка
|
||||
/// «Сумка волка», чтобы за ним прийти; мешок подобран или истлел - метка снята (Patch_removeBackpack);
|
||||
/// - «багажник» (указание 09.10: «почти как у пространственного хранилища»): E не на контейнер и не у тела -
|
||||
/// окно сумки, то же ванильное окно, что у браслета (XUiC_BagStorageWindowGroup, шаблон roboticDrone), но
|
||||
/// без рюкзака персонажа: внутри перекладывать, вытащить за окно - выбросить у волка (Patch_GetDropPosition);
|
||||
/// - мешки с зомби волк лутает так же, как ящики (RatLoot, TakeFrom(EntityLootContainer)).
|
||||
///
|
||||
/// ХРАНЕНИЕ. Сумку сущности игра не сохраняет (Entity.bag нет в Entity.Write), поэтому она своя.
|
||||
/// Упаковка - та же, что у пространственного хранилища (SpatialVaultPersistence.BuildPayload/ParsePayload,
|
||||
@@ -25,6 +30,11 @@ namespace NecromancerTome
|
||||
public const int Slots = 3;
|
||||
const string FileName = "NecromancerTome_wolfbag.bin";
|
||||
public const float UnloadRange = 3f;
|
||||
/// <summary>Метка выпавшей сумки: имя - ключ локализации, по нему и находим свою метку среди меток игрока.</summary>
|
||||
public const string WaypointName = "necroWolfBagWaypoint";
|
||||
public const string WaypointIcon = "ui_game_symbol_backpack";
|
||||
/// <summary>Открыто окно сумки («багажник»).</summary>
|
||||
public static bool WindowOpen;
|
||||
static Bag _bag;
|
||||
static string _dir;
|
||||
|
||||
@@ -112,11 +122,41 @@ namespace NecromancerTome
|
||||
/// в пустую ячейку; в контейнере остаётся ровно то, что не влезло.</summary>
|
||||
public static bool TakeFrom(TEFeatureStorage _te, int _idx)
|
||||
{
|
||||
ItemStack src = _te.ItemGrid.GetItem(_idx);
|
||||
if (src == null || src.IsEmpty())
|
||||
ItemStack rest = Take(_te.ItemGrid.GetItem(_idx));
|
||||
if (rest == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_te.UpdateSlot(_idx, rest);
|
||||
_te.SetModified();
|
||||
_te.Parent?.NotifyListeners(); // окно контейнера перерисует ячейку (как ванильная сортировка)
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>То же из мешка, упавшего с зомби. Окно мешка перерисуется по событию сумки (SetSlot).</summary>
|
||||
public static bool TakeFrom(EntityLootContainer _src, int _idx)
|
||||
{
|
||||
if (_idx >= _src.bag.SlotCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ItemStack rest = Take(_src.bag.ItemGrid.items[_idx]);
|
||||
if (rest == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_src.bag.SetSlot(_idx, rest);
|
||||
_src.OnBagModified(); // на клиенте - отправить сумку мешка серверу, как делает окно
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Переложить стопку в сумку; что осталось в источнике (пустая стопка, если всё), или null - не влезло.</summary>
|
||||
static ItemStack Take(ItemStack src)
|
||||
{
|
||||
if (src == null || src.IsEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Bag b = Get();
|
||||
ItemStack moving = src.Clone();
|
||||
int before = moving.count;
|
||||
@@ -130,17 +170,15 @@ namespace NecromancerTome
|
||||
{
|
||||
Audio.Manager.PlayInsidePlayerHead("ui_denied");
|
||||
Show("necroWolfBagFull");
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
ItemStack rest = src.Clone();
|
||||
rest.count = moving.count;
|
||||
_te.UpdateSlot(_idx, rest.count > 0 ? rest : ItemStack.Empty.Clone());
|
||||
_te.SetModified();
|
||||
_te.Parent?.NotifyListeners(); // окно контейнера перерисует ячейку (как ванильная сортировка)
|
||||
Save();
|
||||
Audio.Manager.PlayInsidePlayerHead("UseActions/takeall1");
|
||||
Show("necroWolfBagTook");
|
||||
return true;
|
||||
Debug.Log("[NecromancerTome] WolfBag: в сумку " + src.itemValue.ItemClass.GetItemName() + " x" + moved + " - " + Describe());
|
||||
return rest.count > 0 ? rest : ItemStack.Empty.Clone();
|
||||
}
|
||||
|
||||
/// <summary>«Взять всё» - по ячейкам, пока есть место.</summary>
|
||||
@@ -161,6 +199,54 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>«Взять всё» из мешка - по ячейкам, пока есть место.</summary>
|
||||
public static void TakeAll(EntityLootContainer _src)
|
||||
{
|
||||
bool any = false;
|
||||
ItemStack[] slots = _src.bag.ItemGrid.items;
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
{
|
||||
if (!slots[i].IsEmpty() && Get().CanTakeItem(slots[i]))
|
||||
{
|
||||
any |= TakeFrom(_src, i);
|
||||
}
|
||||
}
|
||||
if (!any)
|
||||
{
|
||||
Audio.Manager.PlayInsidePlayerHead("ui_denied");
|
||||
Show("necroWolfBagFull");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>«Багажник»: окно сумки, как у пространственного браслета (шаблон дрона), без рюкзака персонажа.
|
||||
/// Перекладка внутри пишется сразу (Save на каждое изменение и на закрытие).</summary>
|
||||
public static void OpenWindow(EntityPlayerLocal _player, EntityAlive _wolf)
|
||||
{
|
||||
LocalPlayerUI ui = LocalPlayerUI.GetUIForPlayer(_player);
|
||||
if (ui == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
WindowOpen = true;
|
||||
XUiC_BagStorageWindowGroup.Open(ui.xui, _wolf, Get(), LootContainer.GetLootContainer("roboticDrone"),
|
||||
Localization.Get("necroWolfBagWindowTitle"), Save, () =>
|
||||
{
|
||||
WindowOpen = false;
|
||||
Save();
|
||||
});
|
||||
Debug.Log("[NecromancerTome] WolfBag: окно сумки - " + Describe());
|
||||
}
|
||||
|
||||
/// <summary>Выход из одержимости при открытом окне сумки - закрыть его.</summary>
|
||||
public static void CloseWindow()
|
||||
{
|
||||
if (WindowOpen)
|
||||
{
|
||||
LocalPlayerUI.GetUIForPrimaryPlayer()?.windowManager.CloseAllOpenModalWindows();
|
||||
WindowOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>E рядом с телом: сумка - в рюкзак, что не влезло - мешком у тела.</summary>
|
||||
public static bool TryUnload(EntityPlayerLocal _body, EntityAlive _wolf)
|
||||
{
|
||||
@@ -228,9 +314,61 @@ namespace NecromancerTome
|
||||
b.Clear();
|
||||
Save();
|
||||
Drop(_wolf.position, items);
|
||||
if (_why == "отозван")
|
||||
{
|
||||
AddWaypoint(_wolf.position);
|
||||
}
|
||||
Debug.Log("[NecromancerTome] WolfBag: волк " + _why + " - сумка выпала мешком (" + items.Count + ")");
|
||||
}
|
||||
|
||||
/// <summary>Метка на карте и компасе - как игра ставит метку транспорта (XUiC_MapArea.RefreshVehiclePositionWaypoint),
|
||||
/// но обычная, не «последнее место сущности»: номер мешка после перезахода другой. Метка - в списке меток игрока,
|
||||
/// сохраняется игрой вместе с ним; удалить можно и вручную.</summary>
|
||||
static void AddWaypoint(Vector3 _at)
|
||||
{
|
||||
EntityPlayerLocal p = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||||
if (p?.Waypoints == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Waypoint w = new Waypoint();
|
||||
w.pos = World.worldToBlockPos(_at);
|
||||
w.icon = WaypointIcon;
|
||||
w.ownerId = Platform.PlatformManager.InternalLocalUserIdentifier;
|
||||
w.name.Update(WaypointName, Platform.PlatformManager.MultiPlatform.User.PlatformUserId);
|
||||
w.bUsingLocalizationId = true;
|
||||
p.Waypoints.Collection.Add(w);
|
||||
w.navObject = NavObjectManager.Instance.RegisterNavObject("waypoint", w.pos.ToVector3(), w.icon);
|
||||
w.navObject.IsActive = false;
|
||||
w.navObject.name = w.name.Text;
|
||||
w.navObject.usingLocalizationId = true;
|
||||
Debug.Log("[NecromancerTome] WolfBag: метка сумки на карте " + w.pos);
|
||||
}
|
||||
|
||||
/// <summary>Мешок исчез (опустел или истлел) - снять метку сумки, стоящую рядом с ним.</summary>
|
||||
public static void RemoveWaypointAt(Vector3 _at)
|
||||
{
|
||||
EntityPlayerLocal p = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||||
if (p?.Waypoints == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<Waypoint> list = p.Waypoints.Collection.list;
|
||||
for (int i = list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
Waypoint w = list[i];
|
||||
if (w.name.Text == WaypointName && (w.pos.ToVector3() - _at).sqrMagnitude < 3f * 3f)
|
||||
{
|
||||
p.Waypoints.Collection.Remove(w);
|
||||
if (w.navObject != null)
|
||||
{
|
||||
NavObjectManager.Instance.UnRegisterNavObject(w.navObject);
|
||||
}
|
||||
Debug.Log("[NecromancerTome] WolfBag: мешок сумки подобран - метка снята " + w.pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void Drop(Vector3 _at, List<ItemStack> _items)
|
||||
{
|
||||
GameManager.Instance.DropContentInLootContainerServer(-1, "DroppedLootContainer", _at + Vector3.up * 0.5f, _items.ToArray());
|
||||
@@ -244,6 +382,47 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Мешок исчез (пустой после обыска - «empty», или истлел - «old») - снять метку сумки волка рядом.</summary>
|
||||
[HarmonyPatch(typeof(EntityContainerAbs), nameof(EntityContainerAbs.removeBackpack))]
|
||||
public static class Patch_EntityContainerAbs_removeBackpack_WolfBag
|
||||
{
|
||||
public static void Postfix(EntityContainerAbs __instance)
|
||||
{
|
||||
if (__instance is EntityLootContainer)
|
||||
{
|
||||
WolfBag.RemoveWaypointAt(__instance.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>«Багажник» без рюкзака персонажа: окно мешка открывает его само (OnOpen), закрываем сразу же.</summary>
|
||||
[HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.OnOpen))]
|
||||
public static class Patch_XUiC_BagStorageWindowGroup_OnOpen_WolfBag
|
||||
{
|
||||
public static void Postfix(XUiC_BagStorageWindowGroup __instance)
|
||||
{
|
||||
if (WolfBag.WindowOpen && Possession.Active)
|
||||
{
|
||||
__instance.xui.playerUI.windowManager.Close("backpack");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Предмет, вытащенный за окно сумки волка, падает у волка, а не у тела (XUiM_PlayerInventory.DropItem
|
||||
/// берёт место у игрока - GetDropPosition).</summary>
|
||||
[HarmonyPatch(typeof(EntityPlayerLocal), nameof(EntityPlayerLocal.GetDropPosition))]
|
||||
public static class Patch_EntityPlayerLocal_GetDropPosition_WolfBag
|
||||
{
|
||||
public static void Postfix(EntityPlayerLocal __instance, ref Vector3 __result)
|
||||
{
|
||||
EntityAlive wolf = Possession.Zombie;
|
||||
if (WolfBag.WindowOpen && Possession.Active && __instance == Possession.Player && wolf != null)
|
||||
{
|
||||
__result = wolf.position + wolf.GetForwardVector() * 0.8f + Vector3.up * 0.5f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.OnEntityDeath))]
|
||||
public static class Patch_EntityAlive_OnEntityDeath_WolfBag
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user