diff --git a/Framework/Constants/SharedConst.cs b/Framework/Constants/SharedConst.cs
index 5e00e987c..f5a8797cd 100644
--- a/Framework/Constants/SharedConst.cs
+++ b/Framework/Constants/SharedConst.cs
@@ -52,7 +52,7 @@ namespace Framework.Constants
///
/// Lfg Const
///
- public const uint LFGTimeRolecheck = 45 * Time.InMilliseconds;
+ public const uint LFGTimeRolecheck = 45;
public const uint LFGTimeBoot = 120;
public const uint LFGTimeProposal = 45;
public const uint LFGQueueUpdateInterval = 15 * Time.InMilliseconds;
diff --git a/Framework/Constants/Spells/SpellConst.cs b/Framework/Constants/Spells/SpellConst.cs
index 95f315a2f..8b43b7fac 100644
--- a/Framework/Constants/Spells/SpellConst.cs
+++ b/Framework/Constants/Spells/SpellConst.cs
@@ -1840,7 +1840,7 @@ namespace Framework.Constants
Threat = 63,
TriggerSpell = 64,
ApplyAreaAuraRaid = 65,
- CreateManaGem = 66,
+ RechargeItem = 66,
HealMaxHealth = 67,
InterruptCast = 68,
Distract = 69,
diff --git a/Game/Achievements/CriteriaHandler.cs b/Game/Achievements/CriteriaHandler.cs
index de5dbbfec..768d32238 100644
--- a/Game/Achievements/CriteriaHandler.cs
+++ b/Game/Achievements/CriteriaHandler.cs
@@ -1263,7 +1263,7 @@ namespace Game.Achievements
return false;
break;
case CriteriaAdditionalCondition.TargetLevel: // 40
- if (unit == null || unit.getLevel() != reqValue)
+ if (unit == null || unit.GetLevelForTarget(referencePlayer) != reqValue)
return false;
break;
case CriteriaAdditionalCondition.TargetZone: // 41
@@ -2035,7 +2035,7 @@ namespace Game.Achievements
case CriteriaDataType.TLevel:
if (target == null)
return false;
- return target.getLevel() >= Level.Min;
+ return target.GetLevelForTarget(source) >= Level.Min;
case CriteriaDataType.TGender:
if (target == null)
return false;
diff --git a/Game/Entities/Creature/Creature.cs b/Game/Entities/Creature/Creature.cs
index ce5cd14df..7e33ae546 100644
--- a/Game/Entities/Creature/Creature.cs
+++ b/Game/Entities/Creature/Creature.cs
@@ -288,8 +288,8 @@ namespace Game.Entities
if (updateLevel)
SelectLevel();
-
- UpdateLevelDependantStats();
+ else
+ UpdateLevelDependantStats(); // We still re-initialize level dependant stats on entry update
SetMeleeDamageSchool((SpellSchools)cInfo.DmgSchool);
SetModifierValue(UnitMods.ResistanceHoly, UnitModifierType.BaseValue, cInfo.Resistance[(int)SpellSchools.Holy]);
@@ -1090,11 +1090,23 @@ namespace Game.Entities
{
CreatureTemplate cInfo = GetCreatureTemplate();
- // level
- byte minlevel = (byte)Math.Min(cInfo.Maxlevel, cInfo.Minlevel);
- byte maxlevel = (byte)Math.Max(cInfo.Maxlevel, cInfo.Minlevel);
- byte level = (byte)(minlevel == maxlevel ? minlevel : RandomHelper.URand(minlevel, maxlevel));
- SetLevel(level);
+ if (!HasScalableLevels())
+ {
+ // level
+ byte minlevel = (byte)Math.Min(cInfo.Maxlevel, cInfo.Minlevel);
+ byte maxlevel = (byte)Math.Max(cInfo.Maxlevel, cInfo.Minlevel);
+ byte level = (byte)(minlevel == maxlevel ? minlevel : RandomHelper.URand(minlevel, maxlevel));
+ SetLevel(level);
+ }
+ else
+ {
+ SetLevel(cInfo.levelScaling.Value.MaxLevel);
+ SetUInt32Value(UnitFields.ScalingLevelMin, cInfo.levelScaling.Value.MinLevel);
+ SetUInt32Value(UnitFields.ScalingLevelMax, cInfo.levelScaling.Value.MaxLevel);
+ SetUInt32Value(UnitFields.ScalingLevelDelta, (uint)cInfo.levelScaling.Value.DeltaLevel);
+ }
+
+ UpdateLevelDependantStats();
}
void UpdateLevelDependantStats()
@@ -1663,8 +1675,8 @@ namespace Game.Entities
if (m_originalEntry != GetEntry())
UpdateEntry(m_originalEntry);
-
- SelectLevel();
+ else
+ SelectLevel();
setDeathState(DeathState.JustRespawned);
@@ -2344,17 +2356,91 @@ namespace Game.Entities
m_respawnTime = m_corpseRemoveTime + m_respawnDelay;
}
+ public bool HasScalableLevels()
+ {
+ CreatureTemplate cinfo = GetCreatureTemplate();
+ return cinfo.levelScaling.HasValue;
+ }
+
+ ulong GetMaxHealthByLevel(uint level)
+ {
+ CreatureTemplate cInfo = GetCreatureTemplate();
+ CreatureBaseStats stats = Global.ObjectMgr.GetCreatureBaseStats(level, cInfo.UnitClass);
+ return stats.GenerateHealth(cInfo);
+ }
+
+ float GetHealthMultiplierForTarget(WorldObject target)
+ {
+ if (!HasScalableLevels())
+ return 1.0f;
+
+ uint levelForTarget = GetLevelForTarget(target);
+ if (getLevel() < levelForTarget)
+ return 1.0f;
+
+ return GetMaxHealthByLevel(levelForTarget) / GetCreateHealth();
+ }
+
+ float GetBaseDamageForLevel(uint level)
+ {
+ CreatureTemplate cInfo = GetCreatureTemplate();
+ CreatureBaseStats stats = Global.ObjectMgr.GetCreatureBaseStats(level, cInfo.UnitClass);
+ return stats.GenerateBaseDamage(cInfo);
+ }
+
+ float GetDamageMultiplierForTarget(WorldObject target)
+ {
+ if (!HasScalableLevels())
+ return 1.0f;
+
+ uint levelForTarget = GetLevelForTarget(target);
+
+ return GetBaseDamageForLevel(levelForTarget) / GetBaseDamageForLevel(getLevel());
+ }
+
+ float GetBaseArmorForLevel(uint level)
+ {
+ CreatureTemplate cInfo = GetCreatureTemplate();
+ CreatureBaseStats stats = Global.ObjectMgr.GetCreatureBaseStats(level, cInfo.UnitClass);
+ return stats.GenerateArmor(cInfo);
+ }
+
+ float GetArmorMultiplierForTarget(WorldObject target)
+ {
+ if (!HasScalableLevels())
+ return 1.0f;
+
+ uint levelForTarget = GetLevelForTarget(target);
+
+ return GetBaseArmorForLevel(levelForTarget) / GetBaseArmorForLevel(getLevel());
+ }
+
public override uint GetLevelForTarget(WorldObject target)
{
- if (!isWorldBoss() || !target.IsTypeId(TypeId.Unit))
- return base.GetLevelForTarget(target);
+ Unit unitTarget = target.ToUnit();
+ if (unitTarget)
+ {
+ if (isWorldBoss())
+ {
+ int level = (int)(unitTarget.getLevel() + WorldConfig.GetIntValue(WorldCfg.WorldBossLevelDiff));
+ return (uint)MathFunctions.RoundToInterval(ref level, 1u, 255u);
+ }
- uint level = target.ToUnit().getLevel() + WorldConfig.GetUIntValue(WorldCfg.WorldBossLevelDiff);
- if (level < 1)
- return 1;
- if (level > 255)
- return 255;
- return level;
+ // If this creature should scale level, adapt level depending of target level
+ // between UNIT_FIELD_SCALING_LEVEL_MIN and UNIT_FIELD_SCALING_LEVEL_MAX
+ if (HasScalableLevels())
+ {
+ uint targetLevelWithDelta = (uint)(unitTarget.getLevel() + GetCreatureTemplate().levelScaling.Value.DeltaLevel);
+
+ if (target.IsPlayer())
+ targetLevelWithDelta += target.GetUInt32Value(PlayerFields.ScalingLevelDelta);
+
+ return MathFunctions.RoundToInterval(ref targetLevelWithDelta, GetUInt32Value(UnitFields.ScalingLevelMin), GetUInt32Value(UnitFields.ScalingLevelMax));
+ }
+
+ }
+
+ return base.GetLevelForTarget(target);
}
public string GetAIName()
diff --git a/Game/Entities/Creature/CreatureData.cs b/Game/Entities/Creature/CreatureData.cs
index 52f833777..32f7fa91a 100644
--- a/Game/Entities/Creature/CreatureData.cs
+++ b/Game/Entities/Creature/CreatureData.cs
@@ -19,6 +19,7 @@ using Framework.Collections;
using Framework.Constants;
using System;
using System.Collections.Generic;
+using Framework.Dynamic;
namespace Game.Entities
{
@@ -37,6 +38,7 @@ namespace Game.Entities
public string IconName;
public uint GossipMenuId;
public short Minlevel;
+ public Optional levelScaling;
public short Maxlevel;
public int HealthScalingExpansion;
public uint RequiredExpansion;
@@ -384,4 +386,11 @@ namespace Game.Entities
m_items.Clear();
}
}
+
+ public struct CreatureLevelScaling
+ {
+ public ushort MinLevel;
+ public ushort MaxLevel;
+ public short DeltaLevel;
+ }
}
diff --git a/Game/Entities/Creature/Trainer.cs b/Game/Entities/Creature/Trainer.cs
index aaa78270a..8098fdecd 100644
--- a/Game/Entities/Creature/Trainer.cs
+++ b/Game/Entities/Creature/Trainer.cs
@@ -18,7 +18,8 @@ namespace Game.Entities
public Array ReqAbility = new Array(3);
public byte ReqLevel;
- public uint CastSpellId;
+ public uint LearnedSpellId;
+ public bool IsCastable() { return LearnedSpellId != SpellId; }
}
public class Trainer
@@ -84,8 +85,8 @@ namespace Game.Entities
player.SendPlaySpellVisualKit(362, 1, 0); // 113 EmoteSalute
// learn explicitly or cast explicitly
- if (trainerSpell.CastSpellId != 0)
- player.CastSpell(player, trainerSpell.CastSpellId, true);
+ if (trainerSpell.IsCastable())
+ player.CastSpell(player, trainerSpell.SpellId, true);
else
player.LearnSpell(trainerSpell.SpellId, false);
}
@@ -129,6 +130,12 @@ namespace Game.Entities
if (player.getLevel() < trainerSpell.ReqLevel)
return TrainerSpellState.Unavailable;
+ // check ranks
+ uint previousRankSpellId = Global.SpellMgr.GetPrevSpellInChain(trainerSpell.LearnedSpellId);
+ if (previousRankSpellId != 0)
+ if (!player.HasSpell(previousRankSpellId))
+ return TrainerSpellState.Unavailable;
+
// check additional spell requirement
foreach (var spellId in Global.SpellMgr.GetSpellsRequiredForSpellBounds(trainerSpell.SpellId))
if (!player.HasSpell(spellId))
diff --git a/Game/Entities/Object/WorldObject.cs b/Game/Entities/Object/WorldObject.cs
index 5439a8f27..ff8f23661 100644
--- a/Game/Entities/Object/WorldObject.cs
+++ b/Game/Entities/Object/WorldObject.cs
@@ -850,7 +850,7 @@ namespace Game.Entities
DynamicUpdateMask arrayMask = new DynamicUpdateMask((uint)values.Length);
arrayMask.EncodeDynamicFieldChangeType(_dynamicChangesMask[index], updateType);
if (updateType == UpdateType.Values && _dynamicChangesMask[index] == DynamicFieldChangeType.ValueAndSizeChanged)
- arrayMask.SetCount(values.Length);
+ arrayMask.SetCount(values.Length);
for (var v = 0; v < values.Length; ++v)
{
@@ -2029,7 +2029,7 @@ namespace Game.Entities
Map map = GetMap();
GameObject go = new GameObject();
- if (!go.Create(entry, map, GetPhaseMask(), pos, rotation, 255, GameObjectState.Ready))
+ if (!go.Create(entry, map, GetPhaseMask(), pos, rotation, 255, GameObjectState.Ready))
return null;
go.CopyPhaseFrom(this);
@@ -2498,14 +2498,23 @@ namespace Game.Entities
public void SetFieldNotifyFlag(uint flag) { _fieldNotifyFlags |= flag; }
public void RemoveFieldNotifyFlag(uint flag) { _fieldNotifyFlags &= ~flag; }
- public Creature ToCreature() { return IsTypeId(TypeId.Unit) ? (this as Creature) : null; }
- public Player ToPlayer() { return IsTypeId(TypeId.Player) ? (this as Player) : null; }
- public GameObject ToGameObject() { return IsTypeId(TypeId.GameObject) ? (this as GameObject) : null; }
- public Unit ToUnit() { return IsTypeId(TypeId.Unit) || IsTypeId(TypeId.Player) ? (this as Unit) : null; }
- public Corpse ToCorpse() { return IsTypeId(TypeId.Corpse) ? (this as Corpse) : null; }
- public DynamicObject ToDynamicObject() { return IsTypeId(TypeId.DynamicObject) ? (this as DynamicObject) : null; }
- public AreaTrigger ToAreaTrigger() { return IsTypeId(TypeId.AreaTrigger) ? (this as AreaTrigger) : null; }
- public Conversation ToConversation() { return IsTypeId(TypeId.Conversation) ? (this as Conversation) : null; }
+ bool IsCreature() { return GetTypeId() == TypeId.Unit; }
+ public bool IsPlayer() { return GetTypeId() == TypeId.Player; }
+ bool IsGameObject() { return GetTypeId() == TypeId.GameObject; }
+ bool IsUnit() { return isTypeMask(TypeMask.Unit); }
+ bool IsCorpse() { return GetTypeId() == TypeId.Corpse; }
+ bool IsDynObject() { return GetTypeId() == TypeId.DynamicObject; }
+ bool IsAreaTrigger() { return GetTypeId() == TypeId.AreaTrigger; }
+ bool IsConversation() { return GetTypeId() == TypeId.Conversation; }
+
+ public Creature ToCreature() { return IsCreature() ? (this as Creature) : null; }
+ public Player ToPlayer() { return IsPlayer() ? (this as Player) : null; }
+ public GameObject ToGameObject() { return IsGameObject() ? (this as GameObject) : null; }
+ public Unit ToUnit() { return IsUnit() ? (this as Unit) : null; }
+ public Corpse ToCorpse() { return IsCorpse() ? (this as Corpse) : null; }
+ public DynamicObject ToDynamicObject() { return IsDynObject() ? (this as DynamicObject) : null; }
+ public AreaTrigger ToAreaTrigger() { return IsAreaTrigger() ? (this as AreaTrigger) : null; }
+ public Conversation ToConversation() { return IsConversation() ? (this as Conversation) : null; }
public virtual void Update(uint diff) { }
diff --git a/Game/Entities/Player/KillRewarder.cs b/Game/Entities/Player/KillRewarder.cs
index cfff3a236..2d0b665bd 100644
--- a/Game/Entities/Player/KillRewarder.cs
+++ b/Game/Entities/Player/KillRewarder.cs
@@ -117,7 +117,7 @@ namespace Game.Entities
// 2.4. _maxNotGrayMember - maximum level of alive group member within reward distance,
// for whom victim is not gray;
uint grayLevel = Formulas.GetGrayLevel(lvl);
- if (_victim.getLevel() > grayLevel && (!_maxNotGrayMember || _maxNotGrayMember.getLevel() < lvl))
+ if (_victim.GetLevelForTarget(member) > grayLevel && (!_maxNotGrayMember || _maxNotGrayMember.getLevel() < lvl))
_maxNotGrayMember = member;
}
}
diff --git a/Game/Entities/Player/Player.DB.cs b/Game/Entities/Player/Player.DB.cs
index 45b9094e1..5bc3f998e 100644
--- a/Game/Entities/Player/Player.DB.cs
+++ b/Game/Entities/Player/Player.DB.cs
@@ -2363,7 +2363,7 @@ namespace Game.Entities
}
SetUInt32Value(UnitFields.Level, result.Read(6));
- SetUInt32Value(PlayerFields.Xp, result.Read(7));
+ SetXP(result.Read(7));
_LoadIntoDataField(result.Read(65), (int)PlayerFields.ExploredZones1, PlayerConst.ExploredZonesSize);
_LoadIntoDataField(result.Read(66), (int)PlayerFields.KnownTitles, PlayerConst.KnowTitlesSize * 2);
diff --git a/Game/Entities/Player/Player.PvP.cs b/Game/Entities/Player/Player.PvP.cs
index 21bef7d74..7ad682c0b 100644
--- a/Game/Entities/Player/Player.PvP.cs
+++ b/Game/Entities/Player/Player.PvP.cs
@@ -104,7 +104,7 @@ namespace Game.Entities
byte k_level = (byte)getLevel();
byte k_grey = (byte)Formulas.GetGrayLevel(k_level);
- byte v_level = (byte)victim.getLevel();
+ byte v_level = (byte)victim.GetLevelForTarget(this);
if (v_level <= k_grey)
return false;
diff --git a/Game/Entities/Player/Player.cs b/Game/Entities/Player/Player.cs
index fe40fe148..7974dca6b 100644
--- a/Game/Entities/Player/Player.cs
+++ b/Game/Entities/Player/Player.cs
@@ -1646,7 +1646,7 @@ namespace Game.Entities
if (Rep.RepFaction1 != 0 && (!Rep.TeamDependent || team == Team.Alliance))
{
- int donerep1 = CalculateReputationGain(ReputationSource.Kill, victim.getLevel(), Rep.RepValue1, (int)(ChampioningFaction != 0 ? ChampioningFaction : Rep.RepFaction1));
+ int donerep1 = CalculateReputationGain(ReputationSource.Kill, victim.GetLevelForTarget(this), Rep.RepValue1, (int)(ChampioningFaction != 0 ? ChampioningFaction : Rep.RepFaction1));
donerep1 = (int)(donerep1 * rate);
FactionRecord factionEntry1 = CliDB.FactionStorage.LookupByKey(ChampioningFaction != 0 ? ChampioningFaction : Rep.RepFaction1);
@@ -1657,7 +1657,7 @@ namespace Game.Entities
if (Rep.RepFaction2 != 0 && (!Rep.TeamDependent || team == Team.Horde))
{
- int donerep2 = CalculateReputationGain(ReputationSource.Kill, victim.getLevel(), Rep.RepValue2, (int)(ChampioningFaction != 0 ? ChampioningFaction : Rep.RepFaction2));
+ int donerep2 = CalculateReputationGain(ReputationSource.Kill, victim.GetLevelForTarget(this), Rep.RepValue2, (int)(ChampioningFaction != 0 ? ChampioningFaction : Rep.RepFaction2));
donerep2 = (int)(donerep2 * rate);
FactionRecord factionEntry2 = CliDB.FactionStorage.LookupByKey(ChampioningFaction != 0 ? ChampioningFaction : Rep.RepFaction2);
@@ -4116,9 +4116,12 @@ namespace Game.Entities
if (!IsSectionFlagValid(hair, class_, create))
return false;
- CharSectionsRecord facialHair = Global.DB2Mgr.GetCharSectionEntry(race, CharSectionType.Hair, gender, facialHairId, hairColor);
- if (facialHair == null)
- return false;
+ if (facialHairId != 0)
+ {
+ CharSectionsRecord facialHair = Global.DB2Mgr.GetCharSectionEntry(race, CharSectionType.FacialHair, gender, facialHairId, hairColor);
+ if (facialHair == null)
+ return false;
+ }
for (int i = 0; i < PlayerConst.CustomDisplaySize; ++i)
{
@@ -5064,7 +5067,7 @@ namespace Game.Entities
// Used in triggers for check "Only to targets that grant experience or honor" req
public bool isHonorOrXPTarget(Unit victim)
{
- uint v_level = victim.getLevel();
+ uint v_level = victim.GetLevelForTarget(this);
uint k_grey = Formulas.GetGrayLevel(getLevel());
// Victim level less gray level
@@ -6412,6 +6415,19 @@ namespace Game.Entities
SendMovementSetCollisionHeight(scale * GetCollisionHeight(IsMounted()));
}
+ void SetXP(uint xp)
+ {
+ SetUInt32Value(PlayerFields.Xp, xp);
+
+ int playerLevelDelta = 0;
+
+ // If XP < 50%, player should see scaling creature with -1 level except for level max
+ if (getLevel() < SharedConst.MaxLevel && xp < (GetUInt32Value(PlayerFields.NextLevelXp) / 2))
+ playerLevelDelta = -1;
+
+ SetInt32Value(PlayerFields.ScalingLevelDelta, playerLevelDelta);
+ }
+
public void GiveXP(uint xp, Unit victim, float group_rate = 1.0f)
{
if (xp < 1)
@@ -6467,7 +6483,7 @@ namespace Game.Entities
nextLvlXP = GetUInt32Value(PlayerFields.NextLevelXp);
}
- SetUInt32Value(PlayerFields.Xp, newXP);
+ SetXP(newXP);
}
public void HandleBaseModValue(BaseModGroup modGroup, BaseModType modType, float amount, bool apply)
diff --git a/Game/Entities/StatSystem.cs b/Game/Entities/StatSystem.cs
index e809fe04d..a92de6606 100644
--- a/Game/Entities/StatSystem.cs
+++ b/Game/Entities/StatSystem.cs
@@ -420,6 +420,10 @@ namespace Game.Entities
public ulong CountPctFromMaxHealth(int pct) { return MathFunctions.CalculatePct(GetMaxHealth(), pct); }
ulong CountPctFromCurHealth(int pct) { return MathFunctions.CalculatePct(GetHealth(), pct); }
+ public virtual float GetHealthMultiplierForTarget(WorldObject target) { return 1.0f; }
+ public virtual float GetDamageMultiplierForTarget(WorldObject target) { return 1.0f; }
+ public virtual float GetArmorMultiplierForTarget(WorldObject target) { return 1.0f; }
+
//Powers
public PowerType getPowerType()
{
diff --git a/Game/Entities/Unit/Unit.Combat.cs b/Game/Entities/Unit/Unit.Combat.cs
index be8775a93..84079ff86 100644
--- a/Game/Entities/Unit/Unit.Combat.cs
+++ b/Game/Entities/Unit/Unit.Combat.cs
@@ -724,7 +724,10 @@ namespace Game.Entities
{
absorb += damage;
damage = 0;
+ return;
}
+
+ damage *= (uint)GetDamageMultiplierForTarget(victim);
}
void DealMeleeDamage(CalcDamageInfo damageInfo, bool durabilityLoss)
{
@@ -787,7 +790,7 @@ namespace Game.Entities
// there is a newbie protection, at level 10 just 7% base chance; assuming linear function
if (victim.getLevel() < 30)
- Probability = 0.65f * victim.getLevel() + 0.5f;
+ Probability = 0.65f * victim.GetLevelForTarget(this) + 0.5f;
uint VictimDefense = victim.GetMaxSkillValueForLevel(this);
uint AttackerMeleeSkill = GetMaxSkillValueForLevel();
@@ -1018,6 +1021,8 @@ namespace Game.Entities
victim.ToCreature().LowerPlayerDamageReq(health < damage ? health : damage);
}
+ damage /= (uint)victim.GetHealthMultiplierForTarget(this);
+
if (health <= damage)
{
Log.outDebug(LogFilter.Unit, "DealDamage: victim just died");
@@ -1221,6 +1226,10 @@ namespace Game.Entities
packet.BlockAmount = (int)damageInfo.blocked_amount;
packet.LogData.Initialize(damageInfo.attacker);
+ SandboxScalingData sandboxScalingData = new SandboxScalingData();
+ if (sandboxScalingData.GenerateDataForUnits(damageInfo.attacker, damageInfo.target))
+ packet.SandboxScaling = sandboxScalingData;
+
SendCombatLogMessage(packet);
}
public void CombatStart(Unit target, bool initialAggro = true)
@@ -2289,7 +2298,7 @@ namespace Game.Entities
byte bossLevel = 83;
uint bossResistanceConstant = 510;
uint resistanceConstant = 0;
- uint level = victim.getLevel();
+ uint level = victim.GetLevelForTarget(this);
if (level == bossLevel)
resistanceConstant = bossResistanceConstant;
@@ -2626,6 +2635,8 @@ namespace Game.Entities
{
float armor = victim.GetArmor();
+ armor *= victim.GetArmorMultiplierForTarget(attacker);
+
// bypass enemy armor by SPELL_AURA_BYPASS_ARMOR_FOR_CASTER
int armorBypassPct = 0;
var reductionAuras = victim.GetAuraEffectsByType(AuraType.BypassArmorForCaster);
@@ -2655,10 +2666,10 @@ namespace Game.Entities
if (IsTypeId(TypeId.Player))
{
float maxArmorPen = 0;
- if (victim.getLevel() < 60)
- maxArmorPen = 400 + 85 * victim.getLevel();
+ if (victim.GetLevelForTarget(attacker) < 60)
+ maxArmorPen = 400 + 85 * victim.GetLevelForTarget(attacker);
else
- maxArmorPen = 400 + 85 * victim.getLevel() + 4.5f * 85 * (victim.getLevel() - 59);
+ maxArmorPen = 400 + 85 * victim.GetLevelForTarget(attacker) + 4.5f * 85 * (victim.GetLevelForTarget(attacker) - 59);
// Cap armor penetration to this number
maxArmorPen = Math.Min((armor + maxArmorPen) / 3, armor);
@@ -2671,7 +2682,7 @@ namespace Game.Entities
if (MathFunctions.fuzzyLe(armor, 0.0f))
return damage;
- uint attackerLevel = attacker.getLevel();
+ uint attackerLevel = attacker.GetLevelForTarget(victim);
if (attackerLevel > CliDB.ArmorMitigationByLvlGameTable.GetTableRowCount())
attackerLevel = (uint)CliDB.ArmorMitigationByLvlGameTable.GetTableRowCount();
diff --git a/Game/Entities/Unit/Unit.Pets.cs b/Game/Entities/Unit/Unit.Pets.cs
index fd2233fbb..e4b298826 100644
--- a/Game/Entities/Unit/Unit.Pets.cs
+++ b/Game/Entities/Unit/Unit.Pets.cs
@@ -745,7 +745,7 @@ namespace Game.Entities
if (!pet.CreateBaseAtCreature(creatureTarget))
return null;
- uint level = creatureTarget.getLevel() + 5 < getLevel() ? (getLevel() - 5) : creatureTarget.getLevel();
+ uint level = creatureTarget.GetLevelForTarget(this) + 5 < getLevel() ? (getLevel() - 5) : creatureTarget.GetLevelForTarget(this);
InitTamedPet(pet, level, spell_id);
diff --git a/Game/Entities/Unit/Unit.Spells.cs b/Game/Entities/Unit/Unit.Spells.cs
index 8944dd25d..9f1ee9f9e 100644
--- a/Game/Entities/Unit/Unit.Spells.cs
+++ b/Game/Entities/Unit/Unit.Spells.cs
@@ -3079,6 +3079,11 @@ namespace Game.Entities
packet.Absorbed = (int)log.absorb;
packet.Periodic = log.periodicLog;
packet.Flags = (int)log.HitInfo;
+
+ SandboxScalingData sandboxScalingData = new SandboxScalingData();
+ if (sandboxScalingData.GenerateDataForUnits(log.attacker, log.target))
+ packet.SandboxScaling.Set(sandboxScalingData);
+
SendCombatLogMessage(packet);
}
@@ -3102,6 +3107,12 @@ namespace Game.Entities
spellLogEffect.Crit = info.critical;
/// @todo: implement debug info
+ SandboxScalingData sandboxScalingData = new SandboxScalingData();
+ Unit caster = Global.ObjAccessor.GetUnit(this, aura.GetCasterGUID());
+ if (caster)
+ if (sandboxScalingData.GenerateDataForUnits(caster, this))
+ spellLogEffect.SandboxScaling.Set(sandboxScalingData);
+
data.Effects.Add(spellLogEffect);
SendCombatLogMessage(data);
diff --git a/Game/Entities/Unit/Unit.cs b/Game/Entities/Unit/Unit.cs
index 713196532..17ff7fb61 100644
--- a/Game/Entities/Unit/Unit.cs
+++ b/Game/Entities/Unit/Unit.cs
@@ -2127,6 +2127,7 @@ namespace Game.Entities
{
return GetUInt32Value(UnitFields.Level);
}
+
public override uint GetLevelForTarget(WorldObject target)
{
return getLevel();
diff --git a/Game/Globals/ObjectManager.cs b/Game/Globals/ObjectManager.cs
index 8318c6cdc..167ea456a 100644
--- a/Game/Globals/ObjectManager.cs
+++ b/Game/Globals/ObjectManager.cs
@@ -2280,6 +2280,41 @@ namespace Game
Log.outInfo(LogFilter.ServerLoading, "Loaded {0} creature model based info in {1} ms", count, Time.GetMSTimeDiffToNow(time));
}
+ public void LoadCreatureScalingData()
+ {
+ uint oldMSTime = Time.GetMSTime();
+
+ // 0 1 2 3
+ SQLResult result = DB.World.Query("SELECT Entry, LevelScalingMin, LevelScalingMax, LevelScalingDelta FROM creature_template_scaling");
+ if (result.IsEmpty())
+ {
+ Log.outInfo(LogFilter.ServerLoading, "Loaded 0 creature template scaling definitions. DB table `creature_template_scaling` is empty.");
+ return;
+ }
+
+ uint count = 0;
+ do
+ {
+ uint entry = result.Read(0);
+
+ var template = creatureTemplateStorage.LookupByKey(entry);
+ if (template == null)
+ {
+ Log.outError(LogFilter.Sql, $"Creature template (Entry: {entry}) does not exist but has a record in `creature_template_scaling`");
+ continue;
+ }
+
+ CreatureLevelScaling creatureLevelScaling;
+ creatureLevelScaling.MinLevel = result.Read(1);
+ creatureLevelScaling.MaxLevel = result.Read(2);
+ creatureLevelScaling.DeltaLevel = result.Read(3);
+ template.levelScaling.Set(creatureLevelScaling);
+
+ ++count;
+ } while (result.NextRow());
+
+ Log.outInfo(LogFilter.ServerLoading, $"Loaded {count} creature template scaling data in {Time.GetMSTimeDiffToNow(oldMSTime)} ms");
+ }
public void CheckCreatureTemplate(CreatureTemplate cInfo)
{
if (cInfo == null)
@@ -2952,12 +2987,13 @@ namespace Game
if (!allReqValid)
continue;
+ spell.LearnedSpellId = spell.SpellId;
foreach (SpellEffectInfo spellEffect in spellInfo.GetEffectsForDifficulty(Difficulty.None))
{
- if (spellEffect.IsEffect(SpellEffectName.LearnSpell))
+ if (spellEffect != null && spellEffect.IsEffect(SpellEffectName.LearnSpell))
{
- spell.CastSpellId = spell.SpellId;
- spell.SpellId = spellEffect.TriggerSpell;
+ Contract.Assert(spell.LearnedSpellId == spell.SpellId, $"Only one learned spell is currently supported - spell {spell.SpellId} already teaches {spell.LearnedSpellId} but it tried to overwrite it with {spellEffect.TriggerSpell}");
+ spell.LearnedSpellId = spellEffect.TriggerSpell;
break;
}
}
diff --git a/Game/Handlers/SpellHandler.cs b/Game/Handlers/SpellHandler.cs
index e1d721685..d9396f70c 100644
--- a/Game/Handlers/SpellHandler.cs
+++ b/Game/Handlers/SpellHandler.cs
@@ -292,7 +292,7 @@ namespace Game
// auto-selection buff level base at target level (in spellInfo)
if (targets.GetUnitTarget() != null)
{
- SpellInfo actualSpellInfo = spellInfo.GetAuraRankForLevel(targets.GetUnitTarget().getLevel());
+ SpellInfo actualSpellInfo = spellInfo.GetAuraRankForLevel(targets.GetUnitTarget().GetLevelForTarget(caster));
// if rank not found then function return NULL but in explicit cast case original spell can be casted and later failed with appropriate error message
if (actualSpellInfo != null)
diff --git a/Game/Miscellaneous/Formulas.cs b/Game/Miscellaneous/Formulas.cs
index 74dd17f9b..18b418742 100644
--- a/Game/Miscellaneous/Formulas.cs
+++ b/Game/Miscellaneous/Formulas.cs
@@ -149,7 +149,7 @@ namespace Game
{
float xpMod = 1.0f;
- gain = BaseGain(player.getLevel(), u.getLevel());
+ gain = BaseGain(player.getLevel(), u.GetLevelForTarget(player));
if (gain != 0 && creature)
{
diff --git a/Game/Network/Packets/CombatLogPackets.cs b/Game/Network/Packets/CombatLogPackets.cs
index 51b1a39eb..f1b0a4c9f 100644
--- a/Game/Network/Packets/CombatLogPackets.cs
+++ b/Game/Network/Packets/CombatLogPackets.cs
@@ -99,7 +99,7 @@ namespace Game.Network.Packets
public int Absorbed;
public int Flags;
// Optional DebugInfo;
- Optional SandboxScaling = new Optional();
+ public Optional SandboxScaling = new Optional();
}
class EnvironmentalDamageLog : CombatLogServerPacket
@@ -310,7 +310,7 @@ namespace Game.Network.Packets
public uint Resisted;
public bool Crit;
public Optional DebugInfo;
- Optional SandboxScaling = new Optional();
+ public Optional SandboxScaling = new Optional();
}
}
@@ -593,7 +593,7 @@ namespace Game.Network.Packets
public int RageGained;
public UnkAttackerState UnkState;
public float Unk;
- SandboxScalingData SandboxScaling = new SandboxScalingData();
+ public SandboxScalingData SandboxScaling = new SandboxScalingData();
}
//Structs
diff --git a/Game/Network/Packets/SpellPackets.cs b/Game/Network/Packets/SpellPackets.cs
index 4dfe2b04f..1b63db147 100644
--- a/Game/Network/Packets/SpellPackets.cs
+++ b/Game/Network/Packets/SpellPackets.cs
@@ -1146,6 +1146,86 @@ namespace Game.Network.Packets
class SandboxScalingData
{
+ public bool GenerateDataForUnits(Creature attacker, Player target)
+ {
+ CreatureTemplate creatureTemplate = attacker.GetCreatureTemplate();
+
+ Type = SandboxScalingDataType.CreatureToPlayerDamage;
+ PlayerLevelDelta = (short)target.GetInt32Value(PlayerFields.ScalingLevelDelta);
+ PlayerItemLevel = (ushort)target.GetAverageItemLevel();
+ TargetLevel = (byte)target.getLevel();
+ Expansion = (byte)creatureTemplate.RequiredExpansion;
+ Class = (byte)creatureTemplate.UnitClass;
+ TargetMinScalingLevel = (byte)creatureTemplate.levelScaling.Value.MinLevel;
+ TargetMaxScalingLevel = (byte)creatureTemplate.levelScaling.Value.MaxLevel;
+ TargetScalingLevelDelta = (sbyte)creatureTemplate.levelScaling.Value.DeltaLevel;
+ return true;
+ }
+
+ public bool GenerateDataForUnits(Player attacker, Creature target)
+ {
+ CreatureTemplate creatureTemplate = target.GetCreatureTemplate();
+
+ Type = SandboxScalingDataType.PlayerToCreatureDamage;
+ PlayerLevelDelta = (short)attacker.GetInt32Value(PlayerFields.ScalingLevelDelta);
+ PlayerItemLevel = (ushort)attacker.GetAverageItemLevel();
+ TargetLevel = (byte)target.getLevel();
+ Expansion = (byte)creatureTemplate.RequiredExpansion;
+ Class = (byte)creatureTemplate.UnitClass;
+ TargetMinScalingLevel = (byte)creatureTemplate.levelScaling.Value.MinLevel;
+ TargetMaxScalingLevel = (byte)creatureTemplate.levelScaling.Value.MaxLevel;
+ TargetScalingLevelDelta = (sbyte)creatureTemplate.levelScaling.Value.DeltaLevel;
+ return true;
+ }
+
+ public bool GenerateDataForUnits(Creature attacker, Creature target)
+ {
+ CreatureTemplate creatureTemplate = target.HasScalableLevels() ? target.GetCreatureTemplate() : attacker.GetCreatureTemplate();
+
+ Type = SandboxScalingDataType.CreatureToCreatureDamage;
+ PlayerLevelDelta = 0;
+ PlayerItemLevel = 0;
+ TargetLevel = (byte)target.getLevel();
+ Expansion = (byte)creatureTemplate.RequiredExpansion;
+ Class = (byte)creatureTemplate.UnitClass;
+ TargetMinScalingLevel = (byte)creatureTemplate.levelScaling.Value.MinLevel;
+ TargetMaxScalingLevel = (byte)creatureTemplate.levelScaling.Value.MaxLevel;
+ TargetScalingLevelDelta = (sbyte)creatureTemplate.levelScaling.Value.DeltaLevel;
+ return true;
+ }
+
+ public bool GenerateDataForUnits(Unit attacker, Unit target)
+ {
+ Player playerAttacker = attacker.ToPlayer();
+ Creature creatureAttacker = attacker.ToCreature();
+ if (playerAttacker)
+ {
+ Player playerTarget = target.ToPlayer();
+ Creature creatureTarget = target.ToCreature();
+ if (playerTarget)
+ return GenerateDataForUnits(playerAttacker, playerTarget);
+ else if (creatureTarget)
+ {
+ if (creatureTarget.HasScalableLevels())
+ return GenerateDataForUnits(playerAttacker, creatureTarget);
+ }
+ }
+ else if (creatureAttacker)
+ {
+ Player playerTarget = target.ToPlayer();
+ Creature creatureTarget = target.ToCreature();
+ if (playerTarget)
+ return GenerateDataForUnits(creatureAttacker, playerTarget);
+ else if (creatureTarget)
+ {
+ if (creatureAttacker.HasScalableLevels() || creatureTarget.HasScalableLevels())
+ return GenerateDataForUnits(creatureAttacker, creatureTarget);
+ }
+ }
+
+ return false;
+ }
+
public void Write(WorldPacket data)
{
data.WriteBits(Type, 3);
@@ -1159,15 +1239,23 @@ namespace Game.Network.Packets
data.WriteInt8(TargetScalingLevelDelta);
}
- public uint Type;
+ public SandboxScalingDataType Type;
public short PlayerLevelDelta;
public ushort PlayerItemLevel;
public byte TargetLevel;
public byte Expansion;
- public byte Class = 1;
- public byte TargetMinScalingLevel = 1;
- public byte TargetMaxScalingLevel = 1;
- public sbyte TargetScalingLevelDelta = 1;
+ public byte Class;
+ public byte TargetMinScalingLevel;
+ public byte TargetMaxScalingLevel;
+ public sbyte TargetScalingLevelDelta;
+
+ public enum SandboxScalingDataType
+ {
+ PlayerToPlayer = 1, // NYI
+ CreatureToPlayerDamage = 2,
+ PlayerToCreatureDamage = 3,
+ CreatureToCreatureDamage = 4
+ }
}
public class AuraDataInfo
diff --git a/Game/Server/WorldManager.cs b/Game/Server/WorldManager.cs
index e53c91cbf..b1f702cb7 100644
--- a/Game/Server/WorldManager.cs
+++ b/Game/Server/WorldManager.cs
@@ -478,6 +478,9 @@ namespace Game
Log.outInfo(LogFilter.ServerLoading, "Loading Creature template addons...");
Global.ObjectMgr.LoadCreatureTemplateAddons();
+ Log.outInfo(LogFilter.ServerLoading, "Loading Creature template scaling...");
+ Global.ObjectMgr.LoadCreatureScalingData();
+
Log.outInfo(LogFilter.ServerLoading, "Loading Reputation Reward Rates...");
Global.ObjectMgr.LoadReputationRewardRate();
diff --git a/Game/Spells/Auras/Aura.cs b/Game/Spells/Auras/Aura.cs
index c43be84e2..786f2c6a9 100644
--- a/Game/Spells/Auras/Aura.cs
+++ b/Game/Spells/Auras/Aura.cs
@@ -150,6 +150,7 @@ namespace Game.Spells
}
if (GetBase().GetSpellInfo().HasAttribute(SpellAttr8.AuraSendAmount) ||
+ GetBase().HasEffectType(AuraType.ModSpellCategoryCooldown) ||
GetBase().HasEffectType(AuraType.ModMaxCharges) ||
GetBase().HasEffectType(AuraType.ChargeRecoveryMod) ||
GetBase().HasEffectType(AuraType.ChargeRecoveryMultiplier))
diff --git a/Game/Spells/Spell.cs b/Game/Spells/Spell.cs
index 7ef22ffea..89aae4d39 100644
--- a/Game/Spells/Spell.cs
+++ b/Game/Spells/Spell.cs
@@ -1625,7 +1625,7 @@ namespace Game.Spells
if (m_auraScaleMask != 0 && ihit.effectMask == m_auraScaleMask && m_caster != target)
{
SpellInfo auraSpell = Global.SpellMgr.GetSpellInfo(Global.SpellMgr.GetFirstSpellInChain(m_spellInfo.Id));
- if (target.getLevel() + 10 >= auraSpell.SpellLevel)
+ if (target.GetLevelForTarget(m_caster) + 10 >= auraSpell.SpellLevel)
ihit.scaleAura = true;
}
return;
@@ -1646,7 +1646,7 @@ namespace Game.Spells
if (m_auraScaleMask != 0 && targetInfo.effectMask == m_auraScaleMask && m_caster != target)
{
SpellInfo auraSpell = Global.SpellMgr.GetSpellInfo(Global.SpellMgr.GetFirstSpellInChain(m_spellInfo.Id));
- if (target.getLevel() + 10 >= auraSpell.SpellLevel)
+ if (target.GetLevelForTarget(m_caster) + 10 >= auraSpell.SpellLevel)
targetInfo.scaleAura = true;
}
@@ -4874,7 +4874,7 @@ namespace Game.Spells
SkillType skill = creature.GetCreatureTemplate().GetRequiredLootSkill();
ushort skillValue = m_caster.ToPlayer().GetSkillValue(skill);
- uint TargetLevel = m_targets.GetUnitTarget().getLevel();
+ uint TargetLevel = m_targets.GetUnitTarget().GetLevelForTarget(m_caster);
int ReqValue = (int)(skillValue < 100 ? (TargetLevel - 10) * 10 : TargetLevel * 5);
if (ReqValue > skillValue)
return SpellCastResult.LowCastlevel;
@@ -5199,7 +5199,7 @@ namespace Game.Spells
return SpellCastResult.TargetIsPlayerControlled;
int damage = CalculateDamage(effect.EffectIndex, target1);
- if (damage != 0 && target1.getLevel() > damage)
+ if (damage != 0 && target1.GetLevelForTarget(m_caster) > damage)
return SpellCastResult.Highlevel;
}
@@ -6118,19 +6118,19 @@ namespace Game.Spells
return SpellCastResult.EquippedItem;
break;
}
- case SpellEffectName.CreateManaGem:
+ case SpellEffectName.RechargeItem:
{
- uint item_id = effect.ItemType;
- ItemTemplate proto = Global.ObjectMgr.GetItemTemplate(item_id);
+ uint itemId = effect.ItemType;
+ ItemTemplate proto = Global.ObjectMgr.GetItemTemplate(itemId);
if (proto == null)
return SpellCastResult.ItemAtMaxCharges;
- Item pitem = player.GetItemByEntry(item_id);
- if (pitem != null)
+ Item item = player.GetItemByEntry(itemId);
+ if (item != null)
{
for (int x = 0; x < proto.Effects.Count && x < 5; ++x)
- if (proto.Effects[x].Charges != 0 && pitem.GetSpellCharges(x) == proto.Effects[x].Charges)
+ if (proto.Effects[x].Charges != 0 && item.GetSpellCharges(x) == proto.Effects[x].Charges)
return SpellCastResult.ItemAtMaxCharges;
}
break;
@@ -6343,7 +6343,7 @@ namespace Game.Spells
return false;
int damage = CalculateDamage(effect.EffectIndex, target);
if (damage != 0)
- if (target.getLevel() > damage)
+ if (target.GetLevelForTarget(m_caster) > damage)
return false;
break;
default:
diff --git a/Game/Spells/SpellEffects.cs b/Game/Spells/SpellEffects.cs
index 8c93b61c5..af763db1d 100644
--- a/Game/Spells/SpellEffects.cs
+++ b/Game/Spells/SpellEffects.cs
@@ -2464,7 +2464,7 @@ namespace Game.Spells
// "kill" original creature
creatureTarget.DespawnOrUnsummon();
- uint level = (creatureTarget.getLevel() < (m_caster.getLevel() - 5)) ? (m_caster.getLevel() - 5) : creatureTarget.getLevel();
+ uint level = (creatureTarget.GetLevelForTarget(m_caster) < (m_caster.GetLevelForTarget(creatureTarget) - 5)) ? (m_caster.GetLevelForTarget(creatureTarget) - 5) : creatureTarget.GetLevelForTarget(m_caster);
// prepare visual effect for levelup
pet.SetUInt32Value(UnitFields.Level, level - 1);
@@ -4169,7 +4169,7 @@ namespace Game.Spells
return;
Creature creature = unitTarget.ToCreature();
- int targetLevel = (int)creature.getLevel();
+ int targetLevel = (int)creature.GetLevelForTarget(m_caster);
SkillType skill = creature.GetCreatureTemplate().GetRequiredLootSkill();
@@ -5376,35 +5376,27 @@ namespace Game.Spells
}
}
- [SpellEffectHandler(SpellEffectName.CreateManaGem)]
- void EffectRechargeManaGem(uint effIndex)
+ [SpellEffectHandler(SpellEffectName.RechargeItem)]
+ void EffectRechargeItem(uint effIndex)
{
if (effectHandleMode != SpellEffectHandleMode.HitTarget)
return;
- if (unitTarget == null || !unitTarget.IsTypeId(TypeId.Player))
+ if (unitTarget == null)
return;
- Player player = m_caster.ToPlayer();
-
+ Player player = unitTarget.ToPlayer();
if (player == null)
return;
- uint item_id = GetEffect(0).ItemType;
-
- ItemTemplate proto = Global.ObjectMgr.GetItemTemplate(item_id);
- if (proto == null)
- {
- player.SendEquipError(InventoryResult.ItemNotFound);
- return;
- }
-
- Item pItem = player.GetItemByEntry(item_id);
- if (pItem != null)
+ Item item = player.GetItemByEntry(effectInfo.ItemType);
+ if (item != null)
{
+ ItemTemplate proto = item.GetTemplate();
for (int x = 0; x < proto.Effects.Count && x < 5; ++x)
- pItem.SetSpellCharges(x, proto.Effects[x].Charges);
- pItem.SetState(ItemUpdateState.Changed, player);
+ item.SetSpellCharges(x, proto.Effects[x].Charges);
+
+ item.SetState(ItemUpdateState.Changed, player);
}
}
diff --git a/Game/Spells/SpellInfo.cs b/Game/Spells/SpellInfo.cs
index c63397073..d9b0d0c30 100644
--- a/Game/Spells/SpellInfo.cs
+++ b/Game/Spells/SpellInfo.cs
@@ -3124,7 +3124,7 @@ namespace Game.Spells
new StaticData(SpellEffectImplicitTargetTypes.Explicit, SpellTargetObjectTypes.Unit), // 63 SPELL_EFFECT_THREAT
new StaticData(SpellEffectImplicitTargetTypes.None, SpellTargetObjectTypes.None), // 64 SPELL_EFFECT_TRIGGER_SPELL
new StaticData(SpellEffectImplicitTargetTypes.Explicit, SpellTargetObjectTypes.Unit), // 65 SPELL_EFFECT_APPLY_AREA_AURA_RAID
- new StaticData(SpellEffectImplicitTargetTypes.Explicit, SpellTargetObjectTypes.Unit), // 66 SPELL_EFFECT_CREATE_MANA_GEM
+ new StaticData(SpellEffectImplicitTargetTypes.Explicit, SpellTargetObjectTypes.Unit), // 66 SPELL_EFFECT_RECHARGE_ITEM
new StaticData(SpellEffectImplicitTargetTypes.Explicit, SpellTargetObjectTypes.Unit), // 67 SPELL_EFFECT_HEAL_MAX_HEALTH
new StaticData(SpellEffectImplicitTargetTypes.Explicit, SpellTargetObjectTypes.Unit), // 68 SPELL_EFFECT_INTERRUPT_CAST
new StaticData(SpellEffectImplicitTargetTypes.Explicit, SpellTargetObjectTypes.UnitAndDest), // 69 SPELL_EFFECT_DISTRACT