Одержимость: приёмы Чака, ранчера и крикуньи на E, откаты в HUD
Чак: E - замах, ПКМ - бросок валуна в прицел или в существо. Ранчер: ПКМ - Рой фараона, E - невидимость. Крикунья: E - орда под девиацией. Здоровье одержимого и откаты - на месте полосок здоровья и выносливости игрока. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
cb2a4f19b4
commit
37ad57f1bc
@@ -223,8 +223,12 @@ necroHoldPifPaf,items,Item,,,,Bang-bang,,Peng-peng,Pum-pum,Pan-pan,Bang-bang,バ
|
||||
necroPossessHintBase,items,Item,,,,"LMB - strike, R - leave the body",,"LMT - Schlag, R - Körper verlassen","LMB: golpe, R: salir del cuerpo","Clic gauche : frapper, R : quitter le corps","LMB - colpo, R - lascia il corpo",左クリック: 攻撃、R: 体から出る,"좌클릭 - 공격, R - 몸에서 나가기","LPM - cios, R - opuść ciało","LMB - golpe, R - sair do corpo","ЛКМ - удар, R - выйти из тела","Sol tık - vuruş, R - bedenden çık",左键 - 攻击,R - 离开身体,左鍵 - 攻擊,R - 離開身體
|
||||
necroPossessHintRanged,items,Item,,,,RMB - ranged attack,,RMT - Fernangriff,RMB: ataque a distancia,Clic droit : attaque à distance,RMB - attacco a distanza,右クリック: 遠距離攻撃,우클릭 - 원거리 공격,PPM - atak dystansowy,RMB - ataque à distância,ПКМ - дальняя атака,Sağ tık - menzilli saldırı,右键 - 远程攻击,右鍵 - 遠程攻擊
|
||||
necroPossessHintDetonate,items,Item,,,,E - detonate,,E - Sprengen,E: detonar,E : exploser,E - detonare,E: 起爆,E - 폭발,E - detonacja,E - detonar,E - подрыв,E - patlat,E - 引爆,E - 引爆
|
||||
necroPossessHintScream,items,Item,,,,RMB - scream (calls the dead to you),,RMT - Schrei (ruft die Toten herbei),RMB: grito (llama a los muertos),Clic droit : cri (appelle les morts),RMB - urlo (richiama i morti),右クリック: 叫び(死者を呼ぶ),우클릭 - 비명 (죽은 자를 부름),PPM - krzyk (wzywa umarłych),RMB - grito (chama os mortos),ПКМ - крик (созвать мертвецов),Sağ tık - çığlık (ölüleri çağırır),右键 - 尖叫(召唤亡者),右鍵 - 尖叫(召喚亡者)
|
||||
necroPossessHintScream,items,Item,,,,E - scream (summons a horde under Deviation),,E - Schrei (ruft eine Horde unter Abweichung),E: grito (invoca una horda bajo desviación),E : cri (appelle une horde sous déviation),E - urlo (evoca un'orda sotto deviazione),E: 叫び(逸脱したホードを呼ぶ),E - 비명 (일탈한 무리를 부름),E - krzyk (wzywa hordę pod dewiacją),E - grito (invoca uma horda sob desvio),E - крик (созвать орду под девиацией),E - çığlık (sapkın bir sürü çağırır),E - 尖叫(召唤逸脱的尸潮),E - 尖叫(召喚逸脫的屍潮)
|
||||
necroPossessHintScreamE,items,Item,,,,E - scream (calls the dead to you),,E - Schrei (ruft die Toten herbei),E: grito (llama a los muertos),E : cri (appelle les morts),E - urlo (richiama i morti),E: 叫び(死者を呼ぶ),E - 비명 (죽은 자를 부름),E - krzyk (wzywa umarłych),E - grito (chama os mortos),E - крик (созвать мертвецов),E - çığlık (ölüleri çağırır),E - 尖叫(召唤亡者),E - 尖叫(召喚亡者)
|
||||
necroPossessHintThrow,items,Item,,,,"E - lift a boulder, RMB - throw it",,"E - Felsbrocken heben, RMT - werfen","E: levantar una roca, RMB: lanzarla","E : soulever un rocher, clic droit : le lancer","E - solleva un masso, RMB - lancialo",E: 岩を持ち上げる、右クリック: 投げる,"E - 바위 들기, 우클릭 - 던지기","E - podnieś głaz, PPM - rzuć","E - erguer uma pedra, RMB - arremessar","E - поднять валун, ПКМ - метнуть","E - kaya kaldır, sağ tık - fırlat",E - 举起巨石,右键 - 投掷,E - 舉起巨石,右鍵 - 投擲
|
||||
necroPossessHintSwarm,items,Item,,,,RMB - spew Pharaoh's beetles,,RMT - Käfer des Pharao ausspeien,RMB: escupir escarabajos del faraón,Clic droit : cracher les scarabées du pharaon,RMB - sputa gli scarabei del faraone,右クリック: ファラオの甲虫を吐く,우클릭 - 파라오의 딱정벌레 뱉기,PPM - wypluj żuki faraona,RMB - cuspir besouros do faraó,ПКМ - изрыгнуть жуков фараона,Sağ tık - firavun böceklerini kus,右键 - 喷出法老甲虫,右鍵 - 噴出法老甲蟲
|
||||
necroPossessHintInvis,items,Item,,,,E - invisibility (a strike breaks it),,E - Unsichtbarkeit (ein Schlag bricht sie),E: invisibilidad (un golpe la rompe),E : invisibilité (un coup la rompt),E - invisibilità (un colpo la spezza),E: 透明化(攻撃すると解ける),E - 투명화 (공격하면 풀림),E - niewidzialność (cios ją przerywa),E - invisibilidade (um golpe a quebra),E - невидимость (удар снимает),E - görünmezlik (vuruş bozar),E - 隐身(攻击会解除),E - 隱身(攻擊會解除)
|
||||
necroPossessKeyRmb,items,Item,,,,RMB,,RMT,RMB,Clic droit,RMB,右クリック,우클릭,PPM,RMB,ПКМ,Sağ tık,右键,右鍵
|
||||
necroPossessHintZoom,items,Item,,,,RMB (hold) - keen eye: zoom like a scope,,RMT (halten) - scharfes Auge: Zoom wie ein Zielfernrohr,"RMB (mantener): ojo avizor, zoom como una mira","Clic droit (maintenir) : œil perçant, zoom comme une lunette",RMB (tieni premuto) - occhio acuto: zoom come un mirino,右クリック(長押し): 鋭い目・スコープのようにズーム,우클릭(누르기) - 매의 눈: 조준경처럼 확대,PPM (przytrzymaj) - bystre oko: przybliżenie jak luneta,RMB (segurar) - olho aguçado: zoom como uma luneta,"ПКМ (удерживать) - зоркий глаз: приближение, как у прицела",Sağ tık (basılı tut) - keskin göz: dürbün gibi yakınlaştır,右键(按住)- 锐眼:像瞄准镜一样放大,右鍵(按住)- 銳眼:像瞄準鏡一樣放大
|
||||
necroPossessHintLeap,items,Item,,,,Space - long leap,,Leertaste - weiter Sprung,Espacio: salto largo,Espace : grand bond,Spazio - balzo lungo,スペース: 大ジャンプ,스페이스 - 멀리뛰기,Spacja - daleki skok,Espaço - salto longo,Пробел - дальний прыжок,Boşluk - uzun sıçrayış,空格 - 远跳,空白鍵 - 遠跳
|
||||
necroPetRecallHint,items,Item,,,,Recall pet - E,,Diener zurückrufen - E,Devolver al sirviente - E,Rappeler le serviteur - E,Richiama il servitore - E,僕を呼び戻す - E,종복 불러들이기 - E,Odwołaj sługę - E,Chamar o servo de volta - E,Отозвать питомца - E,Hizmetkârı geri çağır - E,召回仆从 - E,召回僕從 - E
|
||||
|
||||
|
@@ -202,7 +202,8 @@ namespace NecromancerTome
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!NecroCharmSide.IsCharmed(_e))
|
||||
// Одержимый зомби - тоже «свой» (13.10: орда крикуньи под девиацией появляется вокруг неё самой).
|
||||
if (!NecroCharmSide.IsCharmed(_e) && !Possession.IsPossessed(_e))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -219,7 +220,8 @@ namespace NecromancerTome
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!NecroCharmSide.IsCharmed(__instance) || !NecroCharmSide.IsCharmed(_attackTarget))
|
||||
if (!NecroCharmSide.IsCharmed(__instance) ||
|
||||
!NecroCharmSide.IsCharmed(_attackTarget) && !Possession.IsPossessed(_attackTarget))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
+365
-39
@@ -32,8 +32,7 @@ namespace NecromancerTome
|
||||
/// точка под прицелом камеры (SetLookPosition), как у ИИ - голова цели.
|
||||
/// Пробел - прыжок на собственную дальность зомби (jumpMaxDistance из JumpMaxDistance): у паука 7-9 м,
|
||||
/// как его EAILeap, у остальных их 1-4 м.
|
||||
/// ПКМ у крикуньи - её крик (SoundAlert): все зомби в ScreamRange бросаются на неё, даже не видя (её
|
||||
/// ванильный зов орды - часть разведки AIScoutHordeSpawner, напрямую не вызываем).
|
||||
/// Чак, ранчер, крикунья - свои приёмы на E и ПКМ (указание 13.10), см. PossessionPowers.
|
||||
/// E - подрыв у EntityZombieCop (коп, мутант, подрывник): HandlePrimingDetonator, через несколько
|
||||
/// секунд взрыв от имени зомби - он гибнет, игрок возвращается в тело.
|
||||
///
|
||||
@@ -206,9 +205,15 @@ namespace NecromancerTome
|
||||
static int _rItem, _rStartAnim;
|
||||
static float _rRelease, _rDuration, _rCooldown;
|
||||
static string _rSoundStart, _rSoundRelease;
|
||||
enum RState { Idle, StartAnim, Release, Fire }
|
||||
// Held - валун Чака поднят и ждёт ПКМ.
|
||||
enum RState { Idle, StartAnim, Held, Release, Fire }
|
||||
static RState _rState;
|
||||
static float _rTime, _rNext;
|
||||
/// <summary>Сколько ждать позы готовности после замаха; не дождались - валун всё равно считается поднятым,
|
||||
/// а в лог - что показывал аниматор (тест 12.10: замах не начинался, кнопки больше не отвечали).</summary>
|
||||
public const float WindupTimeout = 2.5f;
|
||||
/// <summary>Куда летит валун Чака - выбрано в момент броска (Patch_ItemActionVomit_GetActionEffectsValues_Possession).</summary>
|
||||
public static Vector3 ThrowAim;
|
||||
|
||||
static GameObject _hud;
|
||||
/// <summary>Метка одержимого на карте (указание 06.10, как у змеи) - NecroMapMarker.</summary>
|
||||
@@ -311,6 +316,7 @@ namespace NecromancerTome
|
||||
_bodyViewDim = body.viewDim;
|
||||
body.viewDim = BodyViewDim;
|
||||
}
|
||||
PossessionPowers.Begin(_zombie, _pet);
|
||||
ReadRanged(_zombie);
|
||||
Hint(_player0, _zombie);
|
||||
NecroMapMarker.Hide(ref _mapNav); // старая ссылка после закрытия мира - снимется, только если ещё наша
|
||||
@@ -366,6 +372,7 @@ namespace NecromancerTome
|
||||
}
|
||||
_rState = RState.Idle;
|
||||
_hasRanged = false;
|
||||
PossessionPowers.End(z);
|
||||
Zombie = null;
|
||||
_player = null;
|
||||
Debug.Log("[NecromancerTome] Possession: выход - " + _why);
|
||||
@@ -486,10 +493,13 @@ namespace NecromancerTome
|
||||
_eating = false;
|
||||
_eatCorpse = null;
|
||||
}
|
||||
PossessionPowers.Clear();
|
||||
PossessionHud.Forget();
|
||||
// Цвет прицела - общая таблица игры, переживает выход из мира: вернуть всегда.
|
||||
AimTint(false);
|
||||
return;
|
||||
}
|
||||
PossessionPowers.SpawnTick(world); // орда крикуньи досыпается и после выхода из неё
|
||||
if (!Active)
|
||||
{
|
||||
return;
|
||||
@@ -558,7 +568,33 @@ namespace NecromancerTome
|
||||
cop.HandlePrimingDetonator();
|
||||
NecroChallenges.Award(NecroChallenges.LivingBomb);
|
||||
}
|
||||
Ranged(z, ranged, rangedHeld);
|
||||
switch (PossessionPowers.Current)
|
||||
{
|
||||
case PossessionPowers.Kind.Thrower:
|
||||
Throw(world, z, detonate, ranged); // E - замах, ПКМ - бросок
|
||||
break;
|
||||
case PossessionPowers.Kind.Rancher:
|
||||
if (detonate)
|
||||
{
|
||||
PossessionPowers.StartInvis(z);
|
||||
}
|
||||
if (ranged && _rState == RState.Idle && Time.time >= _rNext)
|
||||
{
|
||||
PossessionPowers.BreakInvis(z, "плевок");
|
||||
}
|
||||
Ranged(z, ranged, rangedHeld); // ПКМ - Рой фараона (Patch_ItemActionSpawnEntity_Spawn_Possession)
|
||||
break;
|
||||
case PossessionPowers.Kind.Screamer:
|
||||
if (detonate)
|
||||
{
|
||||
PossessionPowers.Scream(_player, z);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Ranged(z, ranged, rangedHeld);
|
||||
break;
|
||||
}
|
||||
PossessionPowers.Tick(world, z);
|
||||
|
||||
if (Flying)
|
||||
{
|
||||
@@ -703,12 +739,9 @@ namespace NecromancerTome
|
||||
// Нюх (указание 09.10): все существа рядом с собакой - на компасе и карте.
|
||||
DogSense.Tick(world, z, Time.deltaTime);
|
||||
}
|
||||
if (ranged && IsScreamer(z))
|
||||
{
|
||||
Scream(world, z);
|
||||
}
|
||||
if (attack && Time.time >= _nextAttack)
|
||||
{
|
||||
PossessionPowers.BreakInvis(z, "удар");
|
||||
z.SetLookPosition(MeleeAim(world, z));
|
||||
if (z.Attack(false))
|
||||
{
|
||||
@@ -736,8 +769,12 @@ namespace NecromancerTome
|
||||
}
|
||||
_observer?.SetPosition(z.position);
|
||||
// Спящие в зданиях просыпаются от одержимого, как от змеи (указание 05.10).
|
||||
SnakeControl.Sleepers(world, _player, z.position, ref _sleeperCheck, Time.deltaTime);
|
||||
Taunt(world, z);
|
||||
if (!PossessionPowers.Invisible)
|
||||
{
|
||||
// Невидимого ранчера никто не замечает и спящие от него не просыпаются.
|
||||
SnakeControl.Sleepers(world, _player, z.position, ref _sleeperCheck, Time.deltaTime);
|
||||
Taunt(world, z);
|
||||
}
|
||||
CheckBody(world, z, Time.deltaTime);
|
||||
}
|
||||
|
||||
@@ -1356,6 +1393,12 @@ namespace NecromancerTome
|
||||
_rDuration = t.attackDuration > 0f ? t.attackDuration : 3f;
|
||||
// Вдвое быстрее ИИ (тест 03.10: «перезаряд медленный»).
|
||||
_rCooldown = Mathf.Max(1f, t.baseCooldown * 0.5f);
|
||||
if (PossessionPowers.Current == PossessionPowers.Kind.Rancher)
|
||||
{
|
||||
// Рой фараона - раз в 30 с (решение 13.10); откат не сбрасывается перевселением.
|
||||
_rCooldown = PossessionPowers.SwarmCooldown;
|
||||
_rNext = PossessionPowers.SwarmReadyAt;
|
||||
}
|
||||
_rSoundStart = t.soundStartName;
|
||||
_rSoundRelease = t.soundReleaseName;
|
||||
Debug.Log("[NecromancerTome] Possession: дальняя атака - оружие " + _rItem + ", замах " + _rStartAnim + ", длит. " + _rDuration + " с, откат " + _rCooldown + " с");
|
||||
@@ -1369,13 +1412,23 @@ namespace NecromancerTome
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintFly");
|
||||
}
|
||||
if (_hasRanged)
|
||||
switch (PossessionPowers.Current)
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintRanged");
|
||||
}
|
||||
if (IsScreamer(_z))
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintScream");
|
||||
case PossessionPowers.Kind.Thrower:
|
||||
text += ", " + Localization.Get("necroPossessHintThrow");
|
||||
break;
|
||||
case PossessionPowers.Kind.Rancher:
|
||||
text += ", " + Localization.Get("necroPossessHintSwarm") + ", " + Localization.Get("necroPossessHintInvis");
|
||||
break;
|
||||
case PossessionPowers.Kind.Screamer:
|
||||
text += ", " + Localization.Get("necroPossessHintScream");
|
||||
break;
|
||||
default:
|
||||
if (_hasRanged)
|
||||
{
|
||||
text += ", " + Localization.Get("necroPossessHintRanged");
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (Flying)
|
||||
{
|
||||
@@ -1538,7 +1591,9 @@ namespace NecromancerTome
|
||||
// Доводим до конца по одному нажатию: у плевка сначала 1-3 предупреждения по 1.2 с
|
||||
// (ItemActionVomit), и с отпущенной кнопкой он сбрасывался (тест 03.10: «если зажать
|
||||
// ПКМ - плюёт»). Конец - оружие закончило или вышла длительность задачи.
|
||||
if (_rTime > 0.2f && !_z.IsHoldingItemInUse(_rItem) || _rTime >= _rDuration)
|
||||
// Рой ранчера: действие «появления» само не кончается, пока держат кнопку, - вылетел, и хватит.
|
||||
if (_rTime > 0.2f && !_z.IsHoldingItemInUse(_rItem) || _rTime >= _rDuration ||
|
||||
PossessionPowers.SwarmSpawned && _rTime > 0.5f)
|
||||
{
|
||||
StopRanged(_z);
|
||||
}
|
||||
@@ -1546,6 +1601,79 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Валун Чака (указание 13.10): E - замах, поднять валун над собой (шаг StartAnim ванильной задачи);
|
||||
/// ПКМ - бросок (ContinueAnimAction, отпуск, выстрел - общие шаги Ranged) в точку под прицелом или в существо
|
||||
/// у прицела. Замах - прямо через avatarController.StartAction: EntityAlive.StartAnimAction молча ничего не
|
||||
/// делает, пока аниматор считает, что идёт другое действие (IsActionActive), - так, похоже, и было 12.10.</summary>
|
||||
static void Throw(World _world, EntityAlive _z, bool _windup, bool _throw)
|
||||
{
|
||||
if (!_hasRanged || _rStartAnim < 0)
|
||||
{
|
||||
Ranged(_z, _throw, _throw);
|
||||
return;
|
||||
}
|
||||
switch (_rState)
|
||||
{
|
||||
case RState.Idle:
|
||||
if (!_windup)
|
||||
{
|
||||
if (_throw)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Possession: броска нет - валун не поднят (сначала E)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Time.time < _rNext || !_z.IsAttackValid())
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Possession: замах не готов - откат " + Mathf.Max(0f, _rNext - Time.time).ToString("F1") + " с, атака " + (_z.IsAttackValid() ? "можно" : "нельзя"));
|
||||
return;
|
||||
}
|
||||
RearmRanged(_z);
|
||||
_z.emodel.avatarController.hitWeightMax = 0.5f;
|
||||
if (!string.IsNullOrEmpty(_rSoundStart))
|
||||
{
|
||||
Audio.Manager.BroadcastPlay(_z, _rSoundStart);
|
||||
}
|
||||
Debug.Log("[NecromancerTome] Possession: замах Чака, аниматор до - " + _z.GetAnimActionState());
|
||||
_z.emodel.avatarController.StartAction(_rStartAnim + 3000);
|
||||
_rState = RState.StartAnim;
|
||||
_rTime = 0f;
|
||||
return;
|
||||
case RState.StartAnim:
|
||||
_z.SetLookPosition(AimPoint());
|
||||
_rTime += Time.deltaTime;
|
||||
AvatarController.ActionState s = _z.GetAnimActionState();
|
||||
if (s != AvatarController.ActionState.Ready && _rTime < WindupTimeout)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Debug.Log(s == AvatarController.ActionState.Ready
|
||||
? "[NecromancerTome] Possession: валун поднят за " + _rTime.ToString("F2") + " с"
|
||||
: "[NecromancerTome] Possession: замах не дошёл до готовности за " + WindupTimeout + " с (аниматор - " + s + "), бросок всё равно разрешён");
|
||||
_rState = RState.Held;
|
||||
return;
|
||||
case RState.Held:
|
||||
_z.SetLookPosition(AimPoint());
|
||||
if (!_throw)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ThrowAim = PossessionPowers.ThrowTarget(_world, _z, AimPoint());
|
||||
_z.ContinueAnimAction(_rStartAnim + 1 + 3000);
|
||||
if (!string.IsNullOrEmpty(_rSoundRelease))
|
||||
{
|
||||
Audio.Manager.BroadcastPlay(_z, _rSoundRelease);
|
||||
}
|
||||
Debug.Log("[NecromancerTome] Possession: бросок валуна, до цели " + (ThrowAim - _z.position).magnitude.ToString("F1") + " м");
|
||||
_rState = RState.Release;
|
||||
_rTime = 0f;
|
||||
return;
|
||||
default:
|
||||
Ranged(_z, false, false); // отпуск и выстрел - как у ИИ
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Перед новой дальней атакой - оружие в исходное (тест 03.10: «плюёт, но один раз»).
|
||||
/// У ИИ это делает его собственный цикл; здесь - сброс очереди выстрелов (ResetBurstShot), отпущенный
|
||||
/// курок, отменённая перезарядка и патрон в магазине (Infinite_ammo, магазин 1).</summary>
|
||||
@@ -1589,15 +1717,57 @@ namespace NecromancerTome
|
||||
_z.emodel.avatarController.hitWeightMax = 1f;
|
||||
_rState = RState.Idle;
|
||||
_rNext = Time.time + _rCooldown;
|
||||
if (PossessionPowers.Current == PossessionPowers.Kind.Rancher)
|
||||
{
|
||||
PossessionPowers.SwarmReadyAt = _rNext;
|
||||
}
|
||||
PossessionPowers.SwarmSpawned = false;
|
||||
}
|
||||
|
||||
static bool IsScreamer(EntityAlive _z)
|
||||
/// <summary>Откат приёма для PossessionHud: Rmb - кнопка ПКМ (иначе E), Left/Total - секунды; Active - приём идёт
|
||||
/// (невидимость), Left - сколько ему осталось.</summary>
|
||||
public struct Cooldown
|
||||
{
|
||||
return EntityClass.list.TryGetValue(_z.entityClass, out EntityClass c) && c.entityClassName.StartsWith("zombieScreamer");
|
||||
public bool Rmb;
|
||||
public float Left, Total;
|
||||
public bool Active;
|
||||
}
|
||||
|
||||
/// <summary>Крик одержимой крикуньи (и клич одержимого стервятника): его звук тревоги и все зомби в
|
||||
/// ScreamRange - на него (танкование, приманка).</summary>
|
||||
/// <summary>Откаты одержимого (до двух) - сколько записано в _out.</summary>
|
||||
public static int Cooldowns(Cooldown[] _out)
|
||||
{
|
||||
if (!Active || Zombie == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
float now = Time.time;
|
||||
int n = 0;
|
||||
switch (PossessionPowers.Current)
|
||||
{
|
||||
case PossessionPowers.Kind.Thrower:
|
||||
_out[n++] = new Cooldown { Rmb = false, Left = Mathf.Max(0f, _rNext - now), Total = _rCooldown };
|
||||
break;
|
||||
case PossessionPowers.Kind.Rancher:
|
||||
_out[n++] = PossessionPowers.Invisible
|
||||
? new Cooldown { Rmb = false, Left = PossessionPowers.InvisLeft, Total = PossessionPowers.InvisTime, Active = true }
|
||||
: new Cooldown { Rmb = false, Left = PossessionPowers.InvisCooldownLeft, Total = PossessionPowers.InvisCooldown };
|
||||
_out[n++] = new Cooldown { Rmb = true, Left = Mathf.Max(0f, _rNext - now), Total = _rCooldown };
|
||||
break;
|
||||
case PossessionPowers.Kind.Screamer:
|
||||
_out[n++] = new Cooldown { Rmb = false, Left = PossessionPowers.HordeCooldownLeft, Total = PossessionPowers.HordeCooldown };
|
||||
break;
|
||||
default:
|
||||
if (_hasRanged)
|
||||
{
|
||||
_out[n++] = new Cooldown { Rmb = true, Left = Mathf.Max(0f, _rNext - now), Total = _rCooldown };
|
||||
}
|
||||
break;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/// <summary>Клич одержимого стервятника (у крикуньи с 13.10 - орда, PossessionPowers.Scream): звук и все
|
||||
/// зомби в ScreamRange - на него (приманка).</summary>
|
||||
static void Scream(World _world, EntityAlive _z)
|
||||
{
|
||||
if (Time.time < _nextScream)
|
||||
@@ -1913,19 +2083,96 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Полоска здоровья одержимого (тест 03.10: «а индикатор здоровья у одержимого где?») -
|
||||
/// внизу по центру, над поясом, с именем зомби. Своё здоровье игрок видит как обычно (тело беззащитно).
|
||||
/// OnGUI, как прицел змеи (SnakeCrosshair), - без своего окна XUi.</summary>
|
||||
/// <summary>Полоски одержимого (указание 13.10): здоровье - поверх полоски здоровья игрока, откаты приёмов -
|
||||
/// поверх полоски выносливости (HUDLeftStatBars игры). Место берётся у самих полосок игры, на экране
|
||||
/// (их UiTransform через камеру NGUI): так оно совпадает при любом масштабе интерфейса. Если полоски игры
|
||||
/// сейчас не видно - последнее известное место, а до него - внизу по центру, как было с 03.10.
|
||||
/// OnGUI, как прицел змеи (SnakeCrosshair), - без своего окна XUi.
|
||||
///
|
||||
/// ПАМЯТЬ. Ссылки на две полоски игры (их контроллеры) держим только до выхода из мира (Forget из
|
||||
/// Possession) или до пересборки окна (UiTransform == null). Строки подписей собираются, только когда
|
||||
/// меняется число, - не каждый кадр.</summary>
|
||||
public class PossessionHud : MonoBehaviour
|
||||
{
|
||||
const float Width = 420f, Height = 16f, Bottom = 150f;
|
||||
static readonly Color Back = new Color(0f, 0f, 0f, 0.6f);
|
||||
static readonly Color Back = new Color(0f, 0f, 0f, 0.85f);
|
||||
static readonly Color Fill = new Color(0.55f, 0.08f, 0.06f, 0.95f);
|
||||
static readonly Color CdReady = new Color(0.75f, 0.62f, 0.95f, 0.95f);
|
||||
static readonly Color CdCharging = new Color(0.35f, 0.3f, 0.45f, 0.95f);
|
||||
static readonly Color CdActive = new Color(0.55f, 0.2f, 0.85f, 0.95f);
|
||||
static GUIStyle _style;
|
||||
|
||||
static readonly Color Danger = new Color(1f, 0.15f, 0.1f, 1f);
|
||||
static GUIStyle _dangerStyle;
|
||||
|
||||
static XUiC_HUDStatBar _healthBar, _staminaBar;
|
||||
static Rect _healthRect, _staminaRect;
|
||||
static bool _healthKnown, _staminaKnown;
|
||||
|
||||
static readonly Possession.Cooldown[] _cd = new Possession.Cooldown[2];
|
||||
static readonly string[] _cdText = new string[2];
|
||||
static readonly int[] _cdSec = { -1, -1 };
|
||||
static readonly bool[] _cdRmb = new bool[2];
|
||||
static string _hpText;
|
||||
static int _hpId = -1, _hpCur = -1, _hpMax = -1;
|
||||
|
||||
/// <summary>Полоска игры обновилась - запомнить её (Patch_XUiC_HUDStatBar_Update_PossessionHud).</summary>
|
||||
public static void Remember(XUiC_HUDStatBar _bar)
|
||||
{
|
||||
if (_bar.statType == HUDStatTypes.Health)
|
||||
{
|
||||
_healthBar = _bar;
|
||||
}
|
||||
else if (_bar.statType == HUDStatTypes.Stamina)
|
||||
{
|
||||
_staminaBar = _bar;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Forget()
|
||||
{
|
||||
_healthBar = _staminaBar = null;
|
||||
_healthKnown = _staminaKnown = false;
|
||||
_hpText = null;
|
||||
_hpId = -1;
|
||||
for (int i = 0; i < _cdText.Length; i++)
|
||||
{
|
||||
_cdText[i] = null;
|
||||
_cdSec[i] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Где на экране полоска игры (координаты GUI: y сверху).</summary>
|
||||
static bool ScreenRect(ref XUiC_HUDStatBar _bar, ref Rect _rect, ref bool _known)
|
||||
{
|
||||
Transform t = _bar?.ViewComponent?.UiTransform;
|
||||
if (t == null)
|
||||
{
|
||||
_bar = null; // окно пересобрано или мир закрыт
|
||||
return _known;
|
||||
}
|
||||
if (!t.gameObject.activeInHierarchy)
|
||||
{
|
||||
return _known;
|
||||
}
|
||||
Camera cam = NGUITools.FindCameraForLayer(t.gameObject.layer);
|
||||
if (cam == null)
|
||||
{
|
||||
return _known;
|
||||
}
|
||||
Vector2i size = _bar.ViewComponent.Size;
|
||||
Vector3 a = cam.WorldToScreenPoint(t.position);
|
||||
Vector3 b = cam.WorldToScreenPoint(t.TransformPoint(new Vector3(size.x, -size.y, 0f)));
|
||||
Rect r = Rect.MinMaxRect(Mathf.Min(a.x, b.x), Screen.height - Mathf.Max(a.y, b.y), Mathf.Max(a.x, b.x), Screen.height - Mathf.Min(a.y, b.y));
|
||||
if (r.width < 8f || r.height < 4f)
|
||||
{
|
||||
return _known;
|
||||
}
|
||||
_rect = r;
|
||||
_known = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
EntityAlive z = Possession.Active ? Possession.Zombie : null;
|
||||
@@ -1938,23 +2185,89 @@ namespace NecromancerTome
|
||||
DrawBodyDanger();
|
||||
}
|
||||
float scale = Screen.height / 1080f;
|
||||
float w = Width * scale, h = Height * scale;
|
||||
Rect r = new Rect((Screen.width - w) / 2f, Screen.height - Bottom * scale - h, w, h);
|
||||
float frac = Mathf.Clamp01(z.Health / (float)Mathf.Max(1, z.GetMaxHealth()));
|
||||
Color prev = GUI.color;
|
||||
GUI.color = Back;
|
||||
GUI.DrawTexture(r, Texture2D.whiteTexture);
|
||||
GUI.color = Fill;
|
||||
GUI.DrawTexture(new Rect(r.x + 2f, r.y + 2f, (r.width - 4f) * frac, r.height - 4f), Texture2D.whiteTexture);
|
||||
GUI.color = prev;
|
||||
if (_style == null)
|
||||
{
|
||||
_style = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.LowerCenter };
|
||||
_style.normal.textColor = new Color(0.9f, 0.85f, 0.8f);
|
||||
_style = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleLeft, clipping = TextClipping.Clip };
|
||||
_style.normal.textColor = new Color(0.95f, 0.9f, 0.85f);
|
||||
}
|
||||
_style.fontSize = Mathf.RoundToInt(18f * scale);
|
||||
string name = Localization.Get(z.EntityName);
|
||||
GUI.Label(new Rect(r.x, r.y - 26f * scale, r.width, 24f * scale), name + " " + z.Health + " / " + z.GetMaxHealth(), _style);
|
||||
Rect hr;
|
||||
if (ScreenRect(ref _healthBar, ref _healthRect, ref _healthKnown))
|
||||
{
|
||||
hr = _healthRect;
|
||||
}
|
||||
else
|
||||
{
|
||||
float w = Width * scale, h = Height * scale * 1.6f;
|
||||
hr = new Rect((Screen.width - w) / 2f, Screen.height - Bottom * scale - h, w, h);
|
||||
}
|
||||
int hp = z.Health, max = z.GetMaxHealth();
|
||||
if (_hpText == null || _hpId != z.entityId || _hpCur != hp || _hpMax != max)
|
||||
{
|
||||
_hpId = z.entityId;
|
||||
_hpCur = hp;
|
||||
_hpMax = max;
|
||||
_hpText = Localization.Get(z.EntityName) + " " + hp + "/" + max;
|
||||
}
|
||||
DrawBar(hr, Mathf.Clamp01(hp / (float)Mathf.Max(1, max)), Fill, _hpText);
|
||||
|
||||
int n = Possession.Cooldowns(_cd);
|
||||
if (n == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Rect sr;
|
||||
if (ScreenRect(ref _staminaBar, ref _staminaRect, ref _staminaKnown))
|
||||
{
|
||||
sr = _staminaRect;
|
||||
}
|
||||
else
|
||||
{
|
||||
sr = new Rect(hr.x, hr.y - hr.height - 4f * scale, hr.width, hr.height);
|
||||
}
|
||||
float gap = 4f * scale;
|
||||
float cw = (sr.width - gap * (n - 1)) / n;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
Possession.Cooldown c = _cd[i];
|
||||
int sec = c.Left > 0f ? Mathf.CeilToInt(c.Left) : 0;
|
||||
if (_cdText[i] == null || _cdSec[i] != sec || _cdRmb[i] != c.Rmb)
|
||||
{
|
||||
_cdSec[i] = sec;
|
||||
_cdRmb[i] = c.Rmb;
|
||||
string key = c.Rmb ? Localization.Get("necroPossessKeyRmb") : "E";
|
||||
_cdText[i] = sec > 0 ? key + " " + sec : key;
|
||||
}
|
||||
float frac;
|
||||
Color col;
|
||||
if (c.Active)
|
||||
{
|
||||
frac = c.Total > 0f ? Mathf.Clamp01(c.Left / c.Total) : 0f;
|
||||
col = CdActive;
|
||||
}
|
||||
else if (c.Left > 0f)
|
||||
{
|
||||
frac = c.Total > 0f ? 1f - Mathf.Clamp01(c.Left / c.Total) : 0f;
|
||||
col = CdCharging;
|
||||
}
|
||||
else
|
||||
{
|
||||
frac = 1f;
|
||||
col = CdReady;
|
||||
}
|
||||
DrawBar(new Rect(sr.x + i * (cw + gap), sr.y, cw, sr.height), frac, col, _cdText[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static void DrawBar(Rect _r, float _frac, Color _fill, string _text)
|
||||
{
|
||||
Color prev = GUI.color;
|
||||
GUI.color = Back;
|
||||
GUI.DrawTexture(_r, Texture2D.whiteTexture);
|
||||
GUI.color = _fill;
|
||||
GUI.DrawTexture(new Rect(_r.x + 2f, _r.y + 2f, (_r.width - 4f) * _frac, _r.height - 4f), Texture2D.whiteTexture);
|
||||
GUI.color = prev;
|
||||
_style.fontSize = Mathf.Max(8, Mathf.RoundToInt(_r.height * 0.62f));
|
||||
GUI.Label(new Rect(_r.x + 6f, _r.y, _r.width - 8f, _r.height), _text, _style);
|
||||
}
|
||||
|
||||
/// <summary>Надпись «враг у тела» - та же, что у змеи (SnakeCrosshair.DrawBodyDanger): под прицелом,
|
||||
@@ -1978,6 +2291,19 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Запомнить полоски здоровья и выносливости игры - на их место PossessionHud кладёт свои.</summary>
|
||||
[HarmonyPatch(typeof(XUiC_HUDStatBar), nameof(XUiC_HUDStatBar.Update))]
|
||||
public static class Patch_XUiC_HUDStatBar_Update_PossessionHud
|
||||
{
|
||||
public static void Postfix(XUiC_HUDStatBar __instance)
|
||||
{
|
||||
if (__instance.statType == HUDStatTypes.Health || __instance.statType == HUDStatTypes.Stamina)
|
||||
{
|
||||
PossessionHud.Remember(__instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Приседание одержимого: пока зажата кнопка, рост зомби опускается до CrouchHeight, как игра
|
||||
/// опускает его под низким потолком (EntityAlive.CrouchHeightFixedUpdate, её же шаги). Отпустил - дальше
|
||||
/// считает игра: встаёт, только если сверху свободно. Сама игра пригибает лишь под потолком над головой,
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
using System.Collections.Generic;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Сверхспособности одержимых особых зомби (указание 13.10: «проверь ванильные способности и переставь их на E»).
|
||||
/// Чак (и Морозный коготь): E - замах, поднять валун над собой; ПКМ - метнуть его в точку под прицелом или
|
||||
/// в существо у прицела (тест 12.10: «так и не бросил камень» - замах ждал позы готовности, а она не
|
||||
/// наступала, и все следующие нажатия молча тонули).
|
||||
/// Ранчер (и Чумной плевун): ПКМ - выплюнуть Рой фараона (necroInsectSwarm, наш рой из свитка, обращает
|
||||
/// ужаленных зомби) вместо ванильных мух, откат SwarmCooldown; E - невидимость на InvisTime: зомби его
|
||||
/// не видят и бросают погоню, модель полупрозрачна; удар или плевок её снимают.
|
||||
/// Крикунья: E - крик зовёт орду разведчиков по стадии игры (как в игре, ScoutGSList), и вся орда сразу под
|
||||
/// девиацией. Чужих зомби крик больше не стягивает (решение 13.10).
|
||||
/// Откаты видны в PossessionHud (там, где полоска выносливости игрока).
|
||||
///
|
||||
/// ПАМЯТЬ. Только числа и очередь id классов орды; ссылок на сущности нет. Очередь чистится при выходе из мира.
|
||||
/// </summary>
|
||||
public static class PossessionPowers
|
||||
{
|
||||
public enum Kind { None, Thrower, Rancher, Screamer }
|
||||
public static Kind Current;
|
||||
|
||||
public const float InvisTime = 15f, InvisCooldown = 30f;
|
||||
/// <summary>Прозрачность невидимого ранчера - родной _Fade шейдера персонажей (EntityAlive.renderFadeMax).</summary>
|
||||
public const float InvisFade = 0.25f;
|
||||
public const float InvisClearPeriod = 0.25f;
|
||||
public const float SwarmCooldown = 30f;
|
||||
public const string SwarmEntity = "necroInsectSwarm";
|
||||
public const float HordeCooldown = 60f;
|
||||
public const string HordeSpawner = "ScoutGSList";
|
||||
/// <summary>Не больше стольких за крик (у игры на стадии 52 - 26).</summary>
|
||||
public const int HordeMax = 40;
|
||||
/// <summary>Орда появляется по одному зомби раз в столько секунд - без рывка кадра.</summary>
|
||||
public const float HordeSpawnStep = 0.2f;
|
||||
public const int HordeMinRange = 4, HordeMaxRange = 14;
|
||||
|
||||
static float _invisUntil, _invisNext, _invisClear;
|
||||
static float _hordeNext, _hordeStep;
|
||||
static readonly List<int> _hordeQueue = new List<int>();
|
||||
static Vector3 _hordeAt;
|
||||
static int _hordeFails;
|
||||
/// <summary>Рой фараона уже вылетел в этом плевке - дальняя атака ранчера закончена.</summary>
|
||||
public static bool SwarmSpawned;
|
||||
/// <summary>Когда ранчер снова сможет плюнуть роем (Time.time); переживает перевселение.</summary>
|
||||
public static float SwarmReadyAt;
|
||||
|
||||
public static bool Invisible => Current == Kind.Rancher && Time.time < _invisUntil;
|
||||
|
||||
public static Kind KindOf(EntityAlive _z)
|
||||
{
|
||||
if (_z == null || !EntityClass.list.TryGetValue(_z.entityClass, out EntityClass c))
|
||||
{
|
||||
return Kind.None;
|
||||
}
|
||||
string n = c.entityClassName;
|
||||
if (n.StartsWith("zombieChuck") || n.StartsWith("zombieFrostclaw"))
|
||||
{
|
||||
return Kind.Thrower;
|
||||
}
|
||||
if (n.StartsWith("zombieRancher") || n.StartsWith("zombiePlagueSpitter"))
|
||||
{
|
||||
return Kind.Rancher;
|
||||
}
|
||||
if (n.StartsWith("zombieScreamer"))
|
||||
{
|
||||
return Kind.Screamer;
|
||||
}
|
||||
return Kind.None;
|
||||
}
|
||||
|
||||
public static void Begin(EntityAlive _z, bool _pet)
|
||||
{
|
||||
Current = _pet ? Kind.None : KindOf(_z);
|
||||
_invisUntil = 0f;
|
||||
SwarmSpawned = false;
|
||||
}
|
||||
|
||||
public static void End(EntityAlive _z)
|
||||
{
|
||||
if (Invisible)
|
||||
{
|
||||
SetFade(_z, 1f);
|
||||
_invisNext = Time.time + InvisCooldown;
|
||||
}
|
||||
_invisUntil = 0f;
|
||||
Current = Kind.None;
|
||||
}
|
||||
|
||||
/// <summary>Выход из мира: всё в ноль (откаты тоже - Time.time в новом мире продолжается, но держать их незачем).</summary>
|
||||
public static void Clear()
|
||||
{
|
||||
Current = Kind.None;
|
||||
_invisUntil = _invisNext = _hordeNext = SwarmReadyAt = 0f;
|
||||
_hordeQueue.Clear();
|
||||
SwarmSpawned = false;
|
||||
}
|
||||
|
||||
// ---------- Невидимость ранчера ----------
|
||||
|
||||
public static void StartInvis(EntityAlive _z)
|
||||
{
|
||||
if (Invisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Time.time < _invisNext)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Possession: невидимость не готова - откат " + (_invisNext - Time.time).ToString("F1") + " с");
|
||||
return;
|
||||
}
|
||||
_invisUntil = Time.time + InvisTime;
|
||||
_invisNext = _invisUntil + InvisCooldown;
|
||||
_invisClear = 0f;
|
||||
SetFade(_z, InvisFade);
|
||||
Audio.Manager.BroadcastPlay(_z, "plaguesense");
|
||||
Debug.Log("[NecromancerTome] Possession: невидимость ранчера на " + InvisTime + " с");
|
||||
}
|
||||
|
||||
/// <summary>Удар или плевок - невидимость снята, откат от этой минуты.</summary>
|
||||
public static void BreakInvis(EntityAlive _z, string _why)
|
||||
{
|
||||
if (!Invisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_invisUntil = 0f;
|
||||
_invisNext = Time.time + InvisCooldown;
|
||||
SetFade(_z, 1f);
|
||||
Debug.Log("[NecromancerTome] Possession: невидимость снята - " + _why);
|
||||
}
|
||||
|
||||
static void SetFade(EntityAlive _z, float _fade)
|
||||
{
|
||||
if (_z == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_z.renderFadeMax = _fade;
|
||||
_z.renderFadeTarget = _fade;
|
||||
}
|
||||
|
||||
/// <summary>Каждый кадр одержимости: пока невидим - кто гнался за ранчером, теряет его.</summary>
|
||||
public static void Tick(World _world, EntityAlive _z)
|
||||
{
|
||||
if (Current != Kind.Rancher || _invisUntil <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Time.time >= _invisUntil)
|
||||
{
|
||||
_invisUntil = 0f;
|
||||
SetFade(_z, 1f);
|
||||
Debug.Log("[NecromancerTome] Possession: невидимость кончилась");
|
||||
return;
|
||||
}
|
||||
_z.renderFadeMax = InvisFade; // игра поднимает его только на смерти, но держим явно
|
||||
if (Time.time < _invisClear)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_invisClear = Time.time + InvisClearPeriod;
|
||||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||||
{
|
||||
EntityAlive e = _world.EntityAlives[i];
|
||||
if (e == null || e == _z || e.IsDead())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (e.GetAttackTarget() == _z)
|
||||
{
|
||||
e.SetAttackTarget(null, 0);
|
||||
}
|
||||
if (e.GetRevengeTarget() == _z)
|
||||
{
|
||||
e.SetRevengeTarget(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static float InvisLeft => Invisible ? _invisUntil - Time.time : 0f;
|
||||
public static float InvisCooldownLeft => Invisible ? 0f : Mathf.Max(0f, _invisNext - Time.time);
|
||||
|
||||
// ---------- Орда крикуньи ----------
|
||||
|
||||
public static float HordeCooldownLeft => Mathf.Max(0f, _hordeNext - Time.time);
|
||||
|
||||
public static void Scream(EntityPlayerLocal _player, EntityAlive _z)
|
||||
{
|
||||
if (Time.time < _hordeNext)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Possession: крик не готов - откат " + (_hordeNext - Time.time).ToString("F1") + " с");
|
||||
return;
|
||||
}
|
||||
_hordeNext = Time.time + HordeCooldown;
|
||||
string snd = _z.GetSoundAlert();
|
||||
if (!string.IsNullOrEmpty(snd))
|
||||
{
|
||||
Audio.Manager.BroadcastPlay(_z, snd);
|
||||
}
|
||||
if (!GameStageDefinition.TryGetGameStage(HordeSpawner, out GameStageDefinition def) || def.stages.Count == 0)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Possession: орды нет - нет спавнера " + HordeSpawner);
|
||||
return;
|
||||
}
|
||||
int gs = GameStageDefinition.CalcGameStageAround(_player);
|
||||
GameStageDefinition.Stage stage = def.GetStage(gs) ?? def.stages[0];
|
||||
int last = -1;
|
||||
int added = 0;
|
||||
for (int g = 0; g < stage.Count; g++)
|
||||
{
|
||||
GameStageDefinition.SpawnGroup grp = stage.GetSpawnGroup(g);
|
||||
// У игры на первых стадиях num не задан (1) - даём хотя бы двоих.
|
||||
int n = Mathf.Max(2, (int)grp.spawnCount);
|
||||
for (int k = 0; k < n && _hordeQueue.Count < HordeMax; k++)
|
||||
{
|
||||
int id = EntityGroups.GetRandomFromGroup(grp.groupName, ref last);
|
||||
if (id != -1 && id != 0)
|
||||
{
|
||||
_hordeQueue.Add(id);
|
||||
added++;
|
||||
}
|
||||
}
|
||||
}
|
||||
_hordeAt = _z.position;
|
||||
_hordeStep = 0f;
|
||||
_hordeFails = 0;
|
||||
Debug.Log("[NecromancerTome] Possession: крик крикуньи - орда " + added + " (стадия " + gs + ", ступень " + stage.stageNum + ")");
|
||||
}
|
||||
|
||||
/// <summary>Каждый кадр (и после выхода из одержимости): орда появляется по одному.</summary>
|
||||
public static void SpawnTick(World _world)
|
||||
{
|
||||
if (_hordeQueue.Count == 0 || Time.time < _hordeStep)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_hordeStep = Time.time + HordeSpawnStep;
|
||||
if (!_world.GetMobRandomSpawnPosWithWater(_hordeAt, HordeMinRange, HordeMaxRange, 0, false, out Vector3 pos))
|
||||
{
|
||||
if (++_hordeFails > 20)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Possession: орде негде появиться, осталось " + _hordeQueue.Count + " - отбой");
|
||||
_hordeQueue.Clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
int id = _hordeQueue[_hordeQueue.Count - 1];
|
||||
_hordeQueue.RemoveAt(_hordeQueue.Count - 1);
|
||||
Entity e = EntityFactory.CreateEntity(id, pos, new Vector3(0f, Random.Range(0f, 360f), 0f));
|
||||
e.SetSpawnerSource(EnumSpawnerSource.Dynamic);
|
||||
_world.SpawnEntityInWorld(e);
|
||||
if (e is EntityAlive a && a.Buffs != null)
|
||||
{
|
||||
a.Buffs.AddBuff(Patch_EntityBuffs_AddBuff_DeviatorCharm.CharmBuffName);
|
||||
}
|
||||
if (_hordeQueue.Count == 0)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] Possession: орда крикуньи вся на месте");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Бросок Чака ----------
|
||||
|
||||
public const float ThrowAimRange = 100f;
|
||||
/// <summary>Насколько луч прицела может пройти мимо груди существа, чтобы метнуть в него.</summary>
|
||||
public const float ThrowAimRadius = 1.5f;
|
||||
|
||||
/// <summary>Куда лететь валуну: грудь существа у луча прицела (ближайшее по лучу), иначе точка под прицелом.</summary>
|
||||
public static Vector3 ThrowTarget(World _world, EntityAlive _z, Vector3 _aimPoint)
|
||||
{
|
||||
Transform cam = Possession.Player?.cameraTransform;
|
||||
if (cam == null)
|
||||
{
|
||||
return _aimPoint;
|
||||
}
|
||||
Vector3 from = cam.position + Origin.position;
|
||||
Vector3 dir = cam.forward;
|
||||
EntityAlive best = null;
|
||||
float bestAlong = ThrowAimRange;
|
||||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||||
{
|
||||
EntityAlive e = _world.EntityAlives[i];
|
||||
if (e == null || e == _z || e.IsDead() || e == Possession.Player)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Vector3 to = e.getChestPosition() - from;
|
||||
float along = Vector3.Dot(to, dir);
|
||||
if (along < Possession.CamBack + 0.5f || along > bestAlong)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if ((to - dir * along).sqrMagnitude > ThrowAimRadius * ThrowAimRadius)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
bestAlong = along;
|
||||
best = e;
|
||||
}
|
||||
return best != null ? best.getChestPosition() : _aimPoint;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Валун одержимого Чака - по дуге в точку под прицелом (или в существо): позиция вместо направления
|
||||
/// и 1 - «баллистика», как у ИИ, когда цель есть. Без цели игра кидает прямо по взгляду - камень падал рядом.</summary>
|
||||
[HarmonyPatch(typeof(ItemActionVomit), "GetActionEffectsValues")]
|
||||
public static class Patch_ItemActionVomit_GetActionEffectsValues_Possession
|
||||
{
|
||||
public static void Postfix(ItemActionData _actionData, ref Vector3 _direction, ref int __result)
|
||||
{
|
||||
EntityAlive holder = _actionData?.invData?.holdingEntity;
|
||||
if (!Possession.IsPossessed(holder) || PossessionPowers.Current != PossessionPowers.Kind.Thrower)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_direction = Possession.ThrowAim;
|
||||
__result = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Плевок одержимого ранчера - Рой фараона вместо ванильных мух (указание 13.10).</summary>
|
||||
[HarmonyPatch(typeof(ItemActionSpawnEntity), "Spawn")]
|
||||
public static class Patch_ItemActionSpawnEntity_Spawn_Possession
|
||||
{
|
||||
public static void Prefix(ItemActionSpawnEntity __instance, ItemActionData _actionData, ref string __state)
|
||||
{
|
||||
EntityAlive holder = _actionData?.invData?.holdingEntity;
|
||||
if (!Possession.IsPossessed(holder) || PossessionPowers.Current != PossessionPowers.Kind.Rancher)
|
||||
{
|
||||
return;
|
||||
}
|
||||
__state = __instance.entityToSpawn;
|
||||
__instance.entityToSpawn = PossessionPowers.SwarmEntity;
|
||||
PossessionPowers.SwarmSpawned = true;
|
||||
Debug.Log("[NecromancerTome] Possession: ранчер выплюнул Рой фараона");
|
||||
}
|
||||
|
||||
public static void Finalizer(ItemActionSpawnEntity __instance, string __state)
|
||||
{
|
||||
if (__state != null)
|
||||
{
|
||||
__instance.entityToSpawn = __state;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,7 +184,8 @@ namespace NecromancerTome
|
||||
float bestDistSq = float.MaxValue;
|
||||
foreach (Entity entity in nearby)
|
||||
{
|
||||
if (!(entity is EntityAlive zombie) || zombie.IsDead())
|
||||
// Одержимого (13.10: ранчер плюёт этим роем) не жалить - он свой.
|
||||
if (!(entity is EntityAlive zombie) || zombie.IsDead() || Possession.IsPossessed(zombie))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user