Кодинг перков в ServerPerks: бонусы оружия, стартовое снаряжение, свой перк и новое требование прокачки
На кастомных серверах Killing Floor 1 перки почти всегда реализованы через ServerPerks: каждый перк — отдельный класс SRVet*.uc, наследник SRVeterancyTypes, в пакете ServerPerksP. Здесь собраны четыре темы с форума killingfloor.ru (2011-2012): как прописать бонусы оружию (kok-s), как выдать оружие на старте (NeKRoN), как сделать свой перк (akf817gp) и как добавить новое требование прокачки (Flame). Нужны исходники ServerPerks нужной версии и умение компилировать пакеты.
Бонусы оружию
Каждая функция в классе перка отвечает за свой вид бонуса. Шаблон везде одинаковый: проверка, к какому оружию относится вызов, затем к какому уровню, затем возврат множителя. Чтобы дать перку чужой бонус (например, берсерку броню медика), найдите функцию этого бонуса в классе другого перка, скопируйте её в класс своего перка и поправьте значения; новую функцию вставляйте перед блоком defaultproperties (тема «Как добавить перку скил другого перка?», форум killingfloor.ru).
| Бонус | Функция | Как проверяется оружие | Что возвращает |
|---|---|---|---|
| Патронов в магазине | GetMagCapacityMod | AK47AssaultRifle(Other) != none | множитель, например 1.10 |
| Переносимых патронов | AddExtraAmmoFor | AmmoType == class'AK47Ammo' | множитель |
| Патронов из ящиков | GetAmmoPickupMod | AK47Ammo(Other) != none | множитель |
| Урон | AddDamage | DmgType == class'DamTypeAK47AssaultRifle' | float(InDamage) * 1.05 или формула |
| Скидка на оружие | GetCostScaling | Item == class'AK47Pickup' | доля цены |
| Скидка на патроны | GetAmmoCostScaling | Item == class'AK47Pickup' | доля цены |
| Отдача | ModifyRecoilSpread | AK47AssaultRifle(Other.Weapon) != none | Recoil = 0.90 (меньше — слабее отдача) |
| Скорость перезарядки | GetReloadSpeedModifier | AK47AssaultRifle(Other) != none | множитель |
| Скорострельность | GetFireSpeedMod | AK47AssaultRifle(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(...) разницы он, по его словам, не заметил.
Свой перк
- Основа. Скопируйте ближайший по смыслу перк, например
SRVetSupportSpec.uc, в папкуClassesсвоего пакета и переименуйте файл и класс:class A3MNC extends SRVeterancyTypes abstract;. - Прокачка.
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.
- Описание и иконки в
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).
-
Иконка. Экспортируйте стандартную иконку из
KillingFloorHUD.utx(группа Perks) в KF SDK, нарисуйте свою с альфа-каналом (белое — видимая часть), сохраните в DDS (DXT с альфой) и импортируйте в свой.utx, например в группуPerk_Icons. Нужны две иконки: обычная (красная) и золотая. Подробнее — в статье Как сделать свои значки перков ServerPerksV7. -
Подключение. Скомпилируйте пакет, добавьте его в
ServerPackagesи пропишите перк вServerPerks.iniстрокойPerks=ИмяПакета.ИмяКлассарядом со стандартными. Готовый перк из чужого мода подключается так же.
Новое требование прокачки: убийства сирен из пулемёта
Пример Flame: коммандос получает 5-й и 6-й уровень только после 20 и 50 убитых из пулемёта сирен. Новая статистика должна храниться, реплицироваться клиенту, отображаться в меню и увеличиваться при убийстве.
ServerPerksMut.StatsObject. Добавьтеvar config int MGSirenKillsStat;и допишите поле в конец строки вGetSaveData()и в конец разбора вSetSaveData()(MGSirenKillsStat = GetNextValue(S);). Если используется удалённая база, новое поле ставьте в конец, иначе существующие записи сдвинутся.ServerPerks.ClientPerkRepLink. Переменнаяvar int RMGSirenKillsStat;и её репликация:reliable if( Role==ROLE_Authority && bNetOwner ) RMGSirenKillsStat;.ServerPerksMut.ServerStStats. ВRepCopyStats()добавьтеRep.RMGSirenKillsStat = MyStatsObject.MGSirenKillsStat;и функцию-счётчик:
function AddMGSirenKill()
{
bHasChanged = true;
Rep.RMGSirenKillsStat++;
if( MyStatsObject!=None )
MyStatsObject.MGSirenKillsStat++;
DelayedStatCheck();
}
ServerPerks.SRStatList. ВInitList()добавьте строкуStatProgress[27] = string(L.RMGSirenKillsStat);, а вdefaultproperties—ProgressName(27)="Siren kills with MG"(номер — следующий свободный).- Перк. В
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". - Оружие. В 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 |
| Огнемётчик | bDealBurningDamage | KFStatsAndAchievements.AddFlameThrowerDamage |
| Подрывник | bIsExplosive | KFStatsAndAchievements.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).