mirror of
https://bitbucket.org/Ioncannon/project-meteor-server.git
synced 2025-04-02 19:42:05 -04:00
Added the combo and proc systems Added scripts for most weaponskill and spells as well as some abilities and status effects Added support for multihit attacks Added AbilityState for abilities Added hiteffects that change based on an attack's parameters Added positionals Changed how targeting works for battlecommands Fixed bug that occurred when moving or swapping hotbar commands Fixed bug that occurred when losing status effects
929 lines
31 KiB
C#
929 lines
31 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, Int64> modifiers = new Dictionary<Modifier, long>();
|
|
|
|
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());
|
|
}
|
|
|
|
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;
|
|
animationId = 0; //If more than one packet is sent out, only send the animation once to avoid double playing.
|
|
}
|
|
}
|
|
|
|
#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 Int64 GetMod(uint modifier)
|
|
{
|
|
Int64 res;
|
|
if (modifiers.TryGetValue((Modifier)modifier, out res))
|
|
return res;
|
|
return 0;
|
|
}
|
|
|
|
public void SetMod(uint modifier, Int64 val)
|
|
{
|
|
if (modifiers.ContainsKey((Modifier)modifier))
|
|
modifiers[(Modifier)modifier] = val;
|
|
else
|
|
modifiers.Add((Modifier)modifier, val);
|
|
}
|
|
|
|
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.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)
|
|
{
|
|
this.newMainState = newMainState;
|
|
}
|
|
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)
|
|
{
|
|
this.newMainState = newMainState;
|
|
}
|
|
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();
|
|
}
|
|
|
|
public virtual void Die(DateTime tick)
|
|
{
|
|
// 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] / 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
|
|
if (IsAlive())
|
|
{
|
|
// 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;
|
|
|
|
if (charaWork.parameterSave.hp[0] < 1)
|
|
Die(Program.Tick);
|
|
|
|
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)
|
|
{
|
|
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())
|
|
{
|
|
charaWork.parameterTemp.tp = (short)((charaWork.parameterTemp.tp + tp).Clamp(0, 3000));
|
|
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, uint val)
|
|
{
|
|
charaWork.battleTemp.generalParameter[statId] = (ushort)val;
|
|
}
|
|
|
|
public ushort GetStat(uint statId)
|
|
{
|
|
return charaWork.battleTemp.generalParameter[statId];
|
|
}
|
|
|
|
public virtual float GetSpeed()
|
|
{
|
|
// todo: for battlenpc/player calculate speed
|
|
return 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, action, DamageTakenType.Attack);
|
|
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);
|
|
//Do procs reset on weaponskills?
|
|
ResetProcs();
|
|
lua.LuaEngine.GetInstance().OnSignal("weaponskillUsed");
|
|
}
|
|
|
|
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 OnDamageTaken(Character attacker, BattleAction action, DamageTakenType damageTakenType)
|
|
{
|
|
|
|
}
|
|
|
|
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(5000);
|
|
statusEffects.AddStatusEffect(procEffect, this, true, true);
|
|
|
|
string procFunc = "";
|
|
|
|
//is there any reason we need this
|
|
switch(procId)
|
|
{
|
|
case (0):
|
|
procFunc = "onEvade";
|
|
break;
|
|
case (1):
|
|
procFunc = "onBlock";
|
|
break;
|
|
case (2):
|
|
procFunc = "onParry";
|
|
break;
|
|
case (3):
|
|
procFunc = "onMiss";
|
|
break;
|
|
}
|
|
|
|
//lua.LuaEngine.CallLuaBattleFunction(this, procFunc, this);
|
|
}
|
|
//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)
|
|
{
|
|
}
|
|
|
|
}
|
|
} |