ace/Source/ACE.Server/WorldObjects/Healer.cs
FlaggAC 7f1b58ab24
Change static readonly fields to const fields. (#4211)
This will at least somewhat improve performance, as confirmed through microbenchmark testing.

const fields are replaced with literal values in the Assembly's IL code, while static readonly fields may be optimized by the JIT, and is not guaranteed to happen. const fields may also lend themselves to stronger optimizations at runtime.
2024-08-24 14:32:35 +00:00

299 lines
12 KiB
C#

using System;
using ACE.Common;
using ACE.Entity;
using ACE.Entity.Enum;
using ACE.Entity.Enum.Properties;
using ACE.Entity.Models;
using ACE.Server.Entity;
using ACE.Server.Entity.Actions;
using ACE.Server.Network.GameEvent.Events;
using ACE.Server.Network.GameMessages.Messages;
using ACE.Server.Physics;
using ACE.Server.Physics.Animation;
using ACE.Server.WorldObjects.Entity;
namespace ACE.Server.WorldObjects
{
public class Healer : WorldObject
{
// TODO: change structure / maxstructure to int,
// cast to ushort at network level
public ushort? UsesLeft
{
get => Structure;
set => Structure = value;
}
/// <summary>
/// A new biota be created taking all of its values from weenie.
/// </summary>
public Healer(Weenie weenie, ObjectGuid guid) : base(weenie, guid)
{
SetEphemeralValues();
}
/// <summary>
/// Restore a WorldObject from the database.
/// </summary>
public Healer(Biota biota) : base(biota)
{
SetEphemeralValues();
}
private void SetEphemeralValues()
{
ObjectDescriptionFlags |= ObjectDescriptionFlag.Healer;
}
public override void HandleActionUseOnTarget(Player healer, WorldObject target)
{
if (healer.GetCreatureSkill(Skill.Healing).AdvancementClass < SkillAdvancementClass.Trained)
{
healer.SendUseDoneEvent(WeenieError.YouArentTrainedInHealing);
return;
}
if (healer.IsBusy || healer.Teleporting || healer.suicideInProgress)
{
healer.SendUseDoneEvent(WeenieError.YoureTooBusy);
return;
}
if (!(target is Player targetPlayer) || targetPlayer.Teleporting)
{
healer.SendUseDoneEvent(WeenieError.YouCantHealThat);
return;
}
if (healer.IsJumping)
{
healer.SendUseDoneEvent(WeenieError.YouCantDoThatWhileInTheAir);
return;
}
// ensure same PKType, although PK and PKLite players can heal NPKs:
// https://asheron.fandom.com/wiki/Player_Killer
// https://asheron.fandom.com/wiki/Player_Killer_Lite
if (targetPlayer.PlayerKillerStatus != healer.PlayerKillerStatus && targetPlayer.PlayerKillerStatus != PlayerKillerStatus.NPK)
{
healer.SendWeenieErrorWithString(WeenieErrorWithString.YouFailToAffect_NotSamePKType, targetPlayer.Name);
healer.SendUseDoneEvent();
return;
}
// ensure target player vital < MaxValue
var vital = targetPlayer.GetCreatureVital(BoosterEnum);
if (vital.Current == vital.MaxValue)
{
switch (vital.Vital)
{
case PropertyAttribute2nd.MaxHealth:
healer.Session.Network.EnqueueSend(new GameEventWeenieErrorWithString(healer.Session, WeenieErrorWithString._IsAtFullHealth, target.Name));
break;
case PropertyAttribute2nd.MaxStamina:
healer.Session.Network.EnqueueSend(new GameEventCommunicationTransientString(healer.Session, $"{target.Name} is already at full stamina!"));
break;
case PropertyAttribute2nd.MaxMana:
healer.Session.Network.EnqueueSend(new GameEventCommunicationTransientString(healer.Session, $"{target.Name} is already at full mana!"));
break;
}
healer.SendUseDoneEvent();
return;
}
/*if (!healer.Equals(targetPlayer))
{
// perform moveto
healer.CreateMoveToChain(target, (success) => DoHealMotion(healer, targetPlayer, success));
}
else
DoHealMotion(healer, targetPlayer, true);*/
// MoveTo is now handled in base Player_Use
DoHealMotion(healer, targetPlayer, true);
}
public const float Healing_MaxMove = 5.0f;
public void DoHealMotion(Player healer, Player target, bool success)
{
if (!success || target.IsDead || target.Teleporting || target.suicideInProgress)
{
healer.SendUseDoneEvent();
return;
}
healer.IsBusy = true;
var motionCommand = healer.Equals(target) ? MotionCommand.SkillHealSelf : MotionCommand.SkillHealOther;
var motion = new Motion(healer, motionCommand);
var currentStance = healer.CurrentMotionState.Stance;
var animLength = MotionTable.GetAnimationLength(healer.MotionTableId, currentStance, motionCommand);
var startPos = new Physics.Common.Position(healer.PhysicsObj.Position);
var vital = target.GetCreatureVital(BoosterEnum);
var missingVital = vital.Missing;
var actionChain = new ActionChain();
//actionChain.AddAction(healer, () => healer.EnqueueBroadcastMotion(motion));
actionChain.AddAction(healer, () => healer.SendMotionAsCommands(motionCommand, currentStance));
actionChain.AddDelaySeconds(animLength);
actionChain.AddAction(healer, () =>
{
// check healing move distance cap
var endPos = new Physics.Common.Position(healer.PhysicsObj.Position);
var dist = startPos.Distance(endPos);
//Console.WriteLine($"Dist: {dist}");
// only PKs affected by these caps?
if (dist < Healing_MaxMove || healer.PlayerKillerStatus == PlayerKillerStatus.NPK)
DoHealing(healer, target, missingVital);
else
healer.Session.Network.EnqueueSend(new GameMessageSystemChat("Your movement disrupted healing!", ChatMessageType.Broadcast));
healer.IsBusy = false;
healer.SendUseDoneEvent();
});
healer.EnqueueMotion(actionChain, MotionCommand.Ready);
actionChain.EnqueueChain();
healer.NextUseTime = DateTime.UtcNow.AddSeconds(animLength);
}
public void DoHealing(Player healer, Player target, uint missingVital)
{
if (target.IsDead || target.Teleporting) return;
var remainingMsg = "";
if (!UnlimitedUse)
{
UsesLeft--;
var s = UsesLeft == 1 ? "" : "s";
remainingMsg = UsesLeft > 0 ? $" Your {Name} has {UsesLeft} use{s} left." : $" Your {Name} is used up.";
Value -= StructureUnitValue;
if (Value < 0) // fix negative value
Value = 0;
}
var stackSize = new GameMessagePublicUpdatePropertyInt(this, PropertyInt.Structure, UsesLeft.Value);
var targetName = healer == target ? "yourself" : target.Name;
var vital = target.GetCreatureVital(BoosterEnum);
// skill check
var difficulty = 0;
var skillCheck = DoSkillCheck(healer, target, missingVital, ref difficulty);
if (!skillCheck)
{
var failMsg = new GameMessageSystemChat($"You fail to heal {targetName}.{remainingMsg}", ChatMessageType.Broadcast);
healer.Session.Network.EnqueueSend(failMsg, stackSize);
if (healer != target)
target.Session.Network.EnqueueSend(new GameMessageSystemChat($"{healer.Name} fails to heal you.", ChatMessageType.Broadcast));
if (UsesLeft <= 0 && !UnlimitedUse)
healer.TryConsumeFromInventoryWithNetworking(this, 1);
return;
}
// heal up
var healAmount = GetHealAmount(healer, target, missingVital, out var critical, out var staminaCost);
healer.UpdateVitalDelta(healer.Stamina, (int)-staminaCost);
// Amount displayed to player can exceed actual amount healed due to heal boost ratings, but we only want to record the actual amount healed
var actualHealAmount = (uint)target.UpdateVitalDelta(vital, healAmount);
if (vital.Vital == PropertyAttribute2nd.MaxHealth)
target.DamageHistory.OnHeal(actualHealAmount);
//if (target.Fellowship != null)
//target.Fellowship.OnVitalUpdate(target);
var healingSkill = healer.GetCreatureSkill(Skill.Healing);
Proficiency.OnSuccessUse(healer, healingSkill, difficulty);
var crit = critical ? "expertly " : "";
var message = new GameMessageSystemChat($"You {crit}heal {targetName} for {healAmount} {BoosterEnum.ToString()} points.{remainingMsg}", ChatMessageType.Broadcast);
healer.Session.Network.EnqueueSend(message, stackSize);
if (healer != target)
target.Session.Network.EnqueueSend(new GameMessageSystemChat($"{healer.Name} heals you for {healAmount} {BoosterEnum.ToString()} points.", ChatMessageType.Broadcast));
if (UsesLeft <= 0 && !UnlimitedUse)
healer.TryConsumeFromInventoryWithNetworking(this, 1);
}
/// <summary>
/// Determines if healer successfully heals target for attempt
/// </summary>
public bool DoSkillCheck(Player healer, Player target, uint missingVital, ref int difficulty)
{
// skill check:
// (healing skill + healing kit boost) * trainedMod
// vs. damage * 2 * combatMod
var healingSkill = healer.GetCreatureSkill(Skill.Healing);
var trainedMod = healingSkill.AdvancementClass == SkillAdvancementClass.Specialized ? 1.5f : 1.1f;
var combatMod = healer.CombatMode == CombatMode.NonCombat ? 1.0f : 1.1f;
var effectiveSkill = (int)Math.Round((healingSkill.Current + BoostValue) * trainedMod);
difficulty = (int)Math.Round(missingVital * 2 * combatMod);
var skillCheck = SkillCheck.GetSkillChance(effectiveSkill, difficulty);
return skillCheck > ThreadSafeRandom.Next(0.0f, 1.0f);
}
/// <summary>
/// Returns the healing amount for this attempt
/// </summary>
public uint GetHealAmount(Player healer, Player target, uint missingVital, out bool criticalHeal, out uint staminaCost)
{
// factors: healing skill, healing kit bonus, stamina, critical chance
var healingSkill = healer.GetCreatureSkill(Skill.Healing).Current;
var healBase = healingSkill * (float)HealkitMod.Value;
// todo: determine applicable range from pcaps
var healMin = healBase * 0.2f; // ??
var healMax = healBase * 0.5f;
var healAmount = ThreadSafeRandom.Next(healMin, healMax);
// chance for critical healing
criticalHeal = ThreadSafeRandom.Next(0.0f, 1.0f) < 0.1f;
if (criticalHeal) healAmount *= 2;
// cap to missing vital
if (healAmount > missingVital)
healAmount = missingVital;
// stamina check? On the Q&A board a dev posted that stamina directly effects the amount of damage you can heal
// low stam = less vital healed. I don't have exact numbers for it. Working through forum archive.
// stamina cost: 1 stamina per 5 vital healed
staminaCost = (uint)Math.Round(healAmount / 5.0f);
if (staminaCost > healer.Stamina.Current)
{
staminaCost = healer.Stamina.Current;
healAmount = staminaCost * 5;
}
// verify healing boost comes from target instead of healer?
// sounds like target in LumAugHealingRating...
var ratingMod = target.GetHealingRatingMod();
healAmount *= ratingMod;
return (uint)Math.Round(healAmount);
}
}
}