mirror of
https://bitbucket.org/Ioncannon/project-meteor-server.git
synced 2025-04-26 06:37:45 +00:00

Added formulas for base EXP gain and chain experience Added basic scripts for most player abilities and effects Added stat gains for some abilities Changed status flags Fixed bug with player death Fixed bug where auto attacks didnt work when not locked on Added traits
1124 lines
No EOL
40 KiB
C#
1124 lines
No EOL
40 KiB
C#
|
|
using FFXIVClassic.Common;
|
|
using FFXIVClassic_Map_Server.actors.area;
|
|
using FFXIVClassic_Map_Server.actors.group;
|
|
using FFXIVClassic_Map_Server.Actors.Chara;
|
|
using FFXIVClassic_Map_Server.packets.send.actor;
|
|
using FFXIVClassic_Map_Server.utils;
|
|
using FFXIVClassic_Map_Server.actors.chara.ai;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using FFXIVClassic_Map_Server.actors.chara;
|
|
using FFXIVClassic_Map_Server.packets.send.actor.battle;
|
|
using FFXIVClassic_Map_Server.packets.send;
|
|
using FFXIVClassic_Map_Server.actors.chara.ai.state;
|
|
using FFXIVClassic_Map_Server.actors.chara.ai.utils;
|
|
using FFXIVClassic_Map_Server.actors.chara.npc;
|
|
|
|
namespace FFXIVClassic_Map_Server.Actors
|
|
{
|
|
/// <summary> Which Character types am I friendly with </summary>
|
|
enum CharacterTargetingAllegiance
|
|
{
|
|
/// <summary> Friendly to BattleNpcs </summary>
|
|
BattleNpcs,
|
|
/// <summary> Friendly to Players </summary>
|
|
Player
|
|
}
|
|
|
|
enum DamageTakenType
|
|
{
|
|
None,
|
|
Attack,
|
|
Magic,
|
|
Weaponskill,
|
|
Ability
|
|
}
|
|
|
|
class Character : Actor
|
|
{
|
|
public const int CLASSID_PUG = 2;
|
|
public const int CLASSID_GLA = 3;
|
|
public const int CLASSID_MRD = 4;
|
|
public const int CLASSID_ARC = 7;
|
|
public const int CLASSID_LNC = 8;
|
|
public const int CLASSID_THM = 22;
|
|
public const int CLASSID_CNJ = 23;
|
|
|
|
public const int CLASSID_CRP = 29;
|
|
public const int CLASSID_BSM = 30;
|
|
public const int CLASSID_ARM = 31;
|
|
public const int CLASSID_GSM = 32;
|
|
public const int CLASSID_LTW = 33;
|
|
public const int CLASSID_WVR = 34;
|
|
public const int CLASSID_ALC = 35;
|
|
public const int CLASSID_CUL = 36;
|
|
|
|
public const int CLASSID_MIN = 39;
|
|
public const int CLASSID_BTN = 40;
|
|
public const int CLASSID_FSH = 41;
|
|
|
|
public const int SIZE = 0;
|
|
public const int COLORINFO = 1;
|
|
public const int FACEINFO = 2;
|
|
public const int HIGHLIGHT_HAIR = 3;
|
|
public const int VOICE = 4;
|
|
public const int MAINHAND = 5;
|
|
public const int OFFHAND = 6;
|
|
public const int SPMAINHAND = 7;
|
|
public const int SPOFFHAND = 8;
|
|
public const int THROWING = 9;
|
|
public const int PACK = 10;
|
|
public const int POUCH = 11;
|
|
public const int HEADGEAR = 12;
|
|
public const int BODYGEAR = 13;
|
|
public const int LEGSGEAR = 14;
|
|
public const int HANDSGEAR = 15;
|
|
public const int FEETGEAR = 16;
|
|
public const int WAISTGEAR = 17;
|
|
public const int NECKGEAR = 18;
|
|
public const int L_EAR = 19;
|
|
public const int R_EAR = 20;
|
|
public const int R_WRIST = 21;
|
|
public const int L_WRIST = 22;
|
|
public const int R_RINGFINGER = 23;
|
|
public const int L_RINGFINGER = 24;
|
|
public const int R_INDEXFINGER = 25;
|
|
public const int L_INDEXFINGER = 26;
|
|
public const int UNKNOWN = 27;
|
|
|
|
public bool isStatic = false;
|
|
|
|
public bool isMovingToSpawn = false;
|
|
public bool isAutoAttackEnabled = true;
|
|
|
|
public uint modelId;
|
|
public uint[] appearanceIds = new uint[28];
|
|
|
|
public uint animationId = 0;
|
|
|
|
public uint currentTarget = Actor.INVALID_ACTORID;
|
|
public uint currentLockedTarget = Actor.INVALID_ACTORID;
|
|
|
|
public uint currentActorIcon = 0;
|
|
|
|
public Work work = new Work();
|
|
public CharaWork charaWork = new CharaWork();
|
|
|
|
public Group currentParty = null;
|
|
public ContentGroup currentContentGroup = null;
|
|
|
|
//public DateTime lastAiUpdate;
|
|
|
|
public AIContainer aiContainer;
|
|
public StatusEffectContainer statusEffects;
|
|
|
|
public CharacterTargetingAllegiance allegiance;
|
|
|
|
public Pet pet;
|
|
|
|
private Dictionary<Modifier, double> modifiers = new Dictionary<Modifier, double>();
|
|
|
|
protected ushort hpBase, hpMaxBase, mpBase, mpMaxBase, tpBase;
|
|
protected BattleTemp baseStats = new BattleTemp();
|
|
public ushort currentJob;
|
|
public ushort newMainState;
|
|
public float spawnX, spawnY, spawnZ;
|
|
|
|
//I needed some values I could reuse for random stuff, delete later
|
|
public int extraInt;
|
|
public uint extraUint;
|
|
public float extraFloat;
|
|
|
|
protected Dictionary<string, UInt64> tempVars = new Dictionary<string, UInt64>();
|
|
|
|
public Character(uint actorID) : base(actorID)
|
|
{
|
|
//Init timer array to "notimer"
|
|
for (int i = 0; i < charaWork.statusShownTime.Length; i++)
|
|
charaWork.statusShownTime[i] = 0;
|
|
|
|
this.statusEffects = new StatusEffectContainer(this);
|
|
|
|
// todo: move this somewhere more appropriate
|
|
// todo: base this on equip and shit
|
|
SetMod((uint)Modifier.AttackRange, 3);
|
|
SetMod((uint)Modifier.AttackDelay, (Program.Random.Next(30, 60) * 100));
|
|
SetMod((uint)Modifier.Speed, (uint)moveSpeeds[2]);
|
|
|
|
spawnX = positionX;
|
|
spawnY = positionY;
|
|
spawnZ = positionZ;
|
|
}
|
|
|
|
public SubPacket CreateAppearancePacket()
|
|
{
|
|
SetActorAppearancePacket setappearance = new SetActorAppearancePacket(modelId, appearanceIds);
|
|
return setappearance.BuildPacket(actorId);
|
|
}
|
|
|
|
public SubPacket CreateInitStatusPacket()
|
|
{
|
|
return (SetActorStatusAllPacket.BuildPacket(actorId, charaWork.status));
|
|
}
|
|
|
|
public SubPacket CreateSetActorIconPacket()
|
|
{
|
|
return SetActorIconPacket.BuildPacket(actorId, currentActorIcon);
|
|
}
|
|
|
|
public SubPacket CreateIdleAnimationPacket()
|
|
{
|
|
return SetActorSubStatPacket.BuildPacket(actorId, 0, 0, 0, 0, 0, 0, animationId);
|
|
}
|
|
|
|
public void SetQuestGraphic(Player player, int graphicNum)
|
|
{
|
|
player.QueuePacket(SetActorQuestGraphicPacket.BuildPacket(actorId, graphicNum));
|
|
}
|
|
|
|
public void SetCurrentContentGroup(ContentGroup group)
|
|
{
|
|
if (group != null)
|
|
charaWork.currentContentGroup = group.GetTypeId();
|
|
else
|
|
charaWork.currentContentGroup = 0;
|
|
|
|
currentContentGroup = group;
|
|
|
|
ActorPropertyPacketUtil propPacketUtil = new ActorPropertyPacketUtil("charaWork/currentContentGroup", this);
|
|
propPacketUtil.AddProperty("charaWork.currentContentGroup");
|
|
zone.BroadcastPacketsAroundActor(this, propPacketUtil.Done());
|
|
}
|
|
|
|
//This logic isn't correct, order of GetStatusEffects() is not necessarily the same as the actual effects in game. Also sending every time at once isn't needed
|
|
public List<SubPacket> GetActorStatusPackets()
|
|
{
|
|
var propPacketUtil = new ActorPropertyPacketUtil("charaWork/status", this);
|
|
var i = 0;
|
|
foreach (var effect in statusEffects.GetStatusEffects())
|
|
{
|
|
if (!effect.GetHidden())
|
|
{
|
|
propPacketUtil.AddProperty($"charaWork.statusShownTime[{i}]");
|
|
propPacketUtil.AddProperty(String.Format("charaWork.statusShownTime[{0}]", i));
|
|
i++;
|
|
}
|
|
}
|
|
return propPacketUtil.Done();
|
|
}
|
|
|
|
public void PlayAnimation(uint animId, bool onlySelf = false)
|
|
{
|
|
if (onlySelf)
|
|
{
|
|
if (this is Player)
|
|
((Player)this).QueuePacket(PlayAnimationOnActorPacket.BuildPacket(actorId, animId));
|
|
}
|
|
else
|
|
zone.BroadcastPacketAroundActor(this, PlayAnimationOnActorPacket.BuildPacket(actorId, animId));
|
|
}
|
|
|
|
public void SendChant(int left, int right)
|
|
{
|
|
SetActorSubStatPacket.BuildPacket(actorId, 0, left, right, 0, 0, 0, 0).DebugPrintSubPacket();
|
|
zone.BroadcastPacketAroundActor(this, SetActorSubStatPacket.BuildPacket(actorId, 0, left, right, 0, 0, 0, 0));
|
|
}
|
|
|
|
public void DoBattleAction(ushort commandId, uint animationId)
|
|
{
|
|
zone.BroadcastPacketAroundActor(this, BattleActionX00Packet.BuildPacket(actorId, animationId, commandId));
|
|
}
|
|
|
|
public void DoBattleAction(ushort commandId, uint animationId, BattleAction action)
|
|
{
|
|
zone.BroadcastPacketAroundActor(this, BattleActionX01Packet.BuildPacket(actorId, animationId, commandId, action));
|
|
}
|
|
|
|
public void DoBattleAction(ushort commandId, uint animationId, BattleAction[] actions)
|
|
{
|
|
int currentIndex = 0;
|
|
//AoE abilities only ever hit 16 people, so we probably won't need this loop anymore
|
|
//Apparently aoe are limited to 8?
|
|
while (true)
|
|
{
|
|
if (actions.Length - currentIndex >= 10)
|
|
zone.BroadcastPacketAroundActor(this, BattleActionX18Packet.BuildPacket(actorId, animationId, commandId, actions, ref currentIndex));
|
|
else if (actions.Length - currentIndex > 1)
|
|
zone.BroadcastPacketAroundActor(this, BattleActionX10Packet.BuildPacket(actorId, animationId, commandId, actions, ref currentIndex));
|
|
else if (actions.Length - currentIndex == 1)
|
|
{
|
|
zone.BroadcastPacketAroundActor(this, BattleActionX01Packet.BuildPacket(actorId, animationId, commandId, actions[currentIndex]));
|
|
currentIndex++;
|
|
}
|
|
else
|
|
break;
|
|
|
|
//I think aoe effects play on all hit enemies. Firaga does at least
|
|
//animationId = 0; //If more than one packet is sent out, only send the animation once to avoid double playing.
|
|
}
|
|
}
|
|
|
|
public void DoBattleAction(ushort commandId, uint animationId, List<BattleAction> actions)
|
|
{
|
|
int currentIndex = 0;
|
|
|
|
while (true)
|
|
{
|
|
if (actions.Count - currentIndex >= 10)
|
|
zone.BroadcastPacketAroundActor(this, BattleActionX18Packet.BuildPacket(actorId, animationId, commandId, actions, ref currentIndex));
|
|
else if (actions.Count - currentIndex > 1)
|
|
zone.BroadcastPacketAroundActor(this, BattleActionX10Packet.BuildPacket(actorId, animationId, commandId, actions, ref currentIndex));
|
|
else if (actions.Count - currentIndex == 1)
|
|
{
|
|
zone.BroadcastPacketAroundActor(this, BattleActionX01Packet.BuildPacket(actorId, animationId, commandId, actions[currentIndex]));
|
|
currentIndex++;
|
|
}
|
|
else
|
|
break;
|
|
|
|
//Sending multiple packets at once causes some issues. Setting any combination of these to zero changes what breaks
|
|
//animationId = 0; //If more than one packet is sent out, only send the animation once to avoid double playing.
|
|
//commandId = 0;
|
|
//sourceActorId = 0;
|
|
}
|
|
}
|
|
|
|
#region ai stuff
|
|
public void PathTo(float x, float y, float z, float stepSize = 0.70f, int maxPath = 40, float polyRadius = 0.0f)
|
|
{
|
|
aiContainer?.pathFind?.PreparePath(x, y, z, stepSize, maxPath, polyRadius);
|
|
}
|
|
|
|
public void FollowTarget(Actor target, float stepSize = 1.2f, int maxPath = 25, float radius = 0.0f)
|
|
{
|
|
if (target != null)
|
|
PathTo(target.positionX, target.positionY, target.positionZ, stepSize, maxPath, radius);
|
|
}
|
|
|
|
public double GetMod(Modifier modifier)
|
|
{
|
|
return GetMod((uint)modifier);
|
|
}
|
|
|
|
public double GetMod(uint modifier)
|
|
{
|
|
double res;
|
|
if (modifiers.TryGetValue((Modifier)modifier, out res))
|
|
return res;
|
|
return 0;
|
|
}
|
|
|
|
public void SetMod(uint modifier, double val)
|
|
{
|
|
if (modifiers.ContainsKey((Modifier)modifier))
|
|
modifiers[(Modifier)modifier] = val;
|
|
else
|
|
modifiers.Add((Modifier)modifier, val);
|
|
|
|
if (modifier <= 35)
|
|
updateFlags |= ActorUpdateFlags.Stats;
|
|
}
|
|
|
|
public void AddMod(Modifier modifier, double val)
|
|
{
|
|
AddMod((uint)modifier, val);
|
|
}
|
|
|
|
public void AddMod(uint modifier, double val)
|
|
{
|
|
|
|
double newVal = GetMod(modifier) + val;
|
|
SetMod(modifier, newVal);
|
|
}
|
|
|
|
public void SubtractMod(Modifier modifier, double val)
|
|
{
|
|
AddMod((uint)modifier, val);
|
|
}
|
|
|
|
public void SubtractMod(uint modifier, double val)
|
|
{
|
|
double newVal = GetMod(modifier) - val;
|
|
SetMod(modifier, newVal);
|
|
}
|
|
|
|
public virtual void OnPath(Vector3 point)
|
|
{
|
|
//lua.LuaEngine.CallLuaBattleFunction(this, "onPath", this, point);
|
|
|
|
updateFlags |= ActorUpdateFlags.Position;
|
|
this.isAtSpawn = false;
|
|
}
|
|
|
|
public override void Update(DateTime tick)
|
|
{
|
|
|
|
}
|
|
|
|
public override void PostUpdate(DateTime tick, List<SubPacket> packets = null)
|
|
{
|
|
if (updateFlags != ActorUpdateFlags.None)
|
|
{
|
|
packets = packets ?? new List<SubPacket>();
|
|
|
|
if ((updateFlags & ActorUpdateFlags.Appearance) != 0)
|
|
{
|
|
packets.Add(new SetActorAppearancePacket(modelId, appearanceIds).BuildPacket(actorId));
|
|
}
|
|
|
|
if ((updateFlags & ActorUpdateFlags.State) != 0)
|
|
{
|
|
packets.Add(SetActorStatePacket.BuildPacket(actorId, currentMainState, currentSubState));
|
|
packets.Add(BattleActionX00Packet.BuildPacket(actorId, 0x72000062, 0));
|
|
packets.Add(BattleActionX01Packet.BuildPacket(actorId, 0x7C000062, 21001, new BattleAction(actorId, 0, 1)));
|
|
|
|
updateFlags &= ~ActorUpdateFlags.State;
|
|
//DoBattleAction(21001, 0x7C000062, new BattleAction(this.actorId, 0, 1, 0, 0, 1)); //Attack Mode
|
|
}
|
|
|
|
if ((updateFlags & ActorUpdateFlags.Status) != 0)
|
|
{
|
|
List<SubPacket> statusPackets = statusEffects.GetStatusPackets();
|
|
packets.AddRange(statusPackets);
|
|
statusPackets.Clear();
|
|
updateFlags &= ~ActorUpdateFlags.Status;
|
|
}
|
|
|
|
if ((updateFlags & ActorUpdateFlags.StatusTime) != 0)
|
|
{
|
|
packets.AddRange(statusEffects.GetStatusTimerPackets());
|
|
updateFlags &= ~ActorUpdateFlags.StatusTime;
|
|
|
|
}
|
|
|
|
if ((updateFlags & ActorUpdateFlags.HpTpMp) != 0)
|
|
{
|
|
var propPacketUtil = new ActorPropertyPacketUtil("charaWork/stateAtQuicklyForAll", this);
|
|
propPacketUtil.AddProperty("charaWork.parameterSave.hp[0]");
|
|
propPacketUtil.AddProperty("charaWork.parameterSave.hpMax[0]");
|
|
propPacketUtil.AddProperty("charaWork.parameterSave.mp");
|
|
propPacketUtil.AddProperty("charaWork.parameterSave.mpMax");
|
|
propPacketUtil.AddProperty("charaWork.parameterTemp.tp");
|
|
packets.AddRange(propPacketUtil.Done());
|
|
}
|
|
|
|
base.PostUpdate(tick, packets);
|
|
}
|
|
}
|
|
|
|
public virtual bool IsValidTarget(Character target, ValidTarget validTarget)
|
|
{
|
|
return !target.isStatic;
|
|
}
|
|
|
|
public virtual bool CanAttack()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public virtual bool CanCast(Character target, BattleCommand spell)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public virtual bool CanWeaponSkill(Character target, BattleCommand skill)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public virtual bool CanUseAbility(Character target, BattleCommand ability)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public virtual uint GetAttackDelayMs()
|
|
{
|
|
return (uint)GetMod((uint)Modifier.AttackDelay);
|
|
}
|
|
|
|
public virtual uint GetAttackRange()
|
|
{
|
|
return (uint)GetMod((uint)Modifier.AttackRange);
|
|
}
|
|
|
|
public virtual bool Engage(uint targid = 0, ushort newMainState = 0xFFFF)
|
|
{
|
|
// todo: attack the things
|
|
/*if (newMainState != 0xFFFF)
|
|
{
|
|
currentMainState = newMainState;// this.newMainState = newMainState;
|
|
updateFlags |= ActorUpdateFlags.State;
|
|
}
|
|
else*/ if (aiContainer.CanChangeState())
|
|
{
|
|
if (targid == 0)
|
|
{
|
|
if (currentTarget != Actor.INVALID_ACTORID)
|
|
targid = currentTarget;
|
|
else if (currentLockedTarget != Actor.INVALID_ACTORID)
|
|
targid = currentLockedTarget;
|
|
}
|
|
//if (targid != 0)
|
|
{
|
|
aiContainer.Engage(zone.FindActorInArea<Character>(targid));
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public virtual bool Engage(Character target)
|
|
{
|
|
aiContainer.Engage(target);
|
|
return false;
|
|
}
|
|
|
|
public virtual bool Disengage(ushort newMainState = 0xFFFF)
|
|
{
|
|
/*if (newMainState != 0xFFFF)
|
|
{
|
|
currentMainState = newMainState;// this.newMainState = newMainState;
|
|
updateFlags |= ActorUpdateFlags.State;
|
|
}
|
|
else*/ if (IsEngaged())
|
|
{
|
|
aiContainer.Disengage();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public virtual void Cast(uint spellId, uint targetId = 0)
|
|
{
|
|
if (aiContainer.CanChangeState())
|
|
aiContainer.Cast(zone.FindActorInArea<Character>(targetId == 0 ? currentTarget : targetId), spellId);
|
|
}
|
|
|
|
public virtual void Ability(uint abilityId, uint targetId = 0)
|
|
{
|
|
if (aiContainer.CanChangeState())
|
|
aiContainer.Ability(zone.FindActorInArea<Character>(targetId == 0 ? currentTarget : targetId), abilityId);
|
|
}
|
|
|
|
public virtual void WeaponSkill(uint skillId, uint targetId = 0)
|
|
{
|
|
if (aiContainer.CanChangeState())
|
|
aiContainer.WeaponSkill(zone.FindActorInArea<Character>(targetId == 0 ? currentTarget : targetId), skillId);
|
|
}
|
|
|
|
public virtual void Spawn(DateTime tick)
|
|
{
|
|
aiContainer.Reset();
|
|
// todo: reset hp/mp/tp etc here
|
|
ChangeState(SetActorStatePacket.MAIN_STATE_PASSIVE);
|
|
charaWork.parameterSave.hp = charaWork.parameterSave.hpMax;
|
|
charaWork.parameterSave.mp = charaWork.parameterSave.mpMax;
|
|
RecalculateStats();
|
|
}
|
|
|
|
//AdditionalActions is the list of actions that EXP/Chain messages are added to
|
|
public virtual void Die(DateTime tick, BattleActionContainer actionContainer = null)
|
|
{
|
|
// todo: actual despawn timer
|
|
aiContainer.InternalDie(tick, 10);
|
|
ChangeSpeed(0.0f, 0.0f, 0.0f, 0.0f);
|
|
}
|
|
|
|
public virtual void Despawn(DateTime tick)
|
|
{
|
|
|
|
}
|
|
|
|
public bool IsDead()
|
|
{
|
|
return !IsAlive();
|
|
}
|
|
|
|
public bool IsAlive()
|
|
{
|
|
return !aiContainer.IsDead();// && GetHP() > 0;
|
|
}
|
|
|
|
public short GetHP()
|
|
{
|
|
// todo:
|
|
return charaWork.parameterSave.hp[0];
|
|
}
|
|
|
|
public short GetMaxHP()
|
|
{
|
|
return charaWork.parameterSave.hpMax[0];
|
|
}
|
|
|
|
public short GetMP()
|
|
{
|
|
return charaWork.parameterSave.mp;
|
|
}
|
|
|
|
public ushort GetTP()
|
|
{
|
|
return tpBase;
|
|
}
|
|
|
|
public short GetMaxMP()
|
|
{
|
|
return charaWork.parameterSave.mpMax;
|
|
}
|
|
|
|
public byte GetMPP()
|
|
{
|
|
return (byte)((charaWork.parameterSave.mp / charaWork.parameterSave.mpMax) * 100);
|
|
}
|
|
|
|
public byte GetTPP()
|
|
{
|
|
return (byte)((tpBase / 3000) * 100);
|
|
}
|
|
|
|
public byte GetHPP()
|
|
{
|
|
return (byte)(charaWork.parameterSave.hp[0] == 0 ? 0 : (charaWork.parameterSave.hp[0] / (float) charaWork.parameterSave.hpMax[0]) * 100);
|
|
}
|
|
|
|
public void SetHP(uint hp)
|
|
{
|
|
charaWork.parameterSave.hp[0] = (short)hp;
|
|
if (hp > charaWork.parameterSave.hpMax[0])
|
|
SetMaxHP(hp);
|
|
|
|
updateFlags |= ActorUpdateFlags.HpTpMp;
|
|
}
|
|
|
|
public void SetMaxHP(uint hp)
|
|
{
|
|
charaWork.parameterSave.hpMax[0] = (short)hp;
|
|
updateFlags |= ActorUpdateFlags.HpTpMp;
|
|
}
|
|
|
|
public void SetMP(uint mp)
|
|
{
|
|
charaWork.parameterSave.mpMax = (short)mp;
|
|
if (mp > charaWork.parameterSave.hpMax[0])
|
|
SetMaxMP(mp);
|
|
|
|
updateFlags |= ActorUpdateFlags.HpTpMp;
|
|
}
|
|
|
|
public void SetMaxMP(uint mp)
|
|
{
|
|
charaWork.parameterSave.mp = (short)mp;
|
|
updateFlags |= ActorUpdateFlags.HpTpMp;
|
|
}
|
|
// todo: the following functions are virtuals since we want to check hidden item bonuses etc on player for certain conditions
|
|
public virtual void AddHP(int hp)
|
|
{
|
|
// dont wanna die ded, don't want to send update if hp isn't actually changed
|
|
if (IsAlive() && hp != 0)
|
|
{
|
|
// todo: +/- hp and die
|
|
// todo: battlenpcs probably have way more hp?
|
|
var addHp = charaWork.parameterSave.hp[0] + hp;
|
|
addHp = addHp.Clamp((short)GetMod((uint)Modifier.MinimumHpLock), charaWork.parameterSave.hpMax[0]);
|
|
charaWork.parameterSave.hp[0] = (short)addHp;
|
|
|
|
updateFlags |= ActorUpdateFlags.HpTpMp;
|
|
}
|
|
}
|
|
|
|
public short GetClass()
|
|
{
|
|
return charaWork.parameterSave.state_mainSkill[0];
|
|
}
|
|
|
|
public short GetLevel()
|
|
{
|
|
return charaWork.parameterSave.state_mainSkillLevel;
|
|
}
|
|
|
|
public void AddMP(int mp)
|
|
{
|
|
if (IsAlive() && mp != 0)
|
|
{
|
|
charaWork.parameterSave.mp = (short)(charaWork.parameterSave.mp + mp).Clamp(ushort.MinValue, charaWork.parameterSave.mpMax);
|
|
|
|
// todo: check hidden effects and shit
|
|
|
|
updateFlags |= ActorUpdateFlags.HpTpMp;
|
|
}
|
|
}
|
|
|
|
public void AddTP(int tp)
|
|
{
|
|
if (IsAlive() && tp != 0)
|
|
{
|
|
var addTp = charaWork.parameterTemp.tp + tp;
|
|
|
|
addTp = addTp.Clamp((int) GetMod(Modifier.MinimumTpLock), 3000);
|
|
charaWork.parameterTemp.tp = (short) addTp;
|
|
tpBase = (ushort)charaWork.parameterTemp.tp;
|
|
updateFlags |= ActorUpdateFlags.HpTpMp;
|
|
|
|
if (tpBase >= 1000)
|
|
lua.LuaEngine.GetInstance().OnSignal("tpOver1000");
|
|
}
|
|
}
|
|
|
|
public void DelHP(int hp)
|
|
{
|
|
AddHP((short)-hp);
|
|
}
|
|
|
|
public void DelMP(int mp)
|
|
{
|
|
AddMP(-mp);
|
|
}
|
|
|
|
public void DelTP(int tp)
|
|
{
|
|
AddTP(-tp);
|
|
}
|
|
|
|
virtual public void CalculateBaseStats()
|
|
{
|
|
// todo: apply mods and shit here, get race/level/job and shit
|
|
uint hpMod = (uint) GetMod((uint)Modifier.Hp);
|
|
if (hpMod != 0)
|
|
{
|
|
SetMaxHP(hpMod);
|
|
uint hpp = (uint) GetMod((uint) Modifier.HpPercent);
|
|
uint hp = hpMod;
|
|
if(hpp != 0)
|
|
{
|
|
hp = (uint) Math.Ceiling(((float)hpp / 100.0f) * hpMod);
|
|
}
|
|
SetHP(hp);
|
|
}
|
|
|
|
uint mpMod = (uint)GetMod((uint)Modifier.Mp);
|
|
if (mpMod != 0)
|
|
{
|
|
SetMaxMP(mpMod);
|
|
uint mpp = (uint)GetMod((uint)Modifier.MpPercent);
|
|
uint mp = mpMod;
|
|
if (mpp != 0)
|
|
{
|
|
mp = (uint)Math.Ceiling(((float)mpp / 100.0f) * mpMod);
|
|
}
|
|
SetMP(mp);
|
|
}
|
|
// todo: recalculate stats and crap
|
|
updateFlags |= ActorUpdateFlags.HpTpMp;
|
|
|
|
|
|
SetMod((uint)Modifier.HitCount, 1);
|
|
}
|
|
|
|
public void RecalculateStats()
|
|
{
|
|
//CalculateBaseStats();
|
|
}
|
|
|
|
public void SetStat(uint statId, int val)
|
|
{
|
|
charaWork.battleTemp.generalParameter[statId] = (short)val;
|
|
}
|
|
|
|
public short GetStat(uint statId)
|
|
{
|
|
return charaWork.battleTemp.generalParameter[statId];
|
|
}
|
|
|
|
public virtual float GetSpeed()
|
|
{
|
|
// todo: for battlenpc/player calculate speed
|
|
return (float) GetMod((uint)Modifier.Speed);
|
|
}
|
|
|
|
public virtual void OnAttack(State state, BattleAction action, ref BattleAction error)
|
|
{
|
|
var target = state.GetTarget();
|
|
// todo: change animation based on equipped weapon
|
|
// todo: get hitrate and shit, handle protect effect and whatever
|
|
if (BattleUtils.TryAttack(this, target, action, ref error))
|
|
{
|
|
//var packet = BattleActionX01Packet.BuildPacket(owner.actorId, owner.actorId, target.actorId, (uint)0x19001000, (uint)0x8000604, (ushort)0x765D, (ushort)BattleActionX01PacketCommand.Attack, (ushort)damage, (byte)0x1);
|
|
}
|
|
|
|
// todo: call onAttack/onDamageTaken
|
|
//BattleUtils.DamageTarget(this, target, DamageTakenType.Attack, action);
|
|
AddTP(200);
|
|
target.AddTP(100);
|
|
}
|
|
|
|
public virtual void OnCast(State state, BattleAction[] actions, BattleCommand spell, ref BattleAction[] errors)
|
|
{
|
|
// damage is handled in script
|
|
var spellCost = spell.CalculateMpCost(this);
|
|
this.DelMP(spellCost); // mpCost can be set in script e.g. if caster has something for free spells
|
|
|
|
foreach (BattleAction action in actions)
|
|
{
|
|
if (zone.FindActorInArea<Character>(action.targetId) is Character chara)
|
|
{
|
|
//BattleUtils.HandleHitType(this, chara, action);
|
|
//BattleUtils.DoAction(this, chara, action, DamageTakenType.Magic);
|
|
}
|
|
}
|
|
lua.LuaEngine.GetInstance().OnSignal("spellUsed");
|
|
}
|
|
|
|
public virtual void OnWeaponSkill(State state, BattleAction[] actions, BattleCommand skill, ref BattleAction[] errors)
|
|
{
|
|
// damage is handled in script
|
|
|
|
foreach (BattleAction action in actions)
|
|
{
|
|
//Should we just store the character insteado f having to find it again?
|
|
if (zone.FindActorInArea<Character>(action.targetId) is Character chara)
|
|
{
|
|
//BattleUtils.DoAction(this, chara, action, DamageTakenType.Weaponskill);
|
|
}
|
|
}
|
|
|
|
this.DelTP(skill.tpCost);
|
|
}
|
|
|
|
public virtual void OnAbility(State state, BattleAction[] actions, BattleCommand ability, ref BattleAction[] errors)
|
|
{
|
|
foreach (var action in actions)
|
|
{
|
|
if (zone.FindActorInArea<Character>(action.targetId) is Character chara)
|
|
{
|
|
//BattleUtils.DoAction(this, chara, action, DamageTakenType.Ability);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void OnSpawn()
|
|
{
|
|
|
|
}
|
|
|
|
public virtual void OnDeath()
|
|
{
|
|
|
|
}
|
|
|
|
public virtual void OnDespawn()
|
|
{
|
|
|
|
}
|
|
|
|
public virtual void OnDamageDealt(Character defender, BattleAction action, BattleActionContainer actionContainer = null)
|
|
{
|
|
switch (action.hitType)
|
|
{
|
|
case (HitType.Miss):
|
|
OnMiss(this, action, actionContainer);
|
|
break;
|
|
default:
|
|
OnHit(defender, action, actionContainer);
|
|
break;
|
|
}
|
|
|
|
//TP is only gained from autoattacks and abilities
|
|
if (action.commandType == CommandType.AutoAttack || action.commandType == CommandType.Ability)
|
|
{
|
|
//TP gained on an attack is usually 100 * delay.
|
|
//Store TP seems to add .1% per point.
|
|
double weaponDelay = GetMod(Modifier.AttackDelay) / 1000.0;
|
|
var storeTPPercent = 1 + (GetMod(Modifier.StoreTP) * 0.1);
|
|
AddTP((int)(weaponDelay * 100 * storeTPPercent));
|
|
}
|
|
}
|
|
|
|
public virtual void OnDamageTaken(Character attacker, BattleAction action, BattleActionContainer actionContainer = null)
|
|
{
|
|
switch (action.hitType)
|
|
{
|
|
case (HitType.Miss):
|
|
OnEvade(attacker, action, actionContainer);
|
|
break;
|
|
case (HitType.Parry):
|
|
OnParry(attacker, action, actionContainer);
|
|
break;
|
|
case (HitType.Block):
|
|
OnBlock(attacker, action, actionContainer);
|
|
break;
|
|
}
|
|
|
|
statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnDamageTaken, "onDamageTaken", attacker, this, action);
|
|
|
|
//TP gain formula seems to be something like 5 * e ^ ( -0.667 * [defender's level] ) * damage taken, rounded up
|
|
//This should be completely accurate at level 50, but isn't totally accurate at lower levels.
|
|
//Don't know if store tp impacts this
|
|
double tpModifier = 5 * Math.Pow(Math.E, (-0.0667 * GetLevel()));
|
|
AddTP((int)Math.Ceiling(tpModifier * action.amount));
|
|
|
|
|
|
if (charaWork.parameterSave.hp[0] < 1)
|
|
Die(Program.Tick, actionContainer);
|
|
}
|
|
|
|
public UInt64 GetTempVar(string name)
|
|
{
|
|
UInt64 retVal = 0;
|
|
if (tempVars.TryGetValue(name, out retVal))
|
|
return retVal;
|
|
return 0;
|
|
}
|
|
|
|
// cause lua is a dick
|
|
public void SetTempVar(string name, uint val)
|
|
{
|
|
if (tempVars.ContainsKey(name))
|
|
tempVars[name] = val;
|
|
}
|
|
|
|
public void SetTempVar(string name, UInt64 val)
|
|
{
|
|
if (tempVars.ContainsKey(name))
|
|
tempVars[name] = val;
|
|
}
|
|
|
|
public void ResetTempVars()
|
|
{
|
|
tempVars.Clear();
|
|
}
|
|
|
|
#region lua helpers
|
|
public bool IsEngaged()
|
|
{
|
|
return aiContainer.IsEngaged();
|
|
}
|
|
|
|
public bool IsPlayer()
|
|
{
|
|
return this is Player;
|
|
}
|
|
|
|
public bool IsMonster()
|
|
{
|
|
return this is BattleNpc;
|
|
}
|
|
|
|
public bool IsPet()
|
|
{
|
|
return this is Pet;
|
|
}
|
|
|
|
public bool IsAlly()
|
|
{
|
|
return this is Ally;
|
|
}
|
|
|
|
public bool IsDiscipleOfWar()
|
|
{
|
|
return GetClass() < CLASSID_THM;
|
|
}
|
|
|
|
public bool IsDiscipleOfMagic()
|
|
{
|
|
return GetClass() >= CLASSID_THM && currentJob < CLASSID_CRP;
|
|
}
|
|
|
|
public bool IsDiscipleOfHand()
|
|
{
|
|
return GetClass() >= CLASSID_CRP && currentJob < CLASSID_MIN;
|
|
}
|
|
|
|
public bool IsDiscipleOfLand()
|
|
{
|
|
return GetClass() >= CLASSID_MIN;
|
|
}
|
|
#endregion lua helpers
|
|
#endregion ai stuff
|
|
|
|
//Reset procs. Only send packet if any procs were actually reset.
|
|
//This assumes you can't use weaponskills between getting a proc and using the procced ability
|
|
public void ResetProcs()
|
|
{
|
|
var propPacketUtil = new ActorPropertyPacketUtil("charaWork/timingCommand", this);
|
|
bool shouldSend = false;
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
if (charaWork.battleTemp.timingCommandFlag[i])
|
|
{
|
|
shouldSend = true;
|
|
charaWork.battleTemp.timingCommandFlag[i] = false;
|
|
propPacketUtil.AddProperty($"charaWork.battleTemp.timingCommandFlag[{i}]");
|
|
}
|
|
}
|
|
|
|
if (shouldSend && this is Player player)
|
|
player.QueuePackets(propPacketUtil.Done());
|
|
}
|
|
|
|
//Set given proc to true and send packet if this is a player
|
|
// todo: hidden status effects for timing when the procs fall off
|
|
public void SetProc(int procId, bool val = true)
|
|
{
|
|
charaWork.battleTemp.timingCommandFlag[procId] = val;
|
|
uint effectId = (uint)StatusEffectId.EvadeProc + (uint)procId;
|
|
|
|
//If a proc just occurred, add a hidden effect effect
|
|
if (val)
|
|
{
|
|
StatusEffect procEffect = Server.GetWorldManager().GetStatusEffect(effectId);
|
|
procEffect.SetDuration(5);
|
|
statusEffects.AddStatusEffect(procEffect, this, true, true);
|
|
}
|
|
//Otherwise we're reseting a proc, remove the status
|
|
else
|
|
{
|
|
statusEffects.RemoveStatusEffect(statusEffects.GetStatusEffectById((uint)effectId));
|
|
}
|
|
|
|
if (this is Player player)
|
|
{
|
|
var propPacketUtil = new ActorPropertyPacketUtil("charaWork/timingCommand", this);
|
|
propPacketUtil.AddProperty($"charaWork.battleTemp.timingCommandFlag[{procId}]");
|
|
player.QueuePackets(propPacketUtil.Done());
|
|
}
|
|
}
|
|
|
|
public HitDirection GetHitDirection(Actor target)
|
|
{
|
|
//Get between taget's position and our position
|
|
double angle = Vector3.GetAngle(target.GetPosAsVector3(), GetPosAsVector3());
|
|
//Add to the target's rotation, mod by 2pi. This is the angle relative to where the target is looking
|
|
//Actor's rotation is 0 degrees on their left side, rotate it by 45 degrees so that quadrants line up with sides
|
|
angle = (angle + target.rotation - (.25 * Math.PI)) % (2 * Math.PI);
|
|
//Make positive
|
|
if (angle < 0)
|
|
angle = angle + (2 * Math.PI);
|
|
|
|
//Get the side we're on. 0 is front, 1 is right, 2 is rear, 3 is left
|
|
var side = (int) (angle / (.5 * Math.PI)) % 4;
|
|
|
|
return (HitDirection) (1 << side);
|
|
}
|
|
|
|
//Called when this character evades attacker's action
|
|
public void OnEvade(Character attacker, BattleAction action, BattleActionContainer actionContainer = null)
|
|
{
|
|
SetProc((ushort)HitType.Evade);
|
|
statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnEvade, "onEvade", attacker, this, action, actionContainer);
|
|
}
|
|
|
|
//Called when this character blocks attacker's action
|
|
public void OnBlock(Character attacker, BattleAction action, BattleActionContainer actionContainer = null)
|
|
{
|
|
SetProc((ushort)HitType.Block);
|
|
statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnBlock, "onBlock", attacker, this, action, actionContainer);
|
|
}
|
|
|
|
//Called when this character parries attacker's action
|
|
public void OnParry(Character attacker, BattleAction action, BattleActionContainer actionContainer = null)
|
|
{
|
|
SetProc((ushort)HitType.Parry);
|
|
statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnParry, "onParry", attacker, this, action, actionContainer);
|
|
}
|
|
|
|
//Called when this character misses
|
|
public void OnMiss(Character defender, BattleAction action, BattleActionContainer actionContainer = null)
|
|
{
|
|
SetProc((ushort)HitType.Miss);
|
|
statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnMiss, "onMiss", this, defender, action, actionContainer);
|
|
}
|
|
|
|
public void OnHit(Character defender, BattleAction action, BattleActionContainer actionContainer = null)
|
|
{
|
|
statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnHit, "onHit", this, defender, action, actionContainer);
|
|
}
|
|
|
|
//The order of messages that appears after using a command is:
|
|
|
|
//1. Cast start messages. (ie "You begin casting... ")
|
|
//2. Messages from buffs that activate before the command actually starts, like Power Surge or Presence of Mind. (This may be wrong and these could be the same as 4.)
|
|
//3. If the command is a multi-hit command, this is where the "You use [command] on [target]" message goes
|
|
|
|
//Then, for each hit:
|
|
//4. Buffs that activate before a command hits, like Blindside
|
|
//5. The hit itself. For single hit commands this message is "Your [command] hits [target] for x damage" for multi hits it's "[Target] takes x points of damage"
|
|
//6. Stoneskin falling off
|
|
//6. Buffs that activate after a command hits, like Aegis Boon and Divine Veil
|
|
|
|
//After all hits
|
|
//7. If it's a multi-hit command there's a "{numhits]fold attack..." message or if all hits miss an "All attacks missed" message
|
|
//8. Buffs that fall off after the skill ends, like Excruciate
|
|
|
|
//For every target defeated:
|
|
//8. Defeat message
|
|
//9. EXP message
|
|
//10. EXP chain message
|
|
|
|
|
|
//folder is probably temporary until move to cached scripts is complete
|
|
public void DoBattleCommand(BattleCommand command, string folder)
|
|
{
|
|
//List<BattleAction> actions = new List<BattleAction>();
|
|
BattleActionContainer actions = new BattleActionContainer();
|
|
|
|
var targets = command.targetFind.GetTargets();
|
|
bool hitTarget = false;
|
|
|
|
if (targets.Count > 0)
|
|
{
|
|
statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnCommandStart, "onCommandStart", this, command, actions);
|
|
|
|
foreach (var chara in targets)
|
|
{
|
|
ushort hitCount = 0;
|
|
for (int hitNum = 1; hitNum <= command.numHits; hitNum++)
|
|
{
|
|
var action = new BattleAction(chara.actorId, command, (byte)GetHitDirection(chara), (byte)hitNum);
|
|
|
|
//uncached script
|
|
lua.LuaEngine.CallLuaBattleCommandFunction(this, command, folder, "onSkillFinish", this, chara, command, action, actions);
|
|
//cached script
|
|
//skill.CallLuaFunction(owner, "onSkillFinish", this, chara, command, action, actions);
|
|
|
|
if (action.hitType > HitType.Evade && action.hitType != HitType.Resist)
|
|
{
|
|
hitTarget = true;
|
|
hitCount++;
|
|
}
|
|
}
|
|
|
|
if (command.numHits > 1)
|
|
{
|
|
//You use [command] on [target].
|
|
actions.AddAction(new BattleAction(chara.actorId, 30442, 0, 0, (byte)hitCount));
|
|
}
|
|
}
|
|
|
|
statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnCommandFinish, "onCommandFinish", this, command, actions);
|
|
}
|
|
else
|
|
{
|
|
actions.AddAction(new BattleAction(target.actorId, 30202, 0));
|
|
}
|
|
|
|
//Now that we know if we hit the target we can check if the combo continues
|
|
if (this is Player player && command.commandType == CommandType.WeaponSkill)
|
|
if (command.isCombo && hitTarget)
|
|
player.SetCombos(command.comboNextCommandId);
|
|
else
|
|
player.SetCombos();
|
|
|
|
BattleAction error = new BattleAction(actorId, 0, 0);
|
|
actions.CombineLists();
|
|
DoBattleAction(command.id, command.battleAnimation, actions.GetList());
|
|
}
|
|
|
|
public List<Character> GetPartyMembersInRange(uint range)
|
|
{
|
|
TargetFind targetFind = new TargetFind(this);
|
|
targetFind.SetAOEType(ValidTarget.PartyMember, TargetFindAOEType.Circle, TargetFindAOETarget.Self, range);
|
|
targetFind.FindWithinArea(this, ValidTarget.PartyMember, TargetFindAOETarget.Self);
|
|
return targetFind.GetTargets();
|
|
}
|
|
}
|
|
} |