/// <summary> /// Public method for adding a new skill by spending skill credits. /// </summary> /// <remarks> /// The client will throw up more then one train skill dialog and the user has the chance to spend twice. /// </remarks> public void HandleActionTrainSkill(Skill skill, int creditsSpent) { if (AvailableSkillCredits >= creditsSpent) { // attempt to train the specified skill bool trainNewSkill = TrainSkill(skill, creditsSpent); // create an update to send to the client var currentCredits = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.AvailableSkillCredits, AvailableSkillCredits ?? 0); // as long as the skill is sent, the train new triangle button on the client will not lock up. // Sending Skill.None with status untrained worked in test var trainSkillUpdate = new GameMessagePrivateUpdateSkill(this, Skill.None, SkillAdvancementClass.Untrained, 0, 0, 0); // create a string placeholder for the correct after string trainSkillMessageText; // if the skill has already been trained or we do not have enough credits, then trainNewSkill be set false if (trainNewSkill) { // replace the trainSkillUpdate message with the correct skill assignment: var creatureSkill = GetCreatureSkill(skill); trainSkillUpdate = new GameMessagePrivateUpdateSkill(this, creatureSkill); trainSkillMessageText = $"{skill.ToSentence()} trained. You now have {AvailableSkillCredits} credits available."; } else { trainSkillMessageText = $"Failed to train {skill.ToSentence()}! You now have {AvailableSkillCredits} credits available."; } // create the final game message and send to the client var message = new GameMessageSystemChat(trainSkillMessageText, ChatMessageType.Advancement); Session.Network.EnqueueSend(trainSkillUpdate, currentCredits, message); } }
/// <summary> /// Broadcasts the player death animation, updates vitae, and sends network messages for player death /// Queues the action to call TeleportOnDeath and enter portal space soon /// </summary> protected override void Die(WorldObject lastDamager, WorldObject topDamager) { UpdateVital(Health, 0); NumDeaths++; // killer = top damager for looting rights if (topDamager != null) { Killer = topDamager.Guid.Full; } // broadcast death animation var deathAnim = new Motion(MotionStance.NonCombat, MotionCommand.Dead); EnqueueBroadcastMotion(deathAnim); // killer death message = last damager var killerMsg = lastDamager != null ? " to " + lastDamager.Name : ""; var currentDeathMessage = $"died{killerMsg}."; // create network messages for player death var msgHealthUpdate = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Health, 0); // TODO: death sounds? seems to play automatically in client // var msgDeathSound = new GameMessageSound(Guid, Sound.Death1, 1.0f); var msgNumDeaths = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.NumDeaths, NumDeaths ?? 0); // send network messages for player death Session.Network.EnqueueSend(msgHealthUpdate, msgNumDeaths); // update vitae // players who died in a PKLite fight do not accrue vitae var pkLiteKiller = GetKiller_PKLite(); if (pkLiteKiller == null) { InflictVitaePenalty(); } var msgPurgeEnchantments = new GameEventMagicPurgeEnchantments(Session); EnchantmentManager.RemoveAllEnchantments(); Session.Network.EnqueueSend(msgPurgeEnchantments); // wait for the death animation to finish var dieChain = new ActionChain(); var animLength = DatManager.PortalDat.ReadFromDat <MotionTable>(MotionTableId).GetAnimationLength(MotionCommand.Dead); dieChain.AddDelaySeconds(animLength + 1.0f); // enter portal space dieChain.AddAction(this, CreateCorpse); dieChain.AddAction(this, TeleportOnDeath); dieChain.AddAction(this, SetLifestoneProtection); dieChain.AddAction(this, SetMinimumTimeSincePK); dieChain.EnqueueChain(); }
public void HandleActionTeleToHouse() { if (PKTimerActive) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveBeenInPKBattleTooRecently)); return; } if (RecallsDisabled) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.ExitTrainingAcademyToUseCommand)); return; } if (House == null) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouMustOwnHouseToUseCommand)); return; } if (CombatMode != CombatMode.NonCombat) { // this should be handled by a different thing, probably a function that forces player into peacemode var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.NonCombat); SetCombatMode(CombatMode.NonCombat); Session.Network.EnqueueSend(updateCombatMode); } EnqueueBroadcast(new GameMessageSystemChat($"{Name} is recalling home.", ChatMessageType.Recall), 96.0f); EnqueueBroadcastMotion(motionHouseRecall); var startPos = new Position(Location); // Wait for animation var actionChain = new ActionChain(); // Then do teleport var animLength = DatManager.PortalDat.ReadFromDat <MotionTable>(MotionTableId).GetAnimationLength(MotionCommand.HouseRecall); actionChain.AddDelaySeconds(animLength); actionChain.AddAction(this, () => { var endPos = new Position(Location); if (startPos.SquaredDistanceTo(endPos) > RecallMoveThresholdSq) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveMovedTooFar)); return; } if (House != null) { Teleport(House.SlumLord.Location); } }); actionChain.EnqueueChain(); }
/// <summary> /// Handles teleporting a player to the lifestone (/ls or /lifestone command) /// </summary> public void HandleActionTeleToLifestone() { if (PKTimerActive) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveBeenInPKBattleTooRecently)); return; } if (RecallsDisabled) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.ExitTrainingAcademyToUseCommand)); return; } if (Sanctuary == null) { Session.Network.EnqueueSend(new GameMessageSystemChat("Your spirit has not been attuned to a sanctuary location.", ChatMessageType.Broadcast)); return; } // FIXME(ddevec): I should probably make a better interface for this UpdateVital(Mana, Mana.Current / 2); if (CombatMode != CombatMode.NonCombat) { // this should be handled by a different thing, probably a function that forces player into peacemode var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.NonCombat); SetCombatMode(CombatMode.NonCombat); Session.Network.EnqueueSend(updateCombatMode); } EnqueueBroadcast(new GameMessageSystemChat($"{Name} is recalling to the lifestone.", ChatMessageType.Recall), 96.0f); EnqueueBroadcastMotion(motionLifestoneRecall); var startPos = new Position(Location); // Wait for animation ActionChain lifestoneChain = new ActionChain(); // Then do teleport lifestoneChain.AddDelaySeconds(DatManager.PortalDat.ReadFromDat <MotionTable>(MotionTableId).GetAnimationLength(MotionCommand.LifestoneRecall)); lifestoneChain.AddAction(this, () => { var endPos = new Position(Location); if (startPos.SquaredDistanceTo(endPos) > RecallMoveThresholdSq) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveMovedTooFar)); return; } Teleport(Sanctuary); }); lifestoneChain.EnqueueChain(); }
/// <summary> /// Broadcasts the player death animation, updates vitae, and sends network messages for player death /// Queues the action to call TeleportOnDeath and enter portal space soon /// </summary> protected override void Die(WorldObject lastDamager, WorldObject topDamager) { UpdateVital(Health, 0); NumDeaths++; DeathLevel = Level; // for calculating vitae XP VitaeCpPool = 0; // reset vitae XP earned // killer = top damager for looting rights if (topDamager != null) { Killer = topDamager.Guid.Full; } // broadcast death animation var deathAnim = new Motion(MotionStance.NonCombat, MotionCommand.Dead); EnqueueBroadcastMotion(deathAnim); // killer death message = last damager var killerMsg = lastDamager != null ? " to " + lastDamager.Name : ""; var currentDeathMessage = $"died{killerMsg}."; // create network messages for player death var msgHealthUpdate = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Health, 0); // TODO: death sounds? seems to play automatically in client // var msgDeathSound = new GameMessageSound(Guid, Sound.Death1, 1.0f); var msgNumDeaths = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.NumDeaths, NumDeaths ?? 0); var msgDeathLevel = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.DeathLevel, DeathLevel ?? 0); var msgVitaeCpPool = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.VitaeCpPool, VitaeCpPool.Value); var msgPurgeEnchantments = new GameEventMagicPurgeEnchantments(Session); // update vitae var vitae = EnchantmentManager.UpdateVitae(); var spellID = (uint)SpellId.Vitae; var spell = new Spell(spellID); var vitaeEnchantment = new Enchantment(this, Guid, spellID, spell.Duration, 0, (EnchantmentMask)spell.StatModType, vitae); var msgVitaeEnchantment = new GameEventMagicUpdateEnchantment(Session, vitaeEnchantment); // send network messages for player death Session.Network.EnqueueSend(msgHealthUpdate, msgNumDeaths, msgDeathLevel, msgVitaeCpPool, msgPurgeEnchantments, msgVitaeEnchantment); // wait for the death animation to finish var dieChain = new ActionChain(); var animLength = DatManager.PortalDat.ReadFromDat <MotionTable>(MotionTableId).GetAnimationLength(MotionCommand.Dead); dieChain.AddDelaySeconds(animLength + 1.0f); // enter portal space dieChain.AddAction(this, CreateCorpse); dieChain.AddAction(this, TeleportOnDeath); dieChain.EnqueueChain(); }
public void HandleActionRecallAllegianceHometown() { //Console.WriteLine($"{Name}.HandleActionRecallAllegianceHometown()"); // check if player is in an allegiance if (Allegiance == null) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouAreNotInAllegiance)); return; } if (Allegiance.Sanctuary == null) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YourAllegianceDoesNotHaveHometown)); return; } if (CombatMode != CombatMode.NonCombat) { // this should be handled by a different thing, probably a function that forces player into peacemode var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.NonCombat); SetCombatMode(CombatMode.NonCombat); Session.Network.EnqueueSend(updateCombatMode); } EnqueueBroadcast(new GameMessageSystemChat($"{Name} is going to the Allegiance hometown.", ChatMessageType.Recall), 96.0f); EnqueueBroadcastMotion(motionAllegianceHometownRecall); var startPos = new Position(Location); // Wait for animation var actionChain = new ActionChain(); // Then do teleport var animLength = DatManager.PortalDat.ReadFromDat <MotionTable>(MotionTableId).GetAnimationLength(MotionCommand.AllegianceHometownRecall); actionChain.AddDelaySeconds(animLength); actionChain.AddAction(this, () => { var endPos = new Position(Location); if (startPos.SquaredDistanceTo(endPos) > RecallMoveThresholdSq) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveMovedTooFar)); return; } Teleport(Allegiance.Sanctuary); }); actionChain.EnqueueChain(); }
public void HandleActionTeleToMarketPlace() { if (PKTimerActive) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveBeenInPKBattleTooRecently)); return; } if (RecallsDisabled) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.ExitTrainingAcademyToUseCommand)); return; } var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.NonCombat); EnqueueBroadcast(new GameMessageSystemChat($"{Name} is recalling to the marketplace.", ChatMessageType.Recall), 96.0f); Session.Network.EnqueueSend(updateCombatMode); // this should be handled by a different thing, probably a function that forces player into peacemode EnqueueBroadcastMotion(motionMarketplaceRecall); var startPos = new Position(Location); // TODO: (OptimShi): Actual animation length is longer than in retail. 18.4s // float mpAnimationLength = MotionTable.GetAnimationLength((uint)MotionTableId, MotionCommand.MarketplaceRecall); // mpChain.AddDelaySeconds(mpAnimationLength); ActionChain mpChain = new ActionChain(); mpChain.AddDelaySeconds(14); // Then do teleport mpChain.AddAction(this, () => { var endPos = new Position(Location); if (startPos.SquaredDistanceTo(endPos) > RecallMoveThresholdSq) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveMovedTooFar)); return; } Teleport(MarketplaceDrop); }); // Set the chain to run mpChain.EnqueueChain(); }
/// <summary> /// Inflicts vitae /// </summary> public void InflictVitaePenalty(int amount = 5) { DeathLevel = Level; // for calculating vitae XP VitaeCpPool = 0; // reset vitae XP earned var msgDeathLevel = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.DeathLevel, DeathLevel ?? 0); var msgVitaeCpPool = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.VitaeCpPool, VitaeCpPool.Value); Session.Network.EnqueueSend(msgDeathLevel, msgVitaeCpPool); var vitae = EnchantmentManager.UpdateVitae(); var spellID = (uint)SpellId.Vitae; var spell = new Spell(spellID); var vitaeEnchantment = new Enchantment(this, Guid.Full, spellID, 0, (EnchantmentMask)spell.StatModType, vitae); Session.Network.EnqueueSend(new GameEventMagicUpdateEnchantment(Session, vitaeEnchantment)); }
public static void Handle(ClientMessage clientMessage, Session session) { string message = $"{session.Player.Name} is recalling to the marketplace."; var sysChatMessage = new GameMessageSystemChat(message, ChatMessageType.Recall); var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(session, PropertyInt.CombatMode, 1); var motionMarketplaceRecall = new UniversalMotion(MotionStance.Standing, new MotionItem(MotionCommand.MarketplaceRecall)); var animationEvent = new GameMessageUpdateMotion(session.Player, session, motionMarketplaceRecall); // TODO: This needs to be changed to broadcast sysChatMessage to only those in local chat hearing range // FIX: Recall text isn't being broadcast yet, need to address session.Network.EnqueueSend(updateCombatMode, sysChatMessage); session.Player.EnqueueMovementEvent(motionMarketplaceRecall, session.Player.Guid); session.Player.SetDelayedTeleport(TimeSpan.FromSeconds(14), marketplaceDrop); }
/// <summary> /// Handles the GameAction 0x47 - TrainSkill network message from client /// </summary> public bool HandleActionTrainSkill(Skill skill, int creditsSpent) { if (creditsSpent > AvailableSkillCredits) { log.Error($"{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.Error($"{Name}.HandleActionTrainSkill({skill}, {creditsSpent}) - couldn't find skill base"); return(false); } if (creditsSpent != skillBase.TrainedCost) { log.Error($"{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); }
private static readonly Position marketplaceDrop = new Position(23855548, 49.16f, -31.62f, 0.10f, 0f, 0f, -0.71f, 0.71f); // Is this the right drop? public override void Handle() { string message = $"{Session.Player.Name} is recalling to the marketplace."; var sysChatMessage = new GameMessageSystemChat(message, ChatMessageType.Recall); // TODO: This is missing the floaty animation wind up and appropriate pause before teleportation begins. // This is the pcap verified message sent to change just the current mana // Not needed in this command, leaving for example // var updatePlayersMana = new GameMessagePrivateUpdateAttribute2ndLevel(Session, Vital.Mana, Session.Player.Mana.Current / 2); var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(Session, PropertyInt.CombatMode, 1); // TODO: This needs to be changed to broadcast sysChatMessage to only those in local chat hearing range Session.Network.EnqueueSend(updateCombatMode, sysChatMessage); // TODO: Wait until MovementEvent completes then send the following message Session.Player.Teleport(marketplaceDrop); }
// FIXME(ddevec): I broke vendors -- I need to think about how to reimplement this cleanly public override void OnUse(ObjectGuid playerId) { ActionChain chain = new ActionChain(); CurrentLandblock.ChainOnObject(chain, playerId, (WorldObject wo) => { Player player = wo as Player; if (player == null) { return; } string serverMessage = null; // validate within use range, taking into account the radius of the model itself, as well SetupModel csetup = SetupModel.ReadFromDat(PhysicsData.CSetup); float radiusSquared = (GameData.UseRadius + csetup.Radius) * (GameData.UseRadius + csetup.Radius); // We're static, so this is safe -- our Location is never written if (player.Location.SquaredDistanceTo(Location) >= radiusSquared) { serverMessage = "Your way to far away to trust, come closer and buy something or leave me alone!"; } else { // create the outbound server message serverMessage = "You Break it, you bought it!"; } // give player starting money var money = new GameMessagePrivateUpdatePropertyInt(player.Session, PropertyInt.CoinValue, 5000); player.Session.Network.EnqueueSend(money); var welcomemsg = new GameMessageSystemChat(serverMessage, ChatMessageType.Tell); // always send useDone event var sendUseDoneEvent = new GameEventUseDone(player.Session); player.Session.Network.EnqueueSend(welcomemsg, sendUseDoneEvent); player.Session.Network.EnqueueSend(new GameEventApproachVendor(player.Session, Guid)); }); chain.EnqueueChain(); }
public void HandleActionTeleToMarketPlace() { var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.NonCombat); EnqueueBroadcast(new GameMessageSystemChat($"{Name} is recalling to the marketplace.", ChatMessageType.Recall)); Session.Network.EnqueueSend(updateCombatMode); // this should be handled by a different thing, probably a function that forces player into peacemode EnqueueBroadcastMotion(motionMarketplaceRecall); // TODO: (OptimShi): Actual animation length is longer than in retail. 18.4s // float mpAnimationLength = MotionTable.GetAnimationLength((uint)MotionTableId, MotionCommand.MarketplaceRecall); // mpChain.AddDelaySeconds(mpAnimationLength); ActionChain mpChain = new ActionChain(); mpChain.AddDelaySeconds(14); // Then do teleport mpChain.AddAction(this, () => Teleport(MarketplaceDrop)); // Set the chain to run mpChain.EnqueueChain(); }
/// <summary> /// Player Death/Kill, use this to kill a session's player /// </summary> /// <remarks> /// TODO: /// 1. Find the best vitae formula and add vitae /// 2. Generate the correct death message, or have it passed in as a parameter. /// 3. Find the correct player death noise based on the player model and play on death. /// 4. Determine if we need to Send Queued Action for Lifestone Materialize, after Action Location. /// 5. Find the health after death formula and Set the correct health /// </remarks> private void OnKill(Session killerSession) { ObjectGuid killerId = killerSession.Player.Guid; IsAlive = false; Health.Current = 0; // Set the health to zero NumDeaths++; // Increase the NumDeaths counter DeathLevel = Level; // For calculating vitae XP VitaeCpPool = 0; // Set vitae XP // TODO: Generate a death message based on the damage type to pass in to each death message: string currentDeathMessage = $"died to {killerSession.Player.Name}."; // Send Vicitim Notification, or "Your Death" event to the client: // create and send the client death event, GameEventYourDeath var msgYourDeath = new GameEventYourDeath(Session, $"You have {currentDeathMessage}"); var msgNumDeaths = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.NumDeaths, NumDeaths ?? 0); var msgDeathLevel = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.DeathLevel, DeathLevel ?? 0); var msgVitaeCpPool = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.VitaeCpPool, VitaeCpPool.Value); var msgPurgeEnchantments = new GameEventPurgeAllEnchantments(Session); // var msgDeathSound = new GameMessageSound(Guid, Sound.Death1, 1.0f); // handle vitae var vitae = EnchantmentManager.UpdateVitae(); var spellID = (uint)Network.Enum.Spell.Vitae; var spellBase = DatManager.PortalDat.SpellTable.Spells[spellID]; var spell = DatabaseManager.World.GetCachedSpell(spellID); var vitaeEnchantment = new Enchantment(this, spellID, (double)spell.Duration, 0, spell.StatModType, vitae); var msgVitaeEnchantment = new GameEventMagicUpdateEnchantment(Session, vitaeEnchantment); var msgHealthUpdate = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Health, 0); // Send first death message group Session.Network.EnqueueSend(msgHealthUpdate, msgYourDeath, msgNumDeaths, msgDeathLevel, msgVitaeCpPool, msgPurgeEnchantments, msgVitaeEnchantment); // Broadcast the 019E: Player Killed GameMessage ActionBroadcastKill($"{Name} has {currentDeathMessage}", Guid, killerId); }
/// <summary> /// Resets the skill, refunds all experience and skill credits, if allowed. /// </summary> public bool ResetSkill(Skill skill) { 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 // cannot be untrained or unspecialized bool specAug = false; switch (creatureSkill.Skill) { case Skill.ArmorTinkering: specAug = AugmentationSpecializeArmorTinkering > 0; break; case Skill.ItemTinkering: specAug = AugmentationSpecializeItemTinkering > 0; break; case Skill.MagicItemTinkering: specAug = AugmentationSpecializeMagicItemTinkering > 0; break; case Skill.WeaponTinkering: specAug = AugmentationSpecializeWeaponTinkering > 0; break; case Skill.Salvaging: specAug = AugmentationSpecializeSalvaging > 0; break; } //if (specAug) // return false; // send message? var typeOfSkill = creatureSkill.AdvancementClass.ToString().ToLower() + " "; var untrainable = IsSkillUntrainable(skill); var creditRefund = creatureSkill.AdvancementClass == SkillAdvancementClass.Specialized || untrainable; if (creatureSkill.AdvancementClass == SkillAdvancementClass.Specialized && !specAug) { creatureSkill.AdvancementClass = SkillAdvancementClass.Trained; creatureSkill.InitLevel = 0; AvailableSkillCredits += skillBase.UpgradeCostFromTrainedToSpecialized; } // temple untraining 'always trained' skills: // cannot be untrained, but skill XP can be recovered if (untrainable && !specAug) { creatureSkill.AdvancementClass = SkillAdvancementClass.Untrained; creatureSkill.InitLevel = 0; AvailableSkillCredits += skillBase.TrainedCost; } 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 && !specAug ? $"{typeOfSkill}" : "")}{skill.ToSentence()} skill has been {(untrainable && !specAug ? "removed" : "reset")}. "; msg += $"All the experience {(creditRefund && !specAug ? "and skill credits " : "")}that you spent on this skill have been refunded to you."; Session.Network.EnqueueSend(updateSkill, availableSkillCredits, new GameMessageSystemChat(msg, ChatMessageType.Broadcast)); return(true); }
/// <summary> /// Broadcasts the player death animation, updates vitae, and sends network messages for player death /// Queues the action to call TeleportOnDeath and enter portal space soon /// </summary> protected override void Die(WorldObject lastDamager, WorldObject topDamager) { UpdateVital(Health, 0); NumDeaths++; suicideInProgress = false; // killer = top damager for looting rights if (topDamager != null) { KillerId = topDamager.Guid.Full; } // broadcast death animation var deathAnim = new Motion(MotionStance.NonCombat, MotionCommand.Dead); EnqueueBroadcastMotion(deathAnim); // create network messages for player death var msgHealthUpdate = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Health, 0); // TODO: death sounds? seems to play automatically in client // var msgDeathSound = new GameMessageSound(Guid, Sound.Death1, 1.0f); var msgNumDeaths = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.NumDeaths, NumDeaths); // send network messages for player death Session.Network.EnqueueSend(msgHealthUpdate, msgNumDeaths); if (lastDamager.Guid == Guid) // suicide { var msgSelfInflictedDeath = new GameEventWeenieError(Session, WeenieError.YouKilledYourself); Session.Network.EnqueueSend(msgSelfInflictedDeath); } // update vitae // players who died in a PKLite fight do not accrue vitae if (!IsPKLiteDeath(topDamager)) { InflictVitaePenalty(); } if (IsPKDeath(topDamager) || AugmentationSpellsRemainPastDeath == 0) { var msgPurgeEnchantments = new GameEventMagicPurgeEnchantments(Session); EnchantmentManager.RemoveAllEnchantments(); Session.Network.EnqueueSend(msgPurgeEnchantments); } // wait for the death animation to finish var dieChain = new ActionChain(); var animLength = DatManager.PortalDat.ReadFromDat <MotionTable>(MotionTableId).GetAnimationLength(MotionCommand.Dead); dieChain.AddDelaySeconds(animLength + 1.0f); dieChain.AddAction(this, () => { CreateCorpse(topDamager); TeleportOnDeath(); // enter portal space SetLifestoneProtection(); if (IsPKDeath(topDamager) || IsPKLiteDeath(topDamager)) { SetMinimumTimeSincePK(); } }); dieChain.EnqueueChain(); }
/// <summary> /// Determines if the player has advanced a level /// </summary> /// <remarks> /// Known issues: /// 1. XP updates from outside of the grantxp command have not been done yet. /// </remarks> private void CheckForLevelup() { // Question: Where do *we* call CheckForLevelup()? : // From within the player.cs file, the options might be: // GrantXp() // From outside of the player.cs file, we may call CheckForLevelup() durring? : // XP Updates? var xpTable = DatManager.PortalDat.XpTable; var startingLevel = Level; var maxLevel = xpTable.CharacterLevelXPList.Count - 1; bool creditEarned = false; if (Level == maxLevel) { return; } // increases until the correct level is found while (xpTable.CharacterLevelXPList[Level ?? 1 + 1] <= (ulong)TotalExperience) { Level++; // increase the skill credits if the chart allows this level to grant a credit if (xpTable.CharacterLevelSkillCreditList[Level ?? 1] > 0) { AvailableSkillCredits += (int)xpTable.CharacterLevelSkillCreditList[Level ?? 1]; TotalSkillCredits += (int)xpTable.CharacterLevelSkillCreditList[Level ?? 1]; creditEarned = true; } // break if we reach max if (Level == maxLevel) { PlayParticleEffect(ACE.Entity.Enum.PlayScript.WeddingBliss, Guid); break; } } if (Level > startingLevel) { string levelUpMessageText = (Level == maxLevel) ? $"You have reached the maximum level of {Level}!" : $"You are now level {Level}!"; var levelUpMessage = new GameMessageSystemChat(levelUpMessageText, ChatMessageType.Advancement); string xpUpdateText = (AvailableSkillCredits > 0) ? $"You have {AvailableExperience:#,###0} experience points and {AvailableSkillCredits} skill credits available to raise skills and attributes." : $"You have {AvailableExperience:#,###0} experience points available to raise skills and attributes."; var xpUpdateMessage = new GameMessageSystemChat(xpUpdateText, ChatMessageType.Advancement); var levelUp = new GameMessagePrivateUpdatePropertyInt(Session.Player.Sequences, PropertyInt.Level, Level ?? 1); var currentCredits = new GameMessagePrivateUpdatePropertyInt(Session.Player.Sequences, PropertyInt.AvailableSkillCredits, AvailableSkillCredits ?? 0); if (Level != maxLevel && !creditEarned) { var nextLevelWithCredits = 0; for (int i = (Level ?? 1) + 1; i <= maxLevel; i++) { if (xpTable.CharacterLevelSkillCreditList[i] > 0) { nextLevelWithCredits = i; break; } } string nextCreditAtText = $"You will earn another skill credit at {nextLevelWithCredits}"; var nextCreditMessage = new GameMessageSystemChat(nextCreditAtText, ChatMessageType.Advancement); Session.Network.EnqueueSend(levelUp, levelUpMessage, xpUpdateMessage, currentCredits, nextCreditMessage); } else { Session.Network.EnqueueSend(levelUp, levelUpMessage, xpUpdateMessage, currentCredits); } // play level up effect PlayParticleEffect(ACE.Entity.Enum.PlayScript.LevelUp, Guid); } }
/// <summary> /// Determines if the player has advanced a level /// </summary> private void CheckForLevelup() { var xpTable = DatManager.PortalDat.XpTable; var maxLevel = GetMaxLevel(); if (Level >= maxLevel) { return; } var startingLevel = Level; bool creditEarned = false; // increases until the correct level is found while ((ulong)(TotalExperience ?? 0) >= xpTable.CharacterLevelXPList[(Level ?? 0) + 1]) { Level++; // increase the skill credits if the chart allows this level to grant a credit if (xpTable.CharacterLevelSkillCreditList[Level ?? 0] > 0) { AvailableSkillCredits += (int)xpTable.CharacterLevelSkillCreditList[Level ?? 0]; TotalSkillCredits += (int)xpTable.CharacterLevelSkillCreditList[Level ?? 0]; creditEarned = true; } // break if we reach max if (Level == maxLevel) { PlayParticleEffect(PlayScript.WeddingBliss, Guid); break; } } if (Level > startingLevel) { var message = (Level == maxLevel) ? $"You have reached the maximum level of {Level}!" : $"You are now level {Level}!"; message += (AvailableSkillCredits > 0) ? $"\nYou have {AvailableExperience:#,###0} experience points and {AvailableSkillCredits} skill credits available to raise skills and attributes." : $"\nYou have {AvailableExperience:#,###0} experience points available to raise skills and attributes."; var levelUp = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.Level, Level ?? 1); var currentCredits = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.AvailableSkillCredits, AvailableSkillCredits ?? 0); if (Level != maxLevel && !creditEarned) { var nextLevelWithCredits = 0; for (int i = (Level ?? 0) + 1; i <= maxLevel; i++) { if (xpTable.CharacterLevelSkillCreditList[i] > 0) { nextLevelWithCredits = i; break; } } message += $"\nYou will earn another skill credit at level {nextLevelWithCredits}."; } if (Fellowship != null) { Fellowship.OnFellowLevelUp(this); } if (AllegianceNode != null) { AllegianceNode.OnLevelUp(); } Session.Network.EnqueueSend(levelUp); SetMaxVitals(); // play level up effect PlayParticleEffect(PlayScript.LevelUp, Guid); Session.Network.EnqueueSend(new GameMessageSystemChat(message, ChatMessageType.Advancement), currentCredits); } }
/// <summary> /// Broadcasts the player death animation, updates vitae, and sends network messages for player death /// Queues the action to call TeleportOnDeath and enter portal space soon /// </summary> protected override void Die(DamageHistoryInfo lastDamager, DamageHistoryInfo topDamager) { if (topDamager?.Guid == Guid && IsPKType) { var topDamagerOther = DamageHistory.GetTopDamager(false); if (topDamagerOther != null && topDamagerOther.IsPlayer) { topDamager = topDamagerOther; } } UpdateVital(Health, 0); NumDeaths++; suicideInProgress = false; if (CombatMode == CombatMode.Magic && MagicState.IsCasting) { FailCast(false); } // TODO: instead of setting IsBusy here, // eventually all of the places that check for states such as IsBusy || Teleporting // might want to use a common function, and IsDead should return a separate error IsBusy = true; // killer = top damager for looting rights if (topDamager != null) { KillerId = topDamager.Guid.Full; } // broadcast death animation var deathAnim = new Motion(MotionStance.NonCombat, MotionCommand.Dead); EnqueueBroadcastMotion(deathAnim); // create network messages for player death var msgHealthUpdate = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Health, 0); // TODO: death sounds? seems to play automatically in client // var msgDeathSound = new GameMessageSound(Guid, Sound.Death1, 1.0f); var msgNumDeaths = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.NumDeaths, NumDeaths); // send network messages for player death Session.Network.EnqueueSend(msgHealthUpdate, msgNumDeaths); if (lastDamager?.Guid == Guid) // suicide { var msgSelfInflictedDeath = new GameEventWeenieError(Session, WeenieError.YouKilledYourself); Session.Network.EnqueueSend(msgSelfInflictedDeath); } // update vitae // players who died in a PKLite fight do not accrue vitae if (!IsPKLiteDeath(topDamager)) { InflictVitaePenalty(); } if (IsPKDeath(topDamager) || AugmentationSpellsRemainPastDeath == 0) { var msgPurgeEnchantments = new GameEventMagicPurgeEnchantments(Session); EnchantmentManager.RemoveAllEnchantments(); Session.Network.EnqueueSend(msgPurgeEnchantments); } // wait for the death animation to finish var dieChain = new ActionChain(); var animLength = DatManager.PortalDat.ReadFromDat <MotionTable>(MotionTableId).GetAnimationLength(MotionCommand.Dead); dieChain.AddDelaySeconds(animLength + 1.0f); dieChain.AddAction(this, () => { CreateCorpse(topDamager); ThreadSafeTeleportOnDeath(); // enter portal space if (IsPKDeath(topDamager) || IsPKLiteDeath(topDamager)) { SetMinimumTimeSincePK(); } IsBusy = false; }); dieChain.EnqueueChain(); }
/// <summary> /// Determines if the player has advanced a level /// </summary> private void CheckForLevelup() { var xpTable = DatManager.PortalDat.XpTable; var maxLevel = GetMaxLevel(); if (Level >= maxLevel) { return; } var startingLevel = Level; bool creditEarned = false; // if level is not under 275(false) do normal stuff, otherwise if over 274(true) do custom function. // increases until the correct level is found while ((ulong)(TotalExperience ?? 0) >= ((Level < 275) ? xpTable.CharacterLevelXPList[(Level ?? 0) + 1] : (ulong)Catch275(false, startingLevel))) { Level++; // increase the skill credits if the chart allows this level to grant a credit // gotta limit this to levels 275 and under otherwise causes crash. if (Level <= 275) { if (xpTable.CharacterLevelSkillCreditList[Level ?? 0] > 0) { AvailableSkillCredits += (int)xpTable.CharacterLevelSkillCreditList[Level ?? 0]; TotalSkillCredits += (int)xpTable.CharacterLevelSkillCreditList[Level ?? 0]; creditEarned = true; } } // if a player is level 300 or higher and level is also divisible by 30 evenly give a skill credit. if (Level > 275) { if (Level >= 300 && Level % 30 == 0) { AvailableSkillCredits += 1; TotalSkillCredits += 1; creditEarned = true; } } // break if we reach max if (Level == maxLevel) { PlayParticleEffect(PlayScript.WeddingBliss, Guid); break; } } if (Level > startingLevel) { var message = (Level == maxLevel) ? $"You have reached the maximum level of {Level}!" : $"You are now level {Level}!"; message += (AvailableSkillCredits > 0) ? $"\nYou have {AvailableExperience:#,###0} experience points and {AvailableSkillCredits} skill credits available to raise skills and attributes." : $"\nYou have {AvailableExperience:#,###0} experience points available to raise skills and attributes."; var levelUp = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.Level, Level ?? 1); var currentCredits = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.AvailableSkillCredits, AvailableSkillCredits ?? 0); // 275 and under code. if (Level != maxLevel && !creditEarned && Level <= 275) { var nextLevelWithCredits = 0; for (int i = (Level ?? 0) + 1; i <= maxLevel; i++) { if (xpTable.CharacterLevelSkillCreditList[i] > 0) { nextLevelWithCredits = i; break; } } message += $"\nYou will earn another skill credit at level {nextLevelWithCredits}."; } if (Fellowship != null) { Fellowship.OnFellowLevelUp(this); } if (AllegianceNode != null) { AllegianceNode.OnLevelUp(); } Session.Network.EnqueueSend(levelUp); SetMaxVitals(); // play level up effect PlayParticleEffect(PlayScript.LevelUp, Guid); Session.Network.EnqueueSend(new GameMessageSystemChat(message, ChatMessageType.Advancement), currentCredits); } }
public static void HandleRecallDuels(Session session, params string[] parameters) { if (RealmManager.DuelRealm == null) { return; } var player = session.Player; if (!player.ValidatePlayerRealmPosition(DuelRealmHelpers.GetDuelingAreaDrop())) { return; } if (player.PKTimerActive) { session.Network.EnqueueSend(new GameEventWeenieError(session, WeenieError.YouHaveBeenInPKBattleTooRecently)); return; } if (player.RecallsDisabled) { session.Network.EnqueueSend(new GameEventWeenieError(session, WeenieError.ExitTrainingAcademyToUseCommand)); return; } if (player.TooBusyToRecall) { session.Network.EnqueueSend(new GameEventWeenieError(session, WeenieError.YoureTooBusy)); return; } if (player.CombatMode != CombatMode.NonCombat) { // this should be handled by a different thing, probably a function that forces player into peacemode var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(player, PropertyInt.CombatMode, (int)CombatMode.NonCombat); player.SetCombatMode(CombatMode.NonCombat); session.Network.EnqueueSend(updateCombatMode); } player.EnqueueBroadcast(new GameMessageSystemChat($"{player.Name} is recalling to the duel staging area.", ChatMessageType.Recall), Player.LocalBroadcastRange, ChatMessageType.Recall); player.SendMotionAsCommands(MotionCommand.MarketplaceRecall, MotionStance.NonCombat); var startPos = new Position(player.Location); // TODO: (OptimShi): Actual animation length is longer than in retail. 18.4s // float mpAnimationLength = MotionTable.GetAnimationLength((uint)MotionTableId, MotionCommand.MarketplaceRecall); // mpChain.AddDelaySeconds(mpAnimationLength); ActionChain mpChain = new ActionChain(); mpChain.AddDelaySeconds(5); // Then do teleport player.IsBusy = true; mpChain.AddAction(player, () => { player.IsBusy = false; var endPos = new Position(player.Location); if (startPos.SquaredDistanceTo(endPos) > Player.RecallMoveThresholdSq) { session.Network.EnqueueSend(new GameEventWeenieError(session, WeenieError.YouHaveMovedTooFar)); return; } player.Teleport(DuelRealmHelpers.GetDuelingAreaDrop()); }); // Set the chain to run mpChain.EnqueueChain(); }
/// <summary> /// Determines if the player has advanced a level /// </summary> /// <remarks> /// Known issues: /// 1. XP updates from outside of the grantxp command have not been done yet. /// </remarks> private void CheckForLevelup() { // Question: Where do *we* call CheckForLevelup()? : // From within the player.cs file, the options might be: // GrantXp() // From outside of the player.cs file, we may call CheckForLevelup() durring? : // XP Updates? var startingLevel = character.Level; var chart = DatabaseManager.Charts.GetLevelingXpChart(); CharacterLevel maxLevel = chart.Levels.Last(); bool creditEarned = false; if (character.Level == maxLevel.Level) { return; } // increases until the correct level is found while (chart.Levels[Convert.ToInt32(character.Level)].TotalXp <= character.TotalExperience) { character.Level++; CharacterLevel newLevel = chart.Levels.FirstOrDefault(item => item.Level == character.Level); // increase the skill credits if the chart allows this level to grant a credit if (newLevel.GrantsSkillPoint) { character.AvailableSkillCredits++; character.TotalSkillCredits++; creditEarned = true; } // break if we reach max if (character.Level == maxLevel.Level) { PlayParticleEffect(Effect.WeddingBliss); break; } } if (character.Level > startingLevel) { string level = $"{character.Level}"; string skillCredits = $"{character.AvailableSkillCredits}"; string xpAvailable = $"{character.AvailableExperience:#,###0}"; var levelUp = new GameMessagePrivateUpdatePropertyInt(Session, PropertyInt.Level, character.Level); string levelUpMessageText = (character.Level == maxLevel.Level) ? $"You have reached the maximum level of {level}!" : $"You are now level {level}!"; var levelUpMessage = new GameMessageSystemChat(levelUpMessageText, ChatMessageType.Advancement); string xpUpdateText = (character.AvailableSkillCredits > 0) ? $"You have {xpAvailable} experience points and {skillCredits} skill credits available to raise skills and attributes." : $"You have {xpAvailable} experience points available to raise skills and attributes."; var xpUpdateMessage = new GameMessageSystemChat(xpUpdateText, ChatMessageType.Advancement); var currentCredits = new GameMessagePrivateUpdatePropertyInt(Session, PropertyInt.AvailableSkillCredits, character.AvailableSkillCredits); if (character.Level != maxLevel.Level && !creditEarned) { string nextCreditAtText = $"You will earn another skill credit at {chart.Levels.Where(item => item.Level > character.Level).OrderBy(item => item.Level).First(item => item.GrantsSkillPoint).Level}"; var nextCreditMessage = new GameMessageSystemChat(nextCreditAtText, ChatMessageType.Advancement); Session.Network.EnqueueSend(levelUp, levelUpMessage, xpUpdateMessage, currentCredits, nextCreditMessage); } else { Session.Network.EnqueueSend(levelUp, levelUpMessage, xpUpdateMessage, currentCredits); } // play level up effect PlayParticleEffect(Effect.LevelUp); } }
/// <summary> /// Resets the skill, refunds all experience and skill credits, if allowed. /// </summary> 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); }
private void HandleGameAction(QueuedGameAction action, Player player) { switch (action.ActionType) { case GameActionType.TalkDirect: { // TODO: remove this hack (using TalkDirect) ASAP var g = new ObjectGuid(action.ObjectId); WorldObject obj = (WorldObject)player; if (worldObjects.ContainsKey(g)) { obj = worldObjects[g]; } DeathMessageArgs d = new DeathMessageArgs(action.ActionBroadcastMessage, new ObjectGuid(action.ObjectId), new ObjectGuid(action.SecondaryObjectId)); HandleDeathMessage(obj, d); break; } case GameActionType.TeleToHouse: case GameActionType.TeleToLifestone: case GameActionType.TeleToMansion: case GameActionType.TeleToMarketPlace: case GameActionType.TeleToPkArena: case GameActionType.TeleToPklArena: { player.Teleport(action.ActionLocation); break; } case GameActionType.ApplyVisualEffect: { var g = new ObjectGuid(action.ObjectId); WorldObject obj = (WorldObject)player; if (worldObjects.ContainsKey(g)) { obj = worldObjects[g]; } var particleEffect = (PlayScript)action.SecondaryObjectId; HandleParticleEffectEvent(obj, particleEffect); break; } case GameActionType.ApplySoundEffect: { var g = new ObjectGuid(action.ObjectId); WorldObject obj = (WorldObject)player; if (worldObjects.ContainsKey(g)) { obj = worldObjects[g]; } var soundEffect = (Sound)action.SecondaryObjectId; HandleSoundEvent(obj, soundEffect); break; } case GameActionType.IdentifyObject: { // TODO: Throttle this request. The live servers did this, likely for a very good reason, so we should, too. var g = new ObjectGuid(action.ObjectId); WorldObject obj = (WorldObject)player; if (worldObjects.ContainsKey(g)) { obj = worldObjects[g]; } var identifyResponse = new GameEventIdentifyObjectResponse(player.Session, action.ObjectId, obj); player.Session.Network.EnqueueSend(identifyResponse); break; } case GameActionType.Buy: { // todo: lots, need vendor list, money checks, etc. var money = new GameMessagePrivateUpdatePropertyInt(player.Session, PropertyInt.CoinValue, 4000); var sound = new GameMessageSound(player.Guid, Sound.PickUpItem, 1); var sendUseDoneEvent = new GameEventUseDone(player.Session); player.Session.Network.EnqueueSend(money, sound, sendUseDoneEvent); // send updated vendor inventory. player.Session.Network.EnqueueSend(new GameEventApproachVendor(player.Session, action.ObjectId)); // this is just some testing code for now. foreach (ItemProfile item in action.ProfileItems) { // todo: something with vendor id and profile list... iid list from vendor dbs. // todo: something with amounts.. if (item.Iid == 5) { while (item.Amount > 0) { item.Amount--; WorldObject loot = LootGenerationFactory.CreateTestWorldObject(5090); LootGenerationFactory.AddToContainer(loot, player); player.TrackObject(loot); } var rudecomment = "Who do you think you are, Johny Apple Seed ?"; var buyrudemsg = new GameMessageSystemChat(rudecomment, ChatMessageType.Tell); player.Session.Network.EnqueueSend(buyrudemsg); } else if (item.Iid == 10) { while (item.Amount > 0) { item.Amount--; WorldObject loot = LootGenerationFactory.CreateTestWorldObject(30537); LootGenerationFactory.AddToContainer(loot, player); player.TrackObject(loot); } var rudecomment = "That smells awful, Enjoy eating it!"; var buyrudemsg = new GameMessageSystemChat(rudecomment, ChatMessageType.Tell); player.Session.Network.EnqueueSend(buyrudemsg); } } break; } case GameActionType.PutItemInContainer: { var playerGuid = new ObjectGuid(action.ObjectId); var inventoryGuid = new ObjectGuid(action.SecondaryObjectId); // Has to be a player so need to check before I make the cast. // If he is not a player, something is bad wrong. Og II if (playerGuid.IsPlayer()) { var aPlayer = (Player)GetWorldObject(playerGuid); var inventoryItem = GetWorldObject(inventoryGuid); float arrivedRadiusSquared = 0.00f; bool validGuids; if (WithinUseRadius(playerGuid, inventoryGuid, out arrivedRadiusSquared, out validGuids)) { aPlayer.NotifyAndAddToInventory(inventoryItem); } else { if (validGuids) { aPlayer.SetDestinationInformation(inventoryItem.PhysicsData.Position, arrivedRadiusSquared); aPlayer.BlockedGameAction = action; aPlayer.OnAutonomousMove(inventoryItem.PhysicsData.Position, aPlayer.Sequences, MovementTypes.MoveToObject, inventoryGuid); } } } break; } case GameActionType.DropItem: { var g = new ObjectGuid(action.ObjectId); if (worldObjects.ContainsKey(g)) { var playerId = new ObjectGuid(action.ObjectId); var inventoryId = new ObjectGuid(action.SecondaryObjectId); if (playerId.IsPlayer()) { var aPlayer = (Player)worldObjects[playerId]; aPlayer.NotifyAndDropItem(inventoryId); } } break; } case GameActionType.MovementEvent: { var g = new ObjectGuid(action.ObjectId); WorldObject obj = (WorldObject)player; if (worldObjects.ContainsKey(g)) { obj = worldObjects[g]; } var motion = action.Motion; HandleMovementEvent(obj, motion); break; } case GameActionType.ObjectCreate: { AddWorldObject(action.WorldObject); break; } case GameActionType.ObjectDelete: { RemoveWorldObject(action.WorldObject.Guid, false); break; } case GameActionType.QueryHealth: { if (action.ObjectId == 0) { // Deselect the formerly selected Target player.SelectedTarget = 0; break; } object target = null; var targetId = new ObjectGuid(action.ObjectId); // Remember the selected Target player.SelectedTarget = action.ObjectId; // TODO: once items are implemented check if there are items that can trigger // the QueryHealth event. So far I believe it only gets triggered for players and creatures if (targetId.IsPlayer() || targetId.IsCreature()) { if (worldObjects.ContainsKey(targetId)) { target = worldObjects[targetId]; } if (target == null) { // check adjacent landblocks for the targetId foreach (var block in adjacencies) { if (block.Value != null) { if (block.Value.worldObjects.ContainsKey(targetId)) { target = block.Value.worldObjects[targetId]; } } } } if (target != null) { float healthPercentage = 0; if (targetId.IsPlayer()) { Player tmpTarget = (Player)target; healthPercentage = (float)tmpTarget.Health.Current / (float)tmpTarget.Health.MaxValue; } if (targetId.IsCreature()) { Creature tmpTarget = (Creature)target; healthPercentage = (float)tmpTarget.Health.Current / (float)tmpTarget.Health.MaxValue; } var updateHealth = new GameEventUpdateHealth(player.Session, targetId.Full, healthPercentage); player.Session.Network.EnqueueSend(updateHealth); } } break; } case GameActionType.Use: { var g = new ObjectGuid(action.ObjectId); if (worldObjects.ContainsKey(g)) { WorldObject obj = worldObjects[g]; if ((obj.DescriptionFlags & ObjectDescriptionFlag.LifeStone) != 0) { (obj as Lifestone).OnUse(player); } else if ((obj.DescriptionFlags & ObjectDescriptionFlag.Portal) != 0) { // TODO: When Physics collisions are implemented, this logic should be switched there, as normal portals are not onUse. (obj as Portal).OnCollide(player); } else if ((obj.DescriptionFlags & ObjectDescriptionFlag.Door) != 0) { (obj as Door).OnUse(player); } else if ((obj.DescriptionFlags & ObjectDescriptionFlag.Vendor) != 0) { (obj as Vendor).OnUse(player); } // switch (obj.Type) // { // case Enum.ObjectType.Portal: // { // // TODO: When Physics collisions are implemented, this logic should be switched there, as normal portals are not onUse. // // (obj as Portal).OnCollide(player); // // break; // } // case Enum.ObjectType.LifeStone: // { // (obj as Lifestone).OnUse(player); // break; // } // } } break; } } }
/// <summary> /// Broadcasts the player death animation, updates vitae, and sends network messages for player death /// Queues the action to call TeleportOnDeath and enter portal space soon /// </summary> protected override void Die(WorldObject lastDamager, WorldObject topDamager) { UpdateVital(Health, 0); NumDeaths++; DeathLevel = Level; // for calculating vitae XP VitaeCpPool = 0; // reset vitae XP earned // killer = top damager for looting rights if (topDamager != null) { Killer = topDamager.Guid.Full; } // broadcast death animation var deathAnim = new UniversalMotion(MotionStance.NonCombat, new MotionItem(MotionCommand.Dead)); EnqueueBroadcastMotion(deathAnim); // killer death message = last damager var killerMsg = lastDamager != null ? " to " + lastDamager.Name : ""; var currentDeathMessage = $"died{killerMsg}."; // create network messages for player death var msgHealthUpdate = new GameMessagePrivateUpdateAttribute2ndLevel(this, Vital.Health, 0); // TODO: death sounds? seems to play automatically in client // var msgDeathSound = new GameMessageSound(Guid, Sound.Death1, 1.0f); var msgYourDeath = new GameEventYourDeath(Session, $"You have {currentDeathMessage}"); var msgNumDeaths = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.NumDeaths, NumDeaths ?? 0); var msgDeathLevel = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.DeathLevel, DeathLevel ?? 0); var msgVitaeCpPool = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.VitaeCpPool, VitaeCpPool.Value); var msgPurgeEnchantments = new GameEventPurgeAllEnchantments(Session); // update vitae var vitae = EnchantmentManager.UpdateVitae(); var spellID = (uint)Network.Enum.Spell.Vitae; var spellBase = DatManager.PortalDat.SpellTable.Spells[spellID]; var spell = DatabaseManager.World.GetCachedSpell(spellID); var vitaeEnchantment = new Enchantment(this, Guid, spellID, (double)spell.Duration, 0, spell.StatModType, vitae); var msgVitaeEnchantment = new GameEventMagicUpdateEnchantment(Session, vitaeEnchantment); // send network messages for player death Session.Network.EnqueueSend(msgHealthUpdate, msgYourDeath, msgNumDeaths, msgDeathLevel, msgVitaeCpPool, msgPurgeEnchantments, msgVitaeEnchantment); // wait for the death animation to finish var dieChain = new ActionChain(); var animLength = DatManager.PortalDat.ReadFromDat <MotionTable>(MotionTableId).GetAnimationLength(MotionCommand.Dead); dieChain.AddDelaySeconds(animLength + 1.0f); // enter portal space dieChain.AddAction(this, CreateCorpse); dieChain.AddAction(this, TeleportOnDeath); dieChain.EnqueueChain(); // if the player's lifestone is in a different landblock, also broadcast their demise to that landblock if (Sanctuary != null && Location.Landblock != Sanctuary.Landblock) { var killerGuid = lastDamager != null ? lastDamager.Guid : Guid; ActionBroadcastKill($"{Name} has {currentDeathMessage}", Guid, killerGuid); } DamageHistory.Reset(); }
/// <summary> /// Recalls you to your allegiance's Mansion or Villa /// </summary> public void HandleActionTeleToMansion() { //Console.WriteLine($"{Name}.HandleActionTeleToMansion()"); if (RecallsDisabled) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.ExitTrainingAcademyToUseCommand)); return; } // check if player is in an allegiance if (Allegiance == null) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouAreNotInAllegiance)); return; } var allegianceHouse = Allegiance.GetHouse(); if (allegianceHouse == null) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YourMonarchDoesNotOwnAMansionOrVilla)); return; } if (allegianceHouse.HouseType < ACE.Entity.Enum.HouseType.Villa) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YourMonarchsHouseIsNotAMansionOrVilla)); return; } // ensure allegiance housing has allegiance permissions enabled if (allegianceHouse.MonarchId == null) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YourMonarchHasClosedTheMansion)); return; } if (CombatMode != CombatMode.NonCombat) { // this should be handled by a different thing, probably a function that forces player into peacemode var updateCombatMode = new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.NonCombat); SetCombatMode(CombatMode.NonCombat); Session.Network.EnqueueSend(updateCombatMode); } EnqueueBroadcast(new GameMessageSystemChat($"{Name} is recalling to the Allegiance housing.", ChatMessageType.Recall), 96.0f); EnqueueBroadcastMotion(motionHouseRecall); var startPos = new Position(Location); // Wait for animation var actionChain = new ActionChain(); // Then do teleport var animLength = DatManager.PortalDat.ReadFromDat <MotionTable>(MotionTableId).GetAnimationLength(MotionCommand.HouseRecall); actionChain.AddDelaySeconds(animLength); actionChain.AddAction(this, () => { var endPos = new Position(Location); if (startPos.SquaredDistanceTo(endPos) > RecallMoveThresholdSq) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.YouHaveMovedTooFar)); return; } Teleport(allegianceHouse.SlumLord.Location); }); actionChain.EnqueueChain(); }