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;
}
///
/// A new biota be created taking all of its values from weenie.
///
public Healer(Weenie weenie, ObjectGuid guid) : base(weenie, guid)
{
SetEphemeralValues();
}
///
/// Restore a WorldObject from the database.
///
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);
}
///
/// Determines if healer successfully heals target for attempt
///
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);
}
///
/// Returns the healing amount for this attempt
///
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);
}
}
}