Мутаторы и UnrealScript

Кодинг перков в ServerPerks: бонусы оружия, стартовое снаряжение, свой перк и новое требование прокачки

На кастомных серверах Killing Floor 1 перки почти всегда реализованы через ServerPerks: каждый перк — отдельный класс SRVet*.uc, наследник SRVeterancyTypes, в пакете ServerPerksP. Здесь собраны четыре темы с форума killingfloor.ru (2011-2012): как прописать бонусы оружию (kok-s), как выдать оружие на старте (NeKRoN), как сделать свой перк (akf817gp) и как добавить новое требование прокачки (Flame). Нужны исходники ServerPerks нужной версии и умение компилировать пакеты.

Бонусы оружию

Каждая функция в классе перка отвечает за свой вид бонуса. Шаблон везде одинаковый: проверка, к какому оружию относится вызов, затем к какому уровню, затем возврат множителя. Чтобы дать перку чужой бонус (например, берсерку броню медика), найдите функцию этого бонуса в классе другого перка, скопируйте её в класс своего перка и поправьте значения; новую функцию вставляйте перед блоком defaultproperties (тема «Как добавить перку скил другого перка?», форум killingfloor.ru).

БонусФункцияКак проверяется оружиеЧто возвращает
Патронов в магазинеGetMagCapacityModAK47AssaultRifle(Other) != noneмножитель, например 1.10
Переносимых патроновAddExtraAmmoForAmmoType == class'AK47Ammo'множитель
Патронов из ящиковGetAmmoPickupModAK47Ammo(Other) != noneмножитель
УронAddDamageDmgType == class'DamTypeAK47AssaultRifle'float(InDamage) * 1.05 или формула
Скидка на оружиеGetCostScalingItem == class'AK47Pickup'доля цены
Скидка на патроныGetAmmoCostScalingItem == class'AK47Pickup'доля цены
ОтдачаModifyRecoilSpreadAK47AssaultRifle(Other.Weapon) != noneRecoil = 0.90 (меньше — слабее отдача)
Скорость перезарядкиGetReloadSpeedModifierAK47AssaultRifle(Other) != noneмножитель
СкорострельностьGetFireSpeedModAK47AssaultRifle(Other) != noneмножитель

Уровень проверяется через KFPRI.ClientVeteranSkillLevel:

if ( KFPRI.ClientVeteranSkillLevel == 1 )
    return 1.10;

или формулой, как урон в стандартном коммандосе:

return float(InDamage) * (1.00 + (0.10 * float(Min(KFPRI.ClientVeteranSkillLevel, 5))));

Скидка на патроны у стандартных перков считается так: FMax(0.25 - (0.03 * float(KFPRI.ClientVeteranSkillLevel)), 0.01f). В разных классах SRVet* код может немного отличаться, поэтому ищите нужную функцию и правьте по аналогии.

Чтобы не тянуть зависимость от пакета с оружием (и порядок компиляции, см. ниже), кастомное оружие можно проверять по имени, а не по классу: Other.Weapon.IsA('PKM'), DmgType.Name == 'DamTypePKM', AmmoType.Name == 'PKMAmmo'.

Патроны выброшенного оружия. Разбор Flame из темы «Сохранение патронов выброшенного оружия» (форум killingfloor.ru): когда игрок выбрасывает оружие, объект-наследник Weapon уничтожается и создаётся наследник WeaponPickup, в котором сохраняется количество патронов (WeaponPickup.AmmoAmount[]). При подборе всё наоборот: создаётся Weapon с пустым боекомплектом, и сохранённые патроны добавляются позже, в итоге через Engine.Ammunition.AddAmmo, где стоит AmmoAmount = Min(MaxAmmo, AmmoAmount+AmmoToAdd);. Поэтому раньше арбалет, который благодаря AddExtraAmmoFor носил 50 стрел, после выброса и подбора обрезался до MaxAmmo (36 из KFMod.CrossbowAmmo). Позже Flame дописал в тему, что в текущих версиях кода KF этой проблемы нет и его мутатор-исправление не актуален.

Оружие на старте

За стартовое снаряжение отвечает AddDefaultInventory. Каждая строка CreateInventoryVeterancy выдаёт один предмет, второй аргумент — цена продажи с учётом скидки перка. Чтобы на 6-м уровне выдавались две пушки, просто добавьте вторую строку в то же условие:

static function AddDefaultInventory(KFPlayerReplicationInfo KFPRI, Pawn P)
{
    if ( KFPRI.ClientVeteranSkillLevel == 5 )
        KFHumanPawn(P).CreateInventoryVeterancy("KFMod.Shotgun", GetCostScaling(KFPRI, class'ShotgunPickup'));
    if ( KFPRI.ClientVeteranSkillLevel >= 6 )
    {
        KFHumanPawn(P).CreateInventoryVeterancy("KFMod.BoomStick", GetCostScaling(KFPRI, class'BoomStickPickup'));
        KFHumanPawn(P).CreateInventoryVeterancy("KFMod.FNFAL_ACOG_AssaultRifle", GetCostScaling(KFPRI, class'FNFAL_ACOG_Pickup'));
    }
}

Учитывайте вес: если стартовое оружие не помещается, оно выпадет на землю.

== или >=. Из темы «Прописывание оружия !!!» (форум killingfloor.ru), пояснение Винни: условие с == выдаёт оружие только на указанном уровне, с >= - на этом уровне и всех следующих. Поэтому при условиях >= 10 и >= 20 на 20-м уровне сработают оба, и игрок получит ещё и оружие 10-го уровня. Чтобы оружие менялось на определённом уровне, младшие уровни перечисляют через == (в его примере MP7M на 6, 7 и 8 уровнях), а для старшего ставят >= (>= 9 - MP5M). Между KFHumanPawn(P).CreateInventoryVeterancy(...) и AddPerkedWeapon(...) разницы он, по его словам, не заметил.

Свой перк

  1. Основа. Скопируйте ближайший по смыслу перк, например SRVetSupportSpec.uc, в папку Classes своего пакета и переименуйте файл и класс: class A3MNC extends SRVeterancyTypes abstract;.
  2. Прокачка. GetPerkProgressInt для каждого уровня (case 0, case 1, ...) задаёт порог по каждому требованию (ReqNum), а в конце возвращает текущее значение статистики игрока:
static function int GetPerkProgressInt( ClientPerkRepLink StatOther, out int FinalInt, byte CurLevel, byte ReqNum )
{
    switch( CurLevel )
    {
    case 0:
        if( ReqNum==0 ) FinalInt = 50;
        if( ReqNum==1 ) FinalInt = 100;
        if( ReqNum==2 ) FinalInt = 10000;
        break;
    // ... case 1 - case 10 ...
    default:
        if( ReqNum==0 ) FinalInt = 2000+GetDoubleScaling(CurLevel,200);
        if( ReqNum==1 ) FinalInt = 1100+GetDoubleScaling(CurLevel,100);
        if( ReqNum==2 ) FinalInt = 400000+GetDoubleScaling(CurLevel,40000);
        break;
    }
    if( ReqNum==0 )
        return Min(StatOther.RSelfHealsStat,FinalInt);
    if( ReqNum==1 )
        return Min(StatOther.RDamageHealedStat,FinalInt);
    return Min(StatOther.RShotgunDamageStat,FinalInt);
}

Какие счётчики можно использовать, видно в ServerStStats.RepCopyStats(): RDamageHealedStat, RWeldingPointsStat, RShotgunDamageStat, RHeadshotKillsStat, RStalkerKillsStat, RBullpupDamageStat, RMeleeDamageStat, RFlameThrowerDamageStat, RSelfHealsStat, RSoleSurvivorWavesStat, RCashDonatedStat, RFeedingKillsStat, RBurningCrossbowKillsStat, RGibbedFleshpoundsStat, RStalkersKilledWithExplosivesStat, RGibbedEnemiesStat, RBloatKillsStat, RTotalZedTimeStat, RSirenKillsStat, RKillsStat, RExplosivesDamageStat. Уровни выше последнего case считает default через GetDoubleScaling.

  1. Описание и иконки в defaultproperties:
NumRequirements=3
PerkIndex=1
OnHUDIcon=Texture'MyPerkTex.Perk_Icons.MyPerk'
OnHUDGoldIcon=Texture'MyPerkTex.Perk_Icons.MyPerk_Gold'
VeterancyName="Medic-Support"
Requirements(0)="Heal yourself %x times"
Requirements(1)="Heal %x HP on teammates"
Requirements(2)="Deal %x damage with shotguns"
SRLevelEffects(0)="10% more damage with Shotguns|..."
CustomLevelInfo="%s more damage with Shotguns|..."

NumRequirements должно совпадать с числом требований, Requirements(N) — это текст для ReqNum==N, SRLevelEffects(N) — описание бонусов уровня N в меню, CustomLevelInfo — шаблон для уровней выше заданных. PerkIndex (номер перка) автор руководства советует оставить как в исходном классе.

Если на всех уровнях выше 6-го в меню видно одно и то же (например, у командоса «50% more damage»), дело в CustomLevelInfo: в нём меняются только параметры вида %s, %d, %r, %z, которые GetCustomLevelInfo заменяет через ReplaceText на значения, посчитанные от уровня, а число, вписанное в строку текстом («50%»), остаётся одинаковым. Исправить можно двумя способами: продолжить список строками SRLevelEffects(7)="75% more damage with Assault/Battle Rifles|..." и далее для каждого уровня (долго, если уровней много) или заменить фиксированные числа в CustomLevelInfo на такие параметры (Винни, тема «Статистика у перка командо (ServerPerks)», форум killingfloor.ru).

  1. Иконка. Экспортируйте стандартную иконку из KillingFloorHUD.utx (группа Perks) в KF SDK, нарисуйте свою с альфа-каналом (белое — видимая часть), сохраните в DDS (DXT с альфой) и импортируйте в свой .utx, например в группу Perk_Icons. Нужны две иконки: обычная (красная) и золотая. Подробнее — в статье Как сделать свои значки перков ServerPerksV7.

  2. Подключение. Скомпилируйте пакет, добавьте его в ServerPackages и пропишите перк в ServerPerks.ini строкой Perks=ИмяПакета.ИмяКласса рядом со стандартными. Готовый перк из чужого мода подключается так же.

Новое требование прокачки: убийства сирен из пулемёта

Пример Flame: коммандос получает 5-й и 6-й уровень только после 20 и 50 убитых из пулемёта сирен. Новая статистика должна храниться, реплицироваться клиенту, отображаться в меню и увеличиваться при убийстве.

  1. ServerPerksMut.StatsObject. Добавьте var config int MGSirenKillsStat; и допишите поле в конец строки в GetSaveData() и в конец разбора в SetSaveData() (MGSirenKillsStat = GetNextValue(S);). Если используется удалённая база, новое поле ставьте в конец, иначе существующие записи сдвинутся.
  2. ServerPerks.ClientPerkRepLink. Переменная var int RMGSirenKillsStat; и её репликация: reliable if( Role==ROLE_Authority && bNetOwner ) RMGSirenKillsStat;.
  3. ServerPerksMut.ServerStStats. В RepCopyStats() добавьте Rep.RMGSirenKillsStat = MyStatsObject.MGSirenKillsStat; и функцию-счётчик:
function AddMGSirenKill()
{
    bHasChanged = true;
    Rep.RMGSirenKillsStat++;
    if( MyStatsObject!=None )
        MyStatsObject.MGSirenKillsStat++;
    DelayedStatCheck();
}
  1. ServerPerks.SRStatList. В InitList() добавьте строку StatProgress[27] = string(L.RMGSirenKillsStat);, а в defaultproperties — ProgressName(27)="Siren kills with MG" (номер — следующий свободный).
  2. Перк. В SRVetCommando.GetPerkProgressInt добавьте третье требование (ReqNum==2): 0 для уровней 0-4, 20 для 5-го, 50 для 6-го и выше, и верните Min(StatOther.RMGSirenKillsStat, FinalInt). В defaultproperties: NumRequirements=3 и Requirements(2)="Kill %x Sirens with PKM".
  3. Оружие. В damage type пулемёта засчитайте убийство:
static function AwardKill(KFSteamStatsAndAchievements KFStatsAndAchievements, KFPlayerController Killer, KFMonster Killed)
{
    if( Killed.IsA('ZombieStalker') )
        KFStatsAndAchievements.AddStalkerKill();
    if( Killed.IsA('ZombieSiren') )
        ServerStStats(KFStatsAndAchievements).AddMGSirenKill();
}

Порядок компиляции

Пулемёт обращается к ServerStStats из ServerPerksMut, поэтому его EditPackages должен стоять после EditPackages=ServerPerksMut (или класс оружия кладут прямо в ServerPerksMut). Пакет перков ServerPerksP компилируется раньше ServerPerksMut, поэтому в нём оружие проверяют по имени (IsA('PKM'), DmgType.Name == 'DamTypePKM'), а не по классу. Перед пересборкой удалите старые ServerPerks.u, ServerPerksP.u, ServerPerksMut.u.

Броня, сопротивления, командос и сборка ServerPerks 5

По темам «Отслеживание брони игрока», «Berserker resistance», «Вопросы по ServerPerks и его модификациям.» и «Помогите с компиляцией сп5» на форуме killingfloor.ru.

Защита бронёй. Насколько броня снижает урон, задаёт статическая функция GetBodyArmorDamageModifier класса перка (совет Freddy). Вариант VETAL для медика с уровнями выше 6-го: до 8-го уровня по 10% за уровень, с 9-го — постоянные 0.10:

static function float GetBodyArmorDamageModifier(KFPlayerReplicationInfo KFPRI)
{
 if ( KFPRI.ClientVeteranSkillLevel <= 8 )
 return 1.0 - (0.10 * float(KFPRI.ClientVeteranSkillLevel));
 if ( KFPRI.ClientVeteranSkillLevel >= 9 )
 return 0.10;
 return 1.00;
}

Сопротивления. Сопротивления перка к урону (например, у берсерка к желчи блоата DamTypeVomit и общий резист) задаёт функция ReduceDamage в его классе, например в SRVetBerserker:

static function int ReduceDamage(KFPlayerReplicationInfo KFPRI, KFPawn Injured, KFMonster DamageTaker, int InDamage, class<DamageType> DmgType)

Если у берсерка не работают сопротивления, сначала проверьте, есть ли эта функция в SRVetBerserker. По словам 3xzet, в ServerPerks после 6-го уровня резист сбрасывался на 1, и функцию приходилось править под уровни выше 6-го. Ошибка Unexpected end of file at end of Class при такой правке означает, что не совпадает число открывающих и закрывающих фигурных скобок.

Дальность подсветки здоровья мобов у командоса. В SRVetCommando.SpecialHUDInfo дистанция задана только для уровней 1-6 (case 5: и case 6: дают MaxDistanceSquared = 640000; // 100% (800 units)). LLIePLLIeHb добавил в конец того же switch ветку для уровней выше:

 default:
 MaxDistanceSquared = 921600; // 120% (960 units)
 break;

Сборка ServerPerks 5 из исходников. ServerPerks 5 состоит из трёх связанных пакетов — ServerPerks.u, ServerPerksP.u и ServerPerksMut.u; декомпилировать и собирать их нужно вместе. В [Editor.EditorEngine] после последней строки EditPackages= Pascal прописывал их в таком порядке (очерёдность важна):

EditPackages=ServerPerks
EditPackages=ServerPerksP
EditPackages=ServerPerksMut

Если сборка падает с Error, Redefinition of 'function AddKill' differs from original in KFSteamStatsAndAchievements, даже когда в коде ничего не меняли, по ответу с форума Tripwire (его нашёл VETAL) удалите из ServerStStats.uc функции AddKill, AddBloatKill, AddSirenKill и строку:

function KilledFleshpound(bool bWithMeleeAttack, bool bWithAA12, bool bWithKnife);

Урон в голову, волна с боссом, типы урона и подбор оружия

По темам «Вопрос по adddamage», «Ссылка на волну с патриком», «Регенерация хп или новый бонус для перка», «Прошу помощи...», «Linux+ServerPerks», «Хэдшотный урон.», «Подбор оружия», «[цикл статей] Разное» и «[цикл статей] Практическое использование мутатора VipItemMut» на форуме killingfloor.ru.

AddDamage или GetHeadShotDamMulti (разбор Flame). В KFMonster.TakeDamage сначала для любого урона вызывается AddDamage, а затем, если моб уже без головы или bIsHeadShot==true (и урон не DamTypeMelee), урон умножается на GetHeadShotDamMulti:

Damage = KFPRI.ClientVeteranSkill.Static.AddDamage(KFPRI, self, KFPawn(instigatedBy), Damage, DamageType);
...
Damage = float(Damage) * KFPRI.ClientVeteranSkill.Static.GetHeadShotDamMulti(KFPRI, KFPawn(instigatedBy), DamageType);

Бонус к урону в голову правильно прописывать в GetHeadShotDamMulti. У M99 (и арбалета) для попадания в голову есть отдельный тип урона: DamTypeM99HeadShot задан в DamageTypeHeadShot в defaultproperties класса M99Bullet, поэтому при проверке по имени указывайте именно его. Урон в голову дополнительно умножается на HeadShotDamageMult пули и одноимённый коэффициент в DamageType.

Level в статических функциях. В static-функциях перка нет Level, его берут из входных параметров: KFPRI.Level, P.Level, DamageTaker.Level (Flame). Локальная переменная создаётся заново при каждом вызове, поэтому логика вида «лечить раз в секунду» в статической функции не работает: её переносят, например, в SRHumanPawn с глобальной переменной (Dr_Killjoy). Отключить бонус на волне с боссом помогает bWaveBossInProgress, проверку ставят в начало функции:

if(KFGameType(DamageTaker.Level.Game).bWaveBossInProgress) return InDamage * 1.0;

В GetHeadShotDamMulti то же делается через KFGameType(P.Level.Game).bWaveBossInProgress.

Своё описание каждого уровня. Если правки SRLevelEffects/CustomLevelInfo не отображаются в игре, Flame советует переопределить в классе перка static function string GetCustomLevelInfo( byte cLevel ) и в switch( cLevel ) для каждого case присваивать S готовую строку описания, например "10% more HeadShotdamage with heavy rifles|...|Spawn with a VSS".

Прокачка встала на одном уровне. Значения статистики хранятся в int, больше 2147483647 (2^31-1) в стату не запишется (BAHO, Flame). У автора темы «Linux+ServerPerks» с прописанными вручную порогами в GetPerkProgressInt уровни переставали расти на 34-м при заполненной полоске; помогло удалить из ServerPerks RequirementScaling=1.000000 и все места, где на эту переменную что-то умножается.

Урон в голову в прокачку снайпера. GameRules от Dr_Killjoy засчитывают в AddSniperDamage только урон в голову от оружия снайпера; от damage type нужна только выставленная bSniperWeapon:

class DamageRegisterGameRule extends GameRules;
function int NetDamage( int OriginalDamage, int Damage, pawn injured, pawn instigatedBy, vector HitLocation, out vector Momentum, class<DamageType> DamageType )
{
 local class<KFWeaponDamageType> KFWDT;
 local ServerStStats InstigatedByStats;
 local KFMonster KFM;
 local int AwardDamage;
 if ( NextGameRules != None )
 Damage = NextGameRules.NetDamage( OriginalDamage,Damage,injured,instigatedBy,HitLocation,Momentum,DamageType );
 if ( Damage <= 0 ) return Damage;
 KFM = KFMonster(Injured);
 KFWDT = class<KFWeaponDamageType>(DamageType);
 InstigatedByStats = Static.GetServerStStats(InstigatedBy);
 if ( KFM != none && Static.IsSharpDamageType(KFWDT) && InstigatedByStats != none
 && ( KFM.bDecapitated || KFM.IsHeadshot(HitLocation,Momentum,1.00) ) )
 {
 AwardDamage = Clamp(Damage,1,Injured.Health);
 InstigatedByStats.AddSniperDamage(AwardDamage);
 }
 return Damage;
}
static function ServerStStats GetServerStStats(Pawn P)
{
 local PlayerController PC;
 local SteamStatsAndAchievementsBase KFStats;
 if ( P == none ) return none;
 if ( P.Controller != none ) PC = PlayerController(P.Controller);
 if ( PC == none ) return none;
 if ( PC.SteamStatsAndAchievements != none ) KFStats = PC.SteamStatsAndAchievements;
 if ( KFStats == none ) return none;
 return ServerStStats(KFStats);
}
static function bool IsSharpDamageType(class<KFWeaponDamageType> KFWDT)
{
 local class<KFWeapon> KFW;
 local class<KFWeaponPickup> KFWP;
 if ( KFWDT == none || !KFWDT.default.bSniperWeapon || !KFWDT.bCheckForHeadshots ) return false;
 KFW = class<KFWeapon>(KFWDT.default.WeaponClass);
 if ( KFW == none ) return true;
 KFWP = class<KFWeaponPickup>(KFW.default.PickupClass);
 return KFWP == none || KFWP.default.CorrespondingPerkIndex == class'KFVetSharpshooter'.default.PerkIndex;
}

GameRules запускаются из мутатора. Просто заспавнить их мало: их нужно добавить в цепочку, иначе до них не дойдут вызовы (пример Dr_Killjoy, вместо FFPenaltyRules подставьте свой класс):

function PostBeginPlay()
{
 local GameRules G;
 Super.PostBeginPlay();
 G = spawn(class'FFPenaltyRules');
 if ( Level.Game.GameRulesModifiers == None )
 Level.Game.GameRulesModifiers = G;
 else Level.Game.GameRulesModifiers.AddGameRules(G);
}

Поднять оружие может только нужный перк (HATAXA, Flame). В своих GameRules переопределите OverridePickupQuery. Возвращаемое значение говорит, обрабатывает ли функция этот предмет, а решение «поднимать или нет» передаётся через out-параметр bAllowPickup (byte: 0 или 1). Здесь M99 поднимают только снайперы с 6-го уровня:

function bool OverridePickupQuery(Pawn Other, Pickup item, out byte bAllowPickup)
{
 local KFPlayerReplicationInfo KFPRI;
 local Name VeterancyName;
 local int VeterancyLevel;
 if(Other!=none && Other.PlayerReplicationInfo!=none)
 KFPRI=KFPlayerReplicationInfo(Other.PlayerReplicationInfo);
 if(KFPRI!=none)
 {
 VeterancyName=KFPRI.ClientVeteranSkill.Name;
 VeterancyLevel=KFPRI.ClientVeteranSkillLevel;
 }
 if(item.IsA('M99Pickup') && (VeterancyName!='SRVetSharpshooter' || VeterancyLevel<6))
 {
 bAllowPickup=false;
 return true;
 }
 if ( (NextGameRules != None) && NextGameRules.OverridePickupQuery(Other, item, bAllowPickup) )
 return true;
 return false;
}

Типы урона и перки (статья НАТАХИ «Типы урона (DamageTypes)»). Весь урон в KF наследуется от KFMod.KFWeaponDamageType (цепочка Engine.DamageType → UnrealGame.WeaponDamageType → KFMod.KFWeaponDamageType), а к какому перку относится урон, задают переменные damage type:

ПеркПеременнаяСтатистика
БерсеркbIsMeleeDamage (тип урона DamTypeMelee)KFStatsAndAchievements.AddMeleeDamage
ОгнемётчикbDealBurningDamageKFStatsAndAchievements.AddFlameThrowerDamage
ПодрывникbIsExplosiveKFStatsAndAchievements.AddExplosivesDamage

Урон от огня складывается из первоначального урона и урона от дальнейшего горения: FlareRevolver → DamTypeFlareRevolver + DamTypeFlareRevolver, MAC10 → DamTypeMAC10MPInc + DamTypeMAC10MPInc, Trenchgun → DamTypeTrenchgun + DamTypeTrenchgun, FlameThrower → DamTypeBurned + DamTypeFlamethrower, HuskGun → DamTypeHuskGun + DamTypeFlamethrower, FlameNade → DamTypeFlamethrower + DamTypeFlamethrower. Задавая мобу устойчивость к огню, учитывайте оба типа. DamTypeTrenchgun содержит и bDealBurningDamage, и bIsPowerWeapon, то есть качает и саппорта, и огнемётчика; DamTypeHuskGunProjectileImpact и DamTypeFlareProjectileImpact не поджигают, но идут в прокачку. Урон от попадания снаряда подрывника наследует DamTypeRocketImpact, а тот — DamTypeKFSnipe, поэтому такие попадания качают не подрывника, а снайпера.

Броня в 1.5 раза прочнее для VIP (Команда 2/5). Вместо 150 единиц брони проще сделать обычную броню в 1.5 раза прочнее через изменённую GetBodyArmorDamageModifier в каждом классе SRVet* (или в SRVeterancyTypes), проверяя наличие предмета VipItem в инвентаре вспомогательной функцией HasItem (её кладут в абстрактный класс Utilities):

static function bool HasItem(Pawn P, Name ItemName)
{
 local Inventory Inv;
 for(Inv=P.Inventory;Inv!=None;Inv=Inv.Inventory)
 {
 if(Inv.Name==ItemName)
 return true;
 }
 return false;
}
static function float GetBodyArmorDamageModifier(KFPlayerReplicationInfo KFPRI)
{
 if(class'Utilities'.static.HasItem(PlayerController(KFPRI.Owner).Pawn,'VipItem'))
 return 0.66;
 return 1.00;
}

Вариант KFPRI.Owner.Pawn из исходной статьи, по замечанию Essence, не компилируется: нужно приведение PlayerController(KFPRI.Owner).