• Уважаемые гости и новички, приветствуем Вас на нашем форуме
    Здесь вы можете найти ответы практически на все свои вопросы о серии игр «Готика» (в том числе различных модах на нее), «Ведьмак», «Ризен», «Древние свитки», «Эра дракона» и о многих других играх. Можете также узнать свежие новости о разработке новых проектов, восхититься творчеством наших форумчан, либо самим показать, что вы умеете. Ну и наконец, можете обсудить общие увлечения или просто весело пообщаться с посетителями «Таверны».

    Чтобы получить возможность писать на форуме, оставьте сообщение в этой теме.
    Удачи!
  • Друзья, доброго времени суток!
    Стартовал новый литературный конкурс от "Ордена Хранителей" - "Пираты Миртанского моря".
    Каждый может принять в нём участие и снискать славу и уважение, а в случае занятия призового места ещё и получить награду. Дерзайте
  • Дорогие друзья, год подходит к концу, и пришло время подвести его итоги и наградить достойных

    Не ленитесь, голосуйте в этой теме за тех форумчан, которые по вашему мнению больше всех проявили себя в этом году
    По желанию, аргументировать свой выбор можете в теме обсуждения голосования.

Информация Обработка урона в Готике II Ночь Ворона

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
Функция вызывается в каждом кадре, пока у активного NPC в 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 (чтение) - атакующий NPC
  • fDamageTotal (чтение/запись) - может задавать изначальный сырой урон (если в aryDamage нули), итоговый урон в конце (до эффекта минимального урона и до урона от барьера по плывущему NPC)
  • fDamageEffective (запись) - итоговый урон (fDamageTotal)
  • fDamageReal (запись) - то же, что и fDamageEffective, но с учётом минимального урона и с учётом урона от барьера по плывущему NPC
  • pFXHit (чтение) - используется для определения атаки с использованием заклинания (устанавливается только если была коллизия с флагом COLL_DOEVERYTHING или с COLL_APPLYVICTIMSTATE)
Прилагаю переработанный код функции (убраны сообщения zSpy, больше не опирается на то, что атакующий не успеет сменить оружие при дальнобойной атаке, больше нельзя получить иммунитет от атаки с множеством типов урона имея иммунитет только к одному типу, константы 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 (запись) - означает, что жертва должна перейти в бессознательно состояние
В переработанном коде удалены сообщения zSpy, вызов C_DropUnconscious не производится, если он уже ничего не решает.
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 (чтение) - направление полёта при уроне от барьера

Выполнение функции можно разбить на несколько логических шагов:
  1. Проверка наличия модели, контроллера анимаций, соответсвия ИИ типу oCAIHuman и отсутствия активного урона от падения
  2. Расслабление тетивы лука жертвы
  3. Определение, нужно ли запустить полёт жертвы
  4. Определение, нужно ли проигрывать анимации боли
  5. Определение и проигрывание жёстких или более лёгких болевых анимаций
  6. Запуск жертвы в полёт
  7. Запуск анимации лица
  8. Запуск переходной смертельной анимации
Как обычно, переработанный код без сообщений zSpy и с мелкими правками:
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 (чтение) - атакующий NPC
  • fDamageEffective (чтение) - значение урона после вычета брони (игнорируется движком)

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, &params));
}
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);
}
 
Последнее редактирование:

KirTheSeeker

Участник форума
Регистрация
18 Авг 2017
Сообщения
1.931
Благодарности
560
Баллы
275
Это как бы шаблон для составления новой версии плагина AlterDamage?
 

Slavemaster


Модостроитель
Регистрация
10 Июн 2019
Сообщения
1.086
Благодарности
1.904
Баллы
320
Это как бы шаблон для составления новой версии плагина AlterDamage?
Ну типа того. Это справочная информация, которая может помочь другим разрабам делать подобные плагины под свой мод.
Надеюсь, в будущем буду реже наблюдать топорную ванильную формулу урона...
 

Slavemaster


Модостроитель
Регистрация
10 Июн 2019
Сообщения
1.086
Благодарности
1.904
Баллы
320
Может, планируется обновление и AlterDamage?
В планах такого нет. Хотя я и не планирую далеко...

Закончил с описанием функций. Плагин со всем опубликованным кодом прилагаю. Если кто-то найдёт неприятные различия между работой плагина и оригинального движка, пишите... Для удобства плагин можно включать/выключать сочетанием SHIFT+P
 

Вложения

  • zAlterDamage.vdf
    241 KB · Просмотры: 26
Последнее редактирование:

kansler-ice

Участник форума
Регистрация
25 Окт 2020
Сообщения
5
Благодарности
1
Баллы
55
Где высчитывается вероятность попадания при стрельбе из лука или арбалета?
OnDamage_Hit, очевидно, на промахах не срабатывает, а в функциях для работы с коллизиями я не смог найти ничего об уроне.

Если что, интересует именно оригинальная игра, а не то, как прикрутить урон к коллизиям
 

Slavemaster


Модостроитель
Регистрация
10 Июн 2019
Сообщения
1.086
Благодарности
1.904
Баллы
320

KirTheSeeker

Участник форума
Регистрация
18 Авг 2017
Сообщения
1.931
Благодарности
560
Баллы
275
Приветствую. Хочу попросить помощи с изменением принципа действия магического/огненного оружия дальнего боя, а именно:
- Чтобы вместо ограниченных/создаваемых стрел/болтов эти орудия расходовали, скажем, по 5 ед. маны. Т.е. сам снаряд из игры вообще убрать оставив лишь оружие, боезапас которого зависит от маны. Это позволит "воину" полноценно атаковать магией/огнём, а также даст дополнительную причину для прокачки данного атрибута.
Возможно прописать такое через Union?
 

MEG@VOLT

★★★★★★★★★
ТехАдмин
Регистрация
24 Мар 2006
Сообщения
9.900
Благодарности
6.776
Баллы
1.625
KirTheSeeker, Может я не то понял, но разве обычные руны именно так не делают?
 

KirTheSeeker

Участник форума
Регистрация
18 Авг 2017
Сообщения
1.931
Благодарности
560
Баллы
275
KirTheSeeker, Может я не то понял, но разве обычные руны именно так не делают?
Ты когда-то упоминал вариант "создания руны с визуалом арбалета", кажется, но это звучит слишком сложно для меня.
Более простым мне кажется прописать бесконечный снаряд, без отображения оного в инвентаре, который будет сжирать 5 маны при выстреле.
Или я ошибаюсь?
 

KirTheSeeker

Участник форума
Регистрация
18 Авг 2017
Сообщения
1.931
Благодарности
560
Баллы
275
Приветствую всех.
Подскажите, пожалуйста, через какую функцию и как можно прописать взаимодействие с противником, чтобы при убийстве последнего (не оглушении) Когтем Белиара у ГГ восполнялось здоровье (если убито живое существо) или мана (если убита нежить/голем или т.п. магическое существо) в размере 10%/20% от макс. HP убитого?
 
Сверху Снизу