伤害计算
以下所有讨论均基于幻兽帕鲁 v1.0.2.101103 版本
伤害乘区
我们按顺序
- 给出若干定义
- 给出伤害计算公式以及适用范围
- 解释各个乘区的计算公式以及对应参数的数值来源
- 提供省流近似公式
- 补充各乘区详细分析过程
变量、参数的定义
- 定义顺序按照代码中的出现顺序,非最终组合的计算顺序。省流近似公式部分中会提供按照语义组织的乘区列表。
- 定义这么多乘区的意义在于不改变任何语义地完整给出幻兽帕鲁中的原始伤害计算公式:浮点数的非结合性、下取整、整除和乘以倒数再取整的不等价性都需要考虑在内。如果不想深入这些细节可以直接看省流近似公式。不过当你看到我定义了这种乱七八糟的
passiveCompositeRate1和passiveCompositeRate2大概就能明白幻兽帕鲁关于伤害计算的原始代码实际上相当糟糕了。 - 这些变量、函数并非都必然出现在游戏内,并且大部分无法逆向得到符号(因此大部分为我自己根据语义命名)。为了叙述的方便性,我会在保证 C++ 代码最终执行结果不变(游戏内伤害显示数值) 的情况下进行等价改写。
nativeDamageValue : int32 // 初始伤害
palDamageRate : float // 帕鲁乘区
palDefenderDamageRate : float // 帕鲁防御方乘区
palDefenderReceiveDamageRate : float
palDefenderAdditionalReceiveDamageRate : float
palAttackerDamageRate : float // 帕鲁攻击方乘区
palAttackerInflictDamageRate : float
palAttackerAdditionalInflictDamageRate : float
elementStatusAilmentRate : float // 异常状态属性乘区
elementWeakRate : float // 属性克制乘区
passiveCompositeRate1 : float // 被动效果组合乘区1
defPassiveElementResistRate : float // 防御方被动属性减伤乘区
defExposionResistRate : float // 爆炸伤害减伤乘区
defPassiveExplosionResistRate : float // 防御方被动爆炸伤害减伤乘区
defFoodStatusExplosionResistRate : float // 防御方食物效果爆炸伤害减伤乘区
atkPassiveElementBoostRate : float // 进攻方被动属性增伤乘区
atkPassiveElementWeaknessBoostRate : float // 攻击方被动属性克制额外增伤乘区
atkPassiveStatusAilmentBoostRate : flaot // 攻击方被动异常状态额外增伤乘区
atkPassiveBodyPartBoostRate : float // 攻击方被动身体部位弱点额外增伤乘区
atkPassiveBodyPartBoostRate1 : float
atkPassiveBodyPartBoostRate2 : float
atkPassiveNonBattleBoostRate : float // 攻击方被动对非战斗状态增伤乘区
atkPassiveWeaponTypeBoostRate : float // 攻击方被动武器类型增伤乘区
atkPassiveWeaponLastBulletBoostRate : float // 攻击方被动弹匣最后子弹增伤乘区
weaponBodyPartRate : float // 武器攻击身体部位乘区
sleepBoostRate : float // 对睡眠目标增伤乘区
levelDiffResistRate : float // 等级差乘区
arenaRate : flaot // 竞技场乘区
worldSettingRate : float // 世界设置乘区
elementMatchBoostRate : float // 属性匹配增伤乘区
pvpFlyBoostRate : float // pvp 模式对飞行目标增伤乘区
pvpOwnedPalRate : float // pvp 模式对玩家帕鲁乘区
passiveCompositeRate2 : float // 被动效果组合乘区2
atkYakushimaBoostRate : float // 攻击方泰拉瑞亚武器增伤乘区
atkPssivePartnerSkillBoostRate : float // 进攻方被动伙伴技能增伤乘区
gameSettingPalAttackerRate : float // 游戏配置帕鲁攻击乘区
damageRatePerCollision : float // 多段伤害每段倍率乘区
wazaLevelRate : float // 技能类型伤害的等级修正乘区
defDefense : int32 // 防御方防御力
applyDefenseRate : uint64(uint64, int32) // 施加防御力乘区的函数,用法形如 dmg = apply(dmg, def)
randomRate : float // 随机偏移乘区
gameSettingAttackTypeRate : float // 游戏配置攻击类型乘区
damamgeModified : uint64 // 经过乘区修正的伤害
核心计算公式
floatToUInt64Trunc表示 MSVC 的 float 到 uint64 的安全转换,语义为向零截断,在正数情况下等价于下取整。- 给出公式中的整数除法触发符合 C++ 原生整除语义。
完整公式:
damageModified = uint64(nativeDamageValue);
damageModified = floatToUInt64Trunc(damageModified * elementWeakRate);
damageModified = floatToUInt64Trunc(damageModified * elementMatchRate);
damageModified = floatToUInt64Trunc(damageModified * passiveCompositeRate1);
damageModified = floatToUInt64Trunc(damageModified * weaponBodyPartRate);
damageModified = floatToUInt64Trunc(damageModified * damageRatePerCollision);
damageModified = floatToUInt64Trunc(damageModified * wazaLevelRate);
damageModified = applyDefenseRate(damageModified, defDefense);
damageModified = floatToUInt64Trunc(damageModified * sleepBoostRate);
damageModified = floatToUInt64Trunc(damageModified * elementStatusAilmentRate);
damageModified = floatToUInt64Trunc(damageModified * palDamageRate);
damageModified = floatToUInt64Trunc(damageModified * pvpFlyBoostRate);
damageModified = floatToUInt64Trunc(damageModified * pvpOwnedPalRate);
damageModified = floatToUInt64Trunc(damageModified * randomRate);
damageModified = floatToUInt64Trunc(damageModified * levelDiffResistRate);
damageModified = floatToUInt64Trunc(damageModified * gameSettingAttackTypeRate);
damageModified = floatToUInt64Trunc(damageModified * passiveCompositeRate2 * gameSettingPalAttackerRate);
damageModified = floatToUInt64Trunc(damageModified * arenaRate);
damageModified = floatToUInt64Trunc(damageModified * worldSettingRate);
重要细节:
nativeDamage- 这个伤害公式只负责伤害乘区,不包括攻击力乘区和护盾。
- 这个伤害公式不包括 dot 伤害,比如燃烧中毒;目前确认的包括玩家武器攻击、帕鲁技能攻击
世界设置 bIsPvP = False 且游戏内不位于竞技场时 pvpFlyBoostRate、pvpOwnedPalRate、arenaRate 为 1.0f。
乘区细节
palDamageRate 帕鲁乘区
palDamageRate = palDefenderDamageRate * palAttackerDamageRate;
palDefenderDamageRate = palDefenderReceiveDamageRate * palDefenderAdditionalReceiveDamageRate;
palAttackerDamageRate = palAttackerInflictDamageRate * palAttackerAdditionalInflictDamageRate;
palDefenderAdditionalReceiveDamageRate、palAttackerAdditionalInflictDamageRate这两个乘区由对应状态确定,默认为1.0f,目前对应状态的变化机制尚不明晰。- 野生帕鲁定义为非伙伴帕鲁(伙伴队伍中)且非工会帕鲁(据点中工作)
- 每一种帕鲁(包括人类帕鲁)都有一条属性
EnemyReceiveDamageRate和EnemyInflictDamageRate - 如果攻击方是野生帕鲁,那么
palAttackerInflictDamageRate取该帕鲁的EnemyReceiveDamageRate; - 如果防御方是野生帕鲁,那么
palDefenderReceiveDamageRate取该帕鲁的EnemyInflictDamageRate; - 如果防御方是伙伴帕鲁,那么
palDefenderReceiveDamageRate取0.75
elementStatusAilmentRate
elementWeakRate 属性克制乘区
乘区详细分析
定义和通用分析
后面的乘区分析会经常使用这里的内容。乘区分析的文本默认读者已经了解这一部分的内容。
AActor *defender
AActor *attacker
FPalDamageInfo *info
UGameInstance 是进程意义上的单例类,所有 AActor 都有一个 GetGameInstance() 成员函数,当该对象存在于某个 UWorld 时该函数会返回 UGameInstance 单例。UPalGameInstance 继承了 UGameInstance。
配置相关
UPalGameInstance 单例持有属性 DatabaseCharacterParameter : UPalDatabaseCharacterParameter。UPalDatabaseCharacterParameter 是一个数据库类,其持有很多 UDataTable。UDataTable 有查询函数 PalDataTable_FindRow。
CharParamDB_FindRow(db, id)这个查询函数依次查找MonsterParameterDataTable和HumanParameterDataTable(都失败返回NULL),这两张表的行型均为FPalCharacterParameterDatabaseRow。形如CharParamDB_GetXxx(db, id)基本上就是CharParamDB_FindRow(db, id)+ 访问数据行属性
UPalGameInstance 单例持有属性 GameSetting : UPalGameSetting。UPalGameSetting 是一个配置类。
UPalGameInstance 单例持有属性 PassiveSkillManager : UPalPassiveSkillManager。这也是一个配置类,语义侧重明显。
UPalGameInstance 单例持有属性 ItemIDManager : UPalItemIDManager。这也是一个配置类,语义侧重明显。
Helper PalGameInstance_Get:
UPalGameInstance *PalGameInstance_Get(UObject *context)
{
UObject *obj;
(void)context;
obj = (UObject *)GetGameInstanceFromWorld();
if (obj != NULL &&
UClass_IsA(UObject_Class(obj), (UClass *)PalGameInstance_StaticClass()))
return (UPalGameInstance *)obj;
return NULL;
}
UPalUtility 中封装的 Helper UPalGameSetting *UPalUtility::PalUtility_GetGameSetting(UObject *worldContext),失败时回退到 CDO
UPalGameSetting *PalUtility_GetGameSetting(UObject *worldContext)
{
UPalGameInstance *gameInstance = PalGameInstance_Get();
if (gameInstance != NULL) {
UPalGameSetting *settings =
(UPalGameSetting *)RAW_PTR(gameInstance, OFF_GAMEINSTANCE_GAMESETTING);
if (settings != NULL)
return settings;
}
{
UClass *cls = (UClass *)PalGameSetting_StaticClass();
if (RAW_PTR(cls, OFF_UCLASS_CLASS_DEFAULT_OBJECT) == NULL)
((void (*)(UClass *))VTBL_FUNC(cls, UE_VTBL_GET_DEFAULT_OBJECT))(cls);
return (UPalGameSetting *)RAW_PTR(cls, OFF_UCLASS_CLASS_DEFAULT_OBJECT);
}
}
其他配置类大多都存在逻辑类似的 GetXxx 函数
UPalCharacterParameterComponent
UPalCharacterParameterComponent 是主要承载角色(包括帕鲁和玩家)状态信息的组件,其属性 IndividualParameter : UPalIndividualCharacterParameter 侧重非实时的状态信息。
Helper PalUtility_IsWildPal,(忽略有效性检查)其依次检查是否有训练师(属于伙伴帕鲁)、是否在公会中有过归属记录(属于工会帕鲁),否则就是野生帕鲁:
bool PalUtility_IsWildPal(AActor *actor)
{
UPalCharacterParameterComponent *paramComp;
UPalGroupManager *groupManager;
if (!UObject_IsValid(actor)) {
return false;
}
paramComp = (UPalCharacterParameterComponent *)Actor_GetComponentByClass(
actor, (UClass *)PalCharacterParameterComponent_StaticClass());
if (UObject_IsValid(paramComp) && GuildPal_HasTrainer(paramComp)) {
return false;
}
groupManager = (UPalGroupManager *)PalGroupManager_GetForWorld(actor);
if (!UObject_IsValid(groupManager)) {
return false;
}
return !GuildPal_CheckMembership(groupManager, actor);
}
UPassiveSkillComponent
UPassiveSkillComponent 是主要承载角色(包括帕鲁和玩家)被动效果的组件,包括帕鲁伙伴技能的被动效果、词条的被动效果、遗物(雕像)效果、防具头盔饰品被动效果、当前使用武器的被动效果。
- 被动技能的数据在
DT_PassiveSkill_Main中。被动技能本身对应FPalPassiveSkillEffectInfos,被动技能的效果对应FPalPassiveSkillEffect,被动技能的效果类别枚举EPalPassiveSkillEffectType。 - 该组件自身维护了一个
SkillInfos : TArray<PalPassiveSkillEffectInfos>,队伍帕鲁伙伴技能的的被动效果、词条的被动效果、当前使用武器的被动效果会被动态地在这个数组中被维护。遗物效果、防具头盔饰品的被动效果会在查询函数QueryEffectSum中通过CollectExternalEffects收集,这是玩家独有的。
QueryEffectSum 和 CollectExternalEffects(这两个函数都没有被声明为 UFUNCTION,usmap 没有对应符号,所以这是根据语义取的名字):
- 由于很多不同的被动具有相同效果只是数值不同,该组件支持的按效果查询功能必须遍历同效果的被动然后求和,实现这个功能的函数是
PalPassiveSkillComponent::QueryEffectSum(base, effectType, includeEquip, filter)。大致过程为-
在共享读独占写的锁下拷贝
UPalPassiveSkillComponent组件的不具名属性xxx : TArray<FPalPassiveSkillEffect>(由于没有声明UPROPERTY所以被 usmap 识别成了属性SkillInfosMirror的 Padding,这是合理的,因为该属性的功能大概是作为某种加速 cache)到snapshots中(那一坨代码大概是TArray的复制操作被 inline 了)。这个不具名的数组在语义上是收集SkillInfos : TArray<FPalPassiveSkillEffectInfos>中的所有元素的属性SkillEffectArray : TArray<FPalPassiveSkillEffect>中的所有元素 -
然后通过
PalPassiveSkillComponent::CollectExternalEffect收集额外被动效果并 append 到snapshot中 -
构建
PassiveEffectQueryFilter的 filter 施加操作的委托(代码等价于TDelegate<bool(const FPalPassiveSkillEffect&)> filterDelegate; filterDelegate.BindStatic(&PassiveEffect_FilterCallback, component, filterCopy);),目的是封装掉filterCopy和component参数。不把effectType参数也封装掉的原因是后续的AccumulateEffectSum和SumEffectsByType需要用到它。 -
最后通过
PalPassiveSkillManager::AccumulateEffectSum(同样也是自己取的名字;注意这不是UPalPassiveSkillComponent的成员函数,是UPalPassiveSkillManager中的 helper)过滤 + 求和。被求和的数组snapshots类型为TArray<FPalPassiveSkillEffect>。PalPassiveSkillManager::AccumulateEffectSum本身只是对PalPassiveSkillManager::SumEffectsByType(同样也是自己取的名字;同样不是UPalPassiveSkillComponent的成员函数)的一层薄封装,封装了对于不同effectType的元素求和得到sum后的返回值:基于base加sum;基于base乘(0.01f * sum + 1)。- 加和乘的判别方式是遍历判断
effectType是否属于数组accumSumEffectTypeList accumSumEffectTypeList是UPalPassiveSkillManager的0x490偏移处。同样位于非反射区不存在字段名。C++native 的InClassConstructor中将其置为空数组。- 通过全局检索该字段的直接写入形式 register + instant_fixed_offset 可以找到
UPalPassiveSkillManager::Initialize。其根据DT_PassiveSkillEffectCondition对应数据行(键为EPalPassiveSkillEffectType的文本形式)是否满足bIsFixedValue(不存在视为False)来决定是否加入accumSumEffectTypeList。通过 display/pal_info 网页中被动技能效果筛选即可得到完整表格。
- 加和乘的判别方式是遍历判断
-
PalPassiveSkillManager::SumEffectsByType执行真正的effectType检查、过滤检查(具体机制尚未了解)、求和。被求和的数组类型为TArray<FPalPassiveSkillEffect>。求和根据不同的effectType类型分为三路-
取最大值路:取元素的属性
Value : float的最大值- 是否走这条路取决于
effectType是否属于数组sumMaxEffectTypeList。 sumMaxEffectTypeList位于UPalPassiveSkillManager的0x480偏移处,和accumSumEffectTypeList一样位于非反射区不存在字段名,并且 C++ native 的 InClassConstructor 也将其置为空数组- 类似地
UPalPassiveSkillManager::Initialize中根据DT_PassiveSkillEffectCondition对应数据行是否满足bIsHighestOnly(不存在视为False)来决定是否加入sumMaxEffectTypeList数组。
- 是否走这条路取决于
-
分桶,桶间求的和,桶内求元素的属性
Value : float最大值:按照元素的属性PartnerSkillParams : PalPassivePartnerSkillInvocationParams的属性TribeId : EPalTribeID作为分桶的键值。、- 是否走这条路:
effectType不属于数组sumMaxEffectTypeList- AND
effectType->IsPartnerSkill == True,即必须是伙伴技能的被动效果 - AND
effectType->IsStackablePartnerSkillBySameTribe == False(大概由对应的被动技能类型的同名字段决定,在FPalPassiveSkillEffect构建时传入),即同种族帕鲁的特定伙伴技能不叠加(组内求最大值) - AND
GemeSetting->bIsStackablePartnerSkillBySameTribe == False。这是一个配置量,其 C++ 默认值为False,蓝图未覆写默认值。
- 是否走这条路:
-
求和路:取元素的属性
Value : float的和。- 是否走这条路:otherwise
-
-
float PalPassiveSkillComponent_QueryEffectSum(
UPalPassiveSkillComponent *component,
float base,
uchar effectType, /* EPalPassiveSkillEffectType */
bool includeEquip,
PassiveEffectQueryFilter *filter)
{
UPalPassiveSkillManager *manager;
FPalPassiveSkillEffectTArray snapshot;
FPalPassiveSkillEffectTArray external;
PassiveEffectQueryFilter filterCopy;
void *delegate;
void *filterDelegateHolder[2]; /* TDelegate 本体(实例指针 + 函数指针槽) */
float result;
manager = PassiveSkillManager_GetInstance();
if (!UE_IsValid(manager)) { FILTER_FREE_INNER_ARRAYS(filter); return base; }
AcquireSRWLockShared((PSRWLOCK)((char *)component + OFF_PASSIVE_COMP_CACHE_LOCK));
snapshot.Data = NULL; snapshot.Count = 0;
PassiveEffect_ArrayCopy(&snapshot,
RAW_PTR(component, OFF_PASSIVE_COMP_CACHE_ARRAY),
RAW_INT(component, OFF_PASSIVE_COMP_CACHE_COUNT), 0);
ReleaseSRWLockShared((PSRWLOCK)((char *)component + OFF_PASSIVE_COMP_CACHE_LOCK));
if (includeEquip) {
PassiveEffect_CollectExternal(component, &external);
TArray_AppendEffects(&snapshot, external.Data, external.Count);
external.Count = 0;
TArray_FreeContents(&external);
}
PassiveEffect_FilterCopy(&filterCopy, filter);
delegate = FMemory_Malloc(0x58, 8);
*(void **)delegate = &PTR__purecall_147883ce0;
DelegateInstance_MovePayload(delegate, component, &filterCopy);
*(void **)delegate = &PTR_FUN_147883d00;
TDelegate_StoreFunc(&filterDelegateHolder, PassiveEffect_FilterCallback);
result = PassiveEffect_Accumulate(manager, base, effectType, &snapshot);
Delegate_Release(delegate); TArray_FreeContents(&snapshot); FILTER_FREE_INNER_ARRAYS(filter);
return result;
}
float PalPassiveSkillManager_AccumulateEffectSum(
UObject *manager,
float base,
uchar effectType, /* EPalPassiveSkillEffectType */
const FPalPassiveSkillEffectTArray *snapshot)
{
const uchar *typeList, typeListEnd;
float sum = (float)PassiveEffect_SumCore(manager, effectType, snapshot, &filterDelegate);
typeList = (const uchar *)RAW_PTR(manager, OFF_MANAGER_ADDITIVE_TYPES);
typeListEnd = typeList + RAW_INT(manager, OFF_MANAGER_ADDITIVE_TYPES_NUM);
for (; typeList != typeListEnd; ++typeList)
if (*typeList == effectType) { return base + sum; /* 加算型 */ }
return (sum * PASSIVE_EFFECT_PERCENT_SCALE + PASSIVE_EFFECT_BASE_ONE) * base; /* 百分比型 */
}
float PalPassiveSkillManager_SumEffectsByType(UObject *manager, uchar effectType,
const FPalPassiveSkillEffectTArray *snapshot, void *filterDelegate)
{
float maxValue = 0.0f; float addSum = 0.0f; float dedupSum = 0.0f;
TSet dedupSet;
UPalGameSetting *gameSetting = (UPalGameSetting *)PalUtility_GetGameSetting();
bool allowTribeStack = RAW_BYTE(gameSetting, OFF_GAMESETTING_STACK_SWITCH) != 0;
const FPalPassiveSkillEffect *effect = snapshot->Data;
const FPalPassiveSkillEffect *snapshotEnd =
(const FPalPassiveSkillEffect *)((const char *)effect + (longlong)snapshot->Count * 0x98);
for (; effect != snapshotEnd; effect = (const FPalPassiveSkillEffect *)((const char *)effect + 0x98)) {
if (RAW_BYTE(effect, 0x00) != effectType) { continue; }
if (filterDelegate != NULL && !Delegate_Test(filterDelegate, effect)) { continue; }
if (ByteList_Contains(RAW_PTR(manager, OFF_MANAGER_MAX_MODE_TYPES),
RAW_INT(manager, OFF_MANAGER_MAX_MODE_TYPES_NUM),
RAW_BYTE(effect, 0x00))) {
float v = RAW_FLOAT(effect, 0x04);
if (v > maxValue) { maxValue = v; }
continue;
}
if (RAW_BYTE(effect, 0x28) == 0 || RAW_BYTE(effect, 0x29) != 0 || allowTribeStack) {
addSum += RAW_FLOAT(effect, 0x04);
}
else {
TSet_InsertMax(&dedupSet, *(const ushort *)((const char *)effect + 0x30), RAW_FLOAT(effect, 0x04));
}
}
dedupSum = TSet_SumValues(&dedupSet); TSet_FreeContents(&dedupSet);
return maxValue + addSum + dedupSum;
}
void UPalPassiveSkillManager_Initialize(UPalPassiveSkillManager *mgr)
{
// ellipsis
UDataTable *condTable = *(UDataTable **)((char *)mgr + OFF_MANAGER_CONDITION_DATATABLE);
if (condTable != NULL) {
TArray /* <FName> */ rowNames;
DataTable_EnumerateRowNames(condTable, &rowNames);
for (FName *rowName = rowNames.Data; rowName != rowNames.Data + rowNames.Num; ++rowName) {
longlong enumVal = UEnum_GetValueByName(PalEnum_PassiveSkillEffectType(), *rowName, 0);
uchar effectType = (enumVal != -1) ? (uchar)enumVal : 0;
FPalPassiveSkillEffectConditionDataRow *rowData = DataTable_FindRowByName(condTable, *rowName);
/* bIsHighestOnly → 取最大值名单(SumEffectsByType 路 1 消费)。 */
if (RAW_BYTE(rowData, OFF_COND_ROW_HIGHEST_ONLY) != 0) {
TArray<byte> *list = (TArray<byte> *)((char *)mgr + OFF_MANAGER_MAX_MODE_TYPES);
TArrayByte_EnsureCapacity(list, list->Num);
list->Data[list->Num++] = effectType;
}
/* bIsFixedValue → 加算型名单(AccumulateEffectSum 消费)。 */
if (RAW_BYTE(rowData, OFF_COND_ROW_FIXED_VALUE) != 0) {
TArray<byte> *list = (TArray<byte> *)((char *)mgr + OFF_MANAGER_ADDITIVE_TYPES);
TArrayByte_EnsureCapacity(list, list->Num);
list->Data[list->Num++] = effectType;
}
}
}
}
CollectExternalEffects用于收集角色为玩家时的一些额外的被动技能(注意不是被动效果)。收集的结果为outHandles : TArray<FName>,数据来源是UPalPassiveSkillManager处理相关数据表格(DT_ItemDataTable)得到的一系列数据对象。FPalItemData中保存了两个指针,StaticData : UPalStaticItemDataBase *,另一个DynamicData : UPalDynamicItemDataBase *,其中UPalStaticItemDataBase(或某个未经反射的派生类)包含该物品四个被动技能名称的字段(未经反射)。具体过程为-
根据
UPalPassiveSkillComponent组件的 owner 来判断角色是否为玩家。以及一系列容错的有效性判断。 -
利用
PassiveExternal_KeyBuild获取玩家角色对应的的FGuid。方式为- 通过
PassiveExternal_GetHelper找到玩家角色对应的APalPlayerState:遍历AGameStateBase::PlayerArray : TArray<APlayerState*>对比元素的属性PawnPrivate和目标角色。失败时回退到玩家角色的属性CachedPlayerState。 - 获取该
APalPlayerState的属性PlayerUId : FGuid
- 通过
-
PassiveExternal_EnumSources又根据玩家的FGuid来获取玩家的APalPlayerCharacter和APalPlayerState(直接复用通用性高的接口所以显得很冗余)来收集三方面的信息-
当前持有武器
- 获取武器组件
UPalShooterComponent里面维护的当前武器(对应属性没有被反射) - 利用
UPalItemIDManager查询对应武器物品的FPalItemData的StaticData中的四个被动技能名称字段并收集到outHandles中(对应DT_ItemDataTable中数据行的四个字段PassiveSkillName/2/3/4)
- 获取武器组件
-
背包的饰品、防具等
- 首先通过
PassiveExternal_GetInventoryData(大概又是一个通用接口,信息获取链路是UPalPlayerManager / UPalGameState到UPalPlayerState到InventoryData)获取UPalPlayerInventoryData PalInventoryData_FindContainerBySlot(inventory, 4, &armorContainer)取其位置索引为 4 处的容器。我经过 UE4SS 脚本验证该容器包含了:防具、头饰、四个饰品、滑翔伞、护盾、帕鲁球模块这些槽位。不包含任何武器槽位。- 动态遍历该容器,依然利用
UPalItemIDManager通过类似的方式找到每个物品的四个被动技能名称,收集到handles中。由于是动态便利的,还加了一层防止重复统计的保护(以物品类型对应数据行的属性 id 作为键,Seen数组标识是否出现过),也可以当做是不能佩戴相同饰品(穿戴相同装备)的第二层防线。
- 首先通过
-
讨伐证明:
- 数据来源是
APalPlayerState的RecordData字段,过程类似懒得写了。
- 数据来源是
-
-
收集了所有被动技能的名称到
outHandles : TArray<FName>中之后,遍历它对每个元素调用ExternalEffect_FetchPending(命名并不符合语义)收集该被动技能的四个被动效果FPalPassiveSkillEffect并汇总到out中。(其内部调用PalPassiveSkillManager_GetSkillData获取UPalPassiveSkillManager预存的DT_PassiveSkill_Main数据表中对应数据行的四组属性EffectType/Value1/2/3/4,然后结合其他数据表格数据组合得到的FPalPassiveSkillEffect)
-
void PalPassiveSkillComponent_CollectExternalEffects(
UPalPassiveSkillComponent *component,
FPalPassiveSkillEffectTArray *out)
{
AActor *owner;
UPalPassiveSkillManager *manager;
PassiveExternalKey key; /* 16 字节查询键 = FGuid PlayerUId(round7 定论) */
ExternalHandleList handles; /* 平坦 qword 句柄列表(每记录 4 槽) */
longlong i;
out->Data = NULL; out->Count = 0;
owner = (AActor *)RAW_PTR(component, 0x0090); /* UActorComponent Owner(未反射偏移) */
if (owner == NULL) { return; }
if (!UClass_IsA(UObject_Class(owner), (UClass *)PalPlayerCharacter_StaticClass())) { return; }
manager = PassiveSkillManager_GetInstance();
if (manager == NULL) { return; }
ExternalEffect_QueryKeyBuild(&key, owner);
ExternalEffect_EnumSources(component, &key, &handles);
for (i = 0; i < handles.Count; ++i) {
/* fetchPending 复用 key 副本缓冲区作输出数组头;取回后 memmove
* 0x98×num 追加到 out 尾部,再把来源 num 置 0(消费语义)。
* 每轮迭代末尾释放效果元素内部 FString 样板(原始保留)。 */
FPalPassiveSkillEffectTArray *pending =
(FPalPassiveSkillEffectTArray *)ExternalEffect_FetchPending(manager, &key, &handles.Data[i]);
if (pending->Count != 0) {
TArray_AppendEffects(out, pending->Data, pending->Count);
TArray_Consume(pending);
}
}
}
PassiveExternalKey *PassiveExternal_KeyBuild(PassiveExternalKey *outKey, UObject *owner)
{
APalPlayerState *helper;
const FGuid *id;
helper = PassiveExternal_GetHelper((AActor *)owner);
if (UE_IsValid(helper)) {
id = PassiveExternal_ReadId(helper); *outKey = *(const PassiveExternalKey *)id; return outKey;
}
outKey->Lo = DAT_1493da138; outKey->Hi = DAT_1493da140; /* 空键 */ return outKey;
}
void PassiveExternal_EnumSources(
UPalPassiveSkillComponent *component,
const PassiveExternalKey *key,
ExternalHandleList *outHandles)
{
PassiveExternalKey keyCopy;
PassiveExternalKey key2;
AActor *ownerChar;
UObject *recordData;
UPalItemIDManager *itemMgr;
FPalItemData itemData;
/* TArray Reset:count=0;原始代码在 Max != 0 时再调 FUN_140c03470
* 释放容量(TArray::Reset 语义) */
outHandles->Count = 0;
/* ── 块 A:当前手持武器 ───────────────────────────────────────────── */
keyCopy = *key;
ownerChar = PassiveExternal_FindPlayerStateByKey(component, &keyCopy);
if (ownerChar != NULL) {
UObject *shooter = Actor_GetComponentByClass(
ownerChar, (UClass *)PalShooterComponent_StaticClass());
if (shooter != NULL &&
UClass_IsA(UObject_Class(shooter),
(UClass *)PalShooterComponent_StaticClass())) {
APalWeaponBase *weapon =
ShooterComponent_GetCurrentWeapon(shooter); /* HasWeapon +0x5E0 */
if (weapon != NULL) {
UPalItemIDManager *mgr = PalItemIDManager_Get(component);
/* 入参 = 武器 ownItemID(+0x57C,FPalItemId 整体,SDK 反射) */
PalItemIDManager_GetItemData(
mgr, &itemData,
(const FPalItemId *)((char *)weapon + OFF_WEAPON_STATIC_ID));
if (itemData.StaticData != NULL)
PassiveExternal_EmitPassiveRowNames(outHandles, itemData.StaticData);
}
}
}
/* ── 块 B:防具/饰品容器(仅当查询键非全零 = 有明确拥有者玩家)────── */
if (key->Lo != 0 || key->Hi != 0) {
UPalPlayerInventoryData *inventory;
UPalItemContainer *armorContainer = NULL;
ExternalHandleList seen; /* 去重名单:静态数据 ID(FName) */
seen.Data = NULL; seen.Count = 0; seen.Max = 0;
keyCopy = *key;
inventory = PassiveExternal_GetInventoryData(component, &keyCopy);
if (inventory != NULL &&
PalInventoryData_FindContainerBySlot(inventory, 4, &armorContainer)) {
int i = 0;
int num;
/* 原始代码每轮循环重新经 vtbl+0x2B0 取 Num() */
while (num = ((int (*)(UPalItemContainer *))
VTBL_FUNC(armorContainer, UE_VTBL_CONTAINER_NUM))(armorContainer),
i < num) {
UPalItemSlot *slot =
(UPalItemSlot *)PalItemContainer_GetSlot(armorContainer, i);
if (slot != NULL && UE_IsValid(slot) &&
RAW_INT(slot, OFF_ITEM_SLOT_STACK_COUNT) != 0 && /* 非空槽 */
RAW_UINT64(slot, OFF_ITEM_SLOT_STATIC_ID) != DAT_1493dc688) {
UPalItemIDManager *mgr = PalItemIDManager_Get(component);
/* 入参 = 槽位 ItemId(+0x12C,FPalItemId 整体,SDK 反射) */
PalItemIDManager_GetItemData(
mgr, &itemData,
(const FPalItemId *)((char *)slot + OFF_ITEM_SLOT_STATIC_ID));
{
/* 原始代码不对 StaticData 做空检查(前置过滤已保证),
* 直接取 +0x28(= UPalStaticItemDataBase::ID,FName)去重 */
ulonglong dedupKey =
RAW_UINT64(itemData.StaticData, OFF_ITEM_STATIC_DATA_UID);
int k;
bool dup = false;
for (k = 0; k < seen.Count; ++k) {
if (seen.Data[k] == dedupKey) { dup = true; break; }
}
if (!dup) { /* 同种物品只收一次 */
TArray_AddRaw(&seen, &dedupKey, 8);
PassiveExternal_EmitPassiveRowNames(outHandles,
itemData.StaticData);
}
}
}
++i;
}
}
/* 原始代码:operator delete(seen.Data)(FUN_1433d7900) */
}
/* ── 块 C:RecordData 登记来源(Boss/记录奖励物品)────────────────── */
keyCopy = *key;
ownerChar = PassiveExternal_FindPlayerStateByKey(component, &keyCopy);
if (ownerChar == NULL || !UE_IsValid(ownerChar))
return;
/* 对解析出的角色重跑 KeyBuild 后半程,得第二枚键 key2(拥有者玩家
* PlayerUId;无主则为默认空键)——通常与首键相同,但本函数不假设 */
{
APalPlayerState *helper = PassiveExternal_GetHelper(ownerChar);
if (helper != NULL && UE_IsValid(helper))
key2 = *(const PassiveExternalKey *)PassiveExternal_ReadId(helper);
else {
key2.Lo = DAT_1493da138;
key2.Hi = DAT_1493da140;
}
}
/* RecordData:优先 PlayerManager 按 key2 直查 PlayerState →
* RecordData(+0x650);未命中走 GameState 状态门(+0x150 == 3)+
* PlayerManager+0xC0 映射回退(记录 +0x108) */
recordData = NULL;
{
APalPlayerState *psById = PassiveExternal_FindPlayerStateById(
GetPlayerManager(component), &key2);
if (psById != NULL && UE_IsValid(psById)) {
recordData = (UObject *)RAW_PTR(psById, OFF_PLAYERSTATE_RECORD_DATA);
} else if (UE_IsValid(component)) {
APalGameState *gameState = GetPalGameState();
if (gameState != NULL &&
UClass_IsA(UObject_Class(gameState),
(UClass *)PalGameState_StaticClass()) &&
UE_IsValid(gameState) &&
RAW_BYTE(gameState, OFF_GAMESTATE_STATE_BYTE_0150) == 3) {
UPalPlayerManager *pm = GetPlayerManager(component);
if (pm != NULL && UE_IsValid(pm)) {
void *rec = PlayerManagerMapFind(pm, &key2);
if (rec != NULL && UE_IsValid(rec))
recordData = (UObject *)RAW_PTR(rec, OFF_PLAYERMANAGER_MAP_RECORD_DATA);
}
}
}
}
/* ItemIDManager:GameInstance 无效时原始代码组装 ensure 错误日志
* (component+0x18 Outer 链 + DAT_1478cda50 格式串)后放弃块 C */
itemMgr = NULL;
if (UE_IsValid(component)) {
UPalGameInstance *gi = PalGameInstance_Get(component);
if (gi != NULL && UE_IsValid(gi))
itemMgr = (UPalItemIDManager *)RAW_PTR(gi, OFF_GAMEINSTANCE_ITEM_ID_MANAGER);
}
/* 两路齐备才收集并发射 */
if (recordData != NULL && UE_IsValid(recordData) &&
itemMgr != NULL && UE_IsValid(itemMgr)) {
ExternalHandleList idList;
ulonglong *it, *itEnd;
idList.Data = NULL; idList.Count = 0; idList.Max = 0;
PassiveExternal_CollectRecordSources(component, recordData, &idList);
for (it = idList.Data, itEnd = it + idList.Count; it != itEnd; ++it) {
UObject *staticData =
PalItemIDManager_GetStaticItemData(itemMgr, *it);
if (staticData != NULL && UE_IsValid(staticData))
PassiveExternal_EmitPassiveRowNames(outHandles, staticData);
}
/* 原始代码:operator delete(idList.Data)(FUN_1433d7900) */
}
}
改写原则
一个常用的改写是浮点的 *= 和 * 运算的等价改写
// original
float X = a
float Y = b
X *= Y
return X
// transformed
float X = a
float Y = b
float S = X * Y
return S
这两份代码虽然可能对应不同的汇编代码,但是在 x86/x64 以及 SSE/AVX 浮点指令集上的运行结果是完全一样的(未分析过其他架构的情况,不过大概率相同;在 x87 的 FPU 可能会存在浮点异常标志不同的情况,不过当前情境下的数值范围也不会触发)
考虑到我们的转译代码的来源是反汇编,可能源代码中存在 volatile 的情况,我们需要考虑内存中的数值被其他因素修改的情况。这里存在很多 corner case,无法穷举,能够排除影响的包括多线程(无多线程管理代码,且 CalcBase 的下游存在广播函数)、硬件 MMIO(意图不符)、DMA(意图不符)、handler 内部使用的情况(意图不符)。
MISC
伤害在命中时动态获取信息计算,并非基于伤害行为开始时的信息。上个版本导弹发射器面板的取消太刀大概就是这个原因。
palDamageRate 帕鲁乘区
转译之后的反汇编代码位于 /damage_calc/general/transcript/Damage_CalcBase
代码中 baseCoeff 对应 palDamageRate,palDefenderDamageRate 和 palAttackerDamageRate 并没有实际存在而是直接被聚合到 palDamageRate 中。主要逻辑为:
-
获取
defender的UPalCharacterParameterComponent组件,利用该组件的Trainer属性的有效性和类型来判断defender是某个召唤师的伙伴帕鲁还是野生帕鲁、工会帕鲁等。-
如果
defender是伙伴帕鲁:以GameSetting的属性OtomoDamageRate_Defense作为palDefenderDamageRate。OtomoDamageRate_Defense是一个配置量,默认值在UPalGameSetting蓝图中被覆写为0.75。 -
如果是野生帕鲁:
- 获取
DatabaseCharacterParameter,然后以defender的UPalCharacterParameterComponent组件的属性IndividualParameter : UPalIndividualCharacterParameter的属性SaveParameter : PalIndividualCharacterSaveParameter的属性CharacterID : FName作为键值查询它得到数据行,然后以数据行的属性EnemyReceiveDamageRate : float作为palDefenderReceiveDamageRate。这是一个配置量,可在DT_PalMonsterParameter和DT_PalHumanParameter中查询(/display/pal_info 形成了网页化表格)。 - 以
defender的UPalCharacterParameterComponent组件的属性AdditionalEnemyReceiveDamageRate : float作为palDefenderAdditionalReceiveDamageRate。这是一个状态量,C++ native 默认值为1.0f,蓝图没有覆写。
- 获取
-
其他情况:恒等乘区
-
-
通过
PalUtility_IsPal验证attacker是否是帕鲁- 如果
attacker是野生帕鲁:通过同样的方式获取数据库、CharacterID、进行键值查询,以数据行的EnemyInflictDamageRate作为palAttackerInflictDamageRate,这是一个配置量,可在DT_PalMonsterParameter和DT_PalHumanParameter中查询(/display/pal_info 形成了网页化表格);以attacker的UPalCharacterParameterComponent组件的属性AdditionalEnemyInflictDamageRate作为palDefenderAdditionalInflictDamageRate。这是一个状态量,C++ native 默认值为1.0f,蓝图没有覆写。 - 其他情况:恒等乘区
- 如果
乘区语义:
- 野生帕鲁作为防御方都会受到各自
EnemyReceiveDamageRate的影响。此外,伙伴帕鲁作为防御方统一地受到OtomoDamageRate_Defense的影响。 - 野生帕鲁作为攻击方都会受到各自
EnemyInflictDamageRate的影响。为什么没有OtomoDamageRate_Attack?太不对称了! AdditionalEnemyReceiveDamageRate和AdditionalEnemyInflictDamageRate尚不清晰
if (trainer == NULL) {
if (PalUtility_IsWildPal(defender) && UObject_IsValid(defender)) {
gameInstance = (UPalGameInstance *)PalGameInstance_Get(defender);
charParamDB = NULL;
if (UObject_IsValid(gameInstance)) {
charParamDB = (UPalDatabaseCharacterParameter *)
RAW_PTR(gameInstance, OFF_GAMEINSTANCE_DB_CHAR_PARAMETER);
}
individualParam = CharacterParameter_GetIndividualParameter(defParamComp);
if (UObject_IsValid(individualParam) && UObject_IsValid(charParamDB)) {
characterID = *(ulonglong *)IndividualParameter_GetCharacterID(individualParam, &characterID);
baseCoeff = CharParamDB_GetEnemyReceiveDamageRate(charParamDB, characterID);
baseCoeff *= defParamComp->AdditionalEnemyReceiveDamageRate;
}
}
}
else if (UObject_IsA(UObject_Class(trainer),
(UClass *)PalPlayerCharacter_StaticClass())) {
baseCoeff = gameSettings->OtomoDamageRate_Defense;
}
if (UObject_IsValid(info->Attacker) && PalUtility_IsWildPal(info->Attacker)) {
UPalCharacterParameterComponent *attackParamComp;
UPalIndividualCharacterParameter *attackIndividualParam;
gameInstance = NULL;
if (UObject_IsValid(defender)) {
gameInstance = (UPalGameInstance *)PalGameInstance_Get(defender);
}
if (UObject_IsValid(gameInstance)) {
charParamDB = (UPalDatabaseCharacterParameter *)
RAW_PTR(gameInstance, OFF_GAMEINSTANCE_DB_CHAR_PARAMETER);
attackParamComp = (UPalCharacterParameterComponent *)
Character_GetCharacterParameter(info->Attacker);
if (UObject_IsValid(charParamDB) && UObject_IsValid(attackParamComp)) {
attackIndividualParam = CharacterParameter_GetIndividualParameter(attackParamComp);
if (UObject_IsValid(attackIndividualParam)) {
characterID = *(ulonglong *)IndividualParameter_GetCharacterID(attackIndividualParam, &characterID);
attackEnemyRate = CharParamDB_GetEnemyInflictDamageRate(charParamDB, characterID);
baseCoeff = attackEnemyRate * baseCoeff *
attackParamComp->AdditionalEnemyInflictDamageRate;
}
}
}
else {
Character_GetCharacterParameter(info->Attacker);
}
}
elementStatusAilmentRate 异常状态属性乘区
转译之后的反汇编代码位于 /damage_calc/general/transcript/Damage_CalcBase
代码中用 defender 的 UPalCharacterParameterComponent 组件的属性 DamageUpElement_ByElementStatus 和 DamageDownElement_ByElementStatus 来进行弱点属性判断。相对于对于 "获取异常状态然后根据某个表格判断" 这种模式做了一层缓存。
异常状态本身是由一个完全独立的组件 UPalStatusComponent 管理的,修改 UPalCharacterParameterComponent 的 DamageUpElement_ByElementStatus 以及 DamageDownElement_ByElementStatus 应当属于异常状态施加调用链尾端的一个副作用。
考虑直接检索 DamageDownElement_ByElementStatus 和 DamageUpElement_ByElementStatus 的写指令,一个候选是形如 register + instant_fixed_offset 形式的写指令,但是并没有找到。原因是蓝图侧的写入从 FProperty 中读取偏移。从蓝图侧以语义关键词入手的话可以找到核心蓝图 UBP_Status_WithVisualEffect_C,它是所有元素异常的蓝图基类。所有元素异常的子类中都可以覆盖属性 DamageUpElement : EPalElementType 和 DamageDownElement : EPalElementType。列表如下:
BP_Status_IvyCling:"DamageUpElement": "EPalElementType::Fire"BP_Status_Muddy:"DamageUpElement": "EPalElementType::Leaf", "DamageDownElement": "EPalElementType::Electricity"BP_Status_Electrical:"DamageUpElement": "EPalElementType::Water"BP_Status_Wetness:"DamageUpElement": "EPalElementType::Electricity", "DamageDownElement": "EPalElementType::Fire"BP_Status_Burn:无BP_Status_Freeze:无BP_Status_Darkness:无- 其他
BP_Status_Xxx:大概率无,没有验证过
语义总结:存在对于以下几种异常状态的不同攻击行为元素属性的抵抗和易伤倍率,易伤倍率为 2.0f,抵抗倍率为 0.5f。这七种元素异常不能叠加,新的覆盖旧的(待代码确认)
- 缠绕:火易伤
- 泥泞:叶易伤,电抵抗
- 触电:水易伤
- 潮湿:电易伤,火抵抗
float CharacterParameter_GetElementStatusAilmentRate(
UPalCharacterParameterComponent *paramComp,
EPalElementType attackElement)
{
if (attackElement != PAL_ELEMENT_NONE) {
if (attackElement == paramComp->DamageUpElement_ByElementStatus) {
return 2.0f;
}
if (attackElement == paramComp->DamageDownElement_ByElementStatus) {
return 0.5f;
}
}
return 1.0f;
}
elementWeakRate 属性克制乘区
转译之后的反汇编代码位于 /damage_calc/general/transcript/Damage_CollectRates
WeakScale_CompareElements 意在计算一种进攻元素对两种防御元素的克制(+1)、被克制(-1)、平局(+0)数量之和。
ElementRule_IsWeakTo 可以读出元素克制链:火克叶克地克电克水克火克冰克龙克暗克无
GameSetting_QueryWeakScale 并非常规查询 UPalGameSetting 单例的属性,而是用 thunk 机制调用蓝图定义的 GetWeakScale 函数,查看 BP_PalGameSetting.uasset 的 GetWeakScale 字节码可以还原得到下面的函数。可以看到直接硬编码了 0.43f、0.66f 等数值而非通过数值计算 2 / 3 或者 4 / 9。
语义总结:
- 按照属性克制关系链(火克叶克地克电克水克火克冰克龙克暗克无)独立计算攻击行为属性相对于防御方的两种属性的克制数量并求和
- 然后按照克制数量 switch 到
-2: 0.43f, -1: 0.66f, 0: 1.0f, 1: 1.5f, 2: 2.25f作为elementWeakRate
weakCount = WeakScale_CompareElements(
info->AttackElementType, aux->DefenderElementType1, aux->DefenderElementType2);
weakScaleRate = GameSetting_QueryWeakScale(palGameSetting, weakCount);
int WeakScale_CompareElements(uchar attackElement, uchar defenderElement1, uchar defenderElement2)
{
int firstScore, totalScore;
if (ElementRule_IsWeakTo(attackElement, defenderElement1)) { firstScore = 1; }
else {
firstScore = 0;
if (ElementRule_IsResistTo(attackElement, defenderElement1)) { firstScore = -1; }
}
if (ElementRule_IsWeakTo(attackElement, defenderElement2)) { totalScore = firstScore + 1; }
else {
totalScore = firstScore - 1;
if (!ElementRule_IsResistTo(attackElement, defenderElement2)) { totalScore = firstScore; }
}
return totalScore;
}
bool ElementRule_IsWeakTo(uchar attack, uchar defend)
{
bool weak;
if ((uint)(attack - PAL_ELEMENT_FIRE) >= 8) { return false; }
switch (attack - PAL_ELEMENT_FIRE) {
case 0: /* Fire */ weak = (defend == PAL_ELEMENT_LEAF || defend == PAL_ELEMENT_ICE);break;
case 1: /* Water */ weak = (defend == PAL_ELEMENT_FIRE);break;
case 2: /* Leaf */ weak = (defend == PAL_ELEMENT_EARTH);break;
case 3: /* Electricity */ weak = (defend == PAL_ELEMENT_WATER);break;
case 4: /* Ice */ weak = (defend == PAL_ELEMENT_DRAGON);break;
case 5: /* Earth */ weak = (defend == PAL_ELEMENT_ELECTRICITY);break;
case 6: /* Dark */ weak = (defend == PAL_ELEMENT_NORMAL);break;
case 7: /* Dragon */ weak = (defend == PAL_ELEMENT_DARK);break;
}
return weak;
}
float BP_PalGameSetting_C::GetWeakScale(int weakCount) {
switch (weakCount) {
case -2: return 0.43f;
case -1: return 0.66f;
case 0: return 1.0f;
case 1: return 1.5f;
case 2: return 2.25f;
default: return 0.66f;
}
defPassiveElementResistRate 防御方被动属性减伤乘区
转译之后的反汇编代码位于 /damage_calc/general/transcript/Damage_CollectRates
我们已经系统地分析过了 PalPassiveSkillComponent_QueryEffectSum 的逻辑了,PalPassiveSkillComponent_GetElementResistRate 只是它的浅封装
- 收集的被动效果类型为
ElementResist_Xxx : EPalPassiveSkillEffectType,其中 Xxx 对应攻击行为的属性。一共可以取值ElementResist_Normal ElementResist_Fire ElementResist_Water ElementResist_Leaf ElementResist_Electricity ElementResist_Ice ElementResist_Earth ElementResist_Dark ElementResist_Dragon这七个。 - 效果数值会被直接累加,然后写成百分比的形式作为
defPassiveElementResistRate - 下面列举被动效果的来源(饰品,头饰防具,词条,伙伴技能被动效果)(为了表述的直观性直接写成百分比形式),详细内容可以查看 display/info 下的网页在 passive_skill tab 筛选 trans_EffectTypeX 即可:
-
头饰防具 15%(删除线表示对应物品
bIsLegalInGame == False):-
草:
- 长耳发箍1
2345 - 叶泥泥帽1
2345,百合女王的发饰,达鼠泥的发饰,菇咚帽
- 长耳发箍1
-
地:
- 费多拉帽1
2345(费 dollar?) - 趴趴鲶头套
- 费多拉帽1
-
雷:
- 安全帽1
2345 - 雷鸣童子帽子1,救赎王冠(?默世鹿)
- 安全帽1
-
水:
- 三角帽1
2345 - 海月仙帽,盔盔仔帽,墨沫姬帽,武道蛙帽,冥灯鱼的发饰,霸王之角(殁殃)
- 三角帽1
-
火:
- 大礼帽1
2345 - 耐寒皮毛盔甲12345,耐寒金属盔甲12345,耐寒精炼金属盔甲12345,耐寒帕鲁金属盔甲12345,耐寒塑钢盔甲12345,耐寒六棱晶盔甲12345,耐寒远古盔甲12345
- 大礼帽1
-
冰:
- 金冠1
2345,企丸王帽12345,绸笠蛾帽,毛掸儿帽子1,冰姬灵帽,杰诺多兰头套 - 耐寒皮毛盔甲12345,耐寒金属盔甲12345,耐寒精炼金属盔甲12345,耐寒帕鲁金属盔甲12345,耐寒塑钢盔甲12345,耐寒六棱晶盔甲12345,耐寒远古盔甲12345
- 金冠1
-
龙:
- 王冠1
2345,魔女之冠(贝菈露洁) - 远古头盔12345
- 王冠1
-
暗:
- 魔女帽子1
2345 - 暗巫猫帽1
2345,朋克蜥的兜帽12345,勾魂鱿帽子12345,姬小兔的发饰2345(?),毛掸儿帽子2345(?),雷鸣童子帽子2345(?),瞅什魔头套,夜蔓爵帽,幽恋娜帽,桃蛛娘帽,黑鸦隐士帽子
- 魔女帽子1
-
无:
- 圆顶礼帽1
2345 - 炸弹鸟帽1
2345,笑脸炸弹鸟帽12345,姬小兔的发饰1,棉悠悠头套,捣蛋猫帽子,君王美露帕的发饰,皮皮鸡帽子 - 远古头盔12345
- 圆顶礼帽1
-
-
饰品
- 草:草属性抵抗戒指 30%;狱焰王指环 35%
- 地:地属性抵抗戒指 30%;薇莉塔指环 35%
- 雷:雷属性抵抗戒指 30%;金铠蝎指环 35%
- 水:水属性抵抗戒指 30%;雷隐鹿指环 35%
- 火:炎属性抵抗戒指 30%;荷西斯指环 35%;熔岩兽护身符 35%
- 冰:冰属性抵抗戒指 30%;荷鲁斯指环 35%;白绒雪怪护身符 35%
- 龙:龙属性抵抗戒指 30%;冰棘兽指环 35%
- 暗:暗属性抵抗戒指 30%;精灵龙指环 35%
- 无:无属性抵抗戒指 30%;暗巫猫指环 35%
-
词条
- 草:除草效果 10%
- 地:抗震结构 10%
- 雷:绝缘体 10%
- 水:防水性能 10%
- 火:拥抱烈日 10% 未知生物细胞 15%
- 冰:高温体质 10%
- 龙:屠龙者 10%
- 暗:阳光开朗 10%
-
伙伴技能被动效果 15/17/20/24/30%,均同种族不可叠加取最大
- 草:樱丽娜
- 地:绿苔绒怪
- 雷:力士獒
- 水:金涡蜗
- 火:水灵龙
丹烽 - 冰:苍焰狼
冰帝美露帕 - 龙:寒峰兽
- 暗:幽焰狼
- 无:恐炬灵
-
defPassiveElementResistRate = 1.0f;
defPassiveComponent = (UPalPassiveSkillComponent *)Actor_GetComponentByClass(
defender, (UClass *)PalPassiveSkillComponent_StaticClass());
if (defPassiveComponent != NULL) {
defPassiveElementResistRate = (float)PalPassiveSkillComponent_GetElementResistRate(
defPassiveComponent, info->AttackElementType);
}
defExposionResistRate 爆炸伤害减伤乘区
当满足
info->bIsExplosionDamage
|| info->WeaponType == RocketLauncher
|| info->WeaponType == MissileLauncher
|| info->WeaponType == GrenadeLauncher
时本乘区才非平凡,否则为 1.0f。
尚未确定 info->WeaponType 的传入机制,不过直觉上来说应该传入触发伤害行为的武器(或者子弹对应的武器),不过对于召唤类的武器这个直觉仍然成立?
尚未确认 bIsExplosionDamage 的传入机制
其中
bIsExplosionDamage的配置机制尚未追溯,推测世界树的孢子爆炸、技能自爆、技能超自爆属于该类型伤害。WeaponType这里的判断有点像冗余 / 兜底判断。WeaponType的类型为EPalWeaponType,下面列举出各类型的武器(display/info 中网页的 weapon tab),分类很神秘,有点像半途而废的产品。这里列出的只是各武器蓝图中WeaponType字段的默认值,我们并不清楚它和info中字段的具体对应机制,直觉是二者完全对应,不过改写也是可能的(比如泰拉刃的剑气触发伤害行为时覆写为伤害类型为,好像也没有更加合适的枚举值了)-
RocketLauncher:- 陨石发射器15
- 榴弹发射器12345(?)
- 战术榴弹发射器12345(?)
- 火箭发射器12345
- 光束炮发射器12345
- 无人机发射器12345
- 原来无人机是爆炸伤害?
- 更神秘的是泰拉棱镜是近战伤害,整个泰拉武器系列除了星璇机枪都是近战伤害
- 单发型帕鲁球发射器(?似乎不会造成伤害)
- 扩散型帕鲁球发射器(?似乎不会造成伤害)
- 自动追踪帕鲁球发射器(?似乎不会造成伤害)
- 火箭发射器的 NPC、伙伴 NPC 版本
-
MissileLauncher:- 追踪导弹发射器12345
- 多联追踪导弹发射器12345
-
GrenadeLauncher:- 榴弹发射器的 NPC、伙伴 NPC 版本
-
用于计算 defPassiveExplosionResistRate 的 PalPassiveSkillComponent_GetExplosionResistRate 是 PalPassiveSkillComponent_GetElementResistRate 的浅封装
- 以上面提到的爆炸检测条件作为门控
- 收集的被动效果类型为
ExplosionResist : EPalPassiveSkillEffectType - 效果的数值会直接累加,换算为百分比的形式,以
max(0.f, 2.0f - <sum_rate>)作为defPassiveExplosionResistRate。 - 下面列举其来源(display/info 的网页中筛选 trans_EffectTypeX 即可)(为了表述的直观性写了百分号)
- 磐甲龙伙伴技能的被动效果 60/65/70/75/80%(不可叠加)
- 重装甲词条 100%
- 防爆内衣 200%(两条完全一样的 100% ExplosionResist)
计算 defFoodStatusExplosionResistRate
-
以上面提到的爆炸检测条件作为门控
-
经过
Individual_QueryStatusEffectFoodRate,其内部- 通过
defender的UPalIndividualCharacterParameter的属性SaveParameter : UPalIndividualCharacterSaveParameter的属性FoodWithStatusEffect : FName(表示此时正在生效的会施加状态效果的食物)以及属性Tiemr_FoodWithStatusEffect : FloatProperty(表示该食物的效果的剩余时间)来判断是否存在正在生效的会施加状态效果的食物。 - 然后通过
Database_GetStatusEffectFoodValue(这是一个通用函数,命名存在错误)得到当前正在生效的会施加状态效果的食物中类型满足ExplosionResist : EPalFoodStatusEffectType的食物状态效果的值的和,转换为百分比加成的形式- 该函数通过
UPalDatabaseCharacterParameter来查询DT_StatusEffectFood(可在 display/food 网页中筛选对应属性)中对应食物名称的行,然后逐个对比数据行中的状态效果的类型EffectType1和EffectType2是否匹配foodEffectType : EPalFoodStatusEffectType(这里传入的是ExplosionResist)来作为是否加算对应EffectValue1和EffectValue2的依据。然后对于HungerResist和SANResist的食物效果类型需要限制到最少 -90%(这并非网传菇波的降 SAN 上限 90% 的由来,这里统计的不是伙伴技能被动效果)
- 该函数通过
- 通过
-
然后用
max(2.0f - <sum_rate>, 0.f)作为defFoodStatusExplosionResistRate
由于 FoodWithStatusEffect 并不是一个列表,结合游戏经验我们至少能够确信这一类施加状态效果的食物不能叠加,且新食物会覆盖旧食物。(尚未经过代码验证)
提供 ExplosionResist 类型的食物状态效果的食物来源只有:海鲜意面 100%。
defFoodStatusExplosionResistRate = 1.0f;
defPassiveComponent = (UPalPassiveSkillComponent *)Actor_GetComponentByClass(
defender, (UClass *)PalPassiveSkillComponent_StaticClass());
if (defPassiveComponent != NULL) {
defPassiveExplosionResistRate = (float)PalPassiveSkillComponent_GetExplosionResistRate(
defPassiveComponent, info);
}
if ((info->bIsExplosionDamage || info->WeaponType == PAL_WEAPON_TYPE_EXPLOSION_A ||
info->WeaponType == PAL_WEAPON_TYPE_EXPLOSION_B || info->WeaponType == PAL_WEAPON_TYPE_EXPLOSION_C) &&
(defIndividualParameter = (UPalIndividualCharacterParameter *)Character_GetIndividualParameter(defender)) != NULL) {
defFoodStatusExplosionResistRate = 2.0f - (float)StatusEffectFood_QueryRate(
defIndividualParameter, PAL_FOOD_STATUS_EXPLOSION_RESIST);
if (defFoodStatusExplosionResistRate < 0.0f)
defFoodStatusExplosionResistRate = 0.0f;
}
defExplosionResistRate = defPassiveExplosionResistRate * defFoodStatusExplosionResistRate;
float Individual_QueryStatusEffectFoodRate(UPalIndividualCharacterParameter *individual, uchar foodEffectType)
{
UPalDatabaseCharacterParameter *db;
int pct;
/* 食物效果有效性:FName 非 NAME_None(低 8 字节至少一个 int 非零)且剩余计时 > 0。 */
if ((RAW_INT(individual, OFF_INDIVIDUAL_FOOD_STATUS_EFFECT) != 0 ||
RAW_INT(individual, OFF_INDIVIDUAL_FOOD_STATUS_EFFECT + 4) != 0) &&
RAW_INT(individual, OFF_INDIVIDUAL_FOOD_STATUS_TIMER) > 0) {
db = Character_GetDatabaseCharacterParameter((UObject *)individual);
if (db != NULL) {
/* 原始为 *(undefined8 *)(individual + 0x5B0):FName 按 8 字节
* 值传递(ComparisonIndex+Number),非指针。 */
pct = (int)Database_GetStatusEffectFoodValue(
db, RAW_UINT64(individual, OFF_INDIVIDUAL_FOOD_STATUS_EFFECT), foodEffectType);
return (float)(pct + 100) * 0.01f; /* DAT_146728c2c */
}
}
return 1.0f; /* DAT_146728c38 */
}
int Database_GetStatusEffectFoodValue(
UPalDatabaseCharacterParameter *db,
ulonglong foodRowName,
uchar foodEffectType)
{
const FPalStatusEffectFoodDataRow *row;
int value;
row = (const FPalStatusEffectFoodDataRow *)StatusEffectFoodMap_Find(RAW_PTR(db, OFF_DATABASE_FOOD_TABLE), foodRowName);
if (row == NULL) return 0;
value = 0;
if (row->EffectType1 == foodEffectType) value = row->EffectValue1;
if (row->EffectType2 == foodEffectType) value += row->EffectValue2;
/* HungerResist(3)/SANResist(4) 之外,不超过 90%。 */
if (foodEffectType != 3 /* HungerResist */ &&
foodEffectType != 4 /* SANResist */ && value < -90) {
value = -90;
}
return value;
}
atkPassiveElementBoostRate 进攻方被动属性增伤乘区
类似地,Passive_GetElementBoostRate 也只是 PalPassiveSkillComponent_QueryEffectSum 的一层浅封装
- 收集的被动效果类型形如
ElementResist_Xxx,Xxx 对应info->AttackElementType - 收集到的数值会被直接相加,然后换算成百分比加成的形式作为
atkPassiveElementBoostRate - 来源包括讨伐证明,饰品,词条(直接写成了百分比形式)
-
讨伐证明一共:
- 草:1% 7 个;2% 6 个;3% 1 个。一共 22%
- 地:1% 8 个;2% 4 个。一共 16%
- 雷:1% 8 个;2% 3 个。一共 14%
- 水:1% 3 个;2% 3 个;3% 1 个。一共 12%
- 火:1% 5 个;2% 3 个;3% 3 个.一共 20%
- 冰:1% 4 个;2% 3 个;3% 4 个。一共 22%
- 龙:1% 3 个;2% 3 个;3% 2 个。一共 15%
- 暗:1% 15 个;2% 3 个。一共 21%
- 无:1% 5 个;2% 2 个;3% 1 个。一共 12%
-
饰品:
- 草:草属性助威哨子 18%;精灵王指挥棒,百合女王神符,薇莉塔指环 20%
- 地:地属性助威哨子 18%;岩帝指挥棒,阿努比斯神符,金铠蝎指环 20%
- 雷:雷属性助威哨子 18%;雷帝指挥棒,波鲁杰克斯神符,雷隐鹿指环 20%
- 水:水属性助威哨子 18%;海皇指挥棒,海皇鲸神符,伊西斯指环 20%
- 火:火属性助威哨子 18%;炎帝指挥棒,焰煌神符,狱焰王指环,荷鲁斯指环 20%
- 冰:冰属性助威哨子 18%;冰帝指挥棒,唤冬兽神符,冰棘兽指环 20%
- 龙:龙属性助威哨子 18%;神龙指挥棒,精灵龙指环,空涡龙神符 20%
- 暗:暗属性助威哨子 18%;冥王指挥棒,黑月女王神符,暗巫猫指环 20%
- 无:无属性助威哨子 18%;圣天指挥棒,默世鹿神符 20%
-
词条
- 草:精灵王 30%,救世主 30%,草木馨香 10%
- 地:岩帝 30%,大地之力 10%
- 雷:雷帝 30%,永炎 30%,电容 10%,未知生物细胞 15%(与技能提示文本似乎矛盾)
- 水:海皇 30%,湖之主 20%,喜欢戏水 10%
- 火:炎帝 30%,永炎 30%,喜欢玩火 10%
- 冰:冰帝 30%,魔女 30%,湖之主 20%,冷血 10%
- 龙:神龙 30%,侵略者 30%,龙之血脉 10%
- 暗:冥王 30%,侵略者 30%,魔女 30%,夜幕 10%
- 无:圣天 30%,救世主 30%,禅境 10%
-
attacker = info->Attacker;
if (UObject_IsValid(attacker)
&& (attackerPassiveComponent = Character_GetPassiveSkillComponent(attacker)) != NULL) {
atkPassiveElementBoostRate = (float)Passive_GetElementBoostRate(
attackerPassiveComponent, info->AttackElementType);
}
atkPassiveElementWeaknessBoostRate 攻击方被动属性克制额外增伤乘区
Passive_GetElementWeaknessBoostRate 是 PalPassiveSkillComponent_QueryEffectSum 的浅封装
- 首先会计算进攻行为属性和防守方属性的
WeakCount确保存在属性克制关系 - 收集的被动效果类型为
ElementBoostWeakness_Xxx,其中Xxx为进攻方属性 - 收集到的数值会被直接相加然后转化为百分比加成形式作为
atkPassiveElementWeaknessBoostRate - 来源只有帕鲁伙伴技能的被动效果,懒得列举了,形如草:艾小兔。均同种族不可叠加取最大
attacker = info->Attacker;
if (UObject_IsValid(attacker)
&& (attackerPassiveComponent = Character_GetPassiveSkillComponent(attacker)) != NULL) {
atkPassiveElementWeaknessBoostRate = (float)Passive_GetElementWeaknessBoostRate(
attackerPassiveComponent, info->AttackElementType,
aux->DefenderElementType1, aux->DefenderElementType2);
}
atkPassiveStatusAilmentBoostRate 攻击方被动异常状态额外增伤乘区
Passive_GetStatusAilmentBoostRate会收集进攻方的所有异常状态的被动效果DamageRateIfDefender_Xxx(调用PalPassiveSkillComponent_QueryEffectSum,直接累加然后写成百分比加成的形式),然后按照毒、燃烧、潮湿、冰冻、触电、泥泞、缠绕、黑暗的顺序逐一核对该种异常状态是否存在,如果存在就加上对应的额外倍率,对应的迭代形式为rate_final = (rate_final + rate) - 1.0f(浮点数运算不具备结合律;结合的顺序很奇怪)。- 来源只有帕鲁伙伴技能的被动效果,数值为 50/53/56/60/65%,均同种族不可叠加取最大
- 毒:蛊刺妖
- 燃烧:腾炎龙
- 潮湿:覆海龙
- 冰冻:无
- 触电:无
- 泥泞:磐峰兽
- 缠绕:球抱苞
- 黑暗:盗影鸦
考虑到似乎只能同时存在毒和一种其他状态(尚未经过代码验证),这个乘区最大能叠到 2.3f 左右(浮点误差?)
attacker = info->Attacker;
if (UObject_IsValid(attacker)
&& (attackerPassiveComponent = Character_GetPassiveSkillComponent(attacker)) != NULL) {
atkPassiveStatusAilmentBoostRate = (float)Passive_GetStatusAilmentBoostRate(
attackerPassiveComponent, defender);
}
atkPassiveBodyPartBoostRate 攻击方被动身体部位弱点额外增伤乘区
当命中地方弱点部位时(根据受击行为信息 info 的属性 BodyPartsType 来进行判断)
-
这里两个乘区获取对应被动的数值都是通过
UPalPassiveComponent::GetParameterWithPassiveSkillEffect(对应代码中的Passive_GetRate),这个函数并非简单封装QueryEffectSum。QueryEffectSum得到的结果记为base,还有一层额外的条件被动统计,结果记为delta,最终该函数返回base + delta。QueryEffectSum的includeEquip被置零(这一意味着base不统计雕像效果、当前武器效果、各种装备的效果)delta有三个来源-
如果查询的被动效果类型是
ShotAttack(来源基本上对应所有与攻击力增减益相关的词条、伙伴技能、防具饰品等装备,存在少数例外比如捕食者版本狱阎王的伙伴技能??),那么会额外统计类型为如下的被动效果数值,加算后作为deltaAttackRateIfAttacker_Xxx,其中 Xxx 标识任意一种异常状态,在当前版本中该被动效果没有被引用过,在某个历史版本似乎是蛊刺妖伙伴技能的一个被动效果。AttackRateHPThreshold,拉比耶尔伙伴技能是唯一来源BulletHit_StackBuff,波鲁杰克斯DefeatEnemy_StackBuff,焰煌
注意这里的被动效果统计并不是走的QueryEffectSum而是全新的一路(换人写的?),尚未深入分析,不过应该类似吧。
-
如果查询的被动效果类型是
Defense,统计类型与ShotAttack的情况类似但是没有异常状态增伤的部分,同样是加算后作为delta。 -
如果查询的被动效果类型是遗物(雕像)相关的(包括
HungerReduction, JumpPower, SwimSpeed, SwimStaminaRate, GliderSpeed, GliderStaminaRate, ClimbSpeed, ClimbStaminaRate),那么查询走和EnumSources中相同的路线获取各个遗物的数值。
-
在这两个乘区的语境下等价于
QueryEffectSum的薄封装。 -
通过
Passive_GetRate获取被动效果BodyPartsWeakDamage的倍率作为atkPassiveBodyPartBoostRate1。- 来源只有霜歌龙和烽歌龙,均同种族不可叠加(但是霜歌龙和烽歌龙之间是可以叠加的)
-
通过
Pasive_GetRate获取被动效果PlayerInflictEffect_WeakPointHit_DamageUp的倍率,验证状态WeakPointHitDamageUp_Buff的存在性后将其作为atkPassiveBodyPartBoostRate2- 来源只有金棘兽,同种族不可叠加
attacker = info->Attacker;
if (UObject_IsValid(attacker) &&
(attackerPassiveComponent = Character_GetPassiveSkillComponent(attacker)) != NULL) {
if (info->BodyPartsType == EPAL_BODY_PARTS_Weak) {
UPalStatusComponent *attackerStatusComponent;
if (Passive_Has(attackerPassiveComponent,
EPAL_PASSIVE_SKILL_EFFECT_TYPE_BodyPartsWeakDamage, 0)) {
atkPassiveBodyPartBoostRate1 = (float)Passive_GetRate(
attackerPassiveComponent, 1.0f,
EPAL_PASSIVE_SKILL_EFFECT_TYPE_BodyPartsWeakDamage, 0);
}
if (Passive_Has(attackerPassiveComponent,
EPAL_PASSIVE_SKILL_EFFECT_TYPE_PlayerInflictEffect_WeakPointHit_DamageUp, 0)
&& (attackerStatusComponent = Character_GetStatusComponent(attacker),
UObject_IsValid(attackerStatusComponent))) {
UPalStatusBase *weakPointBuffStatus = StatusComponent_FindStatusById(
attackerStatusComponent, EPAL_STATUS_ID_WeakPointHitDamageUp_Buff);
if (UObject_IsValid(weakPointBuffStatus)) {
atkPassiveBodyPartBoostRate2 = (float)Passive_GetRate(attackerPassiveComponent, 1.0f,
EPAL_PASSIVE_SKILL_EFFECT_TYPE_PlayerInflictEffect_WeakPointHit_DamageUp, 0);
}
atkPassiveBodyPartBoostRate = atkPassiveBodyPartBoostRate1 * atkPassiveBodyPartBoostRate2
}
}
}
atkPassiveNonBattleBoostRate 攻击方被动对非战斗状态增伤乘区
走和 "攻击方被动身体部位弱点额外增伤乘区" 同一路的 Passive_GetRate 进行统计,来源只有兜兜灵的伙伴技能以及捕食者面惧伙伴技能(后者大概是遗留代码),同种族不可叠加
attacker = info->Attacker;
if (UObject_IsValid(attacker) &&
(attackerPassiveComponent = Character_GetPassiveSkillComponent(attacker)) != NULL) {
if (Passive_Has(attackerPassiveComponent, EPAL_PASSIVE_SKILL_EFFECT_TYPE_DamageUpToNonBattleEnemy, 0) &&
UObject_IsA(UObject_Class(defender), (UClass *)PalCharacter_StaticClass()) &&
!UObject_CallBoolVFunc((UObject *)defender, 0x9D0)) {
atkPassiveNonBattleBoostRate = (float)Passive_GetRate(
attackerPassiveComponent, 1.0f,
EPAL_PASSIVE_SKILL_EFFECT_TYPE_DamageUpToNonBattleEnemy, 0);
}
atkPassiveWeaponTypeBoostRate 攻击方被动武器类型增伤乘区
统计的被动效果为 DamageRateByEquippedWeapon,统计倍率使用 Passive_GetWeaponRelatedBoostRate,其做了一些有效性检查,包括:确认使用了有效武器、根据 info->AttackStaticItemID 找到对应武器物品的 TypeA 和 TypeB 属性,利用二者和 info->AttackStaticItemID 本身一起来构建过滤器(筛选对应武器类型的被动;QueryEffectSum 的过滤器参数)、其他有效性检查。然后调用 QueryEffectSum。对应数值会被直接累加然后以百分比的形式作为 atkPassiveWeaponTypeBoostRate。
来源包括
- 妮瞅莎 10/15/20/25/35%。伙伴技能筛选相关参数:
"ItemTypeA": "EPalItemTypeA::None",
"ItemTypeB": "EPalItemTypeB::WeaponShotgun",
"ItemIds": [],
"ExcludedItemIds": [],
"bIsContainNoItemId": false,
// 破旧的霰弹枪、双管霰弹枪、泵动式霰弹枪、半自动霰弹枪、能量霰弹枪、原型霰弹枪、散射光束枪、核心喷射霰弹枪
- 羽箭射手 10/15/20/25/35%。参数:
"ItemTypeA": "EPalItemTypeA::None",
"ItemTypeB": "EPalItemTypeB::WeaponBow",
"ItemIds": [], //
"ExcludedItemIds": [],
"bIsContainNoItemId": false,
// 陈旧的弓、三连弓、复合弓、卓越弓、机械弓
- 杰诺路达 10/15/20/25/35%。参数:
"ItemTypeA": "EPalItemTypeA::None",
"ItemTypeB": "EPalItemTypeB::None",
"ItemIds": [
{ "Key": "LaserRifle" }, ..., ..., ..., { "Key": "LaserRifle_5" }, // 激光步枪
{ "Key": "BeamSword" }, ..., ..., ..., { "Key": "LaserGatlingGun_5" }, // 激光加特林
{ "Key": "ChargeLaserRifle" }, ..., ..., ..., { "Key": "ChargeLaserRifle_5" }, // 充能步枪
{ "Key": "SkyBeamSword" }, ..., ..., ..., { "Key": "SkyBeamSword_5" }, // 高出力光剑
{ "Key": "BeamLauncher" }, ..., ..., ..., { "Key": "BeamLauncher_5" }, // 光束炮发射器
{ "Key": "BeamSword_2" }, ..., ..., ..., { "Key": "EnergyRocketLauncher" }, // 等离子跑
{ "Key": "EnergyRocketLauncher_2" }, ..., ..., ..., { "Key": "EnergyShotgun" }, // 能量霰弹枪
{ "Key": "EnergyShotgun_2" }, ..., ..., ..., { "Key": "WidePenetrateShotgun" }, // 散射光束枪
{ "Key": "WidePenetrateShotgun_2" }, ..., ..., ..., { "Key": "OverheatRifle" }, // 过热步枪
{ "Key": "OverheatRifle_2" }, ..., ..., ..., { "Key": "ElectricArcAssaultRifle" }, // 等离子步枪
{ "Key": "ElectricArcAssaultRifle_2" }, ..., ..., ..., { "Key": "WidePenetrateShotgun" }, // 散射光束枪(重复
{ "Key": "WidePenetrateShotgun_2" }, ..., ..., ..., { "Key": "LaserMiningTool" }, // 多功能等离子切割器
{ "Key": "YakushimaGun001" }, ..., ..., ..., { "Key": "YakushimaGun001_5" } // 星璇机枪
],
"ExcludedItemIds": [],
"bIsContainNoItemId": false,
- 宗铭丸 10/15/20/25/35%。参数:
"ItemParam": {
"ItemTypeA": "EPalItemTypeA::None",
"ItemTypeB": "EPalItemTypeB::WeaponMelee",
"ItemIds": [],
"ExcludedItemIds": [
{ "Key": "YakushimaLantern001" }, ..., ..., ..., { "Key": "YakushimaLantern001_5" }, // 夜光
{ "Key": "YakushimaBlade003" }, ..., ..., ..., { "Key": "YakushimaBlade003_5" }, // 泰拉棱镜
{ "Key": "DroneLauncher" }, ..., ..., ..., { "Key": "DroneLauncher_5" } // 无人机发射器
],
}
// 木棒、携带火把、棒球棒、金属球棒、切肉刀、电棍、石头长矛、金属长矛、精炼金属长矛、女皇蜂之杖、莉莉之矛、强化莉莉之矛、原始的剑、剑
// 石头斧、金属斧头、精炼金属斧头、帕鲁金属斧、石制十字镐、金属十字镐、精炼金属十字镐、帕鲁金属十字镐、多功能等离子切割器
// 太刀、光剑、高出力光剑、彩虹猫之刃、传奇彩虹猫之刃、泰拉刃、断钢剑
- 被召唤的杰诺路达?同杰诺路达
attacker = info->Attacker;
if (UObject_IsValid(attacker) &&
(attackerPassiveComponent = Character_GetPassiveSkillComponent(attacker)) != NULL) {
if (Passive_Has(attackerPassiveComponent,
EPAL_PASSIVE_SKILL_EFFECT_TYPE_DamageRateByEquippedWeapon, 0)) {
atkPassiveWeaponTypeBoostRate = (float)Passive_GetWeaponRelatedBoostRate(
attackerPassiveComponent, 1.0f,
EPAL_PASSIVE_SKILL_EFFECT_TYPE_DamageRateByEquippedWeapon, info);
}
}
atkPassiveWeaponLastBulletBoostRate 攻击方被动弹匣最后子弹增伤乘区
杰诺贝达所拥有的专属乘区、info 专属属性。。。真有人用过这个?
走的是和 atkPassiveWeaponTypeBoostRate 相同的统计方式,对应被动效果类型为 DamageUp_LastBullet
来源只有杰诺贝达 100/110/120/135/160%,筛选参数:
"ItemTypeA": "EPalItemTypeA::None",
"ItemTypeB": "EPalItemTypeB::None",
"ItemIds": [
{ "Key": "AssaultRifle_Default1" }, ..., ..., ..., { "Key": "AssaultRifle_Default5" },
{ "Key": "GatlingGun" }, ..., ..., ..., { "Key": "GatlingGun_4" },
{ "Key": "GatlingGun_5" }, ..., ..., ..., { "Key": "LaserGatlingGun_2" },
{ "Key": "LaserGatlingGun_3" }, ..., ..., ..., { "Key": "MakeshiftAssaultRifle_3" },
{ "Key": "MakeshiftAssaultRifle_4" }, ..., ..., ..., { "Key": "MakeshiftSubmachineGun_4" },
{ "Key": "MakeshiftSubmachineGun_5" }, ..., ..., ..., { "Key": "SubmachineGun_5" },
{ "Key": "OverheatRifle" }, ..., ..., ..., { "Key": "OverheatRifle_5" },
{ "Key": "YakushimaGun001" }, ..., ..., ..., { "Key": "YakushimaGun001_5" },
{ "Key": "SkySubmachineGun" }, ..., ..., ..., { "Key": "SkySubmachineGun_5" },
{ "Key": "SkyAssaultRifle" }, ..., ..., ..., { "Key": "SkyAssaultRifle_5" },
{ "Key": "ElectricArcAssaultRifle" }, ..., ..., ..., { "Key": "ElectricArcAssaultRifle_5" },
{ "Key": "WidePenetrateShotgun" }, ..., ..., ..., { "Key": "WidePenetrateShotgun_5" }
],
"ExcludedItemIds": [],
"bIsContainNoItemId": false,
attacker = info->Attacker;
if (UObject_IsValid(attacker) &&
(attackerPassiveComponent = Character_GetPassiveSkillComponent(attacker)) != NULL) {
if (info->bIsLastBullet &&
Passive_Has(attackerPassiveComponent,
EPAL_PASSIVE_SKILL_EFFECT_TYPE_DamageUp_LastBullet, 0)) {
atkPassiveWeaponLastBulletBoostRate = (float)Passive_GetWeaponRelatedBoostRate(
attackerPassiveComponent, 1.0f,
EPAL_PASSIVE_SKILL_EFFECT_TYPE_DamageUp_LastBullet, info);
}
}