AoE rewrite and bug fixes

Rewrote aoe checks for cone and line aoes and added minimum distance values
Added height checks for commands
Fixed combo effects repeating for every target hit by AoE attacks
Fixed teleport sometimes not raising (I think)
Fixed gear checks in some command scripts
This commit is contained in:
yogurt 2018-07-02 00:45:06 -05:00
parent 8c5375f609
commit cf30eef39e
34 changed files with 483 additions and 330 deletions

View file

@ -2258,7 +2258,7 @@ namespace FFXIVClassic_Map_Server
int count = 0;
conn.Open();
var query = ("SELECT `id`, name, classJob, lvl, requirements, mainTarget, validTarget, aoeType, aoeRange, aoeTarget, basePotency, numHits, positionBonus, procRequirement, `range`, statusId, statusDuration, statusChance, " +
var query = ("SELECT `id`, name, classJob, lvl, requirements, mainTarget, validTarget, aoeType, aoeRange, aoeMinRange, aoeConeAngle, aoeRotateAngle, aoeTarget, basePotency, numHits, positionBonus, procRequirement, `range`, minRange, rangeHeight, rangeWidth, statusId, statusDuration, statusChance, " +
"castType, castTime, recastTime, mpCost, tpCost, animationType, effectAnimation, modelAnimation, animationDuration, battleAnimation, validUser, comboId1, comboId2, comboStep, accuracyMod, worldMasterTextId, commandType, actionType, actionProperty FROM server_battle_commands;");
MySqlCommand cmd = new MySqlCommand(query, conn);
@ -2281,7 +2281,10 @@ namespace FFXIVClassic_Map_Server
battleCommand.numHits = reader.GetByte("numHits");
battleCommand.positionBonus = (BattleCommandPositionBonus)reader.GetByte("positionBonus");
battleCommand.procRequirement = (BattleCommandProcRequirement)reader.GetByte("procRequirement");
battleCommand.range = reader.GetInt32("range");
battleCommand.range = reader.GetFloat("range");
battleCommand.minRange = reader.GetFloat("minRange");
battleCommand.rangeHeight = reader.GetInt32("rangeHeight");
battleCommand.rangeWidth = reader.GetInt32("rangeWidth");
battleCommand.statusId = reader.GetUInt32("statusId");
battleCommand.statusDuration = reader.GetUInt32("statusDuration");
battleCommand.statusChance = reader.GetFloat("statusChance");
@ -2295,7 +2298,10 @@ namespace FFXIVClassic_Map_Server
battleCommand.effectAnimation = reader.GetUInt16("effectAnimation");
battleCommand.modelAnimation = reader.GetUInt16("modelAnimation");
battleCommand.animationDurationSeconds = reader.GetUInt16("animationDuration");
battleCommand.aoeRange = reader.GetInt32("aoeRange");
battleCommand.aoeRange = reader.GetFloat("aoeRange");
battleCommand.aoeMinRange = reader.GetFloat("aoeMinRange");
battleCommand.aoeConeAngle = reader.GetFloat("aoeConeAngle");
battleCommand.aoeRotateAngle = reader.GetFloat("aoeRotateAngle");
battleCommand.aoeTarget = (TargetFindAOETarget)reader.GetByte("aoeTarget");
battleCommand.battleAnimation = reader.GetUInt32("battleAnimation");

View file

@ -657,9 +657,7 @@ namespace FFXIVClassic_Map_Server.Actors
var dX = this.positionX - x;
var dY = this.positionZ - z;
var rot2 = Math.Atan2(dY, dX);
var dRot = Math.PI - rot2 + Math.PI / 2;
// pending move, dont need to unset it
@ -668,10 +666,11 @@ namespace FFXIVClassic_Map_Server.Actors
}
// todo: is this legit?
public bool IsFacing(float x, float z, float angle = 40.0f)
public bool IsFacing(float x, float z, float angle = 90.0f)
{
angle = (float)(Math.PI * angle / 180);
return Math.Abs(Vector3.GetAngle(positionX, positionZ, x, z) - rotation) < angle;
var a = Vector3.GetAngle(positionX, positionZ, x, z);
return new Vector3(x, 0, z).IsWithinCone(GetPosAsVector3(), rotation, angle);
}
public bool IsFacing(Actor target, float angle = 40.0f)

View file

@ -828,7 +828,7 @@ namespace FFXIVClassic_Map_Server.Actors
//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);
var storeTPPercent = 1 + (GetMod(Modifier.StoreTP) * 0.001);
AddTP((int)(weaponDelay * 100 * storeTPPercent));
}
}
@ -1106,7 +1106,7 @@ namespace FFXIVClassic_Map_Server.Actors
}
//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 (this is Player player)
if (command.isCombo && hitTarget)
player.SetCombos(command.comboNextCommandId);
else
@ -1122,7 +1122,7 @@ namespace FFXIVClassic_Map_Server.Actors
public List<Character> GetPartyMembersInRange(uint range)
{
TargetFind targetFind = new TargetFind(this);
targetFind.SetAOEType(ValidTarget.PartyMember, TargetFindAOEType.Circle, TargetFindAOETarget.Self, range);
targetFind.SetAOEType(ValidTarget.PartyMember, TargetFindAOEType.Circle, TargetFindAOETarget.Self, range, 0, 10, 0, 0);
targetFind.FindWithinArea(this, ValidTarget.PartyMember, TargetFindAOETarget.Self);
return targetFind.GetTargets();
}

View file

@ -104,7 +104,8 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
public byte numHits; //amount of hits in the skill
public BattleCommandPositionBonus positionBonus; //bonus for front/flank/rear
public BattleCommandProcRequirement procRequirement;//if the skill requires a block/parry/evade before using
public int range; //max distance to use skill
public float range; //maximum distance to target to be able to use this skill
public float minRange; //Minimum distance to target to be able to use this skill
public uint statusId; //id of statuseffect that the skill might inflict
public uint statusDuration; //duration of statuseffect in milliseconds
@ -121,7 +122,12 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
public ushort animationDurationSeconds;
public uint battleAnimation;
public ushort worldMasterTextId;
public int aoeRange; //size of aoe effect. (how will this work for box aoes?)
public float aoeRange; //Radius for circle and cone aoes, length for box aoes
public float aoeMinRange; //Minimum range of aoe effect for things like Lunar Dynamo or Arrow Helix
public float aoeConeAngle; //Angle of aoe cones
public float aoeRotateAngle; //Amount aoes are rotated about the target position (usually the user's position)
public float rangeHeight; //Total height a skill can be used against target above or below user
public float rangeWidth; //Width of box aoes
public int[] comboNextCommandId = new int[2]; //next two skills in a combo
public short comboStep; //Where in a combo string this skill is
public CommandType commandType;
@ -130,12 +136,13 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
public byte statusTier; //tier of status to put on target
public ulong statusMagnitude = 0; //magnitude of status to put on target
public double statusMagnitude = 0; //magnitude of status to put on target
public ushort basePotency; //damage variable
public float enmityModifier; //multiples by damage done to get final enmity
public float accuracyModifier; //modifies accuracy
public float bonusCritRate; //extra crit rate
public bool isCombo;
public bool comboEffectAdded = false; //If the combo effect is added to multiple hiteffects it plays multiple times, so this keeps track of that
public bool isRanged = false;
public bool actionCrit; //Whether any actions were critical hits, used for Excruciate
@ -193,11 +200,11 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
if (aoeType == TargetFindAOEType.Box)
{
targetFind.SetAOEBox(validTarget, aoeTarget, range, aoeRange);
targetFind.SetAOEBox(validTarget, aoeTarget, aoeRange, rangeWidth, aoeRotateAngle);
}
else
{
targetFind.SetAOEType(validTarget, aoeType, aoeTarget, range, aoeRange);
targetFind.SetAOEType(validTarget, aoeType, aoeTarget, aoeRange, aoeMinRange, rangeHeight, aoeRotateAngle, aoeConeAngle);
}
/*

View file

@ -127,7 +127,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return AddStatusEffect(se, owner);
}
public bool AddStatusEffect(uint id, byte tier, UInt64 magnitude)
public bool AddStatusEffect(uint id, byte tier, double magnitude)
{
var se = Server.GetWorldManager().GetStatusEffect(id);
@ -137,7 +137,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return AddStatusEffect(se, owner);
}
public bool AddStatusEffect(uint id, byte tier, UInt64 magnitude, uint duration, int tickMs = 3000)
public bool AddStatusEffect(uint id, byte tier, double magnitude, uint duration, int tickMs = 3000)
{
var se = Server.GetWorldManager().GetStatusEffect(id);
if (se != null)

View file

@ -240,7 +240,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
var targetPos = new Vector3(owner.target.positionX, owner.target.positionY, owner.target.positionZ);
var distance = Utils.Distance(owner.positionX, owner.positionY, owner.positionZ, targetPos.X, targetPos.Y, targetPos.Z);
if (distance > owner.GetAttackRange() - 0.2f || owner.aiContainer.CanFollowPath())
if (distance > owner.GetAttackRange() - 0.2f && owner.aiContainer.CanFollowPath())
{
if (CanMoveForward(distance))
{

View file

@ -70,13 +70,19 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
class TargetFind
{
private Character owner;
private Character masterTarget; // if target is a pet, this is the owner
private Character masterTarget; // if target is a pet, this is the owner
private TargetFindCharacterType findType;
private ValidTarget validTarget;
private TargetFindAOETarget aoeTarget;
private TargetFindAOEType aoeType;
private Vector3 targetPosition;
private float maxDistance;
private Vector3 aoeTargetPosition; //This is the center of circle an cone AOEs and the position where line aoes come out
private float aoeTargetRotation; //This is the direction the aoe target is facing
private float maxDistance; //Radius for circle and cone AOEs, length for line AOEs
private float minDistance; //Minimum distance to that target must be to be able to be hit
private float width; //Width of line AOEs
private float height; //All AoEs are boxes or cylinders. Height is usually 10y regardless of maxDistance, but some commands have different values. Height is total height, so targets can be at most half this distance away on Y axis
private float aoeRotateAngle; //This is the angle that cones and line aoes are rotated about aoeTargetPosition for skills that come out of a side other than the front
private float coneAngle; //The angle of the cone itself in Pi Radians
private float param;
private List<Character> targets;
@ -92,8 +98,14 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
this.validTarget = ValidTarget.Enemy;
this.aoeType = TargetFindAOEType.None;
this.aoeTarget = TargetFindAOETarget.Target;
this.targetPosition = null;
this.aoeTargetPosition = null;
this.aoeTargetRotation = 0;
this.maxDistance = 0.0f;
this.minDistance = 0.0f;
this.width = 0.0f;
this.height = 0.0f;
this.aoeRotateAngle = 0.0f;
this.coneAngle = 0.0f;
this.param = 0.0f;
this.targets = new List<Character>();
}
@ -117,12 +129,16 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
/// <see cref="TargetFindAOEType.Box"/> - width of box / 2 (todo: set box length not just between user and target)
/// </param>
/// <param name="param"> param in degrees of cone (todo: probably use radians and forget converting at runtime) </param>
public void SetAOEType(ValidTarget validTarget, TargetFindAOEType aoeType, TargetFindAOETarget aoeTarget, float maxDistance = -1.0f, float param = -1.0f)
public void SetAOEType(ValidTarget validTarget, TargetFindAOEType aoeType, TargetFindAOETarget aoeTarget, float maxDistance, float minDistance, float height, float aoeRotate, float coneAngle, float param = 0.0f)
{
this.validTarget = validTarget;
this.aoeType = aoeType;
this.maxDistance = maxDistance != -1.0f ? maxDistance : 0.0f;
this.param = param != -1.0f ? param : 0.0f;
this.maxDistance = maxDistance;
this.minDistance = minDistance;
this.param = param;
this.height = height;
this.aoeRotateAngle = aoeRotate;
this.coneAngle = coneAngle;
}
/// <summary>
@ -132,15 +148,16 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
/// <param name="aoeTarget"></param>
/// <param name="length"></param>
/// <param name="width"></param>
public void SetAOEBox(ValidTarget validTarget, TargetFindAOETarget aoeTarget, float length, float width)
public void SetAOEBox(ValidTarget validTarget, TargetFindAOETarget aoeTarget, float length, float width, float aoeRotateAngle)
{
this.validTarget = validTarget;
this.aoeType = TargetFindAOEType.Box;
this.aoeTarget = aoeTarget;
this.aoeRotateAngle = aoeRotateAngle;
float x = owner.positionX - (float)Math.Cos(owner.rotation + (float)(Math.PI / 2)) * (length);
float z = owner.positionZ + (float)Math.Sin(owner.rotation + (float)(Math.PI / 2)) * (length);
this.targetPosition = new Vector3(x, owner.positionY, z);
this.maxDistance = width;
this.maxDistance = length;
this.width = width;
}
/// <summary>
@ -163,9 +180,15 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
validTarget = flags;
// are we creating aoe circles around target or self
if (aoeTarget == TargetFindAOETarget.Self)
this.targetPosition = owner.GetPosAsVector3();
{
this.aoeTargetPosition = owner.GetPosAsVector3();
this.aoeTargetRotation = owner.rotation + (float) (aoeRotateAngle * Math.PI);
}
else
this.targetPosition = target.GetPosAsVector3();
{
this.aoeTargetPosition = target.GetPosAsVector3();
this.aoeTargetRotation = target.rotation + (float) (aoeRotateAngle * Math.PI);
}
masterTarget = TryGetMasterTarget(target) ?? target;
@ -179,66 +202,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
{
AddAllInRange(target, withPet);
}
/*
if (aoeType != TargetFindAOEType.None)
{
if (IsPlayer(owner))
{
if (masterTarget is Player)
{
findType = TargetFindCharacterType.PlayerToPlayer;
if (masterTarget.currentParty != null)
{
if ((validTarget & (ValidTarget.Ally | ValidTarget.PartyMember)) != 0)
AddAllInAlliance(masterTarget, withPet);
else
AddAllInParty(masterTarget, withPet);
}
else
{
AddTarget(masterTarget, withPet);
}
}
else
{
findType = TargetFindCharacterType.PlayerToBattleNpc;
AddAllBattleNpcs(masterTarget, false);
}
}
else
{
// todo: this needs checking..
if (masterTarget is Player || owner.allegiance == CharacterTargetingAllegiance.Player)
findType = TargetFindCharacterType.BattleNpcToPlayer;
else
findType = TargetFindCharacterType.BattleNpcToBattleNpc;
// todo: configurable pet aoe buff
if (findType == TargetFindCharacterType.BattleNpcToBattleNpc && TryGetMasterTarget(target) != null)
withPet = true;
// todo: does ffxiv have call for help flag?
//if ((findFlags & ValidTarget.HitAll) != 0)
//{
// AddAllInZone(masterTarget, withPet);
//}
AddAllInAlliance(target, withPet);
if (findType == TargetFindCharacterType.BattleNpcToPlayer)
{
if (owner.allegiance == CharacterTargetingAllegiance.Player)
AddAllInZone(masterTarget, withPet);
else
AddAllInHateList();
}
}
}*/
if (targets.Count > 8)
targets.RemoveRange(8, targets.Count - 8);
//if (targets.Count > 8)
//targets.RemoveRange(8, targets.Count - 8);
//Curaga starts with lowest health players, so the targets are definitely sorted at least for some abilities
//Other aoe abilities might be sorted by distance?
@ -252,28 +218,33 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
/// </summary>
private bool IsWithinBox(Character target, bool withPet)
{
if (aoeTarget == TargetFindAOETarget.Self)
targetPosition = owner.GetPosAsVector3();
else
targetPosition = target.GetPosAsVector3();
Vector3 vec = target.GetPosAsVector3() - aoeTargetPosition;
Vector3 relativePos = new Vector3();
var myPos = owner.GetPosAsVector3();
var angle = Vector3.GetAngle(myPos, targetPosition);
//Get target's position relative to owner's position where owner's front is facing positive z axis
relativePos.X = (float)(vec.X * Math.Cos(aoeTargetRotation) - vec.Z * Math.Sin(aoeTargetRotation));
relativePos.Z = (float)(vec.X * Math.Sin(aoeTargetRotation) + vec.Z * Math.Cos(aoeTargetRotation));
// todo: actually check this works..
var myCorner = myPos.NewHorizontalVector(angle, maxDistance);
var myCorner2 = myPos.NewHorizontalVector(angle, -maxDistance);
float halfHeight = height / 2;
float halfWidth = width / 2;
var targetCorner = targetPosition.NewHorizontalVector(angle, maxDistance);
var targetCorner2 = targetPosition.NewHorizontalVector(angle, -maxDistance);
Vector3 closeBottomLeft = new Vector3(-halfWidth, -halfHeight, minDistance);
Vector3 farTopRight = new Vector3(halfWidth, halfHeight, maxDistance);
return target.GetPosAsVector3().IsWithinBox(targetCorner2, myCorner);
return relativePos.IsWithinBox(closeBottomLeft, farTopRight);
}
private bool IsWithinCone(Character target, bool withPet)
{
// todo: make this actual cone
return owner.IsFacing(target, param) && Utils.Distance(owner.positionX, owner.positionY, owner.positionZ, target.positionX, target.positionY, target.positionZ) < maxDistance;
double distance = Utils.XZDistance(aoeTargetPosition, target.GetPosAsVector3());
//Make sure target is within the correct range first
if (!IsWithinCircle(target))
return false;
//This might not be 100% right or the most optimal way to do this
//Get between taget's position and our position
return target.GetPosAsVector3().IsWithinCone(aoeTargetPosition, aoeTargetRotation, coneAngle);
}
private void AddTarget(Character target, bool withPet)
@ -364,10 +335,15 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
if ((validTarget & ValidTarget.NPC) == 0 && target.isStatic)
return false;
//This skill can't be used on corpses and target is dead, return false
if ((validTarget & ValidTarget.Corpse) == 0 && target.IsDead())
return false;
//This skill must be used on Allies and target is not an ally, return false
if ((validTarget & ValidTarget.Ally) != 0 && target.allegiance != owner.allegiance)
return false;
//This skill can't be used on players and target is a player, return false
//Do we need a player flag? Ally/Enemy flags probably serve the same purpose
//if ((validTarget & ValidTarget.Player) == 0 && target is Player)
@ -404,7 +380,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
if (!ignoreAOE)
{
// hit everything within zone or within aoe region
if (param == -1.0f || aoeType == TargetFindAOEType.Circle && !IsWithinCircle(target, param))
if (param == -1.0f || aoeType == TargetFindAOEType.Circle && !IsWithinCircle(target))
return false;
if (aoeType == TargetFindAOEType.Cone && !IsWithinCone(target, withPet))
@ -419,16 +395,10 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return true;
}
private bool IsWithinCircle(Character target, float maxDistance)
private bool IsWithinCircle(Character target)
{
// todo: make y diff modifiable?
//if (Math.Abs(owner.positionX - target.positionY) > 6.0f)
// return false;
if (this.targetPosition == null)
this.targetPosition = aoeTarget == TargetFindAOETarget.Self ? owner.GetPosAsVector3() : masterTarget.GetPosAsVector3();
return target.GetPosAsVector3().IsWithinCircle(targetPosition, maxDistance);
//Check if XZ is in circle and that y difference isn't larger than half height
return target.GetPosAsVector3().IsWithinCircle(aoeTargetPosition, maxDistance, minDistance) && Math.Abs(owner.positionY - target.positionY) <= (height / 2);
}
private bool IsPlayer(Character target)

View file

@ -47,7 +47,20 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
}
else
{
//owner.LookAt(target);
if (!skill.IsInstantCast())
{
float castTime = skill.castTimeMs;
// command casting duration
if (owner is Player)
{
// todo: modify spellSpeed based on modifiers and stuff
((Player)owner).SendStartCastbar(skill.id, Utils.UnixTimeStampUTC(DateTime.Now.AddMilliseconds(castTime)));
}
owner.SendChant(0xf, 0x0);
//You ready [skill] (6F000002: BLM, 6F000003: WHM, 0x6F000008: BRD)
owner.DoBattleAction(skill.id, (uint)0x6F000000 | skill.castType, new BattleAction(target.actorId, 30126, 1, 0, 1));
}
}
}
@ -66,7 +79,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
// todo: check weapon delay/haste etc and use that
var actualCastTime = skill.castTimeMs;
if ((tick - startTime).Milliseconds >= skill.castTimeMs)
if ((tick - startTime).TotalMilliseconds >= skill.castTimeMs)
{
OnComplete();
return true;
@ -88,6 +101,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
public override void OnComplete()
{
owner.LookAt(target);
bool hitTarget = false;
skill.targetFind.FindWithinArea(target, skill.validTarget, skill.aoeTarget);
@ -130,6 +144,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
public override void Cleanup()
{
owner.SendChant(0, 0);
owner.aiContainer.UpdateLastActionTime(skill.animationDurationSeconds);
}
}

View file

@ -104,7 +104,6 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
//List<BattleAction> actions = new List<BattleAction>();
BattleActionContainer actions = new BattleActionContainer();
target.SetMod((uint) Modifier.MinimumHpLock, 0);
var i = 0;
for (int hitNum = 0; hitNum < 1 /* owner.GetMod((uint) Modifier.HitCount)*/; hitNum++)
@ -123,7 +122,8 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
BattleAction error = null;// new BattleAction(0, null, 0, 0);
//owner.DoActions(null, actions.GetList(), ref error);
//owner.OnAttack(this, actions[0], ref errorResult);
owner.DoBattleAction(22104, 0x19001000, actions.GetList());
var anim = (uint)(17 << 24 | 1 << 12);
owner.DoBattleAction(22104, anim, actions.GetList());
}
public override void TryInterrupt()
@ -161,6 +161,11 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
{
return false;
}
if (!owner.IsFacing(target))
{
return false;
}
// todo: shouldnt need to check if owner is dead since all states would be cleared
if (owner.IsDead() || target.IsDead())
{
@ -179,7 +184,8 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
{
if (owner is Player)
{
((Player)owner).SendGameMessage(Server.GetWorldManager().GetActor(), 32539, 0x20);
//The target is too far away
((Player)owner).SendGameMessage(Server.GetWorldManager().GetActor(), 32537, 0x20);
}
return false;
}

View file

@ -61,18 +61,21 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
// todo: check within attack range
float[] baseCastDuration = { 1.0f, 0.25f };
//There are no positional spells, so just check onCombo, need to check first because certain spells change aoe type/accuracy
//If owner is a player and the spell being used is part of the current combo
if (owner is Player p && p.GetClass() == spell.job)
{
if (spell.comboStep == 1 || ((p.playerWork.comboNextCommandId[0] == spell.id || p.playerWork.comboNextCommandId[1] == spell.id)))
{
lua.LuaEngine.CallLuaBattleCommandFunction(owner, spell, "magic", "onCombo", owner, target, spell);
spell.isCombo = true;
}
}
//Check combo stuff here because combos can impact spell cast times
float spellSpeed = spell.castTimeMs;
//There are no positional spells, so just check onCombo, need to check first because certain spells change aoe type/accuracy
//If owner is a player and the spell being used is part of the current combo
if (spell.comboStep == 1 || ((owner is Player p) && (p.playerWork.comboNextCommandId[0] == spell.id || p.playerWork.comboNextCommandId[1] == spell.id)))
{
lua.LuaEngine.CallLuaBattleCommandFunction(owner, spell, "magic", "onCombo", owner, target, spell);
spell.isCombo = true;
}
if (!spell.IsInstantCast())
{
// command casting duration

View file

@ -46,7 +46,6 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
}
else
{
owner.LookAt(target);
hitDirection = owner.GetHitDirection(target);
//Do positionals and combo effects first because these can influence accuracy and amount of targets/numhits, which influence the rest of the steps
@ -76,6 +75,21 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
skill.isCombo = true;
}
}
if (!skill.IsInstantCast())
{
float castTime = skill.castTimeMs;
// command casting duration
if (owner is Player)
{
// todo: modify spellSpeed based on modifiers and stuff
((Player)owner).SendStartCastbar(skill.id, Utils.UnixTimeStampUTC(DateTime.Now.AddMilliseconds(castTime)));
}
owner.SendChant(0xf, 0x0);
//You ready [skill] (6F000002: BLM, 6F000003: WHM, 0x6F000008: BRD)
owner.DoBattleAction(skill.id, (uint)0x6F000000 | skill.castType, new BattleAction(target.actorId, 30126, 1, 0, 1));
}
}
}
}
@ -95,7 +109,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
// todo: check weapon delay/haste etc and use that
var actualCastTime = skill.castTimeMs;
if ((tick - startTime).Milliseconds >= skill.castTimeMs)
if ((tick - startTime).TotalMilliseconds >= skill.castTimeMs)
{
OnComplete();
return true;
@ -117,6 +131,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
public override void OnComplete()
{
owner.LookAt(target);
skill.targetFind.FindWithinArea(target, skill.validTarget, skill.aoeTarget);
isCompleted = true;

View file

@ -542,8 +542,11 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
hitEffect |= HitTypeEffects[hitType];
//For combos that land, add the combo effect
if (skill != null && skill.isCombo && hitType > HitType.Evade && hitType != HitType.Evade)
if (skill != null && skill.isCombo && hitType > HitType.Evade && hitType != HitType.Evade && !skill.comboEffectAdded)
{
hitEffect |= (HitEffect)(skill.comboStep << 15);
skill.comboEffectAdded = true;
}
//if attack hit the target, take into account protective status effects
if (hitType >= HitType.Parry)
@ -581,8 +584,11 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
hitEffect |= HitTypeEffects[hitType];
if (skill != null && skill.isCombo)
if (skill != null && skill.isCombo && !skill.comboEffectAdded)
{
hitEffect |= (HitEffect)(skill.comboStep << 15);
skill.comboEffectAdded = true;
}
//if attack hit the target, take into account protective status effects
if (hitType >= HitType.Block)

View file

@ -2124,6 +2124,7 @@ namespace FFXIVClassic_Map_Server.Actors
public override bool CanCast(Character target, BattleCommand spell)
{
//Might want to do these with a BattleAction instead to be consistent with the rest of command stuff
if (GetHotbarTimer(spell.id) > Utils.UnixTimeStampUTC())
{
// todo: this needs confirming
@ -2137,12 +2138,30 @@ namespace FFXIVClassic_Map_Server.Actors
SendGameMessage(Server.GetWorldManager().GetActor(), 32511, 0x20, (uint)spell.id);
return false;
}
if (Utils.Distance(positionX, positionY, positionZ, target.positionX, target.positionY, target.positionZ) > spell.range)
if (Utils.XZDistance(positionX, positionZ, target.positionX, target.positionZ) > spell.range)
{
// The target is out of range.
// The target is too far away.
SendGameMessage(Server.GetWorldManager().GetActor(), 32539, 0x20, (uint)spell.id);
return false;
}
if (Utils.XZDistance(positionX, positionZ, target.positionX, target.positionZ) < spell.minRange)
{
// The target is too close.
SendGameMessage(Server.GetWorldManager().GetActor(), 32538, 0x20, (uint)spell.id);
return false;
}
if (target.positionY - positionY > (spell.rangeHeight / 2))
{
// The target is too far above you.
SendGameMessage(Server.GetWorldManager().GetActor(), 32540, 0x20, (uint)spell.id);
return false;
}
if (positionY - target.positionY > (spell.rangeHeight / 2))
{
// The target is too far below you.
SendGameMessage(Server.GetWorldManager().GetActor(), 32541, 0x20, (uint)spell.id);
return false;
}
if (!IsValidTarget(target, spell.mainTarget) || !spell.IsValidMainTarget(this, target))
{
// error packet is set in IsValidTarget
@ -2169,13 +2188,38 @@ namespace FFXIVClassic_Map_Server.Actors
return false;
}
if (Utils.Distance(positionX, positionY, positionZ, target.positionX, target.positionY, target.positionZ) > skill.range)
//Original game checked height difference before horizontal distance
if (target.positionY - positionY > (skill.rangeHeight / 2))
{
// The target is out of range.
SendGameMessage(Server.GetWorldManager().GetActor(), 32539, 0x20, (uint)skill.id);
// The target is too far above you.
SendGameMessage(Server.GetWorldManager().GetActor(), 32540, 0x20, (uint)skill.id);
return false;
}
if (positionY - target.positionY > (skill.rangeHeight / 2))
{
// The target is too far below you.
SendGameMessage(Server.GetWorldManager().GetActor(), 32541, 0x20, (uint)skill.id);
return false;
}
var targetDist = Utils.XZDistance(positionX, positionZ, target.positionX, target.positionZ);
if (targetDist > skill.range)
{
// The target is out of range.
SendGameMessage(Server.GetWorldManager().GetActor(), 32537, 0x20, (uint)skill.id);
return false;
}
if (targetDist < skill.minRange)
{
// The target is too close.
SendGameMessage(Server.GetWorldManager().GetActor(), 32538, 0x20, (uint)skill.id);
return false;
}
if (!IsValidTarget(target, skill.validTarget) || !skill.IsValidMainTarget(this, target))
{
// error packet is set in IsValidTarget
@ -2379,7 +2423,8 @@ namespace FFXIVClassic_Map_Server.Actors
//If we're starting or continuing a combo chain, add the status effect and combo cost bonus
if (comboIds[0] != 0)
{
StatusEffect comboEffect = new StatusEffect(this, (uint) StatusEffectId.Combo, 1, 0, 13);
StatusEffect comboEffect = new StatusEffect(this, Server.GetWorldManager().GetStatusEffect((uint) StatusEffectId.Combo));
comboEffect.SetDuration(13);
comboEffect.SetOverwritable(1);
statusEffects.AddStatusEffect(comboEffect, this, true);
playerWork.comboCostBonusRate = 1;
@ -2469,5 +2514,12 @@ namespace FFXIVClassic_Map_Server.Actors
}
}
}
public bool HasItemEquippedInSlot(uint itemId, ushort slot)
{
var equippedItem = equipment.GetItemAtSlot(slot);
return equippedItem != null && equippedItem.itemId == itemId;
}
}
}

View file

@ -32,8 +32,8 @@ namespace FFXIVClassic_Map_Server.packets.send.actor
binWriter.Write((byte)breakage);
binWriter.Write((byte)(((leftChant & 0xF) << 4) | (rightChant & 0xF)));
binWriter.Write((byte)(guard & 0xF));
binWriter.Write((byte)((wasteStat & 0xF) << 4));
binWriter.Write((byte)(statMode & 0xF));
binWriter.Write((byte)(wasteStat));
binWriter.Write((byte)(statMode));
binWriter.Write((byte)0);
binWriter.Write((UInt16)(idleAnimationId&0xFFFF));
}