伤害计算

以下所有讨论均基于幻兽帕鲁 v1.0.2.101103 版本

伤害乘区

我们按顺序

变量、参数的定义

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 // 经过乘区修正的伤害

核心计算公式

完整公式:

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);

重要细节:

世界设置 bIsPvP = False 且游戏内不位于竞技场时 pvpFlyBoostRatepvpOwnedPalRatearenaRate1.0f

乘区细节

palDamageRate 帕鲁乘区

palDamageRate = palDefenderDamageRate * palAttackerDamageRate;
palDefenderDamageRate = palDefenderReceiveDamageRate * palDefenderAdditionalReceiveDamageRate;
palAttackerDamageRate = palAttackerInflictDamageRate * palAttackerAdditionalInflictDamageRate;

elementStatusAilmentRate

elementWeakRate 属性克制乘区

乘区详细分析

定义和通用分析

后面的乘区分析会经常使用这里的内容。乘区分析的文本默认读者已经了解这一部分的内容。

AActor *defender
AActor *attacker
FPalDamageInfo *info

UGameInstance 是进程意义上的单例类,所有 AActor 都有一个 GetGameInstance() 成员函数,当该对象存在于某个 UWorld 时该函数会返回 UGameInstance 单例。UPalGameInstance 继承了 UGameInstance

配置相关

UPalGameInstance 单例持有属性 DatabaseCharacterParameter : UPalDatabaseCharacterParameterUPalDatabaseCharacterParameter 是一个数据库类,其持有很多 UDataTableUDataTable 有查询函数 PalDataTable_FindRow

UPalGameInstance 单例持有属性 GameSetting : UPalGameSettingUPalGameSetting 是一个配置类。

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 是主要承载角色(包括帕鲁和玩家)被动效果的组件,包括帕鲁伙伴技能的被动效果、词条的被动效果、遗物(雕像)效果、防具头盔饰品被动效果、当前使用武器的被动效果。

QueryEffectSumCollectExternalEffects(这两个函数都没有被声明为 UFUNCTION,usmap 没有对应符号,所以这是根据语义取的名字):

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;
      }
    }
  }
}
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 对应 palDamageRatepalDefenderDamageRatepalAttackerDamageRate 并没有实际存在而是直接被聚合到 palDamageRate 中。主要逻辑为:

乘区语义:

  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

代码中用 defenderUPalCharacterParameterComponent 组件的属性 DamageUpElement_ByElementStatusDamageDownElement_ByElementStatus 来进行弱点属性判断。相对于对于 "获取异常状态然后根据某个表格判断" 这种模式做了一层缓存。

异常状态本身是由一个完全独立的组件 UPalStatusComponent 管理的,修改 UPalCharacterParameterComponentDamageUpElement_ByElementStatus 以及 DamageDownElement_ByElementStatus 应当属于异常状态施加调用链尾端的一个副作用。

考虑直接检索 DamageDownElement_ByElementStatusDamageUpElement_ByElementStatus 的写指令,一个候选是形如 register + instant_fixed_offset 形式的写指令,但是并没有找到。原因是蓝图侧的写入从 FProperty 中读取偏移。从蓝图侧以语义关键词入手的话可以找到核心蓝图 UBP_Status_WithVisualEffect_C,它是所有元素异常的蓝图基类。所有元素异常的子类中都可以覆盖属性 DamageUpElement : EPalElementTypeDamageDownElement : EPalElementType。列表如下:

语义总结:存在对于以下几种异常状态的不同攻击行为元素属性的抵抗和易伤倍率,易伤倍率为 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.43f0.66f 等数值而非通过数值计算 2 / 3 或者 4 / 9

语义总结:

  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 只是它的浅封装

  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 的传入机制

其中

用于计算 defPassiveExplosionResistRatePalPassiveSkillComponent_GetExplosionResistRatePalPassiveSkillComponent_GetElementResistRate 的浅封装

计算 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 的一层浅封装

attacker = info->Attacker;
if (UObject_IsValid(attacker)
    && (attackerPassiveComponent = Character_GetPassiveSkillComponent(attacker)) != NULL) {
    atkPassiveElementBoostRate = (float)Passive_GetElementBoostRate(
	    attackerPassiveComponent, info->AttackElementType);
}

atkPassiveElementWeaknessBoostRate 攻击方被动属性克制额外增伤乘区

Passive_GetElementWeaknessBoostRatePalPassiveSkillComponent_QueryEffectSum 的浅封装

attacker = info->Attacker;
if (UObject_IsValid(attacker)
	&& (attackerPassiveComponent = Character_GetPassiveSkillComponent(attacker)) != NULL) {
    atkPassiveElementWeaknessBoostRate = (float)Passive_GetElementWeaknessBoostRate(
        attackerPassiveComponent, info->AttackElementType,
        aux->DefenderElementType1, aux->DefenderElementType2);
}

atkPassiveStatusAilmentBoostRate 攻击方被动异常状态额外增伤乘区

考虑到似乎只能同时存在毒和一种其他状态(尚未经过代码验证),这个乘区最大能叠到 2.3f 左右(浮点误差?)

attacker = info->Attacker;
if (UObject_IsValid(attacker)
	&& (attackerPassiveComponent = Character_GetPassiveSkillComponent(attacker)) != NULL) {
    atkPassiveStatusAilmentBoostRate = (float)Passive_GetStatusAilmentBoostRate(
	    attackerPassiveComponent, defender);
}

atkPassiveBodyPartBoostRate 攻击方被动身体部位弱点额外增伤乘区

当命中地方弱点部位时(根据受击行为信息 info 的属性 BodyPartsType 来进行判断)

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 找到对应武器物品的 TypeATypeB 属性,利用二者和 info->AttackStaticItemID 本身一起来构建过滤器(筛选对应武器类型的被动;QueryEffectSum 的过滤器参数)、其他有效性检查。然后调用 QueryEffectSum。对应数值会被直接累加然后以百分比的形式作为 atkPassiveWeaponTypeBoostRate

来源包括

"ItemTypeA": "EPalItemTypeA::None",
"ItemTypeB": "EPalItemTypeB::WeaponShotgun",
"ItemIds": [], 
"ExcludedItemIds": [],  
"bIsContainNoItemId": false,
// 破旧的霰弹枪、双管霰弹枪、泵动式霰弹枪、半自动霰弹枪、能量霰弹枪、原型霰弹枪、散射光束枪、核心喷射霰弹枪
"ItemTypeA": "EPalItemTypeA::None",
"ItemTypeB": "EPalItemTypeB::WeaponBow",
"ItemIds": [], // 
"ExcludedItemIds": [],  
"bIsContainNoItemId": false,
// 陈旧的弓、三连弓、复合弓、卓越弓、机械弓
"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,
"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);
    }
}