Pet Battles: Implement weather system and battle positioning
This commit is contained in:
@@ -901,8 +901,9 @@ namespace PetBattles
|
||||
// Process each effect in order
|
||||
for (BattlePetAbilityEffectEntry const* effectEntry : *effects)
|
||||
{
|
||||
TC_LOG_DEBUG("server.loading", "PetBattle: ProcessEffect effectID={} propsID={} Params=[{},{},{},{},{},{}]",
|
||||
TC_LOG_DEBUG("server.loading", "PetBattle: ProcessEffect effectID={} propsID={} auraAbilityID={} visualID={} Params=[{},{},{},{},{},{}]",
|
||||
effectEntry->ID, effectEntry->BattlePetEffectPropertiesID,
|
||||
effectEntry->AuraBattlePetAbilityID, effectEntry->BattlePetVisualID,
|
||||
effectEntry->Param[0], effectEntry->Param[1], effectEntry->Param[2],
|
||||
effectEntry->Param[3], effectEntry->Param[4], effectEntry->Param[5]);
|
||||
ProcessEffect(effectEntry, attackerTeam, attackerPet, defenderTeam, defenderPet, abilityID);
|
||||
@@ -952,53 +953,17 @@ namespace PetBattles
|
||||
}
|
||||
}
|
||||
|
||||
// Route to appropriate handler based on effect properties ID
|
||||
// We map the DB2 BattlePetEffectPropertiesID to our action types
|
||||
// In practice, the ID maps to specific effect behaviors
|
||||
// Look up the abstract action type from the DB2 PropsID mapping (built at startup)
|
||||
PetBattleAbilityEffectAction effectAction = sPetBattleMgr->GetEffectAction(effectPropsID);
|
||||
|
||||
BattlePetEffectPropertiesEntry const* effectProps = sBattlePetEffectPropertiesStore.LookupEntry(effectPropsID);
|
||||
TC_LOG_DEBUG("server.loading", "PetBattle ProcessEffect: propsID={} found={} basePower={} accuracy={}",
|
||||
effectPropsID, effectProps != nullptr, basePower, accuracy);
|
||||
if (effectProps)
|
||||
TC_LOG_DEBUG("server.loading", "PetBattle ProcessEffect: effectCategory(ParamTypeEnum[0])={}", effectProps->ParamTypeEnum[0]);
|
||||
if (!effectProps)
|
||||
TC_LOG_DEBUG("server.loading", "PetBattle ProcessEffect: propsID={} action={} basePower={} accuracy={}",
|
||||
effectPropsID, uint16(effectAction), basePower, accuracy);
|
||||
|
||||
switch (effectAction)
|
||||
{
|
||||
// If no properties entry, treat as simple damage with basePower
|
||||
if (basePower > 0 && defender.IsAlive())
|
||||
{
|
||||
DamageResult dmg = CalculateAbilityDamage(basePower, attacker.EffectivePower, abilityType, attacker, defender);
|
||||
defender.Health = std::max(0, defender.Health - dmg.Damage);
|
||||
|
||||
PetBattleRoundEffect roundEffect;
|
||||
roundEffect.AbilityEffectID = effect->ID;
|
||||
roundEffect.EffectType = PET_BATTLE_EFFECT_SET_HEALTH;
|
||||
roundEffect.SourceTeam = attackerTeam;
|
||||
roundEffect.SourcePet = attackerPet;
|
||||
roundEffect.TargetTeam = defenderTeam;
|
||||
roundEffect.TargetPet = defenderPet;
|
||||
roundEffect.Param1 = defender.Health;
|
||||
if (dmg.IsCrit)
|
||||
roundEffect.Flags |= PET_BATTLE_EFFECT_FLAG_CRIT;
|
||||
if (dmg.TypeMod > 1.0f)
|
||||
roundEffect.Flags |= PET_BATTLE_EFFECT_FLAG_STRONG;
|
||||
else if (dmg.TypeMod < 1.0f)
|
||||
roundEffect.Flags |= PET_BATTLE_EFFECT_FLAG_WEAK;
|
||||
_roundEffects.push_back(roundEffect);
|
||||
|
||||
ApplyPassiveOnDamageDealt(attackerTeam, attackerPet, defenderTeam, defenderPet, dmg.Damage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Route on the EffectProperties ID ? this directly maps to PetBattleAbilityEffectAction
|
||||
// (0=Damage, 1=Heal, 2=ApplyAura, 3=ChangeState, ... 18=MultiTurnEnd)
|
||||
uint16 effectCategory = effectPropsID;
|
||||
|
||||
// Map effect categories to actions
|
||||
// effectCategory values are stable across client versions
|
||||
switch (effectCategory)
|
||||
{
|
||||
case 0: // Direct damage
|
||||
case 14: // Capped damage
|
||||
case PET_BATTLE_EFFECT_ACTION_DAMAGE:
|
||||
case PET_BATTLE_EFFECT_ACTION_DAMAGE_CAPPED:
|
||||
{
|
||||
if (!defender.IsAlive())
|
||||
break;
|
||||
@@ -1025,8 +990,8 @@ namespace PetBattles
|
||||
ApplyPassiveOnDamageDealt(attackerTeam, attackerPet, defenderTeam, defenderPet, dmg.Damage);
|
||||
break;
|
||||
}
|
||||
case 1: // Healing
|
||||
case 15: // Capped healing
|
||||
case PET_BATTLE_EFFECT_ACTION_HEAL:
|
||||
case PET_BATTLE_EFFECT_ACTION_HEAL_CAPPED:
|
||||
{
|
||||
int32 healing = CalculateAbilityHealing(basePower, attacker.EffectivePower, attacker);
|
||||
attacker.Health = std::min(attacker.MaxHealth, attacker.Health + healing);
|
||||
@@ -1043,22 +1008,83 @@ namespace PetBattles
|
||||
_roundEffects.push_back(roundEffect);
|
||||
break;
|
||||
}
|
||||
case 2: // Apply aura (DoT/HoT/Buff/Debuff)
|
||||
case 12: // Periodic damage aura
|
||||
case PET_BATTLE_EFFECT_ACTION_APPLY_AURA:
|
||||
case PET_BATTLE_EFFECT_ACTION_PERIODIC_DAMAGE:
|
||||
{
|
||||
int8 auraDuration = static_cast<int8>(effect->Param[2]);
|
||||
if (auraDuration <= 0)
|
||||
auraDuration = 3; // Default duration
|
||||
|
||||
DamageResult dmg = CalculateAbilityDamage(basePower, attacker.EffectivePower, abilityType, attacker, defender);
|
||||
int32 tickDamage = dmg.Damage;
|
||||
// Weather auras: if AuraBattlePetAbilityID is a weather ability, apply to environment
|
||||
if (effect->AuraBattlePetAbilityID != 0 && sPetBattleMgr->IsWeatherAbility(effect->AuraBattlePetAbilityID))
|
||||
{
|
||||
TC_LOG_DEBUG("server.loading", "PetBattle WEATHER_AURA: effectID={} auraAbilityID={} duration={}",
|
||||
effect->ID, effect->AuraBattlePetAbilityID, auraDuration);
|
||||
|
||||
PetBattleAuraType auraType = (effectCategory == 12) ? PET_BATTLE_AURA_DOT : PET_BATTLE_AURA_DEBUFF;
|
||||
if (basePower < 0) // Negative power = healing aura
|
||||
// Cancel existing weather on slot 0 if any
|
||||
if (_environments[0].AbilityID != 0 && _environments[0].AuraInstanceID != 0)
|
||||
{
|
||||
PetBattleRoundEffect cancelEffect;
|
||||
cancelEffect.EffectType = PET_BATTLE_EFFECT_AURA_CANCEL;
|
||||
cancelEffect.TargetEnvSlot = 0;
|
||||
cancelEffect.Param1 = _environments[0].AuraInstanceID;
|
||||
cancelEffect.Param2 = _environments[0].AbilityID;
|
||||
_roundEffects.push_back(cancelEffect);
|
||||
}
|
||||
|
||||
// Set up environment slot 0 with the weather aura
|
||||
_environments[0].AbilityID = effect->AuraBattlePetAbilityID;
|
||||
_environments[0].WeatherType = PET_BATTLE_WEATHER_NONE; // Gameplay mods come from SET_STATE effects
|
||||
_environments[0].RemainingRounds = auraDuration;
|
||||
_environments[0].CasterTeam = attackerTeam;
|
||||
_environments[0].AuraInstanceID = _nextAuraInstanceID++;
|
||||
_environments[0].CurrentRound = _currentRound;
|
||||
|
||||
// Emit AURA_APPLY targeting environment PBOID 6 (env slot 0)
|
||||
PetBattleRoundEffect applyEffect;
|
||||
applyEffect.AbilityEffectID = effect->ID;
|
||||
applyEffect.EffectType = PET_BATTLE_EFFECT_AURA_APPLY;
|
||||
applyEffect.SourceTeam = attackerTeam;
|
||||
applyEffect.SourcePet = attackerPet;
|
||||
applyEffect.TargetEnvSlot = 0;
|
||||
applyEffect.Param1 = _environments[0].AuraInstanceID;
|
||||
applyEffect.Param2 = effect->AuraBattlePetAbilityID;
|
||||
applyEffect.Param3 = auraDuration;
|
||||
applyEffect.Param4 = _currentRound;
|
||||
_roundEffects.push_back(applyEffect);
|
||||
break;
|
||||
}
|
||||
|
||||
// Determine if this aura targets self or enemy
|
||||
// DoTs (basePower > 0) target the enemy; HoTs/buffs (basePower <= 0) target self
|
||||
// AuraBattlePetAbilityID != 0 indicates a self-referencing aura (self-buff)
|
||||
bool targetsSelf = false;
|
||||
if (basePower < 0)
|
||||
targetsSelf = true; // Negative = healing aura on self
|
||||
else if (basePower == 0 && effect->AuraBattlePetAbilityID != 0)
|
||||
targetsSelf = true; // Non-damaging aura with ability reference = self-buff
|
||||
else if (basePower == 0 && effectAction == PET_BATTLE_EFFECT_ACTION_APPLY_AURA)
|
||||
targetsSelf = true; // Generic non-damaging aura defaults to self-buff
|
||||
|
||||
uint8 auraTargetTeam = targetsSelf ? attackerTeam : defenderTeam;
|
||||
uint8 auraTargetPet = targetsSelf ? attackerPet : defenderPet;
|
||||
|
||||
DamageResult dmg = CalculateAbilityDamage(std::abs(basePower), attacker.EffectivePower, abilityType, attacker, defender);
|
||||
int32 tickDamage = basePower != 0 ? dmg.Damage : 0;
|
||||
|
||||
PetBattleAuraType auraType;
|
||||
if (effectAction == PET_BATTLE_EFFECT_ACTION_PERIODIC_DAMAGE)
|
||||
auraType = PET_BATTLE_AURA_DOT;
|
||||
else if (basePower < 0)
|
||||
auraType = PET_BATTLE_AURA_HOT;
|
||||
else if (targetsSelf)
|
||||
auraType = PET_BATTLE_AURA_BUFF;
|
||||
else
|
||||
auraType = PET_BATTLE_AURA_DEBUFF;
|
||||
|
||||
// Critter passive: immune to stun, root, sleep
|
||||
if (defender.PetType == PET_TYPE_CRITTER &&
|
||||
PetBattlePetData& auraTarget = _teams[auraTargetTeam].Pets[auraTargetPet];
|
||||
if (auraTarget.PetType == PET_TYPE_CRITTER &&
|
||||
(auraType == PET_BATTLE_AURA_STUN || auraType == PET_BATTLE_AURA_ROOT || auraType == PET_BATTLE_AURA_SLEEP))
|
||||
{
|
||||
PetBattleRoundEffect immuneEffect;
|
||||
@@ -1067,26 +1093,29 @@ namespace PetBattles
|
||||
immuneEffect.Flags = PET_BATTLE_EFFECT_FLAG_IMMUNE;
|
||||
immuneEffect.SourceTeam = attackerTeam;
|
||||
immuneEffect.SourcePet = attackerPet;
|
||||
immuneEffect.TargetTeam = defenderTeam;
|
||||
immuneEffect.TargetPet = defenderPet;
|
||||
immuneEffect.TargetTeam = auraTargetTeam;
|
||||
immuneEffect.TargetPet = auraTargetPet;
|
||||
_roundEffects.push_back(immuneEffect);
|
||||
break;
|
||||
}
|
||||
|
||||
AddAura(defenderTeam, defenderPet, abilityID, effect->ID,
|
||||
TC_LOG_DEBUG("server.loading", "PetBattle AURA: targetsSelf={} auraType={} auraTarget=[{},{}] duration={} tickDmg={} auraAbilityID={}",
|
||||
targetsSelf, uint8(auraType), auraTargetTeam, auraTargetPet, auraDuration, tickDamage, effect->AuraBattlePetAbilityID);
|
||||
|
||||
AddAura(auraTargetTeam, auraTargetPet, abilityID, effect->ID,
|
||||
auraType, auraDuration, tickDamage, attacker.PetType,
|
||||
attackerTeam, attackerPet);
|
||||
|
||||
{
|
||||
PetBattlePetData const& targetPet = _teams[defenderTeam].Pets[defenderPet];
|
||||
PetBattleAura const& newAura = targetPet.Auras.back();
|
||||
PetBattlePetData const& targetPetData = _teams[auraTargetTeam].Pets[auraTargetPet];
|
||||
PetBattleAura const& newAura = targetPetData.Auras.back();
|
||||
PetBattleRoundEffect roundEffect;
|
||||
roundEffect.AbilityEffectID = effect->ID;
|
||||
roundEffect.EffectType = PET_BATTLE_EFFECT_AURA_APPLY;
|
||||
roundEffect.SourceTeam = attackerTeam;
|
||||
roundEffect.SourcePet = attackerPet;
|
||||
roundEffect.TargetTeam = defenderTeam;
|
||||
roundEffect.TargetPet = defenderPet;
|
||||
roundEffect.TargetTeam = auraTargetTeam;
|
||||
roundEffect.TargetPet = auraTargetPet;
|
||||
roundEffect.Param1 = newAura.AuraInstanceID;
|
||||
roundEffect.Param2 = abilityID;
|
||||
roundEffect.Param3 = newAura.RemainingRounds;
|
||||
@@ -1095,12 +1124,49 @@ namespace PetBattles
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 13: // Periodic heal aura
|
||||
case PET_BATTLE_EFFECT_ACTION_PERIODIC_HEAL:
|
||||
{
|
||||
int8 auraDuration = static_cast<int8>(effect->Param[2]);
|
||||
if (auraDuration <= 0)
|
||||
auraDuration = 3;
|
||||
|
||||
// Weather periodic auras (e.g. Moonlight HoT): route to environment
|
||||
if (effect->AuraBattlePetAbilityID != 0 && sPetBattleMgr->IsWeatherAbility(effect->AuraBattlePetAbilityID))
|
||||
{
|
||||
TC_LOG_DEBUG("server.loading", "PetBattle WEATHER_PERIODIC: effectID={} auraAbilityID={} duration={}",
|
||||
effect->ID, effect->AuraBattlePetAbilityID, auraDuration);
|
||||
|
||||
if (_environments[0].AbilityID != 0 && _environments[0].AuraInstanceID != 0)
|
||||
{
|
||||
PetBattleRoundEffect cancelEffect;
|
||||
cancelEffect.EffectType = PET_BATTLE_EFFECT_AURA_CANCEL;
|
||||
cancelEffect.TargetEnvSlot = 0;
|
||||
cancelEffect.Param1 = _environments[0].AuraInstanceID;
|
||||
cancelEffect.Param2 = _environments[0].AbilityID;
|
||||
_roundEffects.push_back(cancelEffect);
|
||||
}
|
||||
|
||||
_environments[0].AbilityID = effect->AuraBattlePetAbilityID;
|
||||
_environments[0].WeatherType = PET_BATTLE_WEATHER_NONE;
|
||||
_environments[0].RemainingRounds = auraDuration;
|
||||
_environments[0].CasterTeam = attackerTeam;
|
||||
_environments[0].AuraInstanceID = _nextAuraInstanceID++;
|
||||
_environments[0].CurrentRound = _currentRound;
|
||||
|
||||
PetBattleRoundEffect applyEffect;
|
||||
applyEffect.AbilityEffectID = effect->ID;
|
||||
applyEffect.EffectType = PET_BATTLE_EFFECT_AURA_APPLY;
|
||||
applyEffect.SourceTeam = attackerTeam;
|
||||
applyEffect.SourcePet = attackerPet;
|
||||
applyEffect.TargetEnvSlot = 0;
|
||||
applyEffect.Param1 = _environments[0].AuraInstanceID;
|
||||
applyEffect.Param2 = effect->AuraBattlePetAbilityID;
|
||||
applyEffect.Param3 = auraDuration;
|
||||
applyEffect.Param4 = _currentRound;
|
||||
_roundEffects.push_back(applyEffect);
|
||||
break;
|
||||
}
|
||||
|
||||
int32 tickHealing = CalculateAbilityHealing(basePower, attacker.EffectivePower, attacker);
|
||||
|
||||
AddAura(attackerTeam, attackerPet, abilityID, effect->ID,
|
||||
@@ -1125,8 +1191,8 @@ namespace PetBattles
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 3: // Change state
|
||||
case 6: // Set state
|
||||
case PET_BATTLE_EFFECT_ACTION_CHANGE_STATE:
|
||||
case PET_BATTLE_EFFECT_ACTION_SET_STATE:
|
||||
{
|
||||
uint32 stateID = static_cast<uint32>(effect->Param[2]);
|
||||
int32 stateValue = static_cast<int32>(effect->Param[3]);
|
||||
@@ -1136,7 +1202,7 @@ namespace PetBattles
|
||||
{
|
||||
if (id == stateID)
|
||||
{
|
||||
val = (effectCategory == 3) ? val + stateValue : stateValue;
|
||||
val = (effectAction == PET_BATTLE_EFFECT_ACTION_CHANGE_STATE) ? val + stateValue : stateValue;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@@ -1156,7 +1222,7 @@ namespace PetBattles
|
||||
_roundEffects.push_back(roundEffect);
|
||||
break;
|
||||
}
|
||||
case 4: // Damage percentage
|
||||
case PET_BATTLE_EFFECT_ACTION_DAMAGE_PERCENTAGE:
|
||||
{
|
||||
if (!defender.IsAlive())
|
||||
break;
|
||||
@@ -1188,7 +1254,7 @@ namespace PetBattles
|
||||
ApplyPassiveOnDamageDealt(attackerTeam, attackerPet, defenderTeam, defenderPet, damage);
|
||||
break;
|
||||
}
|
||||
case 5: // Heal percentage
|
||||
case PET_BATTLE_EFFECT_ACTION_HEAL_PERCENTAGE:
|
||||
{
|
||||
int32 healing = int32(attacker.MaxHealth * (basePower / 100.0f));
|
||||
attacker.Health = std::min(attacker.MaxHealth, attacker.Health + healing);
|
||||
@@ -1205,7 +1271,7 @@ namespace PetBattles
|
||||
_roundEffects.push_back(roundEffect);
|
||||
break;
|
||||
}
|
||||
case 7: // Force pet swap
|
||||
case PET_BATTLE_EFFECT_ACTION_PET_SWAP:
|
||||
{
|
||||
if (defender.IsAlive() && _teams[defenderTeam].PetCount > 1)
|
||||
{
|
||||
@@ -1232,43 +1298,12 @@ namespace PetBattles
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 8: // Apply buff aura to self (stat modifier, shield, etc.)
|
||||
case PET_BATTLE_EFFECT_ACTION_CATCH:
|
||||
{
|
||||
int8 auraDuration = static_cast<int8>(effect->Param[2]);
|
||||
if (auraDuration <= 0)
|
||||
auraDuration = 3;
|
||||
|
||||
// basePower represents the stat modifier percentage (e.g., 25 = +25%)
|
||||
int32 modifierValue = std::abs(basePower);
|
||||
if (modifierValue == 0)
|
||||
modifierValue = 25; // Default modifier
|
||||
|
||||
AddAura(attackerTeam, attackerPet, abilityID, effect->ID,
|
||||
PET_BATTLE_AURA_BUFF, auraDuration, modifierValue, attacker.PetType,
|
||||
attackerTeam, attackerPet);
|
||||
|
||||
{
|
||||
PetBattlePetData const& selfPet = _teams[attackerTeam].Pets[attackerPet];
|
||||
PetBattleAura const& newAura = selfPet.Auras.back();
|
||||
PetBattleRoundEffect roundEffect;
|
||||
roundEffect.AbilityEffectID = effect->ID;
|
||||
roundEffect.EffectType = PET_BATTLE_EFFECT_AURA_APPLY;
|
||||
roundEffect.SourceTeam = attackerTeam;
|
||||
roundEffect.SourcePet = attackerPet;
|
||||
roundEffect.TargetTeam = attackerTeam;
|
||||
roundEffect.TargetPet = attackerPet;
|
||||
roundEffect.Param1 = newAura.AuraInstanceID;
|
||||
roundEffect.Param2 = abilityID;
|
||||
roundEffect.Param3 = newAura.RemainingRounds;
|
||||
roundEffect.Param4 = newAura.CurrentRound;
|
||||
_roundEffects.push_back(roundEffect);
|
||||
}
|
||||
|
||||
// Recalculate stats immediately so the buff takes effect this round
|
||||
attacker.RecalculateEffectiveStats();
|
||||
// Trap/catch is handled separately by the trap input path
|
||||
break;
|
||||
}
|
||||
case 9: // Change max health
|
||||
case PET_BATTLE_EFFECT_ACTION_CHANGE_MAX_HEALTH:
|
||||
{
|
||||
int32 healthChange = static_cast<int32>(basePower);
|
||||
defender.MaxHealth = std::max(1, defender.MaxHealth + healthChange);
|
||||
@@ -1285,17 +1320,29 @@ namespace PetBattles
|
||||
_roundEffects.push_back(roundEffect);
|
||||
break;
|
||||
}
|
||||
case 10: // Set weather
|
||||
case PET_BATTLE_EFFECT_ACTION_WEATHER_SET:
|
||||
{
|
||||
PetBattleWeatherType weatherType = static_cast<PetBattleWeatherType>(effect->Param[2]);
|
||||
int8 weatherDuration = static_cast<int8>(effect->Param[3]);
|
||||
if (weatherDuration <= 0)
|
||||
weatherDuration = 9; // Default weather duration
|
||||
// PropsID 170: labels [0]=Points, [1]=Accuracy, [2]=weatherState, [3]=Unused, [4]=IsPeriodic
|
||||
// Param[2] = BattlePetState ID to set on environment, Param[0] = state value (Points)
|
||||
uint32 stateID = static_cast<uint32>(effect->Param[2]);
|
||||
int32 stateValue = static_cast<int32>(effect->Param[0]);
|
||||
|
||||
SetWeather(weatherType, abilityID, weatherDuration, attackerTeam);
|
||||
TC_LOG_DEBUG("server.loading", "PetBattle WEATHER_SET_STATE: effectID={} stateID={} stateValue={}",
|
||||
effect->ID, stateID, stateValue);
|
||||
|
||||
// Emit SET_STATE targeting environment PBOID (slot 0)
|
||||
PetBattleRoundEffect roundEffect;
|
||||
roundEffect.AbilityEffectID = effect->ID;
|
||||
roundEffect.EffectType = PET_BATTLE_EFFECT_SET_STATE;
|
||||
roundEffect.SourceTeam = attackerTeam;
|
||||
roundEffect.SourcePet = attackerPet;
|
||||
roundEffect.TargetEnvSlot = 0; // Environment slot 0
|
||||
roundEffect.Param1 = stateID;
|
||||
roundEffect.Param2 = stateValue;
|
||||
_roundEffects.push_back(roundEffect);
|
||||
break;
|
||||
}
|
||||
case 11: // Stun
|
||||
case PET_BATTLE_EFFECT_ACTION_STUN:
|
||||
{
|
||||
// Critter passive: immune to stun
|
||||
if (defender.PetType == PET_TYPE_CRITTER)
|
||||
@@ -1325,7 +1372,7 @@ namespace PetBattles
|
||||
_roundEffects.push_back(roundEffect);
|
||||
break;
|
||||
}
|
||||
case 16: // Remove aura
|
||||
case PET_BATTLE_EFFECT_ACTION_REMOVE_AURA:
|
||||
{
|
||||
uint32 auraAbilityID = static_cast<uint32>(effect->Param[2]);
|
||||
if (auraAbilityID == 0)
|
||||
@@ -1333,7 +1380,7 @@ namespace PetBattles
|
||||
RemoveAura(defenderTeam, defenderPet, auraAbilityID);
|
||||
break;
|
||||
}
|
||||
case 17: // Multi-turn begin marker
|
||||
case PET_BATTLE_EFFECT_ACTION_MULTI_TURN_BEGIN:
|
||||
{
|
||||
// DB2 effects with this category appear on the first turn of multi-turn abilities.
|
||||
// They signal to the client that the ability's multi-turn sequence is active.
|
||||
@@ -1395,7 +1442,7 @@ namespace PetBattles
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 18: // Multi-turn end marker
|
||||
case PET_BATTLE_EFFECT_ACTION_MULTI_TURN_END:
|
||||
{
|
||||
// Appears on the last turn of a multi-turn ability.
|
||||
// Param[2] may contain a state ID to clear (matching the one set by case 17).
|
||||
@@ -1436,8 +1483,8 @@ namespace PetBattles
|
||||
for (uint8 i = 0; i < 6; ++i)
|
||||
if (effectProps->ParamLabel[i] && effectProps->ParamLabel[i][0] != '\0')
|
||||
labels += Trinity::StringFormat("[{}]={} ", i, effectProps->ParamLabel[i]);
|
||||
TC_LOG_WARN("server.loading", "PetBattle ProcessEffect: UNHANDLED effectCategory={} basePower={} defenderAlive={} params=[{},{},{},{},{},{}] labels={}",
|
||||
effectCategory, basePower, defender.IsAlive(),
|
||||
TC_LOG_WARN("server.loading", "PetBattle ProcessEffect: UNHANDLED propsID={} action={} basePower={} defenderAlive={} params=[{},{},{},{},{},{}] labels={}",
|
||||
effectPropsID, uint16(effectAction), basePower, defender.IsAlive(),
|
||||
effect->Param[0], effect->Param[1], effect->Param[2], effect->Param[3], effect->Param[4], effect->Param[5],
|
||||
labels);
|
||||
// Unhandled effect category - treat as damage if basePower > 0
|
||||
@@ -1636,13 +1683,13 @@ namespace PetBattles
|
||||
if (!pet.IsAlive())
|
||||
continue;
|
||||
|
||||
// Clear JUST_APPLIED flag from previous round
|
||||
for (PetBattleAura& aura : pet.Auras)
|
||||
aura.StateFlags &= ~PET_BATTLE_AURA_STATE_JUST_APPLIED;
|
||||
|
||||
// Phase 1: Tick periodic effects (DoT/HoT damage/healing)
|
||||
// Skip auras with JUST_APPLIED ? they were added this round and shouldn't tick yet
|
||||
for (PetBattleAura& aura : pet.Auras)
|
||||
{
|
||||
if (aura.StateFlags & PET_BATTLE_AURA_STATE_JUST_APPLIED)
|
||||
continue;
|
||||
|
||||
if (aura.AuraType == PET_BATTLE_AURA_DOT && aura.DamagePerTick > 0)
|
||||
{
|
||||
int32 tickDamage = aura.DamagePerTick;
|
||||
@@ -1689,8 +1736,12 @@ namespace PetBattles
|
||||
}
|
||||
|
||||
// Phase 2: Emit AURA_CHANGE for each active aura (update CurrentRound)
|
||||
// Skip auras with JUST_APPLIED ? they were just shown via AURA_APPLY this round
|
||||
for (PetBattleAura& aura : pet.Auras)
|
||||
{
|
||||
if (aura.StateFlags & PET_BATTLE_AURA_STATE_JUST_APPLIED)
|
||||
continue;
|
||||
|
||||
aura.CurrentRound++;
|
||||
|
||||
PetBattleRoundEffect changeEffect;
|
||||
@@ -1707,9 +1758,17 @@ namespace PetBattles
|
||||
}
|
||||
|
||||
// Phase 3: Decrement remaining rounds and remove expired auras
|
||||
// Skip auras with JUST_APPLIED ? don't decrement on the round they're applied
|
||||
for (int32 i = static_cast<int32>(pet.Auras.size()) - 1; i >= 0; --i)
|
||||
{
|
||||
PetBattleAura& aura = pet.Auras[i];
|
||||
|
||||
if (aura.StateFlags & PET_BATTLE_AURA_STATE_JUST_APPLIED)
|
||||
{
|
||||
aura.StateFlags &= ~PET_BATTLE_AURA_STATE_JUST_APPLIED;
|
||||
continue;
|
||||
}
|
||||
|
||||
aura.RemainingRounds--;
|
||||
|
||||
if (aura.RemainingRounds <= 0)
|
||||
|
||||
@@ -114,28 +114,44 @@ namespace PetBattles
|
||||
TC_LOG_INFO("server.loading", ">> Loaded {} breed quality entries, {} breed stat entries",
|
||||
uint32(_breedQualityMultipliers.size()), uint32(_breedBaseStats.size()));
|
||||
|
||||
// Dump all BattlePetEffectProperties IDs used by ability effects for mapping
|
||||
{
|
||||
std::map<uint16, uint32> propsIdUsageCount;
|
||||
for (BattlePetAbilityEffectEntry const* entry : sBattlePetAbilityEffectStore)
|
||||
propsIdUsageCount[entry->BattlePetEffectPropertiesID]++;
|
||||
// Build the BattlePetEffectPropertiesID -> action type mapping
|
||||
BuildEffectActionMap();
|
||||
|
||||
TC_LOG_INFO("server.loading", ">> BattlePetEffectProperties mapping ({} unique IDs used by {} effects):",
|
||||
uint32(propsIdUsageCount.size()), turnEffectCount);
|
||||
for (auto const& [propsID, count] : propsIdUsageCount)
|
||||
// Build weather ability ID set: find abilities whose effects use WEATHER_SET PropsIDs
|
||||
// Walk: WEATHER_SET PropsIDs ? effects using those PropsIDs ? turns ? abilities
|
||||
{
|
||||
// Step 1: collect all PropsIDs classified as WEATHER_SET
|
||||
std::unordered_set<uint16> weatherPropsIDs;
|
||||
for (auto const& [propsID, action] : _effectActionMap)
|
||||
if (action == PET_BATTLE_EFFECT_ACTION_WEATHER_SET)
|
||||
weatherPropsIDs.insert(propsID);
|
||||
|
||||
// Step 2: find all turns containing effects with weather PropsIDs
|
||||
std::unordered_set<uint32> weatherTurnIDs;
|
||||
for (BattlePetAbilityEffectEntry const* effect : sBattlePetAbilityEffectStore)
|
||||
if (weatherPropsIDs.count(effect->BattlePetEffectPropertiesID))
|
||||
weatherTurnIDs.insert(effect->BattlePetAbilityTurnID);
|
||||
|
||||
// Step 3: find all abilities owning those turns
|
||||
for (BattlePetAbilityTurnEntry const* turn : sBattlePetAbilityTurnStore)
|
||||
if (weatherTurnIDs.count(turn->ID))
|
||||
_weatherAbilityIDs.insert(turn->BattlePetAbilityID);
|
||||
|
||||
// Step 4: also add abilities referenced by AuraBattlePetAbilityID from weather turn effects
|
||||
// (the aura effects reference weather abilities via AuraBattlePetAbilityID)
|
||||
for (BattlePetAbilityEffectEntry const* effect : sBattlePetAbilityEffectStore)
|
||||
{
|
||||
BattlePetEffectPropertiesEntry const* props = sBattlePetEffectPropertiesStore.LookupEntry(propsID);
|
||||
if (props)
|
||||
{
|
||||
std::string labels;
|
||||
for (uint8 i = 0; i < 6; ++i)
|
||||
if (props->ParamLabel[i] && props->ParamLabel[i][0] != '\0')
|
||||
labels += Trinity::StringFormat("[{}]={} ", i, props->ParamLabel[i]);
|
||||
TC_LOG_INFO("server.loading", " PropsID={:3d} count={:3d} visual={} labels: {}",
|
||||
propsID, count, props->BattlePetVisualID, labels);
|
||||
}
|
||||
else
|
||||
TC_LOG_INFO("server.loading", " PropsID={:3d} count={:3d} (NO DB2 ENTRY)", propsID, count);
|
||||
if (weatherTurnIDs.count(effect->BattlePetAbilityTurnID) && effect->AuraBattlePetAbilityID != 0)
|
||||
_weatherAbilityIDs.insert(effect->AuraBattlePetAbilityID);
|
||||
}
|
||||
|
||||
TC_LOG_INFO("server.loading", ">> Found {} weather ability IDs from {} weather PropsIDs",
|
||||
uint32(_weatherAbilityIDs.size()), uint32(weatherPropsIDs.size()));
|
||||
for (uint32 abilityID : _weatherAbilityIDs)
|
||||
{
|
||||
BattlePetAbilityEntry const* ability = sBattlePetAbilityStore.LookupEntry(abilityID);
|
||||
TC_LOG_INFO("server.loading", " WeatherAbility: ID={} name={}", abilityID,
|
||||
ability ? ability->Name.Str[LOCALE_enUS] : "???");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +160,103 @@ namespace PetBattles
|
||||
TC_LOG_INFO("server.loading", ">> Pet Battle system initialized");
|
||||
}
|
||||
|
||||
void PetBattleMgr::BuildEffectActionMap()
|
||||
{
|
||||
// Classify every BattlePetEffectProperties entry into an abstract action type
|
||||
// based on its ParamLabel signatures and BattlePetVisualID.
|
||||
// This runs once at startup so string comparisons are acceptable.
|
||||
|
||||
uint32 mappedCount = 0;
|
||||
for (BattlePetEffectPropertiesEntry const* props : sBattlePetEffectPropertiesStore)
|
||||
{
|
||||
bool hasPoints = false, hasDuration = false, hasState = false;
|
||||
bool hasChance = false, hasPercentage = false;
|
||||
bool hasWeather = false, hasSwap = false, hasTrap = false;
|
||||
|
||||
for (uint8 i = 0; i < 6; ++i)
|
||||
{
|
||||
if (!props->ParamLabel[i] || !props->ParamLabel[i][0])
|
||||
continue;
|
||||
std::string_view label(props->ParamLabel[i]);
|
||||
if (label == "Points") hasPoints = true;
|
||||
else if (label == "Percentage") { hasPoints = true; hasPercentage = true; }
|
||||
else if (label == "Duration") hasDuration = true;
|
||||
else if (label == "weatherState" || label == "WeatherState" || label == "weatherAura" || label == "WeatherAura") hasWeather = true;
|
||||
else if (label.find("State") != std::string_view::npos) hasState = true;
|
||||
else if (label == "Chance") hasChance = true;
|
||||
else if (label == "SwapIndex" || label == "Swap") hasSwap = true;
|
||||
else if (label == "TrapAbility") hasTrap = true;
|
||||
}
|
||||
|
||||
PetBattleAbilityEffectAction action;
|
||||
|
||||
if (hasWeather)
|
||||
action = PET_BATTLE_EFFECT_ACTION_WEATHER_SET;
|
||||
else if (hasSwap)
|
||||
action = PET_BATTLE_EFFECT_ACTION_PET_SWAP;
|
||||
else if (hasTrap)
|
||||
action = PET_BATTLE_EFFECT_ACTION_CATCH;
|
||||
else if (hasDuration && hasPoints && props->BattlePetVisualID == 38)
|
||||
action = PET_BATTLE_EFFECT_ACTION_PERIODIC_DAMAGE;
|
||||
else if (hasDuration && hasPoints && props->BattlePetVisualID != 38)
|
||||
action = PET_BATTLE_EFFECT_ACTION_PERIODIC_HEAL;
|
||||
else if (hasDuration)
|
||||
action = PET_BATTLE_EFFECT_ACTION_APPLY_AURA;
|
||||
else if (hasPoints && hasPercentage && props->BattlePetVisualID == 38)
|
||||
action = PET_BATTLE_EFFECT_ACTION_DAMAGE_PERCENTAGE;
|
||||
else if (hasPoints && hasPercentage)
|
||||
action = PET_BATTLE_EFFECT_ACTION_HEAL_PERCENTAGE;
|
||||
else if (hasPoints && props->BattlePetVisualID == 38)
|
||||
action = PET_BATTLE_EFFECT_ACTION_DAMAGE;
|
||||
else if (hasPoints)
|
||||
action = PET_BATTLE_EFFECT_ACTION_HEAL;
|
||||
else if (hasState)
|
||||
action = PET_BATTLE_EFFECT_ACTION_SET_STATE;
|
||||
else if (hasChance)
|
||||
action = PET_BATTLE_EFFECT_ACTION_DAMAGE; // Conditional ? evaluate at use site, fall through to damage
|
||||
else
|
||||
action = PET_BATTLE_EFFECT_ACTION_DAMAGE; // Safe fallback ? default handler checks basePower
|
||||
|
||||
_effectActionMap[static_cast<uint16>(props->ID)] = action;
|
||||
++mappedCount;
|
||||
}
|
||||
|
||||
// Log all mapped entries for debugging
|
||||
{
|
||||
std::map<uint16, uint32> usageCounts;
|
||||
for (BattlePetAbilityEffectEntry const* entry : sBattlePetAbilityEffectStore)
|
||||
usageCounts[entry->BattlePetEffectPropertiesID]++;
|
||||
|
||||
for (auto const& [propsID, action] : _effectActionMap)
|
||||
{
|
||||
BattlePetEffectPropertiesEntry const* props = sBattlePetEffectPropertiesStore.LookupEntry(propsID);
|
||||
std::string labels;
|
||||
if (props)
|
||||
for (uint8 i = 0; i < 6; ++i)
|
||||
if (props->ParamLabel[i] && props->ParamLabel[i][0] != '\0')
|
||||
labels += Trinity::StringFormat("[{}]={} ", i, props->ParamLabel[i]);
|
||||
TC_LOG_INFO("server.loading", " EffectMap: PropsID={:3d} -> action={:2d} uses={:3d} visual={} labels: {}",
|
||||
propsID, uint16(action), usageCounts.count(propsID) ? usageCounts[propsID] : 0,
|
||||
props ? props->BattlePetVisualID : 0, labels);
|
||||
}
|
||||
}
|
||||
|
||||
TC_LOG_INFO("server.loading", ">> Mapped {} BattlePetEffectProperties entries to action types", mappedCount);
|
||||
}
|
||||
|
||||
PetBattleAbilityEffectAction PetBattleMgr::GetEffectAction(uint16 propsID) const
|
||||
{
|
||||
auto it = _effectActionMap.find(propsID);
|
||||
if (it != _effectActionMap.end())
|
||||
return it->second;
|
||||
return PET_BATTLE_EFFECT_ACTION_DAMAGE; // Fallback for unmapped IDs
|
||||
}
|
||||
|
||||
bool PetBattleMgr::IsWeatherAbility(uint32 abilityID) const
|
||||
{
|
||||
return _weatherAbilityIDs.count(abilityID) > 0;
|
||||
}
|
||||
|
||||
void PetBattleMgr::Update(uint32 diff)
|
||||
{
|
||||
// Update all active battles
|
||||
@@ -575,12 +688,22 @@ namespace PetBattles
|
||||
{
|
||||
battle->Start();
|
||||
|
||||
static constexpr float PET_BATTLE_HALF_DISTANCE = 5.0f;
|
||||
WorldPackets::BattlePet::PetBattleFinalizeLocation finalizeLocation;
|
||||
finalizeLocation.Location.LocationResult = PET_BATTLE_REQUEST_FAIL_OK;
|
||||
finalizeLocation.Location.BattleOrigin = p1->GetPosition();
|
||||
finalizeLocation.Location.BattleFacing = p1->GetAbsoluteAngle(p2);
|
||||
finalizeLocation.Location.PlayerPositions[0] = p1->GetPosition();
|
||||
finalizeLocation.Location.PlayerPositions[1] = p2->GetPosition();
|
||||
Position pvpMidpoint;
|
||||
pvpMidpoint.m_positionX = (p1->GetPositionX() + p2->GetPositionX()) / 2.0f;
|
||||
pvpMidpoint.m_positionY = (p1->GetPositionY() + p2->GetPositionY()) / 2.0f;
|
||||
pvpMidpoint.m_positionZ = (p1->GetPositionZ() + p2->GetPositionZ()) / 2.0f;
|
||||
float pvpFacing = p1->GetAbsoluteAngle(p2);
|
||||
finalizeLocation.Location.BattleOrigin = pvpMidpoint;
|
||||
finalizeLocation.Location.BattleFacing = pvpFacing;
|
||||
finalizeLocation.Location.PlayerPositions[0] = Position(pvpMidpoint.m_positionX - PET_BATTLE_HALF_DISTANCE * std::cos(pvpFacing),
|
||||
pvpMidpoint.m_positionY - PET_BATTLE_HALF_DISTANCE * std::sin(pvpFacing),
|
||||
pvpMidpoint.m_positionZ);
|
||||
finalizeLocation.Location.PlayerPositions[1] = Position(pvpMidpoint.m_positionX + PET_BATTLE_HALF_DISTANCE * std::cos(pvpFacing),
|
||||
pvpMidpoint.m_positionY + PET_BATTLE_HALF_DISTANCE * std::sin(pvpFacing),
|
||||
pvpMidpoint.m_positionZ);
|
||||
p1->SendDirectMessage(finalizeLocation.Write());
|
||||
p2->SendDirectMessage(finalizeLocation.Write());
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "PetBattle.h"
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
class Creature;
|
||||
@@ -98,12 +99,19 @@ namespace PetBattles
|
||||
float GetBreedQualityMultiplier(uint8 quality) const;
|
||||
void GetBreedBaseStats(uint32 breedID, int32& outHP, int32& outPower, int32& outSpeed) const;
|
||||
|
||||
// Effect action mapping (DB2 PropsID -> abstract action type)
|
||||
PetBattleAbilityEffectAction GetEffectAction(uint16 propsID) const;
|
||||
|
||||
// Weather ability detection
|
||||
bool IsWeatherAbility(uint32 abilityID) const;
|
||||
|
||||
// PvP Queue
|
||||
void JoinQueue(ObjectGuid playerGUID);
|
||||
void LeaveQueue(ObjectGuid playerGUID);
|
||||
void HandleProposalResult(ObjectGuid playerGUID, bool accepted);
|
||||
|
||||
private:
|
||||
void BuildEffectActionMap();
|
||||
void TryMatchPlayers();
|
||||
|
||||
uint32 _nextBattleID = 1;
|
||||
@@ -127,6 +135,12 @@ namespace PetBattles
|
||||
// NPC Teams
|
||||
std::unordered_map<uint32, std::vector<NPCTeamPetInfo>> _npcTeams;
|
||||
|
||||
// Effect action map (BattlePetEffectPropertiesID -> PetBattleAbilityEffectAction)
|
||||
std::unordered_map<uint16, PetBattleAbilityEffectAction> _effectActionMap;
|
||||
|
||||
// Weather ability IDs (abilities whose effects set weatherState on environment)
|
||||
std::unordered_set<uint32> _weatherAbilityIDs;
|
||||
|
||||
// PvP Queue
|
||||
std::vector<PvPQueueEntry> _pvpQueue;
|
||||
std::unique_ptr<PvPMatchProposal> _pendingProposal;
|
||||
|
||||
@@ -512,17 +512,24 @@ void WorldSession::HandlePetBattleRequestWild(WorldPackets::BattlePet::PetBattle
|
||||
player->SetFacingToObject(creature);
|
||||
|
||||
// Send finalize location with OK result
|
||||
// BattleOrigin is the midpoint between player and creature (matching retail)
|
||||
// BattleOrigin = midpoint, players placed 5.0 units from center along facing axis (matching retail sniff)
|
||||
static constexpr float PET_BATTLE_HALF_DISTANCE = 5.0f;
|
||||
WorldPackets::BattlePet::PetBattleFinalizeLocation finalizeLocation;
|
||||
finalizeLocation.Location.LocationResult = PetBattles::PET_BATTLE_REQUEST_FAIL_OK;
|
||||
Position midpoint;
|
||||
midpoint.m_positionX = (player->GetPositionX() + creature->GetPositionX()) / 2.0f;
|
||||
midpoint.m_positionY = (player->GetPositionY() + creature->GetPositionY()) / 2.0f;
|
||||
midpoint.m_positionZ = (player->GetPositionZ() + creature->GetPositionZ()) / 2.0f;
|
||||
float facing = player->GetAbsoluteAngle(creature);
|
||||
finalizeLocation.Location.BattleOrigin = midpoint;
|
||||
finalizeLocation.Location.BattleFacing = player->GetAbsoluteAngle(creature);
|
||||
finalizeLocation.Location.PlayerPositions[0] = player->GetPosition();
|
||||
finalizeLocation.Location.PlayerPositions[1] = creature->GetPosition();
|
||||
finalizeLocation.Location.BattleFacing = facing;
|
||||
// Player positions are 5.0 units from center along the facing axis
|
||||
finalizeLocation.Location.PlayerPositions[0] = Position(midpoint.m_positionX - PET_BATTLE_HALF_DISTANCE * std::cos(facing),
|
||||
midpoint.m_positionY - PET_BATTLE_HALF_DISTANCE * std::sin(facing),
|
||||
midpoint.m_positionZ);
|
||||
finalizeLocation.Location.PlayerPositions[1] = Position(midpoint.m_positionX + PET_BATTLE_HALF_DISTANCE * std::cos(facing),
|
||||
midpoint.m_positionY + PET_BATTLE_HALF_DISTANCE * std::sin(facing),
|
||||
midpoint.m_positionZ);
|
||||
SendPacket(finalizeLocation.Write());
|
||||
|
||||
// Build and send initial update
|
||||
@@ -674,16 +681,23 @@ void WorldSession::StartNPCPetBattle(Creature* trainer)
|
||||
player->SetFacingToObject(trainer);
|
||||
|
||||
// Send finalize location with OK result
|
||||
// BattleOrigin = midpoint, players placed 5.0 units from center along facing axis (matching retail sniff)
|
||||
static constexpr float PET_BATTLE_HALF_DISTANCE = 5.0f;
|
||||
WorldPackets::BattlePet::PetBattleFinalizeLocation finalizeLocation;
|
||||
finalizeLocation.Location.LocationResult = PetBattles::PET_BATTLE_REQUEST_FAIL_OK;
|
||||
Position midpoint;
|
||||
midpoint.m_positionX = (player->GetPositionX() + trainer->GetPositionX()) / 2.0f;
|
||||
midpoint.m_positionY = (player->GetPositionY() + trainer->GetPositionY()) / 2.0f;
|
||||
midpoint.m_positionZ = (player->GetPositionZ() + trainer->GetPositionZ()) / 2.0f;
|
||||
float facing = player->GetAbsoluteAngle(trainer);
|
||||
finalizeLocation.Location.BattleOrigin = midpoint;
|
||||
finalizeLocation.Location.BattleFacing = player->GetAbsoluteAngle(trainer);
|
||||
finalizeLocation.Location.PlayerPositions[0] = player->GetPosition();
|
||||
finalizeLocation.Location.PlayerPositions[1] = trainer->GetPosition();
|
||||
finalizeLocation.Location.BattleFacing = facing;
|
||||
finalizeLocation.Location.PlayerPositions[0] = Position(midpoint.m_positionX - PET_BATTLE_HALF_DISTANCE * std::cos(facing),
|
||||
midpoint.m_positionY - PET_BATTLE_HALF_DISTANCE * std::sin(facing),
|
||||
midpoint.m_positionZ);
|
||||
finalizeLocation.Location.PlayerPositions[1] = Position(midpoint.m_positionX + PET_BATTLE_HALF_DISTANCE * std::cos(facing),
|
||||
midpoint.m_positionY + PET_BATTLE_HALF_DISTANCE * std::sin(facing),
|
||||
midpoint.m_positionZ);
|
||||
SendPacket(finalizeLocation.Write());
|
||||
|
||||
// Build and send initial update
|
||||
|
||||
Reference in New Issue
Block a user