using System;
using System.Collections.Generic;
using System.Linq;
using ACE.Database;
using ACE.DatLoader;
using ACE.Entity.Enum;
using ACE.Entity.Enum.Properties;
using ACE.Server.Entity.Actions;
using ACE.Server.Managers;
using ACE.Server.Network.GameMessages.Messages;
using ACE.Server.WorldObjects.Entity;
namespace ACE.Server.WorldObjects
{
partial class Player
{
///
/// Handles the GameAction 0x46 - RaiseSkill network message from client
///
public bool HandleActionRaiseSkill(Skill skill, uint amount)
{
var creatureSkill = GetCreatureSkill(skill, false);
if (creatureSkill == null || creatureSkill.AdvancementClass < SkillAdvancementClass.Trained)
{
log.Warn($"{Name}.HandleActionRaiseSkill({skill}, {amount}) - trained or specialized skill not found");
return false;
}
if (amount > AvailableExperience)
{
log.Warn($"{Name}.HandleActionRaiseSkill({skill}, {amount}) - amount > AvailableExperience ({AvailableExperience})");
return false;
}
var prevRank = creatureSkill.Ranks;
if (!SpendSkillXp(creatureSkill, amount))
return false;
Session.Network.EnqueueSend(new GameMessagePrivateUpdateSkill(this, creatureSkill));
if (prevRank != creatureSkill.Ranks)
{
// if the skill ranks out at the top of our xp chart
// then we will start fireworks effects and have special text!
var suffix = "";
if (creatureSkill.IsMaxRank)
{
// fireworks on rank up is 0x8D
PlayParticleEffect(PlayScript.WeddingBliss, Guid);
suffix = $" and has reached its upper limit";
}
var sound = new GameMessageSound(Guid, Sound.RaiseTrait);
var msg = new GameMessageSystemChat($"Your base {skill.ToSentence()} skill is now {creatureSkill.Base}{suffix}!", ChatMessageType.Advancement);
Session.Network.EnqueueSend(sound, msg);
// retail was missing the 'raise skill' runrate hook here
if (skill == Skill.Run && PropertyManager.GetBool("runrate_add_hooks").Item)
HandleRunRateUpdate();
}
return true;
}
private bool SpendSkillXp(CreatureSkill creatureSkill, uint amount, bool sendNetworkUpdate = true)
{
var skillXPTable = GetSkillXPTable(creatureSkill.AdvancementClass);
if (skillXPTable == null)
{
log.Warn($"{Name}.SpendSkillXp({creatureSkill.Skill}, {amount}) - player tried to raise {creatureSkill.AdvancementClass} skill");
return false;
}
// ensure skill is not already max rank
if (creatureSkill.IsMaxRank)
{
log.Warn($"{Name}.SpendSkillXp({creatureSkill.Skill}, {amount}) - player tried to raise skill beyond max rank");
return false;
}
// the client should already handle this naturally,
// but ensure player can't spend xp beyond the max rank
var amountToEnd = creatureSkill.ExperienceLeft;
if (amount > amountToEnd)
{
//log.Warn($"{Name}.SpendSkillXp({creatureSkill.Skill}, {amount}) - player tried to raise skill beyond {amountToEnd} experience");
return false; // returning error here, instead of setting amount to amountToEnd
}
// everything looks good at this point,
// spend xp on skill
if (!SpendXP(amount, sendNetworkUpdate))
{
log.Warn($"{Name}.SpendSkillXp({creatureSkill.Skill}, {amount}) - SpendXP failed");
return false;
}
creatureSkill.ExperienceSpent += amount;
// calculate new rank
creatureSkill.Ranks = (ushort)CalcSkillRank(creatureSkill.AdvancementClass, creatureSkill.ExperienceSpent);
return true;
}
///
/// Handles the GameAction 0x47 - TrainSkill network message from client
///
public bool HandleActionTrainSkill(Skill skill, int creditsSpent)
{
if (creditsSpent > AvailableSkillCredits)
{
log.Warn($"{Name}.HandleActionTrainSkill({skill}, {creditsSpent}) - not enough skill credits ({AvailableSkillCredits})");
return false;
}
// get the actual cost to train the skill.
if (!DatManager.PortalDat.SkillTable.SkillBaseHash.TryGetValue((uint)skill, out var skillBase))
{
log.Warn($"{Name}.HandleActionTrainSkill({skill}, {creditsSpent}) - couldn't find skill base");
return false;
}
if (creditsSpent != skillBase.TrainedCost)
{
log.Warn($"{Name}.HandleActionTrainSkill({skill}, {creditsSpent}) - client value differs from skillBase.TrainedCost({skillBase.TrainedCost})");
return false;
}
// attempt to train the specified skill
var success = TrainSkill(skill, creditsSpent);
var availableSkillCredits = $"You now have {AvailableSkillCredits} credits available.";
if (success)
{
var updateSkill = new GameMessagePrivateUpdateSkill(this, GetCreatureSkill(skill));
var skillCredits = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.AvailableSkillCredits, AvailableSkillCredits ?? 0);
var msg = new GameMessageSystemChat($"{skill.ToSentence()} trained. {availableSkillCredits}", ChatMessageType.Advancement);
Session.Network.EnqueueSend(updateSkill, skillCredits, msg);
}
else
Session.Network.EnqueueSend(new GameMessageSystemChat($"Failed to train {skill.ToSentence()}! {availableSkillCredits}", ChatMessageType.Advancement));
return success;
}
public bool TrainSkill(Skill skill)
{
// get the amount of skill credits required to train this skill
if (!DatManager.PortalDat.SkillTable.SkillBaseHash.TryGetValue((uint)skill, out var skillBase))
{
log.Error($"{Name}.TrainSkill({skill}) - couldn't find skill base");
return false;
}
// attempt to train the specified skill
return TrainSkill(skill, skillBase.TrainedCost);
}
///
/// Sets the skill to trained status for a character
///
public bool TrainSkill(Skill skill, int creditsSpent, bool applyCreationBonusXP = false)
{
var creatureSkill = GetCreatureSkill(skill);
if (creatureSkill.AdvancementClass >= SkillAdvancementClass.Trained || creditsSpent > AvailableSkillCredits)
return false;
creatureSkill.AdvancementClass = SkillAdvancementClass.Trained;
creatureSkill.Ranks = 0;
creatureSkill.InitLevel = 0;
if (applyCreationBonusXP)
{
creatureSkill.ExperienceSpent = 526;
creatureSkill.Ranks = 5;
}
else
creatureSkill.ExperienceSpent = 0;
AvailableSkillCredits -= creditsSpent;
// Tinkering skills can be reset at Asheron's Castle and Enlightenment, so if player has the augmentation when they train the skill again immediately specialize it again.
if (IsSkillSpecializedViaAugmentation(skill, out var playerHasAugmentation) && playerHasAugmentation)
SpecializeSkill(skill, 0, false);
return true;
}
public bool SpecializeSkill(Skill skill, bool resetSkill = true)
{
// get the amount of skill credits required to upgrade this skill
// from trained -> specialized
if (!DatManager.PortalDat.SkillTable.SkillBaseHash.TryGetValue((uint)skill, out var skillBase))
{
log.Error($"{Name}.SpecializeSkill({skill}, {resetSkill}) - couldn't find skill base");
return false;
}
// attempt to specialize the specified skill
return SpecializeSkill(skill, skillBase.UpgradeCostFromTrainedToSpecialized);
}
///
/// Sets the skill to specialized status
///
/// only set to TRUE during character creation. set to FALSE during temple / asheron's castle
public bool SpecializeSkill(Skill skill, int creditsSpent, bool resetSkill = true)
{
var creatureSkill = GetCreatureSkill(skill);
if (creatureSkill.AdvancementClass != SkillAdvancementClass.Trained || creditsSpent > AvailableSkillCredits)
return false;
if (resetSkill)
{
// this path only during char creation
creatureSkill.Ranks = 0;
creatureSkill.ExperienceSpent = 0;
}
else
{
// this path only during temple / asheron's castle
creatureSkill.Ranks = (ushort)CalcSkillRank(SkillAdvancementClass.Specialized, creatureSkill.ExperienceSpent);
}
creatureSkill.InitLevel = 10;
creatureSkill.AdvancementClass = SkillAdvancementClass.Specialized;
AvailableSkillCredits -= creditsSpent;
return true;
}
///
/// Sets the skill to untrained status
///
public bool UntrainSkill(Skill skill, int creditsSpent)
{
var creatureSkill = GetCreatureSkill(skill);
if (creatureSkill == null || creatureSkill.AdvancementClass == SkillAdvancementClass.Specialized)
return false;
if (creatureSkill.AdvancementClass < SkillAdvancementClass.Trained)
{
// only used to initialize untrained skills for character creation?
creatureSkill.AdvancementClass = SkillAdvancementClass.Untrained; // should this always be Untrained? what about Inactive?
creatureSkill.InitLevel = 0;
creatureSkill.Ranks = 0;
creatureSkill.ExperienceSpent = 0;
}
else
{
// refund xp and skill credits
RefundXP(creatureSkill.ExperienceSpent);
// temple untraining 'always trained' skills:
// cannot be untrained, but skill XP can be recovered
if (IsSkillUntrainable(skill))
{
creatureSkill.AdvancementClass = SkillAdvancementClass.Untrained;
creatureSkill.InitLevel = 0;
AvailableSkillCredits += creditsSpent;
}
creatureSkill.Ranks = 0;
creatureSkill.ExperienceSpent = 0;
}
return true;
}
///
/// Lowers skill from Specialized to Trained and returns both skill credits and invested XP
///
public bool UnspecializeSkill(Skill skill, int creditsSpent)
{
var creatureSkill = GetCreatureSkill(skill);
if (creatureSkill == null || creatureSkill.AdvancementClass != SkillAdvancementClass.Specialized)
return false;
// refund xp and skill credits
RefundXP(creatureSkill.ExperienceSpent);
// salvaging / tinkering skills specialized through augmentation only
// cannot be unspecialized here, only refund xp
if (!IsSkillSpecializedViaAugmentation(skill, out var playerHasAugmentation) || !playerHasAugmentation)
{
creatureSkill.AdvancementClass = SkillAdvancementClass.Trained;
creatureSkill.InitLevel = 0;
AvailableSkillCredits += creditsSpent;
}
creatureSkill.Ranks = 0;
creatureSkill.ExperienceSpent = 0;
return true;
}
///
/// Increases a skill by some amount of points
///
public void AwardSkillPoints(Skill skill, uint amount)
{
var creatureSkill = GetCreatureSkill(skill);
for (var i = 0; i < amount; i++)
{
// get skill xp required for next rank
var xpToNextRank = GetXpToNextRank(creatureSkill);
if (xpToNextRank != null)
AwardSkillXP(skill, xpToNextRank.Value);
else
return;
}
}
///
/// Wrapper method used for increasing totalXP and then using the amount granted by HandleActionRaiseSkill
///
public void AwardSkillXP(Skill skill, uint amount, bool alertPlayer = false)
{
var playerSkill = GetCreatureSkill(skill);
if (playerSkill.AdvancementClass < SkillAdvancementClass.Trained || playerSkill.IsMaxRank)
return;
amount = Math.Min(amount, playerSkill.ExperienceLeft);
GrantXP(amount, XpType.Emote, ShareType.None);
var raiseChain = new ActionChain();
raiseChain.AddDelayForOneTick();
raiseChain.AddAction(this, () =>
{
HandleActionRaiseSkill(skill, amount);
});
raiseChain.EnqueueChain();
if (alertPlayer)
Session.Network.EnqueueSend(new GameMessageSystemChat($"You've earned {amount:N0} experience in your {playerSkill.Skill.ToSentence()} skill.", ChatMessageType.Broadcast));
}
public void SpendAllAvailableSkillXp(CreatureSkill creatureSkill, bool sendNetworkUpdate = true)
{
var amountRemaining = creatureSkill.ExperienceLeft;
if (amountRemaining > AvailableExperience)
amountRemaining = (uint)AvailableExperience;
SpendSkillXp(creatureSkill, amountRemaining, sendNetworkUpdate);
}
///
/// Grants skill XP proportional to the player's skill level
///
public void GrantLevelProportionalSkillXP(Skill skill, double percent, long min, long max)
{
var creatureSkill = GetCreatureSkill(skill, false);
if (creatureSkill == null || creatureSkill.IsMaxRank)
return;
var nextLevelXP = GetXPBetweenSkillLevels(creatureSkill.AdvancementClass, creatureSkill.Ranks, creatureSkill.Ranks + 1);
if (nextLevelXP == null)
return;
var amount = (uint)Math.Round(nextLevelXP.Value * percent);
if (max > 0 && max <= uint.MaxValue)
amount = Math.Min(amount, (uint)max);
amount = Math.Min(amount, creatureSkill.ExperienceLeft);
if (min > 0)
amount = Math.Max(amount, (uint)min);
//Console.WriteLine($"{Name}.GrantLevelProportionalSkillXP({skill}, {percent}, {max:N0})");
//Console.WriteLine($"Amount: {amount:N0}");
AwardSkillXP(skill, amount, true);
}
///
/// Returns the remaining XP required to the next skill level
///
public uint? GetXpToNextRank(CreatureSkill skill)
{
if (skill.AdvancementClass < SkillAdvancementClass.Trained || skill.IsMaxRank)
return null;
var skillXPTable = GetSkillXPTable(skill.AdvancementClass);
return skillXPTable[skill.Ranks + 1] - skill.ExperienceSpent;
}
///
/// Returns the XP curve table based on trained or specialized skill
///
public static List GetSkillXPTable(SkillAdvancementClass status)
{
var xpTable = DatManager.PortalDat.XpTable;
switch (status)
{
case SkillAdvancementClass.Trained:
return xpTable.TrainedSkillXpList;
case SkillAdvancementClass.Specialized:
return xpTable.SpecializedSkillXpList;
default:
return null;
}
}
///
/// Returns the skill XP required to go between fromRank and toRank
///
public ulong? GetXPBetweenSkillLevels(SkillAdvancementClass status, int fromRank, int toRank)
{
var skillXPTable = GetSkillXPTable(status);
if (skillXPTable == null)
return null;
return skillXPTable[toRank] - skillXPTable[fromRank];
}
///
/// Returns the maximum rank that can be purchased with an xp amount
///
/// Trained or specialized skill
/// The amount of xp used to make the purchase
public static int CalcSkillRank(SkillAdvancementClass sac, uint xpAmount)
{
var rankXpTable = GetSkillXPTable(sac);
for (var i = rankXpTable.Count - 1; i >= 0; i--)
{
var rankAmount = rankXpTable[i];
if (xpAmount >= rankAmount)
return i;
}
return -1;
}
private const uint magicSkillCheckMargin = 50;
public bool CanReadScroll(Scroll scroll)
{
var power = scroll.Spell.Power;
// level 1/7/8 scrolls can be learned by anyone?
if (power < 50 || power >= 300) return true;
var magicSkill = scroll.Spell.GetMagicSkill();
var playerSkill = GetCreatureSkill(magicSkill);
var minSkill = power - magicSkillCheckMargin;
return playerSkill.AdvancementClass >= SkillAdvancementClass.Trained && playerSkill.Current >= minSkill;
}
public void AddSkillCredits(int amount)
{
TotalSkillCredits += amount;
AvailableSkillCredits += amount;
Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.AvailableSkillCredits, AvailableSkillCredits ?? 0));
if (amount > 1)
SendTransientError($"You have been awarded {amount:N0} additional skill credits.");
else
SendTransientError("You have been awarded an additional skill credit.");
}
///
/// Called on player login
/// If a player has any skills trained that require updates from ACE-World-16-Patches,
/// ensure these updates are installed, and if they aren't, send a helpful message to player with instructions for installation
///
public void HandleDBUpdates()
{
// dirty fighting
var dfSkill = GetCreatureSkill(Skill.DirtyFighting);
if (dfSkill.AdvancementClass >= SkillAdvancementClass.Trained)
{
foreach (var spellID in SpellExtensions.DirtyFightingSpells)
{
var spell = new Server.Entity.Spell(spellID);
if (spell.NotFound)
{
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(3.0f);
actionChain.AddAction(this, () =>
{
Session.Network.EnqueueSend(new GameMessageSystemChat("To install Dirty Fighting, please apply the latest patches from https://github.com/ACEmulator/ACE-World-16PY-Patches", ChatMessageType.Broadcast));
});
actionChain.EnqueueChain();
}
break; // performance improvement: only check first spell
}
}
// void magic
var voidSkill = GetCreatureSkill(Skill.VoidMagic);
if (voidSkill.AdvancementClass >= SkillAdvancementClass.Trained)
{
foreach (var spellID in SpellExtensions.VoidMagicSpells)
{
var spell = new Server.Entity.Spell(spellID);
if (spell.NotFound)
{
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(3.0f);
actionChain.AddAction(this, () =>
{
Session.Network.EnqueueSend(new GameMessageSystemChat("To install Void Magic, please apply the latest patches from https://github.com/ACEmulator/ACE-World-16PY-Patches", ChatMessageType.Broadcast));
});
actionChain.EnqueueChain();
}
break; // performance improvement: only check first spell (measured 102ms to check 75 uncached void spells)
}
}
// summoning
var summoning = GetCreatureSkill(Skill.Summoning);
if (summoning.AdvancementClass >= SkillAdvancementClass.Trained)
{
uint essenceWCID = 48878;
var weenie = DatabaseManager.World.GetCachedWeenie(essenceWCID);
if (weenie == null)
{
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(3.0f);
actionChain.AddAction(this, () =>
{
Session.Network.EnqueueSend(new GameMessageSystemChat("To install Summoning, please apply the latest patches from https://github.com/ACEmulator/ACE-World-16PY-Patches", ChatMessageType.Broadcast));
});
actionChain.EnqueueChain();
}
}
}
public static HashSet MeleeSkills = new HashSet()
{
Skill.LightWeapons,
Skill.HeavyWeapons,
Skill.FinesseWeapons,
Skill.DualWield,
Skill.TwoHandedCombat,
// legacy
Skill.Axe,
Skill.Dagger,
Skill.Mace,
Skill.Spear,
Skill.Staff,
Skill.Sword,
Skill.UnarmedCombat
};
public static HashSet MissileSkills = new HashSet()
{
Skill.MissileWeapons,
// legacy
Skill.Bow,
Skill.Crossbow,
Skill.Sling,
Skill.ThrownWeapon
};
public static HashSet MagicSkills = new HashSet()
{
Skill.CreatureEnchantment,
Skill.ItemEnchantment,
Skill.LifeMagic,
Skill.VoidMagic,
Skill.WarMagic
};
public static List AlwaysTrained = new List()
{
Skill.ArcaneLore,
Skill.Jump,
Skill.Loyalty,
Skill.MagicDefense,
Skill.Run,
Skill.Salvaging
};
public static List AugSpecSkills = new List()
{
Skill.ArmorTinkering,
Skill.ItemTinkering,
Skill.MagicItemTinkering,
Skill.WeaponTinkering,
Skill.Salvaging
};
public static bool IsSkillUntrainable(Skill skill)
{
return !AlwaysTrained.Contains(skill);
}
public bool IsSkillSpecializedViaAugmentation(Skill skill, out bool playerHasAugmentation)
{
playerHasAugmentation = false;
switch (skill)
{
case Skill.ArmorTinkering:
playerHasAugmentation = AugmentationSpecializeArmorTinkering > 0;
break;
case Skill.ItemTinkering:
playerHasAugmentation = AugmentationSpecializeItemTinkering > 0;
break;
case Skill.MagicItemTinkering:
playerHasAugmentation = AugmentationSpecializeMagicItemTinkering > 0;
break;
case Skill.WeaponTinkering:
playerHasAugmentation = AugmentationSpecializeWeaponTinkering > 0;
break;
case Skill.Salvaging:
playerHasAugmentation = AugmentationSpecializeSalvaging > 0;
break;
}
return AugSpecSkills.Contains(skill);
}
public override bool GetHeritageBonus(WorldObject weapon)
{
if (weapon == null || !weapon.IsMasterable)
return false;
if (PropertyManager.GetBool("universal_masteries").Item)
{
// https://asheron.fandom.com/wiki/Spring_2014_Update
// end of retail - universal masteries
return true;
}
else
return GetHeritageBonus(GetWeaponType(weapon));
}
public bool GetHeritageBonus(WeaponType weaponType)
{
switch (HeritageGroup)
{
case HeritageGroup.Aluvian:
if (weaponType == WeaponType.Dagger || weaponType == WeaponType.Bow)
return true;
break;
case HeritageGroup.Gharundim:
if (weaponType == WeaponType.Staff || weaponType == WeaponType.Magic)
return true;
break;
case HeritageGroup.Sho:
if (weaponType == WeaponType.Unarmed || weaponType == WeaponType.Bow)
return true;
break;
case HeritageGroup.Viamontian:
if (weaponType == WeaponType.Sword || weaponType == WeaponType.Crossbow)
return true;
break;
case HeritageGroup.Shadowbound: // umbraen
case HeritageGroup.Penumbraen:
if (weaponType == WeaponType.Unarmed || weaponType == WeaponType.Crossbow)
return true;
break;
case HeritageGroup.Gearknight:
if (weaponType == WeaponType.Mace || weaponType == WeaponType.Crossbow)
return true;
break;
case HeritageGroup.Undead:
if (weaponType == WeaponType.Axe || weaponType == WeaponType.Thrown)
return true;
break;
case HeritageGroup.Empyrean:
if (weaponType == WeaponType.Sword || weaponType == WeaponType.Magic)
return true;
break;
case HeritageGroup.Tumerok:
if (weaponType == WeaponType.Spear || weaponType == WeaponType.Thrown)
return true;
break;
case HeritageGroup.Lugian:
if (weaponType == WeaponType.Axe || weaponType == WeaponType.Thrown)
return true;
break;
case HeritageGroup.Olthoi:
case HeritageGroup.OlthoiAcid:
break;
}
return false;
}
///
/// If the WeaponType is missing from a weapon, tries to convert from WeaponSkill (for old data)
///
public WeaponType GetWeaponType(WorldObject weapon)
{
if (weapon == null)
return WeaponType.Undef; // unarmed?
if (weapon is Caster)
return WeaponType.Magic;
var weaponType = weapon.GetProperty(PropertyInt.WeaponType);
if (weaponType != null)
return (WeaponType)weaponType;
var weaponSkill = weapon.GetProperty(PropertyInt.WeaponSkill);
if (weaponSkill != null && SkillToWeaponType.TryGetValue((Skill)weaponSkill, out WeaponType converted))
return converted;
else
return WeaponType.Undef;
}
public static Dictionary SkillToWeaponType = new Dictionary()
{
{ Skill.UnarmedCombat, WeaponType.Unarmed },
{ Skill.Sword, WeaponType.Sword },
{ Skill.Axe, WeaponType.Axe },
{ Skill.Mace, WeaponType.Mace },
{ Skill.Spear, WeaponType.Spear },
{ Skill.Dagger, WeaponType.Dagger },
{ Skill.Staff, WeaponType.Staff },
{ Skill.Bow, WeaponType.Bow },
{ Skill.Crossbow, WeaponType.Crossbow },
{ Skill.ThrownWeapon, WeaponType.Thrown },
{ Skill.TwoHandedCombat, WeaponType.TwoHanded },
{ Skill.CreatureEnchantment, WeaponType.Magic }, // only for war/void?
{ Skill.ItemEnchantment, WeaponType.Magic },
{ Skill.LifeMagic, WeaponType.Magic },
{ Skill.WarMagic, WeaponType.Magic },
{ Skill.VoidMagic, WeaponType.Magic },
};
public void HandleSkillCreditRefund()
{
if (!(GetProperty(PropertyBool.UntrainedSkills) ?? false)) return;
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(5.0f);
actionChain.AddAction(this, () =>
{
Session.Network.EnqueueSend(new GameMessageSystemChat("Your trained skills have been reset due to an error with skill credits.\nYou have received a refund for these skill credits and experience.", ChatMessageType.Broadcast));
RemoveProperty(PropertyBool.UntrainedSkills);
});
actionChain.EnqueueChain();
}
public void HandleSkillSpecCreditRefund()
{
if (!(GetProperty(PropertyBool.UnspecializedSkills) ?? false)) return;
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(5.0f);
actionChain.AddAction(this, () =>
{
Session.Network.EnqueueSend(new GameMessageSystemChat("Your specialized skills have been unspecialized due to an error with skill credits.\nYou have received a refund for these skill credits and experience.", ChatMessageType.Broadcast));
RemoveProperty(PropertyBool.UnspecializedSkills);
});
actionChain.EnqueueChain();
}
public void HandleFreeSkillResetRenewal()
{
if (!(GetProperty(PropertyBool.FreeSkillResetRenewed) ?? false)) return;
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(5.0f);
actionChain.AddAction(this, () =>
{
Session.Network.EnqueueSend(new GameMessageSystemChat("Your opportunity to change your skills is renewed! Visit Fianhe to reset your skills.", ChatMessageType.Magic));
RemoveProperty(PropertyBool.FreeSkillResetRenewed);
QuestManager.Erase("UsedFreeSkillReset");
});
actionChain.EnqueueChain();
}
public void HandleFreeAttributeResetRenewal()
{
if (!(GetProperty(PropertyBool.FreeAttributeResetRenewed) ?? false)) return;
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(5.0f);
actionChain.AddAction(this, () =>
{
// Your opportunity to change your attributes is renewed! Visit Chafulumisa to reset your skills [sic attributes].
Session.Network.EnqueueSend(new GameMessageSystemChat("Your opportunity to change your attributes is renewed! Visit Chafulumisa to reset your attributes.", ChatMessageType.Magic));
RemoveProperty(PropertyBool.FreeAttributeResetRenewed);
QuestManager.Erase("UsedFreeAttributeReset");
});
actionChain.EnqueueChain();
}
public void HandleSkillTemplesReset()
{
if (!(GetProperty(PropertyBool.SkillTemplesTimerReset) ?? false)) return;
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(5.0f);
actionChain.AddAction(this, () =>
{
Session.Network.EnqueueSend(new GameMessageSystemChat("The Temples of Forgetfulness and Enlightenment have had the timer for their use reset due to skill changes.", ChatMessageType.Magic));
RemoveProperty(PropertyBool.SkillTemplesTimerReset);
QuestManager.Erase("ForgetfulnessGems1");
QuestManager.Erase("ForgetfulnessGems2");
QuestManager.Erase("ForgetfulnessGems3");
QuestManager.Erase("ForgetfulnessGems4");
QuestManager.Erase("Forgetfulness6days");
QuestManager.Erase("Forgetfulness13days");
QuestManager.Erase("Forgetfulness20days");
});
actionChain.EnqueueChain();
}
public void HandleFreeMasteryResetRenewal()
{
if (!(GetProperty(PropertyBool.FreeMasteryResetRenewed) ?? false)) return;
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(5.0f);
actionChain.AddAction(this, () =>
{
Session.Network.EnqueueSend(new GameMessageSystemChat("Your opportunity to change your Masteries is renewed!", ChatMessageType.Magic));
RemoveProperty(PropertyBool.FreeMasteryResetRenewed);
QuestManager.Erase("UsedFreeMeleeMasteryReset");
QuestManager.Erase("UsedFreeRangedMasteryReset");
QuestManager.Erase("UsedFreeSummoningMasteryReset");
});
actionChain.EnqueueChain();
}
///
/// Resets the skill, refunds all experience and skill credits, if allowed.
///
public bool ResetSkill(Skill skill, bool refund = true)
{
var creatureSkill = GetCreatureSkill(skill, false);
if (creatureSkill == null || creatureSkill.AdvancementClass < SkillAdvancementClass.Trained)
return false;
// gather skill credits to refund
DatManager.PortalDat.SkillTable.SkillBaseHash.TryGetValue((uint)creatureSkill.Skill, out var skillBase);
if (skillBase == null)
return false;
// salvage / tinkering skills specialized via augmentations
// Salvaging cannot be untrained or unspecialized => skillIsSpecializedViaAugmentation && !untrainable
IsSkillSpecializedViaAugmentation(creatureSkill.Skill, out var skillIsSpecializedViaAugmentation);
var typeOfSkill = creatureSkill.AdvancementClass.ToString().ToLower() + " ";
var untrainable = IsSkillUntrainable(skill);
var creditRefund = (creatureSkill.AdvancementClass == SkillAdvancementClass.Specialized && !(skillIsSpecializedViaAugmentation && !untrainable)) || untrainable;
if (creatureSkill.AdvancementClass == SkillAdvancementClass.Specialized && !(skillIsSpecializedViaAugmentation && !untrainable))
{
creatureSkill.AdvancementClass = SkillAdvancementClass.Trained;
creatureSkill.InitLevel = 0;
if (!skillIsSpecializedViaAugmentation) // Tinkering skills can be unspecialized, but do not refund upgrade cost.
AvailableSkillCredits += skillBase.UpgradeCostFromTrainedToSpecialized;
}
// temple untraining 'always trained' skills:
// cannot be untrained, but skill XP can be recovered
if (untrainable)
{
creatureSkill.AdvancementClass = SkillAdvancementClass.Untrained;
creatureSkill.InitLevel = 0;
AvailableSkillCredits += skillBase.TrainedCost;
}
if (refund)
RefundXP(creatureSkill.ExperienceSpent);
creatureSkill.ExperienceSpent = 0;
creatureSkill.Ranks = 0;
var updateSkill = new GameMessagePrivateUpdateSkill(this, creatureSkill);
var availableSkillCredits = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.AvailableSkillCredits, AvailableSkillCredits ?? 0);
var msg = $"Your {(untrainable ? $"{typeOfSkill}" : "")}{skill.ToSentence()} skill has been {(untrainable ? "removed" : "reset")}. ";
msg += $"All the experience {(creditRefund ? "and skill credits " : "")}that you spent on this skill have been refunded to you.";
if (refund)
Session.Network.EnqueueSend(updateSkill, availableSkillCredits, new GameMessageSystemChat(msg, ChatMessageType.Broadcast));
else
Session.Network.EnqueueSend(updateSkill, new GameMessageSystemChat(msg, ChatMessageType.Broadcast));
return true;
}
///
/// All of the skills players have access to @ end of retail
///
public static HashSet PlayerSkills = new HashSet()
{
Skill.MeleeDefense,
Skill.MissileDefense,
Skill.ArcaneLore,
Skill.MagicDefense,
Skill.ManaConversion,
Skill.ItemTinkering,
Skill.AssessPerson,
Skill.Deception,
Skill.Healing,
Skill.Jump,
Skill.Lockpick,
Skill.Run,
Skill.AssessCreature,
Skill.WeaponTinkering,
Skill.ArmorTinkering,
Skill.MagicItemTinkering,
Skill.CreatureEnchantment,
Skill.ItemEnchantment,
Skill.LifeMagic,
Skill.WarMagic,
Skill.Leadership,
Skill.Loyalty,
Skill.Fletching,
Skill.Alchemy,
Skill.Cooking,
Skill.Salvaging,
Skill.TwoHandedCombat,
Skill.VoidMagic,
Skill.HeavyWeapons,
Skill.LightWeapons,
Skill.FinesseWeapons,
Skill.MissileWeapons,
Skill.Shield,
Skill.DualWield,
Skill.Recklessness,
Skill.SneakAttack,
Skill.DirtyFighting,
Skill.Summoning
};
}
}