private static bool AssignMagic_Gem_New(WorldObject wo, TreasureDeath profile, TreasureRoll roll) { // TODO: move to standard AssignMagic() pipeline var spell = SpellSelectionTable.Roll(1); var spellLevel = SpellLevelChance.Roll(profile.Tier); var spellLevels = SpellLevelProgression.GetSpellLevels(spell); if (spellLevels == null || spellLevels.Count != 8) { log.Error($"AssignMagic_Gem_New({wo.Name}, {profile.TreasureType}, {roll.ItemType}) - unknown spell {spell}"); return(false); } var finalSpellId = spellLevels[spellLevel - 1]; wo.SpellDID = (uint)finalSpellId; var _spell = new Server.Entity.Spell(finalSpellId); // retail spellcraft was capped at 370 wo.ItemSpellcraft = Math.Min((int)_spell.Power, 370); var castableMana = (int)_spell.BaseMana * 5; wo.ItemMaxMana = RollItemMaxMana_New(wo, roll, castableMana); wo.ItemCurMana = wo.ItemMaxMana; // verified wo.ItemManaCost = castableMana; return(true); }
private static float RollEnchantmentDifficulty(List <SpellId> spellIds) { var spells = new List <Server.Entity.Spell>(); foreach (var spellId in spellIds) { var spell = new Server.Entity.Spell(spellId); spells.Add(spell); } spells = spells.OrderBy(i => i.Formula.Level).ToList(); var itemDifficulty = 0.0f; // exclude highest spell for (var i = 0; i < spells.Count - 1; i++) { var spell = spells[i]; var rng = (float)ThreadSafeRandom.Next(0.5f, 1.5f); itemDifficulty += spell.Formula.Level * 5.0f * rng; } return(itemDifficulty); }
/// <summary> /// Applies the boost from the consumable, broadcasts the sound, /// sends message to player, and consumes from inventory /// </summary> public void ApplyConsumable(Player player) { var buffType = (ConsumableBuffType)BoosterEnum; GameMessageSystemChat buffMessage = null; // spells if (buffType == ConsumableBuffType.Spell) { var spellID = SpellDID ?? 0; var result = player.CreateSingleSpell(spellID); if (result) { var spell = new Server.Entity.Spell(spellID); buffMessage = new GameMessageSystemChat($"{Name} casts {spell.Name} on you.", ChatMessageType.Magic); } else { buffMessage = new GameMessageSystemChat($"{Name} has invalid spell id {spellID}", ChatMessageType.Broadcast); } } // vitals else { var vital = player.GetCreatureVital(BoosterEnum); if (vital != null) { var vitalChange = (uint)Math.Abs(player.UpdateVitalDelta(vital, BoostValue)); if (BoosterEnum == PropertyAttribute2nd.Health) { if (BoostValue >= 0) { player.DamageHistory.OnHeal(vitalChange); } else { player.DamageHistory.Add(this, DamageType.Health, vitalChange); } } var verb = BoostValue >= 0 ? "restores" : "takes"; buffMessage = new GameMessageSystemChat($"The {Name} {verb} {vitalChange} points of your {BoosterEnum}.", ChatMessageType.Broadcast); } else { buffMessage = new GameMessageSystemChat($"{Name} ({Guid}) contains invalid vital {BoosterEnum}", ChatMessageType.Broadcast); } } var soundEvent = new GameMessageSound(player.Guid, GetUseSound(), 1.0f); player.EnqueueBroadcast(soundEvent); player.Session.Network.EnqueueSend(buffMessage); player.TryConsumeFromInventoryWithNetworking(this, 1); }
/// <summary> /// This is raised by Player.HandleActionUseItem.<para /> /// The item should be in the players possession. /// /// The OnUse method for this class is to use a contract to add a tracked quest to our quest panel. /// This gives the player access to information about the quest such as starting and ending NPC locations, /// and shows our progress for kill tasks as well as any timing information such as when we can repeat the /// quest or how much longer we have to complete it in the case of at timed quest. Og II /// </summary> public override void ActOnUse(WorldObject activator) { if (!(activator is Player player)) { return; } if (UseCreateContractId == null) { player.EnchantmentManager.StartCooldown(this); if (SpellDID.HasValue) { var spell = new Server.Entity.Spell((uint)SpellDID); TryCastSpell(spell, player, this); } if ((GetProperty(PropertyBool.UnlimitedUse) ?? false) == false) { player.TryConsumeFromInventoryWithNetworking(this, 1); } } else { ActOnUseContract(player); } }
private static void MutateCaster_SpellDID(WorldObject wo, TreasureDeath profile) { var firstSpell = CasterSlotSpells.Roll(wo); var spellLevels = SpellLevelProgression.GetSpellLevels(firstSpell); if (spellLevels == null) { log.Error($"MutateCaster_SpellDID: couldn't find {firstSpell}"); return; } if (spellLevels.Count != 8) { log.Error($"MutateCaster_SpellDID: found {spellLevels.Count} spell levels for {firstSpell}, expected 8"); return; } var spellLevel = SpellLevelChance.Roll(profile.Tier); wo.SpellDID = (uint)spellLevels[spellLevel - 1]; var spell = new Server.Entity.Spell(wo.SpellDID.Value); var castableMod = CasterSlotSpells.IsOrb(wo) ? 5.0f : 2.5f; wo.ItemManaCost = (int)(spell.BaseMana * castableMod); wo.ItemUseable = Usable.SourceWieldedTargetRemoteNeverWalk; }
private static void MutateCaster_SpellDID(WorldObject wo, TreasureDeath profile) { var firstSpell = CasterSlotSpells.Roll(wo); var spellLevels = SpellLevelProgression.GetSpellLevels(firstSpell); if (spellLevels == null) { log.Error($"MutateCaster_SpellDID: couldn't find {firstSpell}"); return; } if (spellLevels.Count != 8) { log.Error($"MutateCaster_SpellDID: found {spellLevels.Count} spell levels for {firstSpell}, expected 8"); return; } int minSpellLevel = GetLowSpellTier(profile.Tier); int maxSpellLevel = GetHighSpellTier(profile.Tier); var spellLevel = ThreadSafeRandom.Next(minSpellLevel, maxSpellLevel); wo.SpellDID = (uint)spellLevels[spellLevel - 1]; var spell = new Server.Entity.Spell(wo.SpellDID.Value); wo.ItemManaCost = (int)spell.BaseMana * 5; wo.ItemUseable = Usable.SourceWieldedTargetRemoteNeverWalk; }
private void SetEphemeralValues() { if (SpellDID != null) { Spell = new Server.Entity.Spell(SpellDID.Value, false); } }
private static void AssignMagic(WorldObject wo, TreasureDeath profile, TreasureRoll roll, bool isArmor = false) { int numSpells = 0; if (roll == null) { // previous method if (!AssignMagic_Spells(wo, profile, isArmor, out numSpells)) { return; } } else { // new method if (!AssignMagic_New(wo, profile, roll, out numSpells)) { return; } } wo.UiEffects = UiEffects.Magical; var maxBaseMana = GetMaxBaseMana(wo); wo.ManaRate = CalculateManaRate(maxBaseMana); if (roll == null) { wo.ItemMaxMana = RollItemMaxMana(numSpells, profile.Tier); wo.ItemCurMana = wo.ItemMaxMana; wo.ItemSpellcraft = RollSpellcraft(wo); wo.ItemDifficulty = RollItemDifficulty(wo); } else { var maxSpellMana = maxBaseMana; if (wo.SpellDID != null) { var spell = new Server.Entity.Spell(wo.SpellDID.Value); var castableMana = (int)spell.BaseMana * 5; if (castableMana > maxSpellMana) { maxSpellMana = castableMana; } } wo.ItemMaxMana = RollItemMaxMana_New(wo, roll, maxSpellMana); wo.ItemCurMana = wo.ItemMaxMana; wo.ItemSpellcraft = RollSpellcraft(wo, roll); AddActivationRequirements(wo, roll); } }
/// <summary> /// Perfroms additional set up of the spell projectile based on the spell id or its derived type. /// </summary> public void Setup(Server.Entity.Spell spell, ProjectileSpellType spellType) { Spell = spell; SpellType = spellType; InitPhysicsObj(); // Runtime changes to default state ReportCollisions = true; Missile = true; AlignPath = true; PathClipped = true; IgnoreCollisions = false; if (!Spell.Name.Equals("Rolling Death")) Ethereal = false; if (SpellType == ProjectileSpellType.Bolt || SpellType == ProjectileSpellType.Streak || SpellType == ProjectileSpellType.Arc || SpellType == ProjectileSpellType.Volley || SpellType == ProjectileSpellType.Blast || WeenieClassId == 7276 || WeenieClassId == 7277 || WeenieClassId == 7279 || WeenieClassId == 7280) { PhysicsObj.DefaultScript = PlayScript.ProjectileCollision; PhysicsObj.DefaultScriptIntensity = 1.0f; } // Some wall spells don't have scripted collisions if (WeenieClassId == 7278 || WeenieClassId == 7281 || WeenieClassId == 7282 || WeenieClassId == 23144) { ScriptedCollision = false; } AllowEdgeSlide = false; // No need to send an ObjScale of 1.0f over the wire since that is the default value if (ObjScale == 1.0f) ObjScale = null; if (SpellType == ProjectileSpellType.Ring) { if (spell.Id == 3818) { DefaultScriptId = (uint)PlayScript.Explode; DefaultScriptIntensity = 1.0f; ScriptedCollision = true; } else { ScriptedCollision = false; } } // Whirling Blade spells get omega values and "align path" turned off which // creates the nice swirling animation if (WeenieClassId == 1636 || WeenieClassId == 7268 || WeenieClassId == 20979) { AlignPath = false; Omega = new Vector3(12.56637f, 0, 0); } }
/// <summary> /// This is raised by Player.HandleActionUseItem.<para /> /// The item should be in the players possession. /// /// The OnUse method for this class is to use a contract to add a tracked quest to our quest panel. /// This gives the player access to information about the quest such as starting and ending NPC locations, /// and shows our progress for kill tasks as well as any timing information such as when we can repeat the /// quest or how much longer we have to complete it in the case of at timed quest. Og II /// </summary> public override void ActOnUse(WorldObject activator) { if (!(activator is Player player)) { return; } if (player.IsBusy || player.Teleporting) { player.Session.Network.EnqueueSend(new GameEventWeenieError(player.Session, WeenieError.YoureTooBusy)); return; } if (UseCreateContractId != null) { ActOnUseContract(player); return; } if (!string.IsNullOrWhiteSpace(UseSendsSignal)) { player.CurrentLandblock?.EmitSignal(player, UseSendsSignal); return; } // handle rare gems if (RareUsesTimer) { var currentTime = Time.GetUnixTime(); var timeElapsed = currentTime - player.LastRareUsedTimestamp; if (timeElapsed < RareTimer) { // TODO: get retail message var remainTime = (int)Math.Ceiling(RareTimer - timeElapsed); player.Session.Network.EnqueueSend(new GameMessageSystemChat($"You may use another timed rare in {remainTime}s", ChatMessageType.Broadcast)); return; } player.LastRareUsedTimestamp = currentTime; // local broadcast usage player.EnqueueBroadcast(new GameMessageSystemChat($"{player.Name} used the rare item {Name}", ChatMessageType.Broadcast)); } if (SpellDID.HasValue) { var spell = new Server.Entity.Spell((uint)SpellDID); TryCastSpell(spell, player, this); } if ((GetProperty(PropertyBool.UnlimitedUse) ?? false) == false) { player.TryConsumeFromInventoryWithNetworking(this, 1); } }
public static ProjectileSpellType GetProjectileSpellType(uint spellID) { var spell = new Server.Entity.Spell(spellID); if (spell.Wcid == 0) { return(ProjectileSpellType.Undef); } if (spell.NumProjectiles == 1) { if (spell.Category >= SpellCategory.AcidStreak && spell.Category <= SpellCategory.SlashingStreak || spell.Category == SpellCategory.NetherStreak || spell.Category == SpellCategory.Fireworks) { return(ProjectileSpellType.Streak); } else if (spell.NonTracking) { return(ProjectileSpellType.Arc); } else { return(ProjectileSpellType.Bolt); } } if (spell.Category >= SpellCategory.AcidRing && spell.Category <= SpellCategory.SlashingRing || spell.SpreadAngle == 360) { return(ProjectileSpellType.Ring); } if (spell.Category >= SpellCategory.AcidBurst && spell.Category <= SpellCategory.SlashingBurst || spell.Category == SpellCategory.NetherDamageOverTimeRaising3) { return(ProjectileSpellType.Blast); } // 1481 - Flaming Missile Volley if (spell.Category >= SpellCategory.AcidVolley && spell.Category <= SpellCategory.BladeVolley || spell.Name.Contains("Volley")) { return(ProjectileSpellType.Volley); } if (spell.Category >= SpellCategory.AcidWall && spell.Category <= SpellCategory.SlashingWall) { return(ProjectileSpellType.Wall); } if (spell.Category >= SpellCategory.AcidStrike && spell.Category <= SpellCategory.SlashingStrike) { return(ProjectileSpellType.Strike); } return(ProjectileSpellType.Undef); }
public static ProjectileSpellType GetProjectileSpellType(uint spellID) { var spell = new Server.Entity.Spell(spellID); if (spell.Wcid == 0) { return(ProjectileSpellType.Undef); } // TODO: improve readability if ((spell.Wcid >= 7262 && spell.Wcid <= 7268) || (spellID >= 5345 && spellID <= 5348) || (spellID >= 5357 && spellID <= 5360)) { return(ProjectileSpellType.Streak); } else if (spell.Wcid >= 7269 && spell.Wcid <= 7275 || spell.Wcid == 43233 || spellID == 6320) { return(ProjectileSpellType.Ring); } else if (spell.Wcid >= 7276 && spell.Wcid <= 7282 || spell.Wcid == 23144) { return(ProjectileSpellType.Wall); } else if ((spell.Wcid >= 20973 && spell.Wcid <= 20979) || (spellID >= 5362 && spellID <= 5369)) { return(ProjectileSpellType.Arc); } else if (spell.Wcid == 1499 || spell.Wcid == 1503 || (spell.Wcid >= 1633 && spell.Wcid <= 1667) || (spellID >= 5395 && spellID <= 5402) || (spellID >= 5544 && spellID <= 5551)) { if (spell.SpreadAngle > 0) { return(ProjectileSpellType.Blast); } else if (spell.DimsOrigin.X > 1) { return(ProjectileSpellType.Volley); } else { return(ProjectileSpellType.Bolt); } } if (spell.Name.Equals("Rolling Death")) { return(ProjectileSpellType.Wall); // ?? } if (spell.School == MagicSchool.VoidMagic) { return(ProjectileSpellType.Bolt); } return(ProjectileSpellType.Undef); }
private void SetEphemeralValues() { if (SpellDID != null) { Spell = new Server.Entity.Spell(SpellDID.Value, false); } if (Spell != null) { LongDesc = $"Inscribed spell: {Spell.Name}\n{Spell.Description}"; } Use = "Use this item to attempt to learn its spell."; }
/// <summary> /// Perfroms additional set up of the spell projectile based on the spell id or its derived type. /// </summary> /// <param name="spellId"></param> public void Setup(uint spellId) { Spell = new Server.Entity.Spell(spellId); SpellType = GetProjectileSpellType(spellId); InitPhysicsObj(); // Runtime changes to default state ReportCollisions = true; Missile = true; AlignPath = true; PathClipped = true; Ethereal = false; IgnoreCollisions = false; if (SpellType == ProjectileSpellType.Bolt || SpellType == ProjectileSpellType.Streak || SpellType == ProjectileSpellType.Arc || SpellType == ProjectileSpellType.Volley || SpellType == ProjectileSpellType.Blast || WeenieClassId == 7276 || WeenieClassId == 7277 || WeenieClassId == 7279 || WeenieClassId == 7280) { PhysicsObj.DefaultScript = ACE.Entity.Enum.PlayScript.ProjectileCollision; PhysicsObj.DefaultScriptIntensity = 1.0f; } // Some wall spells don't have scripted collisions if (WeenieClassId == 7278 || WeenieClassId == 7281 || WeenieClassId == 7282 || WeenieClassId == 23144) { ScriptedCollision = false; } AllowEdgeSlide = false; // No need to send an ObjScale of 1.0f over the wire since that is the default value if (ObjScale == 1.0f) { ObjScale = null; } if (SpellType == ProjectileSpellType.Ring) { ScriptedCollision = false; } // Whirling Blade spells get omega values and "align path" turned off which // creates the nice swirling animation if (WeenieClassId == 1636 || WeenieClassId == 7268 || WeenieClassId == 20979) { AlignPath = false; Omega = new AceVector3(12.56637f, 0f, 0f); } }
/// <summary> /// 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 /// </summary> 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) } } }
/// <summary> /// Returns the maximum BaseMana from the spells in item's spellbook /// </summary> public static int GetMaxBaseMana(WorldObject wo) { var maxBaseMana = 0; if (wo.Biota.PropertiesSpellBook != null) { foreach (var spellId in wo.Biota.PropertiesSpellBook.Keys) { var spell = new Server.Entity.Spell(spellId); if (spell.BaseMana > maxBaseMana) { maxBaseMana = (int)spell.BaseMana; } } } return(maxBaseMana); }
public static ProjectileSpellType GetProjectileSpellType(uint spellID) { var spell = new Server.Entity.Spell(spellID); if (spell.Wcid == 0) return ProjectileSpellType.Undef; if (spell.NumProjectiles == 1) { if (spell.Category >= SpellCategory.AcidStreak && spell.Category <= SpellCategory.SlashingStreak || spell.Category == SpellCategory.NetherStreak || spell.Category == SpellCategory.Fireworks) return ProjectileSpellType.Streak; else if (spell.NonTracking) return ProjectileSpellType.Arc; else if (spell.Name.Contains("Rolling Death")) return ProjectileSpellType.Wall; else return ProjectileSpellType.Bolt; } if (spell.Category >= SpellCategory.AcidRing && spell.Category <= SpellCategory.SlashingRing || spell.SpreadAngle == 360) return ProjectileSpellType.Ring; if (spell.Category >= SpellCategory.AcidBurst && spell.Category <= SpellCategory.SlashingBurst || spell.Category == SpellCategory.NetherDamageOverTimeRaising3) return ProjectileSpellType.Blast; // 1481 - Flaming Missile Volley if (spell.Category >= SpellCategory.AcidVolley && spell.Category <= SpellCategory.BladeVolley || spell.Name.Contains("Volley")) return ProjectileSpellType.Volley; if (spell.Category >= SpellCategory.AcidWall && spell.Category <= SpellCategory.SlashingWall) return ProjectileSpellType.Wall; if (spell.Category >= SpellCategory.AcidStrike && spell.Category <= SpellCategory.SlashingStrike) return ProjectileSpellType.Strike; return ProjectileSpellType.Undef; }
/// <summary> /// This is raised by Player.HandleActionUseItem.<para /> /// The item should be in the players possession. /// /// The OnUse method for this class is to use a contract to add a tracked quest to our quest panel. /// This gives the player access to information about the quest such as starting and ending NPC locations, /// and shows our progress for kill tasks as well as any timing information such as when we can repeat the /// quest or how much longer we have to complete it in the case of at timed quest. Og II /// </summary> public override void ActOnUse(WorldObject activator) { if (!(activator is Player player)) { return; } if (player.IsBusy || player.Teleporting) { player.Session.Network.EnqueueSend(new GameEventWeenieError(player.Session, WeenieError.YoureTooBusy)); return; } if (!string.IsNullOrWhiteSpace(UseSendsSignal)) { player.CurrentLandblock?.EmitSignal(player, UseSendsSignal); return; } // handle rare gems if (RareUsesTimer) { var currentTime = Time.GetUnixTime(); var timeElapsed = currentTime - player.LastRareUsedTimestamp; if (timeElapsed < RareTimer) { // TODO: get retail message var remainTime = (int)Math.Ceiling(RareTimer - timeElapsed); player.Session.Network.EnqueueSend(new GameMessageSystemChat($"You may use another timed rare in {remainTime}s", ChatMessageType.Broadcast)); return; } player.LastRareUsedTimestamp = currentTime; // local broadcast usage player.EnqueueBroadcast(new GameMessageSystemChat($"{player.Name} used the rare item {Name}", ChatMessageType.Broadcast)); } if (SpellDID.HasValue) { var spell = new Server.Entity.Spell((uint)SpellDID); TryCastSpell(spell, player, this); } if (UseCreateContractId.HasValue && UseCreateContractId > 0) { if (!player.ContractManager.Add(UseCreateContractId.Value)) { return; } else // this wasn't in retail, but the lack of feedback when using a contract gem just seems jarring so... { player.Session.Network.EnqueueSend(new GameMessageSystemChat($"{Name} accepted. Click on the quill icon in the lower right corner to open your contract tab to view your active contracts.", ChatMessageType.Broadcast)); } } if ((GetProperty(PropertyBool.UnlimitedUse) ?? false) == false) { player.TryConsumeFromInventoryWithNetworking(this, 1); } }
/// <summary> /// Method used to perform the animation, sound, and vital update on consumption of food or potions /// </summary> /// <param name="consumableName">Name of the consumable</param> /// <param name="sound">Either Sound.Eat1 or Sound.Drink1</param> /// <param name="buffType">ConsumableBuffType.Spell,ConsumableBuffType.Health,ConsumableBuffType.Stamina,ConsumableBuffType.Mana</param> /// <param name="boostAmount">Amount the Vital is boosted by; can be null, if buffType = ConsumableBuffType.Spell</param> /// <param name="spellDID">Id of the spell cast by the consumable; can be null, if buffType != ConsumableBuffType.Spell</param> public void ApplyComsumable(string consumableName, Sound sound, ConsumableBuffType buffType, uint?boostAmount, uint?spellDID) { uint spellId = spellDID ?? 0; GameMessageSystemChat buffMessage; MotionCommand motionCommand; if (sound == Sound.Eat1) { motionCommand = MotionCommand.Eat; } else { motionCommand = MotionCommand.Drink; } var soundEvent = new GameMessageSound(Guid, sound, 1.0f); var motion = new UniversalMotion(MotionStance.NonCombat, new MotionItem(motionCommand)); EnqueueBroadcastMotion(motion); if (buffType == ConsumableBuffType.Spell) { bool result = false; if (spellId != 0) { result = CreateSingleSpell(spellId); } var spell = new Server.Entity.Spell(spellId); if (!result) { buffMessage = new GameMessageSystemChat($"Consuming {consumableName} attempted to apply a spell not yet fully implemented.", ChatMessageType.System); } else { buffMessage = new GameMessageSystemChat($"{consumableName} applies {spell.Name} on you.", ChatMessageType.Craft); } } else { CreatureVital creatureVital; string vitalName; // Null check for safety if (boostAmount == null) { boostAmount = 0; } switch (buffType) { case ConsumableBuffType.Health: creatureVital = Health; vitalName = "Health"; break; case ConsumableBuffType.Mana: creatureVital = Mana; vitalName = "Mana"; break; default: creatureVital = Stamina; vitalName = "Stamina"; break; } var vitalChange = UpdateVitalDelta(creatureVital, (uint)boostAmount); buffMessage = new GameMessageSystemChat($"You regain {vitalChange} {vitalName}.", ChatMessageType.Craft); } Session.Network.EnqueueSend(soundEvent, buffMessage); // Wait for animation var motionChain = new ActionChain(); var motionTable = DatManager.PortalDat.ReadFromDat <MotionTable>((uint)MotionTableId); var motionAnimationLength = motionTable.GetAnimationLength(MotionCommand.Eat); motionChain.AddDelaySeconds(motionAnimationLength); // Return to standing position after the animation delay motionChain.AddAction(this, () => EnqueueBroadcastMotion(new UniversalMotion(MotionStance.NonCombat))); motionChain.EnqueueChain(); }
/// <summary> /// Activates the object linked to a pressure plate /// </summary> public override void Activate(WorldObject activator) { if (!Active) { return; } var player = activator as Player; if (player == null) { return; } // prevent continuous event stream var currentTime = DateTime.UtcNow; if (currentTime < LastUseTime + TimeSpan.FromSeconds(2)) { return; } // TODO: this should simply be forwarding the activation event // along to the activation target... // ensure activation target if (ActivationTarget == 0) { return; } var target = CurrentLandblock?.GetObject(new ObjectGuid(ActivationTarget)); if (target == null) { Console.WriteLine($"{Name}.OnCollideObject({activator.Name}): couldn't find activation target {ActivationTarget:X8}"); return; } LastUseTime = currentTime; //Console.WriteLine($"{activator.Name} activated {Name}"); // activate pressure plate sound player.EnqueueBroadcast(new GameMessageSound(player.Guid, (Sound)UseSound)); // activate target - // default use action if (target.ActivationResponse.HasFlag(ActivationResponse.Use)) { target.ActOnUse(this); } // perform motion animation - rarely used (only 4 instances in PY16 db) if (target.ActivationResponse.HasFlag(ActivationResponse.Animate)) { var motion = new Motion(target, ActivationAnimation); target.EnqueueBroadcastMotion(motion); } // send chat text - rarely used (only 8 instances in PY16 db) if (target.ActivationResponse.HasFlag(ActivationResponse.Talk)) { // todo: verify the format of this message player.Session.Network.EnqueueSend(new GameMessageSystemChat(ActivationTalk, ChatMessageType.Broadcast)); //target.EnqueueBroadcast(new GameMessageSystemChat(ActivationTalk, ChatMessageType.Broadcast)); } // perform activation emote if (target.ActivationResponse.HasFlag(ActivationResponse.Emote)) { target.EmoteManager.OnActivation(player); } // cast a spell on the player (spell traps) if (target.ActivationResponse.HasFlag(ActivationResponse.CastSpell)) { if (target.SpellDID != null) { var spell = new Server.Entity.Spell((uint)target.SpellDID); target.TryCastSpell(spell, player); } } // call to generator to spawn new object if (target.ActivationResponse.HasFlag(ActivationResponse.Generate)) { if (target.IsGenerator) { target.SelectProfilesMax(); } } }
/// <summary> /// This is raised by Player.HandleActionUseItem.<para /> /// The item should be in the players possession. /// /// The OnUse method for this class is to use a contract to add a tracked quest to our quest panel. /// This gives the player access to information about the quest such as starting and ending NPC locations, /// and shows our progress for kill tasks as well as any timing information such as when we can repeat the /// quest or how much longer we have to complete it in the case of at timed quest. Og II /// </summary> public override void UseItem(Player player) { // TODO: verify use requirements if (UseCreateContractId == null) { //var spell = new Spell((uint)SpellDID); /* * //These if statements are to catch spells with an apostrophe in the dat file which throws off the client in reading it from the dat. * if (spell.MetaSpellId == 3810) * castMessage = "The gem casts Asheron's Benediction on you"; * if (spell.MetaSpellId == 3811) * castMessage = "The gem casts Blackmoor's Favor on you"; * if (spell.MetaSpellId == 3953) * castMessage = "The gem casts Carraida's Benediction on you"; * if (spell.MetaSpellId == 4024) * castMessage = "The gem casts Asheron's Lesser Benediction on you"; */ if (SpellDID.HasValue) { var spell = new Server.Entity.Spell((uint)SpellDID); TryCastSpell(spell, player, this); } if ((GetProperty(PropertyBool.UnlimitedUse) ?? false) == false) { player.TryConsumeFromInventoryWithNetworking(this, 1); } player.SendUseDoneEvent(); return; } ContractTracker contractTracker = new ContractTracker((uint)UseCreateContractId, player.Guid.Full) { Stage = 0, TimeWhenDone = 0, TimeWhenRepeats = 0, DeleteContract = 0, SetAsDisplayContract = 1 }; if (CooldownId != null && player.LastUseTracker.TryGetValue(CooldownId.Value, out var lastUse)) { var timeRemaining = lastUse.AddSeconds(CooldownDuration ?? 0.00).Subtract(DateTime.Now); if (timeRemaining.Seconds > 0) { ChatPacket.SendServerMessage(player.Session, "You cannot use another contract for " + timeRemaining.Seconds + " seconds", ChatMessageType.Broadcast); player.SendUseDoneEvent(); return; } } // We need to see if we are tracking this quest already. Also, I cannot be used on world, so I must have a container id if (!player.TrackedContracts.ContainsKey((uint)UseCreateContractId) && ContainerId != null) { player.TrackedContracts.Add((uint)UseCreateContractId, contractTracker); // This will track our use for each contract using the shared cooldown server side. if (CooldownId != null) { // add or update. if (!player.LastUseTracker.ContainsKey(CooldownId.Value)) { player.LastUseTracker.Add(CooldownId.Value, DateTime.Now); } else { player.LastUseTracker[CooldownId.Value] = DateTime.Now; } } GameEventSendClientContractTracker contractMsg = new GameEventSendClientContractTracker(player.Session, contractTracker); player.Session.Network.EnqueueSend(contractMsg); ChatPacket.SendServerMessage(player.Session, "You just added " + contractTracker.ContractDetails.ContractName, ChatMessageType.Broadcast); // TODO: Add sending the 02C2 message UpdateEnchantment. They added a second use to this existing system // so they could show the delay on the client side - it is not really an enchantment but the they overloaded the use. Og II // Thanks Slushnas for letting me know about this as well as an awesome pcap that shows it all in action. // TODO: there is a lot of work to do here. I am stubbing this in for now to send the right message. Lots of magic numbers at the moment. Debug.Assert(CooldownId != null, "CooldownId != null"); Debug.Assert(CooldownDuration != null, "CooldownDuration != null"); //const ushort layer = 0x10000; // FIXME: we need to track how many layers of the exact same spell we have in effect. const ushort layer = 1; //const uint spellCategory = 0x8000; // FIXME: Not sure where we get this from var spellBase = new SpellBase(0, CooldownDuration.Value, 0, -666); // cooldown not being used in network packet? var gem = new Enchantment(player, player.Guid.Full, spellBase, spellBase.Duration, layer, /*CooldownId.Value,*/ EnchantmentMask.Cooldown); player.Session.Network.EnqueueSend(new GameEventMagicUpdateEnchantment(player.Session, gem)); // Ok this was not known to us, so we used the contract - now remove it from inventory. // HandleActionRemoveItemFromInventory is has it's own action chain. player.TryConsumeFromInventoryWithNetworking(this, 1); } else { ChatPacket.SendServerMessage(player.Session, "You already have this quest tracked: " + contractTracker.ContractDetails.ContractName, ChatMessageType.Broadcast); } // No mater any condition we need to send the use done event to clear the hour glass from the client. player.SendUseDoneEvent(); }
public void ActOnUse(WorldObject activator, bool confirmed) { if (!(activator is Player player)) { return; } if (player.IsBusy || player.Teleporting) { player.Session.Network.EnqueueSend(new GameEventWeenieError(player.Session, WeenieError.YoureTooBusy)); return; } if (!string.IsNullOrWhiteSpace(UseSendsSignal)) { player.CurrentLandblock?.EmitSignal(player, UseSendsSignal); return; } // handle rare gems if (RareId != null && player.GetCharacterOption(CharacterOption.ConfirmUseOfRareGems) && !confirmed) { var msg = $"Are you sure you want to use {Name}?"; var confirm = new Confirmation_Custom(player.Guid, () => ActOnUse(activator, true)); player.ConfirmationManager.EnqueueSend(confirm, msg); return; } if (RareUsesTimer) { var currentTime = Time.GetUnixTime(); var timeElapsed = currentTime - player.LastRareUsedTimestamp; if (timeElapsed < RareTimer) { // TODO: get retail message var remainTime = (int)Math.Ceiling(RareTimer - timeElapsed); player.Session.Network.EnqueueSend(new GameMessageSystemChat($"You may use another timed rare in {remainTime}s", ChatMessageType.Broadcast)); return; } player.LastRareUsedTimestamp = currentTime; // local broadcast usage player.EnqueueBroadcast(new GameMessageSystemChat($"{player.Name} used the rare item {Name}", ChatMessageType.Broadcast)); } if (SpellDID.HasValue) { var spell = new Server.Entity.Spell((uint)SpellDID); TryCastSpell(spell, player, this); } if (UseCreateContractId > 0) { if (!player.ContractManager.Add(UseCreateContractId.Value)) { return; } else // this wasn't in retail, but the lack of feedback when using a contract gem just seems jarring so... { player.Session.Network.EnqueueSend(new GameMessageSystemChat($"{Name} accepted. Click on the quill icon in the lower right corner to open your contract tab to view your active contracts.", ChatMessageType.Broadcast)); } } if (UseCreateItem > 0) { //if (DatabaseManager.World.GetCachedWeenie(UseCreateItem.Value) is null) //{ // player.Session.Network.EnqueueSend(new GameEventCommunicationTransientString(player.Session, "Unable to create object, WCID is not in database !")); // custom error // return; //} var playerFreeInventorySlots = player.GetFreeInventorySlots(); var playerFreeContainerSlots = player.GetFreeContainerSlots(); var playerAvailableBurden = player.GetAvailableBurden(); var playerOutOfInventorySlots = false; var playerOutOfContainerSlots = false; var playerExceedsAvailableBurden = false; var amount = UseCreateQuantity ?? 1; var itemStacks = player.PreCheckItem(UseCreateItem.Value, amount, playerFreeContainerSlots, playerFreeInventorySlots, playerAvailableBurden, out var itemEncumberance, out bool itemRequiresBackpackSlot); if (itemRequiresBackpackSlot) { playerFreeContainerSlots -= itemStacks; playerAvailableBurden -= itemEncumberance; playerOutOfContainerSlots = playerFreeContainerSlots < 0; } else { playerFreeInventorySlots -= itemStacks; playerAvailableBurden -= itemEncumberance; playerOutOfInventorySlots = playerFreeInventorySlots < 0; } playerExceedsAvailableBurden = playerAvailableBurden < 0; if (playerOutOfInventorySlots || playerOutOfContainerSlots || playerExceedsAvailableBurden) { if (playerExceedsAvailableBurden) { player.Session.Network.EnqueueSend(new GameEventCommunicationTransientString(player.Session, "You are too encumbered to use that!")); } else if (playerOutOfInventorySlots) { player.Session.Network.EnqueueSend(new GameEventCommunicationTransientString(player.Session, "You do not have enough pack space to use that!")); } else //if (playerOutOfContainerSlots) { player.Session.Network.EnqueueSend(new GameEventCommunicationTransientString(player.Session, "You do not have enough container slots to use that!")); } return; } if (itemStacks > 0) { while (amount > 0) { var item = WorldObjectFactory.CreateNewWorldObject(UseCreateItem.Value); if (item is Stackable) { // amount contains a max stack if (item.MaxStackSize <= amount) { item.SetStackSize(item.MaxStackSize); amount -= item.MaxStackSize.Value; } else // not a full stack { item.SetStackSize(amount); amount -= amount; } } else { amount -= 1; } player.TryCreateInInventoryWithNetworking(item); } } else { player.Session.Network.EnqueueSend(new GameEventCommunicationTransientString(player.Session, $"Unable to use {Name} at this time!")); return; } } if ((GetProperty(PropertyBool.UnlimitedUse) ?? false) == false) { player.TryConsumeFromInventoryWithNetworking(this, 1); } }
/// <summary> /// Method used to perform the animation, sound, and vital update on consumption of food or potions /// </summary> /// <param name="consumableName">Name of the consumable</param> /// <param name="sound">Either Sound.Eat1 or Sound.Drink1</param> /// <param name="buffType">ConsumableBuffType.Spell,ConsumableBuffType.Health,ConsumableBuffType.Stamina,ConsumableBuffType.Mana</param> /// <param name="boostAmount">Amount the Vital is boosted by; can be null, if buffType = ConsumableBuffType.Spell</param> /// <param name="spellDID">Id of the spell cast by the consumable; can be null, if buffType != ConsumableBuffType.Spell</param> public void ApplyConsumable(string consumableName, Sound sound, ConsumableBuffType buffType, uint?boostAmount, uint?spellDID) { MotionCommand motionCommand; if (sound == Sound.Eat1) { motionCommand = MotionCommand.Eat; } else { motionCommand = MotionCommand.Drink; } // start the eat/drink motion var motion = new Motion(MotionStance.NonCombat, motionCommand); EnqueueBroadcastMotion(motion); var motionTable = DatManager.PortalDat.ReadFromDat <MotionTable>(MotionTableId); var animTime = motionTable.GetAnimationLength(CurrentMotionState.Stance, motionCommand, MotionCommand.Ready); var actionChain = new ActionChain(); actionChain.AddDelaySeconds(animTime); actionChain.AddAction(this, () => { GameMessageSystemChat buffMessage; if (buffType == ConsumableBuffType.Spell) { bool result = false; uint spellId = spellDID ?? 0; if (spellId != 0) { result = CreateSingleSpell(spellId); } if (result) { var spell = new Server.Entity.Spell(spellId); buffMessage = new GameMessageSystemChat($"{consumableName} casts {spell.Name} on you.", ChatMessageType.Magic); } else { buffMessage = new GameMessageSystemChat($"Consuming {consumableName} attempted to apply a spell not yet fully implemented.", ChatMessageType.System); } } else { CreatureVital creatureVital; string vitalName; // Null check for safety if (boostAmount == null) { boostAmount = 0; } switch (buffType) { case ConsumableBuffType.Health: creatureVital = Health; vitalName = "Health"; break; case ConsumableBuffType.Mana: creatureVital = Mana; vitalName = "Mana"; break; default: creatureVital = Stamina; vitalName = "Stamina"; break; } var vitalChange = UpdateVitalDelta(creatureVital, (uint)boostAmount); if (vitalName == "Health") { DamageHistory.OnHeal((uint)vitalChange); if (Fellowship != null) { Fellowship.OnVitalUpdate(this); } } buffMessage = new GameMessageSystemChat($"You regain {vitalChange} {vitalName}.", ChatMessageType.Craft); } var soundEvent = new GameMessageSound(Guid, sound, 1.0f); EnqueueBroadcast(soundEvent); Session.Network.EnqueueSend(buffMessage); // return to original stance var returnStance = new Motion(CurrentMotionState.Stance); EnqueueBroadcastMotion(returnStance); }); actionChain.EnqueueChain(); }