Slavemaster
Модостроитель
- Регистрация
- 10 Июн 2019
- Сообщения
- 1.086
- Благодарности
- 1.904
- Баллы
- 320
В теме будет описан конвейер обработки урона по NPC
Краткое описание вовлечённых функций
Краткое описание вовлечённых функций
int oCNpc::EV_DamageOnce
- не содержит дополнительной логики: вызываетoCNpc::OnDamage
и возвращаетtrue
int oCNpc::EV_DamagePerFrame
- управляет таймерами периодического урона, вызываетoCNpc::OnDamage
при истечении очередного интервала времениvoid oCNpc::OnDamage(oCMsgDamage*)
- принимает решение о вызове основных функций обработки урона перечисленных нижеvoid oCNpc::OnDamage_Hit
- вычисляет урон и отнимает его из здоровья жертвыvoid oCNpc::OnDamage_Condition
- определяет, нужно ли выполнить переход жертвы в бессознательное или терминальное состояниеvoid oCNpc::OnDamage_Anim
- управляет анимациями жертвыvoid oCNpc::OnDamage_Effects_Start
- запускает эффекты кровотечения, горенияvoid oCNpc::OnDamage_Script
- активирует восприятияNPC_PERC_ASSESSDAMAGE
иNPC_PERC_ASSESSOTHERSDAMAGE
void oCNpc::OnDamage_State
- устанавливает жертве флагBS_MOD_BURNING
, если она начала гореть (огненным уроном) в результате атакиvoid oCNpc::OnDamage_Events
- осуществляет переход жертвы в бессознательное или терминальное состояниеvoid oCNpc::OnDamage_Sound
- проигрывает вопли жертвыvoid oCNpc::OnDamage_Effects_End
- удаляет ранее созданные эффекты, удаляет флагBS_MOD_BURNING
Общее описание процесса
Решение о нанесении мгновенного урона принимается в различных точках движка. При этом формируется дескриптор урона oCNpc::oSDamageDescriptor
и отправляется на обработку посредством вызова oCNpc::OnMessage
или oCNpc::OnDamage
. eventManager
не используется, поэтому сообщение обрабатывается незамедлительно (одно исключение: урон от коллизии с заклинанием, когда низкоприоритетное сообщение об уроне отправляется через eventManager
). Периодический же урон формируется в методе oCNpc::OnDamage_Effects_Start
и отправляется через eventManager
. Также следует отметить, что отменить сам удар в описанных функциях нельзя (например, звук коллизии, если таковой предполагается, будет всё равно проигран).int oCNpc::EV_DamagePerFrame
eventManager
есть сообщение oCMsgDamage
с подтипом EV_DAMAGE_PER_FRAME
.Непосредственно используются следующие поля дескриптора урона:
fTimeDuration
(чтение/запись) - время до полного истечения периодического урона (все таймеры в миллисекундах)fTimeCurrent
(чтение/запись) - время до следующей порции урона, инициализируется внутри функцииfTimeInterval
(чтение) - периодичность нанесения уронаfDamagePerInterval
(чтение) - величина повторяющегося уронаfDamageTotal
(запись) - присваивается при первом вызовеfDamageTotal = fDamagePerInterval
dwFieldsValid
(чтение/запись) - добавляется флагoEDamageDescFlag_OverlayActivate
OnDamage
, который помимо всего прочего может установить флаг дескриптора bFinished
, означающий, что периодический урон был полностью обработан и сообщение подлежит удалению.Прилагаю переработанный код функции (удалены сообщения zSpy, время кадра на границе интервалов не теряется, разрешён множественный вызов
OnDamage
). Все примеры кода созданы с использованием шаблона проекта C++17
, для переноса на стандартный шаблон может потребоваться незначительная корректировка.
C++:
int oCNpc::EV_DamagePerFrame_Union(oCMsgDamage* message)
{
oSDamageDescriptor& desc = message->descDamage;
// инициализируем при первом вызове
if (!message->IsInUse())
{
message->SetInUse(true);
desc.dwFieldsValid |= oEDamageDescFlag_OverlayActivate;
desc.fTimeCurrent = desc.fTimeInterval;
desc.fDamageTotal = desc.fDamagePerInterval;
}
float timeDebt = ztimer->frameTimeFloat;
// пока не обработали всё время кадра и OnDamage не сигнализировал об окончании...
while (timeDebt > 0.0f && !desc.bFinished)
{
// обрабатываем всё время, но не более текущего интервала
const float dt = std::min(timeDebt, desc.fTimeCurrent);
// обновляем таймеры
timeDebt -= dt;
desc.fTimeCurrent -= dt;
desc.fTimeDuration -= dt;
// если интервал истёк, то вызываем OnDamage
if (desc.fTimeCurrent <= 0.0f)
{
OnDamage(desc);
desc.fTimeCurrent += desc.fTimeInterval;
}
}
// возвращаем true, если сообщение полностью обработано и его можно удалить
return desc.bFinished;
}
void oCNpc::OnDamage(oCMsgDamage*)
Непосредственно используются следующие поля дескриптора урона:
bOnce
(запись) - флаг, отличающий однократный урон от периодическогоdwFieldsValid
(чтение) - читается флагoEDamageDescFlag_OverlayActivate
для установки поляbOnce
bFinished
(запись) - флаг обычно устанавливается для периодического урона с целью его прекращенияfTimeDuration
(чтение) - время до истечения действия периодического уронаpVisualFX
(чтение) - визуал периодического урона
bOnce
устанавливается флаг bFinished
. Он получает значение true только в случае, когда bOnce == false
и истекло время действия периодического урона. При заданном pVisualFX
время считается истёкшим не только когда fTimeDuration <= 0
, но и когда проигрывание визуала завершилось. Вся следующая логика (за одним исключением) выполняется только если жертва жива и в сознании (также производятся некоторые другие неактуальные проверки). Если атакующий - игрок, а жертва имеет флаг NPC_FLAG_PROTECTED
, то жертве устанавливается флаг неуязвимости (NPC_FLAG_IMMORTAL
) и вызывается скрипт-функция Player_Victim_Is_Immortal
. Похоже, что это рудимент системы защиты от несанкционированного копирования. Далее, если атака идёт с участием игрока, у жертвы проверяется наличие сообщений oCMsgConversation::EV_(STOP)PROCESSINFOS
, чтобы не обрабатывать урон пока игрок находится в диалоге. Если таковые имеются, то флаг bFinished
устанавливается в true
(даже для непериодического урона) и выполнение функции завершается. Иначе вызываются следующие функции:OnDamageHit
OnDamage_Condition
OnDamage_Anim
(для однократного урона)OnDamage_Effects_Start
(для однократного урона)OnDamage_Script
(для однократного урона)OnDamage_State
(для однократного урона)OnDamage_Events
OnDamage_Sound
(для однократного урона)
bFinished
был ранее установлен в true
, то также вызывается функция OnDamage_Effects_End
(в обход проверки на смерть и бессознательного состояние).Прилагаю переработанный код функции (удалены сообщения zSpy,
bFinished
не устанавливается в true
для непериодического урона, более простая проверка на урон во время диалога, удалена логика защиты от копирования).
C++:
void oCNpc::OnDamage_Union(oSDamageDescriptor& desc)
{
// отличаем однократный урон от периодического
desc.bOnce = !HasFlag(desc.dwFieldsValid, oEDamageDescFlag_OverlayActivate);
// если атака с участием игрока, который находится в диалоге
const bool inDialog = !oCInformationManager::GetInformationManager().HasFinished() && (IsSelfPlayer() || COA(desc.pNpcAttacker, IsSelfPlayer()));
// нужно ли прекратить действие периодического урона
desc.bFinished = false;
// прекращаем периодический урон в диалогах, по истечению времени или по завершении визуала,
// если таковой имеется
if (!desc.bOnce)
desc.bFinished = inDialog || desc.fTimeDuration <= 0.0f || COA(desc.pVisualFX, IsFinished());
// в диалогах даже не снимаем визульный эффект периодического урона
if (inDialog)
return;
// мёртвые и бессознательные перестают получать какой-либо урон
// также снимаются визуальные эффекты
if (!IsConditionValid())
{
if (desc.bFinished)
OnDamage_Effects_End(desc);
return;
}
OnDamage_Hit(desc);
OnDamage_Condition(desc);
if (desc.bOnce)
{
OnDamage_Anim(desc);
OnDamage_Effects_Start(desc);
OnDamage_Script(desc);
OnDamage_State(desc);
}
OnDamage_Events(desc);
if (desc.bOnce)
OnDamage_Sound(desc);
if (desc.bFinished)
OnDamage_Effects_End(desc);
}
void oCNpc::OnDamage_Hit
Используются следующие поля дескриптора урона:
aryDamage
(чтение/запись) - сырой урон по типам (в начале - урон оружия или заклинания, в конце - полный сырой урон)fDamageMultiplier
(чтение) - множитель для изначального сырого уронаaryDamageEffective
(запись) - урон по типам с учётом защитыenuModeDamage
(чтение/запись) - используемые типы урона; в процессе может добавиться рубящий урон для монстровenuModeWeapon
(чтение) - тип боя: неизвестный, кулачный, ближний, дальний, магический (вместо него устанавливается "неизвестный" [баг])pNpcAttacker
(чтение) - атакующий NPCfDamageTotal
(чтение/запись) - может задавать изначальный сырой урон (если в aryDamage нули), итоговый урон в конце (до эффекта минимального урона и до урона от барьера по плывущему NPC)fDamageEffective
(запись) - итоговый урон (fDamageTotal
)fDamageReal
(запись) - то же, что иfDamageEffective
, но с учётом минимального урона и с учётом урона от барьера по плывущему NPCpFXHit
(чтение) - используется для определения атаки с использованием заклинания (устанавливается только если была коллизия с флагомCOLL_DOEVERYTHING
или сCOLL_APPLYVICTIMSTATE
)
NPC_MINIMAL_DAMAGE
и NPC_MINIMAL_PERCENT
читаются из скриптов каждый раз).
C++:
// вспомогательная функция
// делит урон totalDamage между типами из damageType
void SplitDamage(oEDamageType damageType, unsigned long aryDamage[oEDamageIndex_MAX], float& totalDamage)
{
if (damageType == oEDamageType_Unknown)
return;
float divisor = 0.0f;
// считаем количество типов урона
for (int i = 0; i < oEDamageIndex_MAX; i++)
if (damageType & (1 << i))
divisor += 1.0f;
if (!divisor)
return;
// количество урона на каждый тип
const int damage = static_cast<int>(totalDamage / divisor + 0.5f);
totalDamage = 0;
// записываем урон в aryDamage, если там нуль
for (int i = 0; i < oEDamageIndex_MAX; i++)
if ((damageType & (1 << i)) && !aryDamage[i])
{
aryDamage[i] = damage;
totalDamage += damage;
}
}
void oCNpc::OnDamage_Hit_Union(oSDamageDescriptor& desc)
{
// применяем множитель урона
for (int i = 0; i < oEDamageIndex_MAX; i++)
desc.aryDamage[i] = static_cast<int>(desc.aryDamage[i] * desc.fDamageMultiplier);
desc.fDamageTotal *= desc.fDamageMultiplier;
// даём определение получеловека через принадлежность к гильдиям
const bool isSemiHuman = desc.pNpcAttacker &&
(desc.pNpcAttacker->IsHuman() || desc.pNpcAttacker->IsOrc() || desc.pNpcAttacker->IsGoblin() || desc.pNpcAttacker->IsSkeleton());
bool divideDamage = true;
for (int i = 0; i < oEDamageIndex_MAX && divideDamage; i++)
if (HasFlag(desc.enuModeDamage, 1 << i) && desc.aryDamage[i])
divideDamage = false;
// если в aryDamage нули, то заполняем его распределяя fDamageTotal по активным типам урона
if (divideDamage)
{
// если атакующий NPC не получеловек, атака не магическая и общий урон не задан, то используем силу как урон
// примечание: pFXHit не будет задан при магической атаке, если C_CanNpcCollideWithSpell не вернула
// ни флага COLL_DOEVERYTHING, ни COLL_APPLYVICTIMSTATE
if (desc.pNpcAttacker && !isSemiHuman && !desc.pFXHit && !desc.fDamageTotal)
desc.fDamageTotal = desc.pNpcAttacker->GetAttribute(NPC_ATR_STRENGTH);
// распределить общий урон между активными типами урона
SplitDamage(static_cast<oEDamageType>(desc.enuModeDamage), desc.aryDamage, desc.fDamageTotal);
}
// для получеловека увеличиваем сырой урон собственным уроном
if (isSemiHuman)
for (int i = 0; i < oEDamageIndex_MAX; i++)
desc.aryDamage[i] += desc.pNpcAttacker->GetDamageByIndex(static_cast<oEIndexDamage>(i));
// если немагическая атака сделана получеловеком
if (!desc.pFXHit && isSemiHuman)
{
// делим атрибуты между физическими типами урона
int divisor =
(HasFlag(desc.enuModeDamage, oEDamageType_Blunt) ? 1 : 0) +
(HasFlag(desc.enuModeDamage, oEDamageType_Edge) ? 1 : 0) +
(HasFlag(desc.enuModeDamage, oEDamageType_Point) ? 1 : 0);
// монстр (скелет или гоблин в данном случае) без физического урона будет иметь рубящий урон
if (desc.pNpcAttacker->IsMonster() && !divisor)
{
desc.enuModeDamage |= oEDamageType_Edge;
divisor = 1;
}
// делим атрибуты между физическими типами урона
if (divisor)
{
const int strength = static_cast<int>(desc.pNpcAttacker->GetAttribute(NPC_ATR_STRENGTH) / static_cast<float>(divisor));
const int dexterity = static_cast<int>(desc.pNpcAttacker->GetAttribute(NPC_ATR_DEXTERITY) / static_cast<float>(divisor));
if (HasFlag(desc.enuModeDamage, oEDamageType_Blunt))
desc.aryDamage[oEDamageIndex_Blunt] += strength;
if (HasFlag(desc.enuModeDamage, oEDamageType_Edge))
desc.aryDamage[oEDamageIndex_Edge] += strength;
// для колющего - ловкость
if (HasFlag(desc.enuModeDamage, oEDamageType_Point))
desc.aryDamage[oEDamageIndex_Point] += dexterity;
}
}
// учёт защиты
bool immortal = true;
int damageTotal = 0;
for (int i = 0; i < oEDamageIndex_MAX; i++)
{
if (!HasFlag(desc.enuModeDamage, 1 << i))
{
desc.aryDamageEffective[i] = 0;
continue;
}
const int protection = GetProtectionByIndex(static_cast<oEDamageIndex>(i));
immortal = immortal && protection < 0; // неуязвим к атаке, если неуязвим к каждому из активных типов урона
// вычитаем защиту
desc.aryDamageEffective[i] = (protection < 0) ? 0 : std::max(static_cast<int>(desc.aryDamage[i]) - protection, 0);
// суммируем наносимый урон
damageTotal += desc.aryDamageEffective[i];
}
immortal = immortal || HasFlag(NPC_FLAG_IMMORTAL);
desc.fDamageTotal = immortal ? 0 : damageTotal;
desc.fDamageEffective = desc.fDamageTotal;
desc.fDamageReal = desc.fDamageTotal;
// определяем, критический урон или нет
// всегда крит, если атака магическая или атакующий - не NPC или атакующий - монстр или атака не в ближнем бою с оружием
bool hasHit = desc.pVisualFX || !desc.pNpcAttacker || desc.pNpcAttacker->IsMonster() || desc.enuModeWeapon != oETypeWeapon_Melee;
// если промах возможен, определяем шанс попадания и само попадание
if (!hasHit)
{
int talentNr = Invalid;
switch (desc.pNpcAttacker->GetWeaponMode())
{
case NPC_WEAPON_1HS:
case NPC_WEAPON_DAG:
talentNr = NPC_HITCHANCE_1H;
break;
case NPC_WEAPON_2HS:
talentNr = NPC_HITCHANCE_2H;
break;
}
if (talentNr == Invalid)
hasHit = true;
else
{
const int hitChance = desc.pNpcAttacker->GetHitChance(talentNr);
hasHit = rand() / static_cast<float>(RAND_MAX + 1) < hitChance / 100.0f;
}
}
int damage = static_cast<int>(desc.fDamageTotal);
// делим урон, в случае промаха
if (!hasHit)
{
static Symbol npcMinimalPercent{ parser, "NPC_MINIMAL_PERCENT" };
static const float missMultiplier = npcMinimalPercent ? npcMinimalPercent.GetValue<int>(0) / 100.0f : 0.0f;
damage = static_cast<int>(damage * missMultiplier);
}
// применяем минимальный урон, если атака не магическая
if (!desc.pFXHit)
{
static Symbol npcMinimalDamage{ parser, "NPC_MINIMAL_DAMAGE" };
static const int minimalDamage = npcMinimalDamage ? npcMinimalDamage.GetValue<int>(0) : 0;
if (damage < minimalDamage)
{
damage = minimalDamage;
desc.fDamageReal = minimalDamage;
}
}
// барьер смертелен для плывущего NPC
if (HasFlag(desc.enuModeDamage, oEDamageType_Barrier) && COA(GetAnictrl(), GetWaterLevel()) >= 2)
{
damage = GetAttribute(NPC_ATR_HITPOINTS);
desc.fDamageReal = damage;
}
// уменьшаем здоровье, если цель уязвима
if (!immortal)
ChangeAttribute(NPC_ATR_HITPOINTS, -damage);
// сбрасываем таймеры регенерации
hpHeal = GetAttribute(NPC_ATR_REGENERATEHP) * 1000.0f;
manaHeal = GetAttribute(NPC_ATR_REGENERATEMANA) * 1000.0f;
}
void oCNpc::OnDamage_Condition
bIsDead
и bIsUnconscious
.Используются следующие поля дескриптора урона:
enuModeDamage
(чтение) - через него определяется наличие дробящего или рубящего урона, которые могут позволить заменить смерть на потерю сознанияpNpcAttacker
(чтение) - атакующий NPC, от его наличия и его гильдии зависит смертельность уронаbDamageDontKill
(чтение) - флаг, который также может спасти жертву от смертиbIsDead
(запись) - означает, что жертва должна умеретьbIsUnconscious
(запись) - означает, что жертва должна перейти в бессознательно состояние
C++:
void oCNpc::OnDamage_Condition_Union(oSDamageDescriptor& desc)
{
// такого не должно быть, но всё же...
if (!GetAnictrl())
return;
// бессознательность имеет специфические условия,
// поэтому пока ставим false
desc.bIsUnconscious = false;
// инициализируем смертельность удара
desc.bIsDead = IsDead();
// если здоровья ещё много - не переходим ни в одно из состояний
if (GetAttribute(NPC_ATR_HITPOINTS) > 1)
return;
// если нету атакующего НПС или жертва в воде, то бессознательное состояние недопустимо,
// а смертельность повреждения не может измениться
if (!desc.pNpcAttacker || GetAnictrl()->IsInWater())
return;
const bool hasBlunt = HasFlag(desc.enuModeDamage, oEDamageType_Blunt);
const bool hasEdge = HasFlag(desc.enuModeDamage, oEDamageType_Edge);
// замена смерти на бессознательное состояние возможна только в случаях, когда
// есть дробящий урон или есть рубящий урон от человека или установлен флаг bDamageDontKill
if (desc.bIsDead && !desc.bDamageDontKill && !hasBlunt && !(desc.pNpcAttacker->IsHuman() && hasEdge))
return;
static const int C_DropUnconscious = parser->GetIndex("C_DropUnconscious");
if (C_DropUnconscious != Invalid)
{
ParserScope scope{ parser };
static Symbol self{ parser, "SELF" };
static Symbol other{ parser, "OTHER" };
self.GetSymbol()->offset = reinterpret_cast<int>(this);
other.GetSymbol()->offset = reinterpret_cast<int>(desc.pNpcAttacker);
// вызываем func int C_DropUnconscious()
desc.bIsUnconscious = CallParser<bool>(parser, C_DropUnconscious);
}
else
// если функции нет, то вводим в бессознательное состояние людей
desc.bIsUnconscious = IsHuman();
// если определили бессознательное состояние, то отменяем смерть
if (desc.bIsUnconscious)
{
desc.bIsDead = false;
// закрываем инвентарь ГГ
if (IsSelfPlayer())
CloseInventory();
}
}
void oCNpc::OnDamage_Anim
Используются следующие поля дескриптора урона:
enuModeDamage
(чтение) - через него определяется наличие урона от барьера или от паденияpVobAttacker
(чтение) - атакующий воб (если задан, то определяются углы между жертвой и атакующим)fAzimuth
(запись) - угол в горизонтальной плоскости между жертвой и атакующим [-180; 180]fElevation
(запись) - угол в вертикальной плоскости между жертвой и атакующим [-180; 180]aryDamageEffective
(чтение) - сумма урона от полёта и барьера определяет силу отбрасыванияbIsUnconscious
(чтение) - с этим флагом болевые анимации не воспроизводятсяbIsDead
(чтение) - с этим флагом болевые анимации не воспроизводятся, определяет переходные анимации смертиfDamagReal
(чтение) - полёт возможен только при ненулевом уронеvecDirectionFly
(чтение) - направление полёта при уроне от барьера
Выполнение функции можно разбить на несколько логических шагов:
- Проверка наличия модели, контроллера анимаций, соответсвия ИИ типу
oCAIHuman
и отсутствия активного урона от падения - Расслабление тетивы лука жертвы
- Определение, нужно ли запустить полёт жертвы
- Определение, нужно ли проигрывать анимации боли
- Определение и проигрывание жёстких или более лёгких болевых анимаций
- Запуск жертвы в полёт
- Запуск анимации лица
- Запуск переходной смертельной анимации
C++:
void oCNpc::OnDamage_Anim_Union(oSDamageDescriptor& desc)
{
// проверка наличия модели, ИИ и контроллера анимаций
if (!dynamic_cast<oCAIHuman*>(callback_ai) || !GetAnictrl() || !GetModel())
return;
// при активном уроне от падения не проигрываются никакие дополнительные анимации
if (HasFlag(desc.enuModeDamage, oEDamageType_Fall))
return;
// находим углы в градусах от жертвы к атакующему
if (desc.pVobAttacker)
GetAngles(desc.pVobAttacker, desc.fAzimuth, desc.fElevation);
// расслабить тетиву
if (GetWeaponMode() == NPC_WEAPON_BOW)
if (zCMorphMesh* mesh = dynamic_cast<zCMorphMesh*>(GetWeapon()))
mesh->StartAni("S_RELAX", 1.0f, -2.0f);
// при уроне от полёта или от барьера жертва должна отлететь...
bool fly = roundf(desc.aryDamageEffective[oEDamageIndex_Barrier]) + roundf(desc.aryDamageEffective[oEDamageIndex_Fly]);
// ... но не в воде и не в лежачем состоянии
fly = fly && GetAnictrl()->GetWaterLevel() == 0 && GetBodyState() != BS_LIE;
// возможность обычной анимации боли:
// только при нефатальном ударе, при закрытом инвентаре, ненулевом уроне без эффекта полёта
bool feelsPain = !desc.bIsUnconscious && !desc.bIsDead && !inventory2.IsOpen() && desc.fDamageReal && !fly;
// игрок не чувствует боли при выполнении потенциально опасного для противников удара
feelsPain = feelsPain && !(IsSelfPlayer() && (GetAnictrl()->IsInPreHit() || GetAnictrl()->IsInCombo()));
if (feelsPain)
for (zCEventMessage* message : GetEM()->messageList)
if (!message->IsDeleted())
{
// не чувствуем боли при смене/выборе экипировки
if (oCMsgWeapon* weaponMessage = dynamic_cast<oCMsgWeapon*>(message))
if (weaponMessage->IsInUse())
{
feelsPain = false;
break;
}
// не чувствуем боли при добивании лежащего NPC
if (oCMsgAttack* attackMessage = dynamic_cast<oCMsgAttack*>(message))
if (attackMessage->IsInUse())
if (attackMessage->GetSubType() == oCMsgAttack::EV_ATTACKFINISH)
{
feelsPain = false;
break;
}
}
// не чувствуем боли при касте заклинания
if (feelsPain && GetWeaponMode() == NPC_WEAPON_MAG)
if (oCSpell* spell = COA(GetSpellBook(), GetSelectedSpell()))
if (spell->GetSpellStatus() == SPL_STATUS_CAST)
feelsPain = false;
if (feelsPain)
{
// выбираем между жёстким блокирующим эффектом и простой анимацией
bool justAni = IsBodyStateInterruptable() && !bodyStateInterruptableOverride;
// игрок в боевой стойке получает лёгкий эффект
justAni = justAni || IsSelfPlayer() && GetWeaponMode() != NPC_WEAPON_NONE;
oCMsgConversation* message = nullptr;
if (justAni)
message = new oCMsgConversation(oCMsgConversation::EV_PLAYANI, "T_GOTHIT");
else
{
// очистка ИИ и прерывание текущего действия
ClearEM();
Interrupt(false, false);
SetBodyState(BS_STUMBLE);
// выбор направления анимации
zSTRING stumble = "STUMBLE";
// если атакующий спереди, то выполняется отскок назад
if (fabsf(desc.fAzimuth) <= 90.0f)
stumble += "B";
zSTRING aniName = Z"T_" + GetInterruptPrefix() + stumble;
// пробуем найти специализированную для заклинания анимацию
if (desc.pFXHit)
{
static Symbol spellFxAniLetters{ parser, "spellFXAniLetters" };
if (spellFxAniLetters)
{
const zSTRING newAniName = Z"T_" + Z spellFxAniLetters.GetValue<string>(desc.pFXHit->GetSpellType()) + stumble;
if (GetModel()->GetAniIDFromAniName(newAniName) != Invalid)
aniName = newAniName;
}
}
message = new oCMsgConversation(oCMsgConversation::EV_PLAYANI_NOOVERLAY, aniName);
}
// отправляем команду о проигрывании анимации
message->SetHighPriority(true);
GetEM()->OnMessage(message, this);
}
// запускаем полёт
if (fly)
{
// направление задано только для урона от барьера
zVEC3 direction = desc.vecDirectionFly;
if (!HasFlag(desc.enuModeDamage, oEDamageType_Barrier))
{
// по умолчанию удар спереди
zVEC3 from = GetPositionWorld() + GetAtVectorWorld() * 100.0f;
if (desc.pVobAttacker)
from = desc.pVobAttacker->GetPositionWorld();
else
if (desc.pVobHit)
from = desc.pVobHit->GetPositionWorld();
direction = GetPositionWorld() - from;
direction.Normalize();
}
// нет урона от падения при полёте
overrideFallDownHeight = true;
// запускаем анимацию
static_cast<oCAIHuman*>(callback_ai)->StartFlyDamage(desc.aryDamageEffective[oEDamageIndex_Fly] + desc.aryDamageEffective[oEDamageIndex_Barrier], direction);
}
// анимация лица при игре со звуком
if (zsound && !desc.bIsDead && !desc.bIsUnconscious)
StartFaceAni("VISEME", 1.0f, -2.0f);
if (!desc.bIsDead)
return;
// анимация смерти
zSTRING deadAni = "T_DEAD";
// особая анимацию при плавании
if (GetAnictrl()->GetActionMode() == ANI_ACTION_SWIM || GetAnictrl()->GetActionMode() == ANI_ACTION_DIVE)
deadAni = "T_DIVE_2_DROWNED";
else
// иначе, если удар спереди
if (fabs(desc.fAzimuth) <= 90.0f)
deadAni += "B";
GetModel()->StartAni(deadAni, zCModel::zMDL_STARTANI_DEFAULT);
}
void oCNpc::OnDamage_Effects_Start
Используются следующие поля оригинального дескриптора урона (для динамического эффекта будет создан новый):
enuModeDamage
(чтение/запись) - через него определяется/задаётся наличие урона от огняpNpcAttacker
(чтение) - атакующий NPC (если монстр, то не будет эффектов крови)pVobAttacker
(чтение) - атакующий воб (используется только для инициализации динамического эффекта)nSpellLevel
(чтение) - уровень заряда заклинания (используется только для инициализации динамического эффекта)fDamageTotal
(чтение) - общий урон (используется только для инициализации динамического эффекта)nSpellID
(чтение) - тип заклинания (используется только для инициализации динамического эффекта)nSpellCat
(чтение) - категория заклинания (используется только для инициализации динамического эффекта)enuModeWeapon
(чтение) - используется для определения кулачного боя (не будет эффектов крови)aryDamageEffective
(чтение) - две единицы и более урона от горения вызывают эффект горения с периодическим урономstrVisualFX
(чтение/запись) - название динамического эффекта (будет установлен какVOB_BURN
при горении огнём)fDamagePerInterval
(запись) - устанавливается при горении огнёмfTimeDuration
(чтение/запись) - время действия динамического эффекта (устанавливается при горении огнём в зависимости от урона)aryDamage
(запись) - обнуляется при горении огнём, чтобы вызвать перерасчёт урона в дальнейшемvecLocationHit
(чтение) - используется в качестве начальной позиции для брызг кровиpVisualFX
(запись) - новосозданный динамический эффект
Отмечу условия для создания эффектов крови: удар должен пробить броню, NPC должен быть смертным, атака не должна иметь урон от огня, не должен использоваться кулачный бой, атакующий не должен быть монстром. Из-за последнего условия, например, удары скелетов не вызывают кровотечения. К тому же, кровь может быть выключена в настройках или деактивирована для конкретного NPC.
Условия старта горения огнём: как минимум две единицы урона от огня, персонаж не должен находиться в воде или иметь флаг горения на теле. При выполнении условий инициализируются поля периодического урона и задаётся имя визуала
VOB_BURN
. Массив aryDamage
обнуляется с целью вызвать перерасчёт урона с использованием поля fDamagePerInterval
.Далее, если задано имя визуального эффекта, то уничтожается сообщение о периодическом уроне (если такое есть). Следом создаётся визуал с именем
strVisualFX
и копия дескриптора урона используется для создания высокоприоритетного сообщения об уроне. Однако, если поле fTimeDuration
не было задано, то новый дескриптор будет содержать только 3 поля лишь для того, чтобы потом уничтожить визуал динамического эффекта после того, как он закончится.
C++:
// выделил создание эффектов крови в отдельную функцию
void oCNpc::OnDamage_CreateBlood(oSDamageDescriptor& desc)
{
// проверяем, активирован ли режим крови для данного NPC
if (!bloodEnabled)
return;
// проверяем режим крови (общий для всех NPC)
if (modeBlood < oEBloodMode_Particles)
return;
// создаём частицы крови, если NPC находится в мире
if (GetHomeWorld())
{
// создаём воб с визуалом из частиц
ZOwner<zCParticleFX> visual{ new zCParticleFX{} };
ZOwner<zCVob> vob{ new zCVob{} };
vob->SetCollDet(false);
vob->SetPositionWorld(desc.vecLocationHit);
vob->SetVisual(visual.get());
GetHomeWorld()->AddVob(vob.get());
visual->SetAndStartEmitter(bloodEmitter, false);
}
// проверяем, нужно ли создавать лужу крови
if (modeBlood < oEBloodMode_Decals || !GetAnictrl())
return;
// направление и cила потока крови
zVEC3 bloodRay{ 0.0f, -1.0f, 0.0f };
if (desc.pNpcAttacker)
{
bloodRay = GetPositionWorld() - desc.pNpcAttacker->GetPositionWorld();
bloodRay.Normalize();
}
bloodRay *= bloodDistance;
// визуальный размер крови
float bloodSize = 0.0f;
if (Symbol bloodDamageMax{ parser, "BLOOD_DAMAGE_MAX" })
bloodSize = std::max<float>(desc.fDamageEffective, bloodDamageMax.GetValue<int>(0));
bloodSize *= bloodAmount;
if (Symbol bloodSizeDivisor{ parser, "BLOOD_SIZE_DIVISOR" })
if (bloodSizeDivisor.GetValue<int>(0) > 0)
bloodSize /= bloodSizeDivisor.GetValue<int>(0);
// рандомизированное усиление крови не более 10%
bloodSize *= 1.0f + 0.1f * rand() / static_cast<float>(RAND_MAX);
GetAnictrl()->AddBlood(GetPositionWorld(), bloodRay, bloodSize, bloodFlow, &bloodTexture);
}
void oCNpc::OnDamage_Effects_Start_Union(oSDamageDescriptor& desc)
{
if (!GetAnictrl())
return;
// проверка условий создания эффектов крови
if
(
desc.fDamageEffective > 0.0f &&
!HasFlag(NPC_FLAG_IMMORTAL) &&
!HasFlag(desc.enuModeDamage, oEDamageType_Fire) &&
!HasFlag(desc.enuModeWeapon, oETypeWeapon_Fist) &&
!COA(desc.pNpcAttacker, IsMonster())
)
{
OnDamage_CreateBlood(desc);
}
// проверка условий начала горения огнём
if
(
HasFlag(desc.enuModeDamage, oEDamageType_Fire) &&
desc.aryDamageEffective[oEDamageIndex_Fire] >= 2.0f &&
!HasBodyStateModifier(BS_MOD_BURNING) &&
GetAnictrl()->GetWaterLevel() == 0
)
{
// устанавливаем имя визуала горения
desc.strVisualFX = "VOB_BURN";
// урон за каждый тик горения
desc.fDamagePerInterval = 10.0f;
if (Symbol damagePerInterval{ parser, "NPC_BURN_DAMAGE_POINTS_PER_INTERVALL" })
desc.fDamagePerInterval = damagePerInterval.GetValue<int>(0);
// время горения за каждую единицу урона
float ticksPerDamagePoint = 1000.0f;
if (Symbol ticksPerDamagePointSymbol{ parser, "NPC_BURN_TICKS_PER_DAMAGE_POINT" })
ticksPerDamagePoint = ticksPerDamagePointSymbol.GetValue<int>(0);
// общее время горения
desc.fTimeDuration = ticksPerDamagePoint * desc.aryDamageEffective[oEDamageIndex_Fire];
// интервал между нанесением урона
desc.fTimeInterval = ticksPerDamagePoint;
desc.enuModeDamage = oEDamageType_Fire;
std::fill(desc.aryDamage, desc.aryDamage + oEDamageIndex_MAX, 0);
}
// проверяем визуал периодического урона
if (desc.strVisualFX.IsEmpty())
return;
// находим активное сообщение периодического урона
oCMsgDamage* activeDot = nullptr;
for (zCEventMessage* message : GetEM()->messageList)
if (oCMsgDamage* dotMessage = dynamic_cast<oCMsgDamage*>(message))
if (!dotMessage->IsDeleted() && dotMessage->GetSubType() == oCMsgDamage::EV_DAMAGE_PER_FRAME)
{
activeDot = dotMessage;
break;
}
// уничтожаем активный периодический урон
if (activeDot)
{
// убираем реакции восприятия старого визуала
if (oCVisualFX* visual = activeDot->descDamage.pVisualFX)
visual->SetSendsAssessMagic(false);
// удаляем сообщение
OnDamage_Effects_End(activeDot->descDamage);
activeDot->Delete();
}
// создаём визуал периодического урона
ZOwner<oCVisualFX> visual{ new oCVisualFX{} };
visual->SetPositionWorld(GetPositionWorld());
if (GetHomeWorld())
GetHomeWorld()->AddVob(visual.get());
visual->SetLevel(desc.nSpellLevel, false);
visual->SetDamage(desc.fDamageTotal);
visual->SetDamageType(desc.enuModeDamage);
visual->SetSpellType(desc.nSpellID);
visual->SetSpellCat(desc.nSpellCat);
visual->SetByScript(desc.strVisualFX);
visual->Init(this, 0, desc.pVobAttacker);
visual->Cast(true);
desc.SetVisualFX(visual.get());
// создаём новый дескриптор для периодического урона
oSDamageDescriptor dotDesc;
ZeroMemory(&dotDesc, sizeof(dotDesc));
// если время периодического урона не задано, то урон не наносится
if (desc.fTimeDuration == 0.0f)
{
dotDesc.SetVisualFX(desc.pVisualFX);
dotDesc.fTimeDuration = std::numeric_limits<float>::max();
dotDesc.fTimeInterval = 1000.0f;
}
// иначе копируем все значения
else
dotDesc = desc;
// отправляем сообщение о периодическом уроне в менеджер событий
activeDot = new oCMsgDamage{ oCMsgDamage::EV_DAMAGE_PER_FRAME, dotDesc };
activeDot->SetHighPriority(true);
GetEM()->OnMessage(activeDot, this);
}
void oCNpc::OnDamage_Script
oCNpc::OnDamage_Anim
), и активирует восприятие NPC_PERC_ASSESSDAMAGE
для жертвы (в случае несмертельного урона) и восприятие NPC_PERC_ASSESSOTHERSDAMAGE
для окружающих NPC.Используются следующие поля дескриптора урона:
bIsDead
(чтение) - смертельность уронаpNpcAttacker
(чтение) - атакующий NPCfDamageEffective
(чтение) - значение урона после вычета брони (игнорируется движком)
C++:
void oCNpc::OnDamage_Script_Union(oSDamageDescriptor& desc)
{
// прерываются все кроме игрока, а также сам игрок, если находится в небоевом режиме
if (!IsSelfPlayer() || GetWeaponMode() == NPC_WEAPON_NONE)
Interrupt(true, false);
// активируем восприятие AssessDamage окружающим NPC при несмертельном уроне
if (!desc.bIsDead)
AssessDamage_S(desc.pNpcAttacker, static_cast<int>(desc.fDamageEffective + 0.5f));
}
int oCNpc::OnDamage_State
BS_MOD_BURNING
.На мой взгляд, лишняя функция и должна быть включена в
oCNpc::OnDamage_Effects_Start
.Используются следующие поля дескриптора урона:
enuModeDamage
(чтение) - для определения наличия урона от горенияaryDamageEffective
(чтение) - для определения количества урона от горения
C++:
void oCNpc::OnDamage_State_Union(oSDamageDescriptor& desc)
{
// если началось горение, устанавливаем модификатор состояния тела
if
(
!HasBodyStateModifier(BS_MOD_BURNING) &&
HasFlag(desc.enuModeDamage, oEDamageType_Fire) &&
desc.aryDamageEffective[oEDamageIndex_Fire] >= 2.0f &&
COA(GetAnictrl(), GetWaterLevel()) == 0
)
{
SetBodyStateModifier(BS_MOD_BURNING);
}
}
int oCNpc::OnDamage_Events
Используются следующие поля дескриптора урона:
bIsUnconscious
(чтение) - определяет, нужен ли переход в бессознательное состояниеbIsDead
(чтение) - определяет, нужен ли переход в терминальное состояниеpNpcAttacker
(чтение) - используется как аргумент при осуществлении перехода между состояниямиfAzimuth
(чтение) - используется как аргумент при осуществлении перехода в бессознательное состояниеfDamageReal
(чтение) - используется для определения, нужно ли сбить каст заклинания жертве
C++:
void oCNpc::OnDamage_Events_Union(oSDamageDescriptor& desc)
{
if (desc.bIsUnconscious)
DropUnconscious(desc.fAzimuth, desc.pNpcAttacker);
if (desc.bIsDead)
DoDie(desc.pNpcAttacker);
// останавливаем каст заклинания
if (desc.fDamageReal >= 1.0f && !IsSelfPlayer() && !bodyStateInterruptableOverride)
COA(GetSpellBook(), StopSelectedSpell());
}
int oCNpc::OnDamage_Sound
SVM_{voice}_DEAD
, SVM_{voice}_AARGH
, SVM_{voice}_AARGH_1
, SVM_{voice}_AARGH_2
, ...Используется следующее поле дескриптора урона:
bIsDead
(чтение) - определяет, нужен ли предсмертный звук или обычный
C++:
void oCNpc::OnDamage_Sound_Union(oSDamageDescriptor& desc)
{
// проверяем, включен ли звук
if (!zsound)
return;
// определяем название звука
zSTRING soundName = Z"SVM_" + Z voice;
if (desc.bIsDead)
soundName += "_DEAD";
// звуков боли может быть несколько вариантов...
else
{
soundName += "_AARGH";
int variations = 5;
if (Symbol variationsSymbol{ parser, "NPC_VOICE_VARIATION_MAX" })
variations = variationsSymbol.GetValue<int>(0);
if (const int random = (variations <= 1) ? 0 : (rand() % variations))
soundName += Z"_" + Z random;
}
zCSoundSystem::zTSound3DParams params;
params.SetDefaults();
params.pitchOffset = voicePitch;
listOfVoiceHandles.InsertEnd(zsound->PlaySound3D(soundName, this, 2, ¶ms));
}
int oCNpc::OnDamage_Effects_End
BS_MOD_BURNING
.Используются следующие поля дескриптора урона:
pParticleFX
(чтение/запись) - используется для остановки генератора частицpVobParticleFX
(чтение/запись) - используется для деинициализации воба, связанного с генератором частицpVisualFX
(чтение/запись) - используется для завершения работы визуала динамического эффектаenuModeDamage
(чтение) - используется для определения наличия огненного урона, чтобы снять флаг BS_MOD_BURNING
pParticleFX
и pVobParticleFX
никогда не задаются движком.
C++:
void oCNpc::OnDamage_Effects_End_Union(oSDamageDescriptor& desc)
{
// уничтожаем источник частиц (в оригинале его никогда нет)
if (desc.pParticleFX)
{
desc.pParticleFX->StopEmitterOutput();
desc.pParticleFX->Release();
desc.pParticleFX = nullptr;
}
// уничтожаем связанный с генератором частиц воб (в оригинале его тоже никогда нет)
if (desc.pVobParticleFX)
{
desc.pVobParticleFX->Release();
desc.pVobParticleFX = nullptr;
}
// уничтожаем обычный визуал
if (desc.pVisualFX)
{
desc.pVisualFX->Stop(true);
desc.SetVisualFX(nullptr);
}
// снимаем флаг горения
if (HasFlag(desc.enuModeDamage, oEDamageType_Fire))
this->ClrBodyStateModifier(BS_MOD_BURNING);
}
Последнее редактирование: