Combat additions

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
This commit is contained in:
yogurt 2018-04-18 16:06:41 -05:00
parent b8d6a943aa
commit c5ce2ec771
239 changed files with 5125 additions and 1237 deletions

View File

@ -12,7 +12,7 @@ using FFXIVClassic_Map_Server.Actors;
using FFXIVClassic_Map_Server.actors.chara.player; using FFXIVClassic_Map_Server.actors.chara.player;
using FFXIVClassic_Map_Server.packets.receive.supportdesk; using FFXIVClassic_Map_Server.packets.receive.supportdesk;
using FFXIVClassic_Map_Server.actors.chara.ai; using FFXIVClassic_Map_Server.actors.chara.ai;
using FFXIVClassic_Map_Server.Actors.Chara; using FFXIVClassic_Map_Server.packets.send.actor.battle;
using FFXIVClassic_Map_Server.actors.chara; using FFXIVClassic_Map_Server.actors.chara;
namespace FFXIVClassic_Map_Server namespace FFXIVClassic_Map_Server
@ -2261,7 +2261,7 @@ namespace FFXIVClassic_Map_Server
conn.Open(); 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, aoeTarget, basePotency, numHits, positionBonus, procRequirement, `range`, statusId, statusDuration, statusChance, " +
"castType, castTime, recastTime, mpCost, tpCost, animationType, effectAnimation, modelAnimation, animationDuration, battleAnimation, validUser, comboId1, comboId2, comboStep, accuracyMod, worldMasterTextId FROM server_battle_commands;"); "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); MySqlCommand cmd = new MySqlCommand(query, conn);
@ -2306,6 +2306,9 @@ namespace FFXIVClassic_Map_Server
battleCommand.comboNextCommandId[0] = reader.GetInt32("comboId1"); battleCommand.comboNextCommandId[0] = reader.GetInt32("comboId1");
battleCommand.comboNextCommandId[1] = reader.GetInt32("comboId2"); battleCommand.comboNextCommandId[1] = reader.GetInt32("comboId2");
battleCommand.comboStep = reader.GetInt16("comboStep"); battleCommand.comboStep = reader.GetInt16("comboStep");
battleCommand.commandType = (CommandType) reader.GetInt16("commandType");
battleCommand.actionProperty = (ActionProperty)reader.GetInt16("actionProperty");
battleCommand.actionType = (ActionType)reader.GetInt16("actionType");
battleCommand.accuracyModifier = reader.GetFloat("accuracyMod"); battleCommand.accuracyModifier = reader.GetFloat("accuracyMod");
battleCommand.worldMasterTextId = reader.GetUInt16("worldMasterTextId"); battleCommand.worldMasterTextId = reader.GetUInt16("worldMasterTextId");
lua.LuaEngine.LoadBattleCommandScript(battleCommand, "weaponskill"); lua.LuaEngine.LoadBattleCommandScript(battleCommand, "weaponskill");
@ -2338,6 +2341,60 @@ namespace FFXIVClassic_Map_Server
} }
} }
public static void LoadGlobalBattleTraitList(Dictionary<ushort, BattleTrait> battleTraitDict, Dictionary<byte, List<ushort>> battleTraitJobDict)
{
using (MySqlConnection conn = new MySqlConnection(String.Format("Server={0}; Port={1}; Database={2}; UID={3}; Password={4}", ConfigConstants.DATABASE_HOST, ConfigConstants.DATABASE_PORT, ConfigConstants.DATABASE_NAME, ConfigConstants.DATABASE_USERNAME, ConfigConstants.DATABASE_PASSWORD)))
{
try
{
int count = 0;
conn.Open();
var query = ("SELECT `id`, name, classJob, lvl, modifier, bonus FROM server_battle_traits;");
MySqlCommand cmd = new MySqlCommand(query, conn);
using (MySqlDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var id = reader.GetUInt16("id");
var name = reader.GetString("name");
var job = reader.GetByte("classJob");
var level = reader.GetByte("lvl");
uint modifier = reader.GetUInt32("modifier");
var bonus = reader.GetInt32("bonus");
var trait = new BattleTrait(id, name, job, level, modifier, bonus);
battleTraitDict.Add(id, trait);
if(battleTraitJobDict.ContainsKey(job))
{
battleTraitJobDict[job].Add(id);
}
else
{
battleTraitJobDict[job] = new List<ushort>();
battleTraitJobDict[job].Add(id);
}
count++;
}
}
Program.Log.Info(String.Format("Loaded {0} battle traits.", count));
}
catch (MySqlException e)
{
Program.Log.Error(e.ToString());
}
finally
{
conn.Dispose();
}
}
}
public static void SetExp(Player player, byte classId, int exp) public static void SetExp(Player player, byte classId, int exp)
{ {
using (MySqlConnection conn = new MySqlConnection(String.Format("Server={0}; Port={1}; Database={2}; UID={3}; Password={4}", ConfigConstants.DATABASE_HOST, ConfigConstants.DATABASE_PORT, ConfigConstants.DATABASE_NAME, ConfigConstants.DATABASE_USERNAME, ConfigConstants.DATABASE_PASSWORD))) using (MySqlConnection conn = new MySqlConnection(String.Format("Server={0}; Port={1}; Database={2}; UID={3}; Password={4}", ConfigConstants.DATABASE_HOST, ConfigConstants.DATABASE_PORT, ConfigConstants.DATABASE_NAME, ConfigConstants.DATABASE_USERNAME, ConfigConstants.DATABASE_PASSWORD)))

View File

@ -85,6 +85,7 @@
<Compile Include="actors\area\PrivateAreaContent.cs" /> <Compile Include="actors\area\PrivateAreaContent.cs" />
<Compile Include="actors\area\SpawnLocation.cs" /> <Compile Include="actors\area\SpawnLocation.cs" />
<Compile Include="actors\area\Zone.cs" /> <Compile Include="actors\area\Zone.cs" />
<Compile Include="actors\chara\ai\BattleTrait.cs" />
<Compile Include="actors\chara\ai\controllers\AllyController.cs" /> <Compile Include="actors\chara\ai\controllers\AllyController.cs" />
<Compile Include="actors\chara\ai\helpers\ActionQueue.cs" /> <Compile Include="actors\chara\ai\helpers\ActionQueue.cs" />
<Compile Include="actors\chara\ai\AIContainer.cs" /> <Compile Include="actors\chara\ai\AIContainer.cs" />
@ -196,6 +197,7 @@
<Compile Include="packets\send\actor\ActorInstantiatePacket.cs" /> <Compile Include="packets\send\actor\ActorInstantiatePacket.cs" />
<Compile Include="packets\send\actor\ActorSpecialGraphicPacket.cs" /> <Compile Include="packets\send\actor\ActorSpecialGraphicPacket.cs" />
<Compile Include="packets\send\actor\battle\BattleAction.cs" /> <Compile Include="packets\send\actor\battle\BattleAction.cs" />
<Compile Include="packets\send\actor\battle\BattleActionContainer.cs" />
<Compile Include="packets\send\actor\battle\BattleActionX00Packet.cs" /> <Compile Include="packets\send\actor\battle\BattleActionX00Packet.cs" />
<Compile Include="packets\send\actor\battle\BattleActionX18Packet.cs" /> <Compile Include="packets\send\actor\battle\BattleActionX18Packet.cs" />
<Compile Include="packets\send\actor\battle\BattleActionX10Packet.cs" /> <Compile Include="packets\send\actor\battle\BattleActionX10Packet.cs" />

View File

@ -54,9 +54,10 @@ namespace FFXIVClassic_Map_Server
mWorldManager.LoadActorClasses(); mWorldManager.LoadActorClasses();
mWorldManager.LoadSpawnLocations(); mWorldManager.LoadSpawnLocations();
mWorldManager.LoadBattleNpcs(); mWorldManager.LoadBattleNpcs();
mWorldManager.SpawnAllActors();
mWorldManager.LoadStatusEffects(); mWorldManager.LoadStatusEffects();
mWorldManager.LoadBattleCommands(); mWorldManager.LoadBattleCommands();
mWorldManager.LoadBattleTraits();
mWorldManager.SpawnAllActors();
mWorldManager.StartZoneThread(); mWorldManager.StartZoneThread();
IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse(ConfigConstants.OPTIONS_BINDIP), int.Parse(ConfigConstants.OPTIONS_PORT)); IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse(ConfigConstants.OPTIONS_BINDIP), int.Parse(ConfigConstants.OPTIONS_PORT));

View File

@ -41,6 +41,8 @@ namespace FFXIVClassic_Map_Server
private Dictionary<uint, StatusEffect> statusEffectList = new Dictionary<uint, StatusEffect>(); private Dictionary<uint, StatusEffect> statusEffectList = new Dictionary<uint, StatusEffect>();
private Dictionary<ushort, BattleCommand> battleCommandList = new Dictionary<ushort, BattleCommand>(); private Dictionary<ushort, BattleCommand> battleCommandList = new Dictionary<ushort, BattleCommand>();
private Dictionary<Tuple<byte, short>, List<uint>> battleCommandIdByLevel = new Dictionary<Tuple<byte, short>, List<uint>>();//Holds battle command ids keyed by class id and level (in that order) private Dictionary<Tuple<byte, short>, List<uint>> battleCommandIdByLevel = new Dictionary<Tuple<byte, short>, List<uint>>();//Holds battle command ids keyed by class id and level (in that order)
private Dictionary<ushort, BattleTrait> battleTraitList = new Dictionary<ushort, BattleTrait>();
private Dictionary<byte, List<ushort>> battleTraitIdsForClass = new Dictionary<byte, List<ushort>>();
private Dictionary<uint, ModifierList> battleNpcGenusMods = new Dictionary<uint, ModifierList>(); private Dictionary<uint, ModifierList> battleNpcGenusMods = new Dictionary<uint, ModifierList>();
private Dictionary<uint, ModifierList> battleNpcPoolMods = new Dictionary<uint, ModifierList>(); private Dictionary<uint, ModifierList> battleNpcPoolMods = new Dictionary<uint, ModifierList>();
private Dictionary<uint, ModifierList> battleNpcSpawnMods = new Dictionary<uint, ModifierList>(); private Dictionary<uint, ModifierList> battleNpcSpawnMods = new Dictionary<uint, ModifierList>();
@ -1473,6 +1475,11 @@ namespace FFXIVClassic_Map_Server
Database.LoadGlobalBattleCommandList(battleCommandList, battleCommandIdByLevel); Database.LoadGlobalBattleCommandList(battleCommandList, battleCommandIdByLevel);
} }
public void LoadBattleTraits()
{
Database.LoadGlobalBattleTraitList(battleTraitList, battleTraitIdsForClass);
}
public BattleCommand GetBattleCommand(uint id) public BattleCommand GetBattleCommand(uint id)
{ {
BattleCommand battleCommand; BattleCommand battleCommand;
@ -1484,5 +1491,19 @@ namespace FFXIVClassic_Map_Server
List<uint> ids; List<uint> ids;
return battleCommandIdByLevel.TryGetValue(Tuple.Create(classId, level), out ids) ? ids : new List<uint>(); return battleCommandIdByLevel.TryGetValue(Tuple.Create(classId, level), out ids) ? ids : new List<uint>();
} }
public BattleTrait GetBattleTrait(ushort id)
{
BattleTrait battleTrait;
battleTraitList.TryGetValue(id, out battleTrait);
return battleTrait;
}
public List<ushort> GetAllBattleTraitIdsForClass(byte classId)
{
List<ushort> ids;
return battleTraitIdsForClass.TryGetValue(classId, out ids) ? ids : new List<ushort>();
}
} }
} }

View File

@ -26,6 +26,9 @@ namespace FFXIVClassic_Map_Server.Actors
Appearance = 0x10, Appearance = 0x10,
Speed = 0x20, Speed = 0x20,
Work = 0x40, Work = 0x40,
Stats = 0x80,
Status = 0x100,
StatusTime = 0x200,
AllNpc = 0x6F, AllNpc = 0x6F,
AllPlayer = 0x9F AllPlayer = 0x9F
@ -375,6 +378,15 @@ namespace FFXIVClassic_Map_Server.Actors
} }
} }
public void ModifySpeed(float mod)
{
for (int i = 0; i < 4; i++)
{
moveSpeeds[i] *= mod;
}
updateFlags |= ActorUpdateFlags.Speed;
}
public void ChangeSpeed(int type, float value) public void ChangeSpeed(int type, float value)
{ {
moveSpeeds[type] = value; moveSpeeds[type] = value;

View File

@ -500,8 +500,8 @@ namespace FFXIVClassic_Map_Server.Actors
npc = new Npc(mActorList.Count + 1, actorClass, uniqueId, this, x, y, z, rot, state, animId, null); npc = new Npc(mActorList.Count + 1, actorClass, uniqueId, this, x, y, z, rot, state, animId, null);
npc.LoadEventConditions(actorClass.eventConditions); npc.LoadEventConditions(actorClass.eventConditions);
npc.SetMaxHP(30000); npc.SetMaxHP(100);
npc.SetHP(30000); npc.SetHP(100);
npc.ResetMoveSpeeds(); npc.ResetMoveSpeeds();
AddActorToZone(npc); AddActorToZone(npc);

View File

@ -2,10 +2,11 @@
{ {
class BattleTemp class BattleTemp
{ {
//Are these right?
public const uint NAMEPLATE_SHOWN = 0; public const uint NAMEPLATE_SHOWN = 0;
public const uint TARGETABLE = 1; public const uint TARGETABLE = 1;
//public const uint NAMEPLATE_SHOWN2 = 2; public const uint NAMEPLATE_SHOWN2 = 2;
public const uint NAMEPLATE_SHOWN2 = 3; //public const uint NAMEPLATE_SHOWN2 = 3;
public const uint STAT_STRENGTH = 3; public const uint STAT_STRENGTH = 3;
public const uint STAT_VITALITY = 4; public const uint STAT_VITALITY = 4;
@ -25,13 +26,13 @@
public const uint STAT_ACCURACY = 15; public const uint STAT_ACCURACY = 15;
public const uint STAT_NORMALDEFENSE = 18; public const uint STAT_NORMALDEFENSE = 18;
public const uint STAT_EVASION = 16; public const uint STAT_EVASION = 16;
public const uint STAT_ATTACK_MAGIC = 24; public const uint STAT_ATTACK_MAGIC = 23;
public const uint STAT_HEAL_MAGIC = 25; public const uint STAT_HEAL_MAGIC = 24;
public const uint STAT_ENCHANCEMENT_MAGIC_POTENCY = 26; public const uint STAT_ENCHANCEMENT_MAGIC_POTENCY = 25;
public const uint STAT_ENFEEBLING_MAGIC_POTENCY = 27; public const uint STAT_ENFEEBLING_MAGIC_POTENCY = 26;
public const uint STAT_MAGIC_ACCURACY = 28; public const uint STAT_MAGIC_ACCURACY = 27;
public const uint STAT_MAGIC_EVASION = 29; public const uint STAT_MAGIC_EVASION = 28;
public const uint STAT_CRAFT_PROCESSING = 30; public const uint STAT_CRAFT_PROCESSING = 30;
public const uint STAT_CRAFT_MAGIC_PROCESSING = 31; public const uint STAT_CRAFT_MAGIC_PROCESSING = 31;
@ -43,6 +44,6 @@
public float[] castGauge_speed = { 1.0f, 0.25f}; public float[] castGauge_speed = { 1.0f, 0.25f};
public bool[] timingCommandFlag = new bool[4]; public bool[] timingCommandFlag = new bool[4];
public ushort[] generalParameter = new ushort[35]; public short[] generalParameter = new short[35];
} }
} }

View File

@ -117,7 +117,7 @@ namespace FFXIVClassic_Map_Server.Actors
public Pet pet; public Pet pet;
private Dictionary<Modifier, Int64> modifiers = new Dictionary<Modifier, long>(); private Dictionary<Modifier, double> modifiers = new Dictionary<Modifier, double>();
protected ushort hpBase, hpMaxBase, mpBase, mpMaxBase, tpBase; protected ushort hpBase, hpMaxBase, mpBase, mpMaxBase, tpBase;
protected BattleTemp baseStats = new BattleTemp(); protected BattleTemp baseStats = new BattleTemp();
@ -191,6 +191,7 @@ namespace FFXIVClassic_Map_Server.Actors
zone.BroadcastPacketsAroundActor(this, propPacketUtil.Done()); 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() public List<SubPacket> GetActorStatusPackets()
{ {
var propPacketUtil = new ActorPropertyPacketUtil("charaWork/status", this); var propPacketUtil = new ActorPropertyPacketUtil("charaWork/status", this);
@ -275,7 +276,11 @@ namespace FFXIVClassic_Map_Server.Actors
} }
else else
break; break;
animationId = 0; //If more than one packet is sent out, only send the animation once to avoid double playing.
//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;
} }
} }
@ -291,20 +296,51 @@ namespace FFXIVClassic_Map_Server.Actors
PathTo(target.positionX, target.positionY, target.positionZ, stepSize, maxPath, radius); PathTo(target.positionX, target.positionY, target.positionZ, stepSize, maxPath, radius);
} }
public Int64 GetMod(uint modifier) public double GetMod(Modifier modifier)
{ {
Int64 res; return GetMod((uint)modifier);
}
public double GetMod(uint modifier)
{
double res;
if (modifiers.TryGetValue((Modifier)modifier, out res)) if (modifiers.TryGetValue((Modifier)modifier, out res))
return res; return res;
return 0; return 0;
} }
public void SetMod(uint modifier, Int64 val) public void SetMod(uint modifier, double val)
{ {
if (modifiers.ContainsKey((Modifier)modifier)) if (modifiers.ContainsKey((Modifier)modifier))
modifiers[(Modifier)modifier] = val; modifiers[(Modifier)modifier] = val;
else else
modifiers.Add((Modifier)modifier, val); 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) public virtual void OnPath(Vector3 point)
@ -341,6 +377,21 @@ namespace FFXIVClassic_Map_Server.Actors
//DoBattleAction(21001, 0x7C000062, new BattleAction(this.actorId, 0, 1, 0, 0, 1)); //Attack Mode //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) if ((updateFlags & ActorUpdateFlags.HpTpMp) != 0)
{ {
var propPacketUtil = new ActorPropertyPacketUtil("charaWork/stateAtQuicklyForAll", this); var propPacketUtil = new ActorPropertyPacketUtil("charaWork/stateAtQuicklyForAll", this);
@ -351,6 +402,7 @@ namespace FFXIVClassic_Map_Server.Actors
propPacketUtil.AddProperty("charaWork.parameterTemp.tp"); propPacketUtil.AddProperty("charaWork.parameterTemp.tp");
packets.AddRange(propPacketUtil.Done()); packets.AddRange(propPacketUtil.Done());
} }
base.PostUpdate(tick, packets); base.PostUpdate(tick, packets);
} }
} }
@ -393,11 +445,12 @@ namespace FFXIVClassic_Map_Server.Actors
public virtual bool Engage(uint targid = 0, ushort newMainState = 0xFFFF) public virtual bool Engage(uint targid = 0, ushort newMainState = 0xFFFF)
{ {
// todo: attack the things // todo: attack the things
if (newMainState != 0xFFFF) /*if (newMainState != 0xFFFF)
{ {
this.newMainState = newMainState; currentMainState = newMainState;// this.newMainState = newMainState;
updateFlags |= ActorUpdateFlags.State;
} }
else if (aiContainer.CanChangeState()) else*/ if (aiContainer.CanChangeState())
{ {
if (targid == 0) if (targid == 0)
{ {
@ -423,11 +476,12 @@ namespace FFXIVClassic_Map_Server.Actors
public virtual bool Disengage(ushort newMainState = 0xFFFF) public virtual bool Disengage(ushort newMainState = 0xFFFF)
{ {
if (newMainState != 0xFFFF) /*if (newMainState != 0xFFFF)
{ {
this.newMainState = newMainState; currentMainState = newMainState;// this.newMainState = newMainState;
updateFlags |= ActorUpdateFlags.State;
} }
else if (IsEngaged()) else*/ if (IsEngaged())
{ {
aiContainer.Disengage(); aiContainer.Disengage();
return true; return true;
@ -463,7 +517,8 @@ namespace FFXIVClassic_Map_Server.Actors
RecalculateStats(); RecalculateStats();
} }
public virtual void Die(DateTime tick) //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 // todo: actual despawn timer
aiContainer.InternalDie(tick, 10); aiContainer.InternalDie(tick, 10);
@ -523,7 +578,7 @@ namespace FFXIVClassic_Map_Server.Actors
public byte GetHPP() public byte GetHPP()
{ {
return (byte)(charaWork.parameterSave.hp[0] == 0 ? 0 : (charaWork.parameterSave.hp[0] / charaWork.parameterSave.hpMax[0]) * 100); return (byte)(charaWork.parameterSave.hp[0] == 0 ? 0 : (charaWork.parameterSave.hp[0] / (float) charaWork.parameterSave.hpMax[0]) * 100);
} }
public void SetHP(uint hp) public void SetHP(uint hp)
@ -558,8 +613,8 @@ namespace FFXIVClassic_Map_Server.Actors
// todo: the following functions are virtuals since we want to check hidden item bonuses etc on player for certain conditions // 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) public virtual void AddHP(int hp)
{ {
// dont wanna die ded // dont wanna die ded, don't want to send update if hp isn't actually changed
if (IsAlive()) if (IsAlive() && hp != 0)
{ {
// todo: +/- hp and die // todo: +/- hp and die
// todo: battlenpcs probably have way more hp? // todo: battlenpcs probably have way more hp?
@ -567,9 +622,6 @@ namespace FFXIVClassic_Map_Server.Actors
addHp = addHp.Clamp((short)GetMod((uint)Modifier.MinimumHpLock), charaWork.parameterSave.hpMax[0]); addHp = addHp.Clamp((short)GetMod((uint)Modifier.MinimumHpLock), charaWork.parameterSave.hpMax[0]);
charaWork.parameterSave.hp[0] = (short)addHp; charaWork.parameterSave.hp[0] = (short)addHp;
if (charaWork.parameterSave.hp[0] < 1)
Die(Program.Tick);
updateFlags |= ActorUpdateFlags.HpTpMp; updateFlags |= ActorUpdateFlags.HpTpMp;
} }
} }
@ -585,6 +637,8 @@ namespace FFXIVClassic_Map_Server.Actors
} }
public void AddMP(int mp) public void AddMP(int mp)
{
if (IsAlive() && mp != 0)
{ {
charaWork.parameterSave.mp = (short)(charaWork.parameterSave.mp + mp).Clamp(ushort.MinValue, charaWork.parameterSave.mpMax); charaWork.parameterSave.mp = (short)(charaWork.parameterSave.mp + mp).Clamp(ushort.MinValue, charaWork.parameterSave.mpMax);
@ -592,12 +646,16 @@ namespace FFXIVClassic_Map_Server.Actors
updateFlags |= ActorUpdateFlags.HpTpMp; updateFlags |= ActorUpdateFlags.HpTpMp;
} }
}
public void AddTP(int tp) public void AddTP(int tp)
{ {
if (IsAlive()) if (IsAlive() && tp != 0)
{ {
charaWork.parameterTemp.tp = (short)((charaWork.parameterTemp.tp + tp).Clamp(0, 3000)); var addTp = charaWork.parameterTemp.tp + tp;
addTp = addTp.Clamp((int) GetMod(Modifier.MinimumTpLock), 3000);
charaWork.parameterTemp.tp = (short) addTp;
tpBase = (ushort)charaWork.parameterTemp.tp; tpBase = (ushort)charaWork.parameterTemp.tp;
updateFlags |= ActorUpdateFlags.HpTpMp; updateFlags |= ActorUpdateFlags.HpTpMp;
@ -661,12 +719,12 @@ namespace FFXIVClassic_Map_Server.Actors
//CalculateBaseStats(); //CalculateBaseStats();
} }
public void SetStat(uint statId, uint val) public void SetStat(uint statId, int val)
{ {
charaWork.battleTemp.generalParameter[statId] = (ushort)val; charaWork.battleTemp.generalParameter[statId] = (short)val;
} }
public ushort GetStat(uint statId) public short GetStat(uint statId)
{ {
return charaWork.battleTemp.generalParameter[statId]; return charaWork.battleTemp.generalParameter[statId];
} }
@ -674,7 +732,7 @@ namespace FFXIVClassic_Map_Server.Actors
public virtual float GetSpeed() public virtual float GetSpeed()
{ {
// todo: for battlenpc/player calculate speed // todo: for battlenpc/player calculate speed
return GetMod((uint)Modifier.Speed); return (float) GetMod((uint)Modifier.Speed);
} }
public virtual void OnAttack(State state, BattleAction action, ref BattleAction error) public virtual void OnAttack(State state, BattleAction action, ref BattleAction error)
@ -688,7 +746,7 @@ namespace FFXIVClassic_Map_Server.Actors
} }
// todo: call onAttack/onDamageTaken // todo: call onAttack/onDamageTaken
BattleUtils.DamageTarget(this, target, action, DamageTakenType.Attack); //BattleUtils.DamageTarget(this, target, DamageTakenType.Attack, action);
AddTP(200); AddTP(200);
target.AddTP(100); target.AddTP(100);
} }
@ -704,7 +762,7 @@ namespace FFXIVClassic_Map_Server.Actors
if (zone.FindActorInArea<Character>(action.targetId) is Character chara) if (zone.FindActorInArea<Character>(action.targetId) is Character chara)
{ {
//BattleUtils.HandleHitType(this, chara, action); //BattleUtils.HandleHitType(this, chara, action);
BattleUtils.DoAction(this, chara, action, DamageTakenType.Magic); //BattleUtils.DoAction(this, chara, action, DamageTakenType.Magic);
} }
} }
lua.LuaEngine.GetInstance().OnSignal("spellUsed"); lua.LuaEngine.GetInstance().OnSignal("spellUsed");
@ -719,14 +777,11 @@ namespace FFXIVClassic_Map_Server.Actors
//Should we just store the character insteado f having to find it again? //Should we just store the character insteado f having to find it again?
if (zone.FindActorInArea<Character>(action.targetId) is Character chara) if (zone.FindActorInArea<Character>(action.targetId) is Character chara)
{ {
BattleUtils.DoAction(this, chara, action, DamageTakenType.Weaponskill); //BattleUtils.DoAction(this, chara, action, DamageTakenType.Weaponskill);
} }
} }
this.DelTP(skill.tpCost); 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) public virtual void OnAbility(State state, BattleAction[] actions, BattleCommand ability, ref BattleAction[] errors)
@ -735,7 +790,7 @@ namespace FFXIVClassic_Map_Server.Actors
{ {
if (zone.FindActorInArea<Character>(action.targetId) is Character chara) if (zone.FindActorInArea<Character>(action.targetId) is Character chara)
{ {
BattleUtils.DoAction(this, chara, action, DamageTakenType.Ability); //BattleUtils.DoAction(this, chara, action, DamageTakenType.Ability);
} }
} }
} }
@ -755,9 +810,55 @@ namespace FFXIVClassic_Map_Server.Actors
} }
public virtual void OnDamageTaken(Character attacker, BattleAction action, DamageTakenType damageTakenType) 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) public UInt64 GetTempVar(string name)
@ -865,29 +966,8 @@ namespace FFXIVClassic_Map_Server.Actors
if (val) if (val)
{ {
StatusEffect procEffect = Server.GetWorldManager().GetStatusEffect(effectId); StatusEffect procEffect = Server.GetWorldManager().GetStatusEffect(effectId);
procEffect.SetDuration(5000); procEffect.SetDuration(5);
statusEffects.AddStatusEffect(procEffect, this, true, true); 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 //Otherwise we're reseting a proc, remove the status
else else
@ -921,9 +1001,124 @@ namespace FFXIVClassic_Map_Server.Actors
} }
//Called when this character evades attacker's action //Called when this character evades attacker's action
public void OnEvade(Character attacker, BattleAction 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();
}
} }
} }

View File

@ -6,60 +6,92 @@ using System.Threading.Tasks;
namespace FFXIVClassic_Map_Server.actors.chara namespace FFXIVClassic_Map_Server.actors.chara
{ {
//These will need to be redone at some point. remember to update tables in db.
//Consider using text_paramname sheet. that matches up with the stats on armor, but some things will need special handling
//Also, 0-35 should probably match with up BattleTemp
enum Modifier : UInt32 enum Modifier : UInt32
{ {
NAMEPLATE_SHOWN = 0,
TARGETABLE = 1,
NAMEPLATE_SHOWN2 = 2,
//NAMEPLATE_SHOWN2 = 3,
None = 0, Strength = 3,
Hp = 1, Vitality = 4,
HpPercent = 2, Dexterity = 5,
Mp = 3, Intelligence = 6,
MpPercent = 4, Mind = 7,
Tp = 5, Piety = 8,
TpPercent = 6,
Regen = 7,
Refresh = 8,
Strength = 9,
Vitality = 10,
Dexterity = 11,
Intelligence = 12,
Mind = 13,
Piety = 14,
Attack = 15,
Accuracy = 16,
Defense = 17,
Evasion = 18,
MagicAttack = 19,
MagicHeal = 20, // is this needed? shouldnt it just be calc'd from mind
MagicAccuracy = 21,
MagicEvasion = 22,
MagicDefense = 23,
MagicEnhancePotency = 24,
MagicEnfeeblingPotency = 25,
ResistFire = 26,
ResistIce = 27,
ResistWind = 28,
ResistLightning = 29,
ResistEarth = 30,
ResistWater = 31, // <3 u jorge
AttackRange = 32,
Speed = 33,
AttackDelay = 34,
CraftProcessing = 35, ResistFire = 9,
CraftMagicProcessing = 36, ResistIce = 10,
CraftProcessControl = 37, ResistWind = 11,
ResistLightning = 12,
ResistEarth = 13,
ResistWater = 14,
HarvestPotency = 38, Accuracy = 15,
HarvestLimit = 39, Evasion = 16,
HarvestRate = 40, Attack = 17,
Defense = 18, //Is there a magic defense stat? 19 maybe?
MagicAttack = 23,
MagicHeal = 24,
MagicEnhancePotency = 25,
MagicEnfeeblingPotency = 26,
Raise = 41, MagicAccuracy = 27,
MinimumHpLock = 42, // hp cannot fall below this value MagicEvasion = 28,
AttackType = 43, // slashing, piercing, etc
BlockRate = 44, CraftProcessing = 30,
Block = 45, CraftMagicProcessing = 31,
CritRating = 46, CraftProcessControl = 32,
HasShield = 47, // Need this because shields are required for blocks. Could have used BlockRate or Block but BlockRate is provided by Gallant Sollerets and Block is provided by some buffs.
HitCount = 48 // Amount of hits in an auto attack. Usually 1, 2 for h2h, 3 with spinning heel HarvestPotency = 33,
HarvestLimit = 34,
HarvestRate = 35,
None = 36,
Hp = 37,
HpPercent = 38,
Mp = 39,
MpPercent = 40,
Tp = 41,
TpPercent = 42,
Regen = 43,
Refresh = 44,
AttackRange = 45,
Speed = 46,
AttackDelay = 47,
Raise = 48,
MinimumHpLock = 49, // hp cannot fall below this value
AttackType = 50, // slashing, piercing, etc
BlockRate = 51,
Block = 52,
CritRating = 53,
HasShield = 54, // Need this because shields are required for blocks. Could have used BlockRate or Block but BlockRate is provided by Gallant Sollerets and Block is provided by some buffs.
HitCount = 55, // Amount of hits in an auto attack. Usually 1, 2 for h2h, 3 with spinning heel
//Flat percent increases to these rates. Probably a better way to do this
RawEvadeRate = 56,
RawParryRate = 57,
RawBlockRate = 58,
RawResistRate = 59,
RawHitRate = 60,
RawCritRate = 61,
DamageTakenDown = 62, // Percent damage taken down
StoreTP = 63, //.1% extra tp per point. Lancer trait is 50 StoreTP
PhysicalCritRate = 64, //CritRating but only for physical attacks. Increases chance of critting.
PhysicalCritEvasion = 65, //Opposite of CritRating. Reduces chance of being crit by phyiscal attacks
PhysicalCritAttack = 66, //Increases damage done by Physical Critical hits
PhysicalCritResilience = 67, //Decreases damage taken by Physical Critical hits
Parry = 68, //Increases chance to parry
MagicCritPotency = 69, //Increases
Regain = 70, //TP regen, should be -90 out of combat, Invigorate sets to 100+ depending on traits
RegenDown = 71, //Damage over time effects. Separate from normal Regen because of how they are displayed in game
Stoneskin = 72, //Nullifies damage
MinimumTpLock = 73
} }
} }

View File

@ -6,7 +6,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using FFXIVClassic_Map_Server.actors.chara.player; using FFXIVClassic_Map_Server.actors.chara.player;
using FFXIVClassic.Common; using FFXIVClassic.Common;
using FFXIVClassic_Map_Server.packets.send.actor; using FFXIVClassic_Map_Server.packets.send.actor.battle;
using FFXIVClassic_Map_Server.actors.chara.ai.utils; using FFXIVClassic_Map_Server.actors.chara.ai.utils;
using MoonSharp.Interpreter; using MoonSharp.Interpreter;
@ -62,6 +62,19 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
SongMagic = 8 SongMagic = 8
} }
//What type of command it is
[Flags]
public enum CommandType : ushort
{
//Type of action
None = 0,
AutoAttack = 1,
WeaponSkill = 2,
Ability =3,
Spell = 4
}
class BattleCommand class BattleCommand
{ {
public ushort id; public ushort id;
@ -96,13 +109,22 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
public int aoeRange; //size of aoe effect. (how will this work for box aoes?) public int aoeRange; //size of aoe effect. (how will this work for box aoes?)
public int[] comboNextCommandId = new int[2]; //next two skills in a combo public int[] comboNextCommandId = new int[2]; //next two skills in a combo
public short comboStep; //Where in a combo string this skill is public short comboStep; //Where in a combo string this skill is
public CommandType commandType;
public ActionProperty actionProperty;
public ActionType actionType;
public byte statusTier; //tief of status to put on target public byte statusTier; //tier of status to put on target
public ulong statusMagnitude = 0; //magnitude of status to put on target
public ushort basePotency; //damage variable public ushort basePotency; //damage variable
public float enmityModifier; //multiples by damage done to get final enmity public float enmityModifier; //multiples by damage done to get final enmity
public float accuracyModifier; //modifies accuracy public float accuracyModifier; //modifies accuracy
public float bonusCritRate; //extra crit rate
public bool isCombo; public bool isCombo;
public bool isRanged;
public bool actionCrit; //Whether any actions were critical hits, used for Excruciate
public lua.LuaScript script; //cached script public lua.LuaScript script; //cached script
public TargetFind targetFind; public TargetFind targetFind;
@ -114,7 +136,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
this.name = name; this.name = name;
this.range = 0; this.range = 0;
this.enmityModifier = 1; this.enmityModifier = 1;
this.accuracyModifier = 1; this.accuracyModifier = 0;
this.statusTier = 1; this.statusTier = 1;
this.statusChance = 50; this.statusChance = 50;
this.recastTimeMs = (uint) maxRecastTimeSeconds * 1000; this.recastTimeMs = (uint) maxRecastTimeSeconds * 1000;
@ -163,7 +185,6 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
targetFind.SetAOEType(validTarget, aoeType, aoeTarget, range, aoeRange); targetFind.SetAOEType(validTarget, aoeType, aoeTarget, range, aoeRange);
} }
/* /*
worldMasterTextId worldMasterTextId
32512 cannot be performed on a KO'd target. 32512 cannot be performed on a KO'd target.
@ -191,7 +212,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
{ {
if (user is Player) if (user is Player)
((Player)user).SendGameMessage(Server.GetWorldManager().GetActor(), 32527, 0x20, (uint)id); ((Player)user).SendGameMessage(Server.GetWorldManager().GetActor(), 32527, 0x20, (uint)id);
return false; //return false;
} }
//Proc requirement //Proc requirement
@ -362,5 +383,10 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return target; return target;
} }
public ushort GetCommandType()
{
return (ushort) commandType;
}
} }
} }

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FFXIVClassic_Map_Server.actors.chara.ai
{
class BattleTrait
{
public ushort id;
public string name;
public byte job;
public byte level;
public uint modifier;
public int bonus;
public BattleTrait(ushort id, string name, byte job, byte level, uint modifier, int bonus)
{
this.id = id;
this.name = name;
this.job = job;
this.level = level;
this.modifier = modifier;
this.bonus = bonus;
}
}
}

View File

@ -8,6 +8,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MoonSharp.Interpreter; using MoonSharp.Interpreter;
using FFXIVClassic.Common;
namespace FFXIVClassic_Map_Server.actors.chara.ai namespace FFXIVClassic_Map_Server.actors.chara.ai
{ {
@ -99,7 +100,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
Retaliation = 223082, Retaliation = 223082,
Foresight = 223083, Foresight = 223083,
Defender = 223084, Defender = 223084,
Rampage = 223085, Rampage = 223085, //old effect
Enraged = 223086, Enraged = 223086,
Warmonger = 223087, Warmonger = 223087,
Disorientx1 = 223088, Disorientx1 = 223088,
@ -119,10 +120,10 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
LifeSap = 223102, LifeSap = 223102,
Farshot = 223103, Farshot = 223103,
QuellingStrike = 223104, QuellingStrike = 223104,
RagingStrike = 223105, RagingStrike = 223105, //old effect
HawksEye = 223106, HawksEye = 223106,
SubtleRelease = 223107, SubtleRelease = 223107,
Decoy = 223108, Decoy = 223108, //Untraited
Profundity = 223109, Profundity = 223109,
TranceChant = 223110, TranceChant = 223110,
RoamingSoul = 223111, RoamingSoul = 223111,
@ -140,10 +141,10 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
ConcussiveBlowx2 = 223124, ConcussiveBlowx2 = 223124,
ConcussiveBlowx3 = 223125, ConcussiveBlowx3 = 223125,
SkullSunder = 223126, SkullSunder = 223126,
Bloodletter = 223127, Bloodletter = 223127, //comboed effect
Levinbolt = 223128, Levinbolt = 223128,
Protect = 223129, Protect = 223129, //old Protect
Shell = 223130, Shell = 223130, //old shell
Reraise = 223131, Reraise = 223131,
ShockSpikes = 223132, ShockSpikes = 223132,
Stoneskin = 223133, Stoneskin = 223133,
@ -219,8 +220,8 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
StealthIV = 223204, StealthIV = 223204,
Combo = 223205, Combo = 223205,
GoringBlade = 223206, GoringBlade = 223206,
Berserk2 = 223207, Berserk2 = 223207, //new effect
Rampage2 = 223208, Rampage2 = 223208, //new effect
FistsofFire = 223209, FistsofFire = 223209,
FistsofEarth = 223210, FistsofEarth = 223210,
FistsofWind = 223211, FistsofWind = 223211,
@ -249,10 +250,10 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
Aero = 223235, Aero = 223235,
Outmaneuver2 = 223236, Outmaneuver2 = 223236,
Blindside2 = 223237, Blindside2 = 223237,
Decoy2 = 223238, Decoy2 = 223238, //Traited
Protect2 = 223239, Protect2 = 223239, //new Protect
SanguineRite3 = 223240, SanguineRite3 = 223240,
Bloodletter2 = 223241, Bloodletter2 = 223241, //uncomboed effect
FullyBlissfulMind = 223242, FullyBlissfulMind = 223242,
MagicEvasionDown = 223243, MagicEvasionDown = 223243,
HundredFists = 223244, HundredFists = 223244,
@ -327,7 +328,8 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
EvadeProc = 253003, EvadeProc = 253003,
BlockProc = 253004, BlockProc = 253004,
ParryProc = 253005, ParryProc = 253005,
MissProc = 253006 MissProc = 253006,
EXPChain = 253007
} }
[Flags] [Flags]
@ -343,30 +345,37 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
LoseOnDispel = 1 << 4, // some buffs which player might be able to dispel from mob LoseOnDispel = 1 << 4, // some buffs which player might be able to dispel from mob
LoseOnLogout = 1 << 5, // effects removed on logging out LoseOnLogout = 1 << 5, // effects removed on logging out
LoseOnAttacking = 1 << 6, // effects removed when owner attacks another entity LoseOnAttacking = 1 << 6, // effects removed when owner attacks another entity
LoseOnCasting = 1 << 7, // effects removed when owner starts casting LoseOnCastStart = 1 << 7, // effects removed when owner starts casting
LoseOnDamageTaken = 1 << 8, // effects removed when owner takes damage LoseOnAggro = 1 << 8, // effects removed when owner gains enmity (swiftsong)
LoseOnParry = 1 << 9, // effects removed when owner parries an attack (foresight)
LoseOnEvade = 1 << 10, // effects removed when owner evades an attack (decoy)
LoseOnCrit = 1 << 11, // effects removed when owner deals a critical hit (excruciate)
LoseOnAggro = 1 << 12, // effects removed when owner gains enmity (swiftsong)
//Activate flags, do we need the LoseOn flags if we have these? could just remove itself in the activate function //Activate flags
ActivateOnAttack = 1 << 13, ActivateOnCastStart = 1 << 9, //Activates when a cast starts.
ActivateOnSpell = 1 << 14, ActivateOnCommandStart = 1 << 10, //Activates when a command is used, before iterating over targets. Used for things like power surge, excruciate.
ActivateOnDamageTaken = 1 << 15, ActivateOnCommandFinish = 1 << 11, //Activates when the command is finished, after all targets have been iterated over. Used for things like Excruciate and Resonance falling off.
ActivateOnBlock = 1 << 16, ActivateOnPreactionTarget = 1 << 12, //Activates after initial rates are calculated for an action against owner
ActivateOnMiss = 1 << 17, ActivateOnPreactionCaster = 1 << 13, //Activates after initial rates are calculated for an action by owner
ActivateOnDamageTaken = 1 << 14,
ActivateOnHealed = 1 << 15,
//Should these be rolled into DamageTaken?
ActivateOnMiss = 1 << 16, //Activates when owner misses
ActivateOnEvade = 1 << 17, //Activates when owner evades
ActivateOnParry = 1 << 18, //Activates when owner parries
ActivateOnBlock = 1 << 19, //Activates when owner evades
ActivateOnHit = 1 << 20, //Activates when owner hits
ActivateOnCrit = 1 << 21, //Activates when owner crits
//Prevent flags. Sleep/stun/petrify/etc combine these //Prevent flags. Sleep/stun/petrify/etc combine these
PreventSpell= 1 << 18, // effects which prevent using spells, such as silence PreventSpell = 1 << 22, // effects which prevent using spells, such as silence
PreventWeaponSkill = 1 << 19, // effects which prevent using weaponskills, such as pacification PreventWeaponSkill = 1 << 23, // effects which prevent using weaponskills, such as pacification
PreventAbility = 1 << 20, // effects which prevent using abilities, such as amnesia PreventAbility = 1 << 24, // effects which prevent using abilities, such as amnesia
PreventAttack = 1 << 21, // effects which prevent basic attacks PreventAttack = 1 << 25, // effects which prevent basic attacks
PreventMovement = 1 << 22, // effects which prevent movement such as bind, still allows turning in place PreventMovement = 1 << 26, // effects which prevent movement such as bind, still allows turning in place
PreventTurn = 1 << 23, // effects which prevent turning such as fixation, still allows movement and actions PreventTurn = 1 << 27, // effects which prevent turning, such as stun
PreventUntarget = 1 << 28, // effects which prevent changing targets, such as fixation
Stealth = 1 << 24, // sneak/invis Stealth = 1 << 29, // sneak/invis
Stance = 1 << 25, // effects that do not have a timer Stance = 1 << 30, // effects that do not have a timer
} }
enum StatusEffectOverwrite : byte enum StatusEffectOverwrite : byte
@ -380,18 +389,18 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
class StatusEffect class StatusEffect
{ {
// todo: probably use get;set; // todo: probably use get;set;
private Character owner; private Character owner;
private Character source; private Character source;
private StatusEffectId id; private StatusEffectId id;
private string name; // name of this effect private string name; // name of this effect
private DateTime startTime; // when was this effect added private DateTime startTime; // when was this effect added
private DateTime endTime; // when this status falls off
private DateTime lastTick; // when did this effect last tick private DateTime lastTick; // when did this effect last tick
private uint duration; // how long should this effect last in seconds private uint duration; // how long should this effect last in seconds
private uint tickMs; // how often should this effect proc private uint tickMs; // how often should this effect proc
private UInt64 magnitude; // a value specified by scripter which is guaranteed to be used by all effects private double magnitude; // a value specified by scripter which is guaranteed to be used by all effects
private byte tier; // same effect with higher tier overwrites this private byte tier; // same effect with higher tier overwrites this
private UInt64 extra; // optional value private double extra; // optional value
private StatusEffectFlags flags; // death/erase/dispel etc private StatusEffectFlags flags; // death/erase/dispel etc
private StatusEffectOverwrite overwrite; // how to handle adding an effect with same id (see StatusEfectOverwrite) private StatusEffectOverwrite overwrite; // how to handle adding an effect with same id (see StatusEfectOverwrite)
private bool silent = false; // do i send a message on losing effect private bool silent = false; // do i send a message on losing effect
@ -400,7 +409,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
HitEffect animationEffect; HitEffect animationEffect;
public StatusEffect(Character owner, uint id, UInt64 magnitude, uint tickMs, uint duration, byte tier = 0) public StatusEffect(Character owner, uint id, double magnitude, uint tickMs, uint duration, byte tier = 0)
{ {
this.owner = owner; this.owner = owner;
this.source = owner; this.source = owner;
@ -448,10 +457,11 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
if (tickMs != 0 && (tick - lastTick).TotalMilliseconds >= tickMs) if (tickMs != 0 && (tick - lastTick).TotalMilliseconds >= tickMs)
{ {
lastTick = tick; lastTick = tick;
LuaEngine.CallLuaStatusEffectFunction(this.owner, this, "onTick", this.owner, this); if (LuaEngine.CallLuaStatusEffectFunction(this.owner, this, "onTick", this.owner, this) > 0)
return true;
} }
if (duration != 0xFFFFFFFF && (tick - startTime).TotalSeconds >= duration) if (duration >= 0 && tick >= endTime)
{ {
return true; return true;
} }
@ -463,6 +473,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
DynValue res = new DynValue(); DynValue res = new DynValue();
return lua.LuaEngine.CallLuaStatusEffectFunction(chara, this, functionName, args);
if (!script.Globals.Get(functionName).IsNil()) if (!script.Globals.Get(functionName).IsNil())
{ {
res = script.Call(script.Globals.Get(functionName), args); res = script.Call(script.Globals.Get(functionName), args);
@ -470,7 +481,6 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return (int)res.Number; return (int)res.Number;
} }
return -1;
} }
public Character GetOwner() public Character GetOwner()
@ -498,6 +508,11 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return startTime; return startTime;
} }
public DateTime GetEndTime()
{
return endTime;
}
public string GetName() public string GetName()
{ {
return name; return name;
@ -513,7 +528,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return tickMs; return tickMs;
} }
public UInt64 GetMagnitude() public double GetMagnitude()
{ {
return magnitude; return magnitude;
} }
@ -523,7 +538,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return tier; return tier;
} }
public UInt64 GetExtra() public double GetExtra()
{ {
return extra; return extra;
} }
@ -554,6 +569,21 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
this.lastTick = time; this.lastTick = time;
} }
public void SetEndTime(DateTime time)
{
endTime = time;
}
//Refresh the status, updating the end time based on the duration of the status and broadcasts the new time
public void RefreshTime()
{
endTime = DateTime.Now.AddSeconds(GetDuration());
int index = Array.IndexOf(owner.charaWork.status, GetStatusId());
if (index >= 0)
owner.statusEffects.SetTimeAtIndex(index, (uint) Utils.UnixTimeStampUTC(endTime));
}
public void SetOwner(Character owner) public void SetOwner(Character owner)
{ {
this.owner = owner; this.owner = owner;
@ -569,7 +599,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
this.name = name; this.name = name;
} }
public void SetMagnitude(UInt64 magnitude) public void SetMagnitude(double magnitude)
{ {
this.magnitude = magnitude; this.magnitude = magnitude;
} }
@ -589,7 +619,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
this.tier = tier; this.tier = tier;
} }
public void SetExtra(UInt64 val) public void SetExtra(double val)
{ {
this.extra = val; this.extra = val;
} }

View File

@ -21,22 +21,36 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
private readonly Dictionary<uint, StatusEffect> effects; private readonly Dictionary<uint, StatusEffect> effects;
public static readonly int MAX_EFFECTS = 20; public static readonly int MAX_EFFECTS = 20;
private bool sendUpdate = false; private bool sendUpdate = false;
private DateTime lastTick;// Do all effects tick at the same time like regen?
private List<SubPacket> statusSubpackets;
private ActorPropertyPacketUtil statusTimerPropPacketUtil;
public StatusEffectContainer(Character owner) public StatusEffectContainer(Character owner)
{ {
this.owner = owner; this.owner = owner;
this.effects = new Dictionary<uint, StatusEffect>(); this.effects = new Dictionary<uint, StatusEffect>();
statusSubpackets = new List<SubPacket>();
statusTimerPropPacketUtil = new ActorPropertyPacketUtil("charawork/Status", owner);
} }
public void Update(DateTime tick) public void Update(DateTime tick)
{ {
//Regen/Refresh/Regain effects tick every 3 seconds
if ((DateTime.Now - lastTick).Seconds >= 3)
{
RegenTick(tick);
lastTick = DateTime.Now;
}
// list of effects to remove // list of effects to remove
// if (owner is Player) UpdateTimeAtIndex(4, 4294967295);
var removeEffects = new List<StatusEffect>(); var removeEffects = new List<StatusEffect>();
foreach (var effect in effects.Values) for (int i = 0; i < effects.Values.Count; i++)
{ {
// effect's update function returns true if effect has completed // effect's update function returns true if effect has completed
if (effect.Update(tick)) if (effects.Values.ElementAt(i).Update(tick))
removeEffects.Add(effect); removeEffects.Add(effects.Values.ElementAt(i));
} }
// remove effects from this list // remove effects from this list
@ -52,6 +66,31 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
} }
} }
//regen/refresh/regain
public void RegenTick(DateTime tick)
{
ushort dotTick = (ushort) owner.GetMod(Modifier.RegenDown);
ushort regenTick = (ushort) owner.GetMod(Modifier.Regen);
ushort refreshtick = (ushort) owner.GetMod(Modifier.Refresh);
short regainTick = (short) owner.GetMod(Modifier.Regain);
//DoTs tick before regen and the full dot damage is displayed, even if some or all of it is nullified by regen. Only effects like stoneskin actually alter the number shown
if (dotTick > 0)
{
BattleAction action = new BattleAction(owner.actorId, 30331, (uint)(HitEffect.HitEffectType | HitEffect.Hit), dotTick);
utils.BattleUtils.HandleStoneskin(owner, action);
// todo: figure out how to make red numbers appear for enemies getting hurt by dots
owner.DelHP(action.amount);
owner.DoBattleAction(0, 0, action);
}
//DoTs are the only effect to show numbers, so that doesnt need to be handled for these
owner.AddHP(regenTick);
owner.AddMP(refreshtick);
owner.AddTP(regainTick);
}
public bool HasStatusEffect(uint id) public bool HasStatusEffect(uint id)
{ {
return effects.ContainsKey(id); return effects.ContainsKey(id);
@ -62,7 +101,43 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return effects.ContainsKey((uint)id); return effects.ContainsKey((uint)id);
} }
public bool AddStatusEffect(uint id, UInt64 magnitude, uint tickMs, uint duration, byte tier = 0) public BattleAction AddStatusForBattleAction(uint id, byte tier = 1)
{
BattleAction action = null;
if (AddStatusEffect(id, tier))
action = new BattleAction(owner.actorId, 30328, id | (uint)HitEffect.StatusEffectType);
return action;
}
public bool AddStatusEffect(uint id)
{
var se = Server.GetWorldManager().GetStatusEffect(id);
return AddStatusEffect(se, owner);
}
public bool AddStatusEffect(uint id, byte tier)
{
var se = Server.GetWorldManager().GetStatusEffect(id);
se.SetTier(tier);
return AddStatusEffect(se, owner);
}
public bool AddStatusEffect(uint id, byte tier, UInt64 magnitude)
{
var se = Server.GetWorldManager().GetStatusEffect(id);
se.SetMagnitude(magnitude);
se.SetTier(tier);
return AddStatusEffect(se, owner);
}
public bool AddStatusEffect(uint id, byte tier, UInt64 magnitude, uint duration, int tickMs = 3000)
{ {
var se = Server.GetWorldManager().GetStatusEffect(id); var se = Server.GetWorldManager().GetStatusEffect(id);
if (se != null) if (se != null)
@ -71,7 +146,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
se.SetStartTime(DateTime.Now); se.SetStartTime(DateTime.Now);
se.SetOwner(owner); se.SetOwner(owner);
} }
return AddStatusEffect(se ?? new StatusEffect(this.owner, id, magnitude, tickMs, duration, tier), owner); return AddStatusEffect(se ?? new StatusEffect(this.owner, id, magnitude, 3000, duration, tier), owner);
} }
public bool AddStatusEffect(StatusEffect newEffect, Character source, bool silent = false, bool hidden = false) public bool AddStatusEffect(StatusEffect newEffect, Character source, bool silent = false, bool hidden = false)
@ -82,12 +157,6 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
32002 [@SHEET(xtx/status,$E8(11),3)] fails to take effect. 32002 [@SHEET(xtx/status,$E8(11),3)] fails to take effect.
*/ */
if (HasStatusEffect(newEffect.GetStatusEffectId()) && (newEffect.GetFlags() & (uint)StatusEffectFlags.Stance) != 0)
{
RemoveStatusEffect(newEffect);
return false;
}
var effect = GetStatusEffectById(newEffect.GetStatusEffectId()); var effect = GetStatusEffectById(newEffect.GetStatusEffectId());
bool canOverwrite = false; bool canOverwrite = false;
@ -112,6 +181,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
effects.Remove(newEffect.GetStatusEffectId()); effects.Remove(newEffect.GetStatusEffectId());
newEffect.SetStartTime(DateTime.Now); newEffect.SetStartTime(DateTime.Now);
newEffect.SetEndTime(DateTime.Now.AddSeconds(newEffect.GetDuration()));
newEffect.SetOwner(owner); newEffect.SetOwner(owner);
if (effects.Count < MAX_EFFECTS) if (effects.Count < MAX_EFFECTS)
@ -127,22 +197,22 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
if (!newEffect.GetHidden()) if (!newEffect.GetHidden())
{ {
int index = 0; int index = 0;
//If effect is already in the list of statuses, get that index, otherwise find the first open index
if (owner.charaWork.status.Contains(newEffect.GetStatusId())) if (owner.charaWork.status.Contains(newEffect.GetStatusId()))
index = Array.IndexOf(owner.charaWork.status, newEffect.GetStatusId()); index = Array.IndexOf(owner.charaWork.status, newEffect.GetStatusId());
else else
index = Array.IndexOf(owner.charaWork.status, (ushort) 0); index = Array.IndexOf(owner.charaWork.status, (ushort) 0);
owner.charaWork.status[index] = newEffect.GetStatusId(); //owner.charaWork.status[index] = newEffect.GetStatusId();
SetStatusAtIndex(index, newEffect.GetStatusId());
//Stance statuses need their time set to an extremely high number so their icon doesn't flash //Stance statuses need their time set to an extremely high number so their icon doesn't flash
//Adding getduration with them doesn't work because it overflows //Adding getduration with them doesn't work because it overflows
uint time = (newEffect.GetFlags() & (uint) StatusEffectFlags.Stance) == 0 ? Utils.UnixTimeStampUTC() + (newEffect.GetDuration()) : 0xFFFFFFFF; uint time = (newEffect.GetFlags() & (uint) StatusEffectFlags.Stance) == 0 ? Utils.UnixTimeStampUTC(newEffect.GetEndTime()) : 0xFFFFFFFF;
owner.charaWork.statusShownTime[index] = time; SetTimeAtIndex(index, time);
this.owner.zone.BroadcastPacketAroundActor(this.owner, SetActorStatusPacket.BuildPacket(this.owner.actorId, (ushort)index, (ushort)newEffect.GetStatusId())); //owner.charaWork.statusShownTime[index] = time;
} //owner.zone.BroadcastPacketAroundActor(owner, SetActorStatusPacket.BuildPacket(owner.actorId, (ushort)index, newEffect.GetStatusId()));
//owner.zone.BroadcastPacketsAroundActor(owner, owner.GetActorStatusPackets());
{
owner.zone.BroadcastPacketsAroundActor(owner, owner.GetActorStatusPackets());
} }
owner.RecalculateStats(); owner.RecalculateStats();
} }
@ -151,14 +221,15 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return false; return false;
} }
public void RemoveStatusEffect(StatusEffect effect, bool silent = false) public bool RemoveStatusEffect(StatusEffect effect, bool silent = false)
{ {
bool removedEffect = false;
if (effect != null && effects.ContainsKey(effect.GetStatusEffectId())) if (effect != null && effects.ContainsKey(effect.GetStatusEffectId()))
{ {
// send packet to client with effect remove message // send packet to client with effect remove message
if (!silent && !effect.GetSilent() || (effect.GetFlags() & (uint)StatusEffectFlags.Silent) == 0) if (!silent && !effect.GetSilent() && (effect.GetFlags() & (uint)StatusEffectFlags.Silent) == 0)
{ {
// todo: send packet to client with effect added message owner.DoBattleAction(0, 0, new BattleAction(owner.actorId, 30331, effect.GetStatusEffectId()));
} }
//hidden effects not in charawork //hidden effects not in charawork
@ -178,25 +249,52 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
LuaEngine.CallLuaStatusEffectFunction(this.owner, effect, "onLose", this.owner, effect); LuaEngine.CallLuaStatusEffectFunction(this.owner, effect, "onLose", this.owner, effect);
owner.RecalculateStats(); owner.RecalculateStats();
sendUpdate = true; sendUpdate = true;
} removedEffect = true;
} }
public void RemoveStatusEffect(uint effectId, bool silent = false) return removedEffect;
}
public bool RemoveStatusEffect(uint effectId, bool silent = false)
{ {
bool removedEffect = false;
foreach (var effect in effects.Values) foreach (var effect in effects.Values)
{ {
if (effect.GetStatusEffectId() == effectId) if (effect.GetStatusEffectId() == effectId)
{ {
RemoveStatusEffect(effect, effect.GetSilent() || silent); RemoveStatusEffect(effect, effect.GetSilent() || silent);
removedEffect = true;
break; break;
} }
} }
return removedEffect;
}
//Remove status effect and return the battleaction message instead of sending it immediately
public BattleAction RemoveStatusEffectForBattleAction(uint effectId, ushort worldMasterTextId = 30331)
{
BattleAction action = null;
if (RemoveStatusEffect(effectId, true))
action = new BattleAction(owner.actorId, worldMasterTextId, effectId);
return action;
}
//Remove status effect and return the battleaction message instead of sending it immediately
public BattleAction RemoveStatusEffectForBattleAction(StatusEffect effect, ushort worldMasterTextId = 30331)
{
BattleAction action = null;
if (RemoveStatusEffect(effect, true))
action = new BattleAction(owner.actorId, worldMasterTextId, effect.GetStatusEffectId());
return action;
} }
public StatusEffect CopyEffect(StatusEffect effect) public StatusEffect CopyEffect(StatusEffect effect)
{ {
var newEffect = new StatusEffect(this.owner, effect); var newEffect = new StatusEffect(owner, effect);
newEffect.SetOwner(this.owner); newEffect.SetOwner(owner);
// todo: should source be copied too? // todo: should source be copied too?
return AddStatusEffect(newEffect, effect.GetSource()) ? newEffect : null; return AddStatusEffect(newEffect, effect.GetSource()) ? newEffect : null;
} }
@ -204,10 +302,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
public bool RemoveStatusEffectsByFlags(uint flags, bool silent = false) public bool RemoveStatusEffectsByFlags(uint flags, bool silent = false)
{ {
// build list of effects to remove // build list of effects to remove
var removeEffects = new List<StatusEffect>(); var removeEffects = GetStatusEffectsByFlag(flags);
foreach (var effect in effects.Values)
if ((effect.GetFlags() & flags) != 0)
removeEffects.Add(effect);
// remove effects from main list // remove effects from main list
foreach (var effect in removeEffects) foreach (var effect in removeEffects)
@ -231,12 +326,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
{ {
var list = new List<StatusEffect>(); var list = new List<StatusEffect>();
foreach (var effect in effects.Values) foreach (var effect in effects.Values)
{
if ((effect.GetFlags() & flag) != 0) if ((effect.GetFlags() & flag) != 0)
{
list.Add(effect); list.Add(effect);
}
}
return list; return list;
} }
@ -268,5 +360,88 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
Database.SavePlayerStatusEffects((Player)owner); Database.SavePlayerStatusEffects((Player)owner);
} }
} }
public void CallLuaFunctionByFlag(uint flag, string function, params object[] args)
{
var effects = GetStatusEffectsByFlag(flag);
object[] argsWithEffect = new object[args.Length + 1];
for (int i = 0; i < args.Length; i++)
argsWithEffect[i + 1] = args[i];
foreach (var effect in effects)
{
argsWithEffect[0] = effect;
effect.CallLuaFunction(owner, function, argsWithEffect);
}
}
//Sets the status id at an index.
//Changing a status to another doesn't seem to work. If updating an index that already has an effect, set it to 0 first then to the correct status
public void SetStatusAtIndex(int index, ushort statusId)
{
owner.charaWork.status[index] = statusId;
//owner.zone.BroadcastPacketAroundActor(owner, SetActorStatusPacket.BuildPacket(owner.actorId, (ushort)index, statusId));
statusSubpackets.Add(SetActorStatusPacket.BuildPacket(owner.actorId, (ushort)index, statusId));
owner.updateFlags |= ActorUpdateFlags.Status;
}
public void SetTimeAtIndex(int index, uint time)
{
owner.charaWork.statusShownTime[index] = time;
statusTimerPropPacketUtil.AddProperty($"charaWork.statusShownTime[{index}]");
owner.updateFlags |= ActorUpdateFlags.StatusTime;
}
public List<SubPacket> GetStatusPackets()
{
return statusSubpackets;
}
public List<SubPacket> GetStatusTimerPackets()
{
return statusTimerPropPacketUtil.Done();
}
public void ResetPropPacketUtil()
{
statusTimerPropPacketUtil = new ActorPropertyPacketUtil("charaWork/status", owner);
}
//Overwrites effectToBeReplaced with a new status effect
//Returns the message of the new effect being added
//Doing this instead of simply calling remove then add so that the new effect is in the same slot as the old one
//There should be a better way to do this
//Currently causes the icons to blink whenb eing rpelaced
public BattleAction ReplaceEffect(StatusEffect effectToBeReplaced, uint newEffectId, byte tier, double magnitude, uint duration)
{
StatusEffect newEffect = Server.GetWorldManager().GetStatusEffect(newEffectId);
newEffect.SetTier(tier);
newEffect.SetMagnitude(magnitude);
newEffect.SetDuration(duration);
newEffect.SetOwner(effectToBeReplaced.GetOwner());
effectToBeReplaced.CallLuaFunction(owner, "onLose", owner, effectToBeReplaced);
newEffect.CallLuaFunction(owner, "onGain", owner, newEffect);
effects.Remove(effectToBeReplaced.GetStatusEffectId());
newEffect.SetStartTime(DateTime.Now);
newEffect.SetEndTime(DateTime.Now.AddSeconds(newEffect.GetDuration()));
uint time = (newEffect.GetFlags() & (uint)StatusEffectFlags.Stance) == 0 ? Utils.UnixTimeStampUTC(newEffect.GetEndTime()) : 0xFFFFFFFF;
int index = Array.IndexOf(owner.charaWork.status, effectToBeReplaced.GetStatusId());
//owner.charaWork.status[index] = newEffect.GetStatusId();
owner.charaWork.statusShownTime[index] = time;
effects[newEffectId] = newEffect;
SetStatusAtIndex(index, 0);
//charawork/status
SetStatusAtIndex(index, (ushort) (newEffectId - 200000));
SetTimeAtIndex(index, time);
return new BattleAction(owner.actorId, 30328, (uint) HitEffect.StatusEffectType | newEffectId);
}
} }
} }

View File

@ -149,11 +149,15 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
public override void Cast(Character target, uint spellId) public override void Cast(Character target, uint spellId)
{ {
// todo: // todo:
if(owner.aiContainer.CanChangeState())
owner.aiContainer.InternalCast(target, spellId);
} }
public override void Ability(Character target, uint abilityId) public override void Ability(Character target, uint abilityId)
{ {
// todo: // todo:
if (owner.aiContainer.CanChangeState())
owner.aiContainer.InternalAbility(target, abilityId);
} }
public override void RangedAttack(Character target) public override void RangedAttack(Character target)
@ -211,7 +215,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
return; return;
} }
owner.SetMod((uint)Modifier.Speed, 5); owner.SetMod((uint)Modifier.Speed, 5);
if ((tick - lastCombatTickScript).TotalSeconds > 3)//Program.Random.Next(10, 15)) if ((tick - lastCombatTickScript).TotalSeconds > 3)
{ {
Move(); Move();
//if (owner.aiContainer.CanChangeState()) //if (owner.aiContainer.CanChangeState())

View File

@ -21,6 +21,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
public override void Update(DateTime tick) public override void Update(DateTime tick)
{ {
/*
if (owner.newMainState != owner.currentMainState) if (owner.newMainState != owner.currentMainState)
{ {
if (owner.newMainState == SetActorStatePacket.MAIN_STATE_ACTIVE) if (owner.newMainState == SetActorStatePacket.MAIN_STATE_ACTIVE)
@ -32,7 +33,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
owner.Disengage(); owner.Disengage();
} }
owner.currentMainState = (ushort)owner.newMainState; owner.currentMainState = (ushort)owner.newMainState;
} }*/
} }
public override void ChangeTarget(Character target) public override void ChangeTarget(Character target)

View File

@ -172,7 +172,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
// todo: this is stupid // todo: this is stupid
bool withPet = (flags & ValidTarget.Ally) != 0 || masterTarget.allegiance != owner.allegiance; bool withPet = (flags & ValidTarget.Ally) != 0 || masterTarget.allegiance != owner.allegiance;
if (masterTarget != null) if (masterTarget != null && CanTarget(masterTarget))
targets.Add(masterTarget); targets.Add(masterTarget);
if (aoeType != TargetFindAOEType.None) if (aoeType != TargetFindAOEType.None)
@ -237,8 +237,13 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
}*/ }*/
if (targets.Count > 16) if (targets.Count > 8)
targets.RemoveRange(16, targets.Count - 16); 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?
//Protect is random
targets.Sort(delegate (Character a, Character b) { return a.GetHP().CompareTo(b.GetHP()); });
} }
/// <summary> /// <summary>
@ -351,6 +356,12 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
if (target == null || !retarget && targets.Contains(target)) if (target == null || !retarget && targets.Contains(target))
return false; return false;
if ((validTarget & ValidTarget.Player) != 0 && target is Player)
return true;
if ((validTarget & ValidTarget.PartyMember) != 0 && target.currentParty == owner.currentParty)
return true;
// cant target dead // cant target dead
if ((validTarget & ValidTarget.Corpse) == 0 && target.IsDead()) if ((validTarget & ValidTarget.Corpse) == 0 && target.IsDead())
return false; return false;
@ -361,7 +372,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
if ((validTarget & ValidTarget.Enemy) != 0 && target.allegiance == owner.allegiance) if ((validTarget & ValidTarget.Enemy) != 0 && target.allegiance == owner.allegiance)
return false; return false;
if ((validTarget & ValidTarget.PartyMember) == 0 && target.currentParty == owner.currentParty) if (((validTarget & ValidTarget.PartyMember) == 0) && ((validTarget & ValidTarget.Self) == 0) && target.currentParty == owner.currentParty)
return false; return false;
if ((validTarget & ValidTarget.PartyMember) != 0 && target.currentParty != owner.currentParty) if ((validTarget & ValidTarget.PartyMember) != 0 && target.currentParty != owner.currentParty)
@ -384,6 +395,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
if (validTarget == ValidTarget.Self && aoeType == TargetFindAOEType.None && owner != target) if (validTarget == ValidTarget.Self && aoeType == TargetFindAOEType.None && owner != target)
return false; return false;
if ((validTarget & ValidTarget.Self) == 0 && target == owner)
return false;
// this is fuckin retarded, think of a better way l8r // this is fuckin retarded, think of a better way l8r
if (!ignoreAOE) if (!ignoreAOE)
{ {

View File

@ -48,15 +48,6 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
else else
{ {
//owner.LookAt(target); //owner.LookAt(target);
//If owner already has this status effect and it's a stance that gets removed on reuse, remove it and don't continue
var effect = owner.statusEffects.GetStatusEffectById(skill.statusId);
if (effect!= null && (effect.GetFlags() & (uint) StatusEffectFlags.Stance) != 0)
{
owner.statusEffects.RemoveStatusEffect(effect);
interrupt = true;
}
} }
} }
@ -101,35 +92,8 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
skill.targetFind.FindWithinArea(target, skill.validTarget, skill.aoeTarget); skill.targetFind.FindWithinArea(target, skill.validTarget, skill.aoeTarget);
isCompleted = true; isCompleted = true;
var targets = skill.targetFind.GetTargets();
List<BattleAction> actions = new List<BattleAction>(); owner.DoBattleCommand(skill, "ability");
List<StatusEffect> effects = owner.statusEffects.GetStatusEffectsByFlag((uint)StatusEffectFlags.ActivateOnAttack);
foreach (var chara in targets)
{
for (int hitNum = 0; hitNum < skill.numHits; hitNum++)
{
//30328 - Your [ability] grants you the effect of [status]
//30320 - You use [ability]. You recover x HP.
var action = new BattleAction(chara.actorId, skill.worldMasterTextId, 0, 0, 1, 1);
//uncached
lua.LuaEngine.CallLuaBattleCommandFunction(owner, skill, "ability", "onAbilityFinish", owner, target, skill, action);
//cached
//skill.CallLuaFunction(owner, "onAbilityFinish", owner, target, skill, action);
//if hit type isn't evade or miss
if (((action.hitType & HitType.Evade) | (action.hitType & HitType.Miss)) == 0)
hitTarget = true;
actions.AddRange(action.GetAllActions());
}
}
// todo: this is fuckin stupid, probably only need *one* error packet, not an error for each action
BattleAction[] errors = (BattleAction[])actions.ToArray().Clone();
owner.OnAbility(this, actions.ToArray(), skill, ref errors);
owner.DoBattleAction(skill.id, skill.battleAnimation, actions);
} }
public override void TryInterrupt() public override void TryInterrupt()

View File

@ -32,9 +32,8 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
public override bool Update(DateTime tick) public override bool Update(DateTime tick)
{ {
if ((target == null || owner.target != target || owner.target?.actorId != owner.currentLockedTarget) && owner.isAutoAttackEnabled) if ((target == null || owner.target != target || owner.target?.actorId != owner.currentLockedTarget) && owner.isAutoAttackEnabled)
owner.aiContainer.ChangeTarget(target = owner.zone.FindActorInArea<Character>(owner.currentLockedTarget)); owner.aiContainer.ChangeTarget(target = owner.zone.FindActorInArea<Character>(owner.currentTarget));
if (target == null || target.IsDead()) if (target == null || target.IsDead())
{ {
@ -101,25 +100,30 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
// handle paralyze/intimidate/sleep/whatever in Character.OnAttack // handle paralyze/intimidate/sleep/whatever in Character.OnAttack
List<BattleAction> actions = new List<BattleAction>(); // todo: Change this to use a BattleCommand like the other states
//List<BattleAction> actions = new List<BattleAction>();
BattleActionContainer actions = new BattleActionContainer();
target.SetMod((uint) Modifier.MinimumHpLock, 0);
var i = 0; var i = 0;
for (int hitNum = 0; hitNum < owner.GetMod((uint) Modifier.HitCount); hitNum++) for (int hitNum = 0; hitNum < 1 /* owner.GetMod((uint) Modifier.HitCount)*/; hitNum++)
{ {
BattleAction action = new BattleAction(target.actorId, 0x765D, (uint)HitEffect.Hit, 100, (byte)HitDirection.None, (byte) hitNum); BattleAction action = new BattleAction(target.actorId, 0x765D, (uint)HitEffect.Hit, 100, (byte)HitDirection.None, (byte) hitNum);
action.battleActionType = BattleActionType.AttackPhysical; action.commandType = CommandType.AutoAttack;
action.actionType = ActionType.Physical;
action.actionProperty = (ActionProperty) owner.GetMod(Modifier.AttackType);
// evasion, miss, dodge, etc to be handled in script, calling helpers from scripts/weaponskills.lua // evasion, miss, dodge, etc to be handled in script, calling helpers from scripts/weaponskills.lua
// temporary evade/miss/etc function to test hit effects // temporary evade/miss/etc function to test hit effects
utils.BattleUtils.CalcHitType(owner, target, null, action); action.DoAction(owner, target, null, actions);
actions.AddRange(action.GetAllActions());
} }
// todo: this is fuckin stupid, probably only need *one* error packet, not an error for each action // todo: this is fuckin stupid, probably only need *one* error packet, not an error for each action
BattleAction[] errors = (BattleAction[])actions.ToArray().Clone(); BattleAction[] errors = (BattleAction[])actions.GetList().ToArray().Clone();
BattleAction error = null;// new BattleAction(0, null, 0, 0);
owner.OnAttack(this, actions[0], ref errorResult); //owner.DoActions(null, actions.GetList(), ref error);
owner.DoBattleAction(22104, 0x19001000, actions); //owner.OnAttack(this, actions[0], ref errorResult);
target.SetMod((uint) Modifier.MinimumHpLock, 0); owner.DoBattleAction(22104, 0x19001000, actions.GetList());
} }
public override void TryInterrupt() public override void TryInterrupt()
@ -148,7 +152,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
private bool CanAttack() private bool CanAttack()
{ {
if (!owner.isAutoAttackEnabled) if (!owner.isAutoAttackEnabled || target.allegiance == owner.allegiance)
{ {
return false; return false;
} }

View File

@ -64,6 +64,14 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
lua.LuaEngine.CallLuaBattleCommandFunction(owner, spell, "magic", "onCombo", owner, target, spell); lua.LuaEngine.CallLuaBattleCommandFunction(owner, spell, "magic", "onCombo", owner, target, spell);
spell.isCombo = true; spell.isCombo = true;
} }
//Modify spell based on status effects. Need to do it here because they can modify cast times
List<StatusEffect> effects = owner.statusEffects.GetStatusEffectsByFlag((uint) (StatusEffectFlags.ActivateOnCastStart));
//modify skill based on status effects
foreach (var effect in effects)
lua.LuaEngine.CallLuaStatusEffectFunction(owner, effect, "onMagicCast", owner, effect, spell);
if (!spell.IsInstantCast()) if (!spell.IsInstantCast())
{ {
// command casting duration // command casting duration
@ -124,50 +132,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
isCompleted = true; isCompleted = true;
var targets = spell.targetFind.GetTargets(); var targets = spell.targetFind.GetTargets();
owner.DoBattleCommand(spell, "magic");
List<BattleAction> actions = new List<BattleAction>();
if (targets.Count > 0)
{
List<StatusEffect> effects = owner.statusEffects.GetStatusEffectsByFlag((uint)StatusEffectFlags.ActivateOnSpell);
//modify skill based on status effects
foreach (var effect in effects)
lua.LuaEngine.CallLuaStatusEffectFunction(owner, effect, "onWeaponSkill", owner, effect, spell);
//Now that combos and positionals bonuses are done, we can calculate hits/crits/etc and damage
foreach (var chara in targets)
{
for (int hitNum = 0; hitNum < spell.numHits; hitNum++)
{
var action = new BattleAction(chara.actorId, spell.worldMasterTextId, 0, 0, (byte) hitDir, (byte) hitNum);
lua.LuaEngine.CallLuaBattleCommandFunction(owner, spell, "magic", "onMagicFinish", owner, chara, spell, action);
//if hit type isn't evade or miss
if (action.hitType > HitType.Evade)
hitTarget = true;
actions.AddRange(action.GetAllActions());
}
}
}
else
{
//No targets hit, cast failed
actions.Add(new BattleAction(target.actorId, 30202, (uint) (0)));
}
// todo: this is fuckin stupid, probably only need *one* error packet, not an error for each action
BattleAction[] errors = (BattleAction[])actions.ToArray().Clone();
owner.OnCast(this, actions.ToArray(), spell, ref errors);
owner.DoBattleAction(spell.id, spell.battleAnimation, actions);
owner.statusEffects.RemoveStatusEffectsByFlags((uint)StatusEffectFlags.LoseOnCasting);
//Now that we know if we hit the target we can check if the combo continues
if (owner is Player player)
if (spell.isCombo && hitTarget)
player.SetCombos(spell.comboNextCommandId);
else
player.SetCombos();
} }
public override void TryInterrupt() public override void TryInterrupt()

View File

@ -77,7 +77,6 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
} }
} }
} }
} }
} }
@ -118,55 +117,13 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
public override void OnComplete() public override void OnComplete()
{ {
bool hitTarget = false;
skill.targetFind.FindWithinArea(target, skill.validTarget, skill.aoeTarget); skill.targetFind.FindWithinArea(target, skill.validTarget, skill.aoeTarget);
isCompleted = true; isCompleted = true;
var targets = skill.targetFind.GetTargets();
//Need a variable size list of actions because status effects may add extra actions owner.DoBattleCommand(skill, "weaponskill");
//BattleAction[] actions = new BattleAction[targets.Count * skill.numHits];
List<BattleAction> actions = new List<BattleAction>();
List<StatusEffect> effects = owner.statusEffects.GetStatusEffectsByFlag((uint)StatusEffectFlags.ActivateOnAttack);
//modify skill based on status effects
foreach (var effect in effects)
effect.CallLuaFunction(owner, "onWeaponSkill", skill);
//Now that combos and positionals bonuses are done, we can calculate hits/crits/etc and damage for each action
foreach (var chara in targets)
{
for (int hitNum = 0; hitNum < skill.numHits; hitNum++)
{
var action = new BattleAction(chara.actorId, skill.worldMasterTextId, 0, 0, (byte) hitDirection, 1);
//For older versions this will need to be in the database for magic weaponskills
action.battleActionType = BattleActionType.AttackPhysical;
//uncached script
lua.LuaEngine.CallLuaBattleCommandFunction(owner, skill, "weaponskill", "onSkillFinish", owner, target, skill, action);
//cached script
//skill.CallLuaFunction(owner, "onSkillFinish", owner, target, skill, action);
action.hitNum = (byte)hitNum;
if (action.hitType > HitType.Evade)
hitTarget = true;
actions.AddRange(action.GetAllActions());
}
}
// todo: this is fuckin stupid, probably only need *one* error packet, not an error for each action
BattleAction[] errors = (BattleAction[]) actions.ToArray().Clone();
owner.OnWeaponSkill(this, actions.ToArray(), skill, ref errors);
owner.DoBattleAction(skill.id, skill.battleAnimation, actions);
owner.statusEffects.RemoveStatusEffectsByFlags((uint) StatusEffectFlags.LoseOnAttacking); owner.statusEffects.RemoveStatusEffectsByFlags((uint) StatusEffectFlags.LoseOnAttacking);
//Now that we know if we hit the target we can check if the combo continues lua.LuaEngine.GetInstance().OnSignal("weaponskillUsed");
if (owner is Player player)
if (skill.isCombo && hitTarget)
player.SetCombos(skill.comboNextCommandId);
else
player.SetCombos();
} }
public override void TryInterrupt() public override void TryInterrupt()

View File

@ -8,6 +8,7 @@ using FFXIVClassic_Map_Server.Actors;
using FFXIVClassic_Map_Server.packets.send.actor; using FFXIVClassic_Map_Server.packets.send.actor;
using FFXIVClassic_Map_Server.packets.send.actor.battle; using FFXIVClassic_Map_Server.packets.send.actor.battle;
using FFXIVClassic_Map_Server.actors.chara.player; using FFXIVClassic_Map_Server.actors.chara.player;
using FFXIVClassic_Map_Server.actors.chara.npc;
using FFXIVClassic_Map_Server.dataobjects; using FFXIVClassic_Map_Server.dataobjects;
using FFXIVClassic.Common; using FFXIVClassic.Common;
@ -16,17 +17,28 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
static class BattleUtils static class BattleUtils
{ {
public static Dictionary<HitType, ushort> HitTypeTextIds = new Dictionary<HitType, ushort>() public static Dictionary<HitType, ushort> SingleHitTypeTextIds = new Dictionary<HitType, ushort>()
{ {
{ HitType.Miss, 30311 }, { HitType.Miss, 30311 },
{ HitType.Evade, 30310 }, { HitType.Evade, 30310 },
{ HitType.Parry, 30308 }, { HitType.Parry, 30308 },
{ HitType.Block, 30306 }, { HitType.Block, 30306 },
{ HitType.Resist, 30306 },//I can't find what the actual textid for resists is { HitType.Resist, 30310 }, //Resists seem to use the evade text id
{ HitType.Hit, 30301 }, { HitType.Hit, 30301 },
{ HitType.Crit, 30302 } { HitType.Crit, 30302 }
}; };
public static Dictionary<HitType, ushort> MultiHitTypeTextIds = new Dictionary<HitType, ushort>()
{
{ HitType.Miss, 30449 }, //The attack misses.
{ HitType.Evade, 0 }, //Evades were removed before multi hit skills got their own messages, so this doesnt exist
{ HitType.Parry, 30448 }, //[Target] parries, taking x points of damage.
{ HitType.Block, 30447 }, //[Target] blocks, taking x points of damage.
{ HitType.Resist, 0 }, //No spells are multi-hit, so this doesn't exist
{ HitType.Hit, 30443 }, //[Target] tales x points of damage
{ HitType.Crit, 30444 } //Critical! [Target] takes x points of damage.
};
public static Dictionary<HitType, HitEffect> HitTypeEffects = new Dictionary<HitType, HitEffect>() public static Dictionary<HitType, HitEffect> HitTypeEffects = new Dictionary<HitType, HitEffect>()
{ {
{ HitType.Miss, 0 }, { HitType.Miss, 0 },
@ -38,6 +50,13 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
{ HitType.Crit, HitEffect.Crit } { HitType.Crit, HitEffect.Crit }
}; };
//Most of these numbers I'm fairly certain are correct. The repeated numbers at levels 23 and 48 I'm less sure about but they do match some weird spots in the EXP graph
public static ushort[] BASEEXP = {150, 150, 150, 150, 150, 150, 150, 150, 150, 150, //Level <= 10
150, 150, 150, 150, 150, 150, 150, 150, 160, 170, //Level <= 20
180, 190, 190, 200, 210, 220, 230, 240, 250, 260, //Level <= 30
270, 280, 290, 300, 310, 320, 330, 340, 350, 360, //Level <= 40
370, 380, 380, 390, 400, 410, 420, 430, 430, 440}; //Level <= 50
public static bool TryAttack(Character attacker, Character defender, BattleAction action, ref BattleAction error) public static bool TryAttack(Character attacker, Character defender, BattleAction action, ref BattleAction error)
{ {
@ -65,53 +84,61 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
//Damage calculations //Damage calculations
//Calculate damage of action //Calculate damage of action
//We could probably just do this when determining the action's hit type //We could probably just do this when determining the action's hit type
public static void CalculateDamage(Character attacker, Character defender, BattleCommand skill, BattleAction action) public static void CalculatePhysicalDamageTaken(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{ {
short dlvl = (short)(defender.GetLevel() - attacker.GetLevel()); short dlvl = (short)(defender.GetLevel() - attacker.GetLevel());
switch (action.hitType) // todo: physical resistances
{
//Misses and evades deal no damage.
case (HitType.Miss):
case (HitType.Evade):
action.amount = 0;
break;
// todo: figure out parry damage reduction. For now assume 25% reduction
case (HitType.Parry):
CalculateParryDamage(attacker, defender, skill, action);
break;
case (HitType.Block):
CalculateBlockDamage(attacker, defender, skill, action);
break;
//There are 3 (or 4?) tiers of resists, each decreasing damage dealt by 25%. For now just assume level 2 resist (50% reduction)
// todo: figure out resist tiers
case (HitType.Resist):
CalculateResistDamage(attacker, defender, skill, action);
break;
case (HitType.Crit):
CalculateCritDamage(attacker, defender, skill, action);
break;
}
ushort finalAmount = action.amount;
//dlvl, Defense, and Vitality all effect how much damage is taken after hittype takes effect //dlvl, Defense, and Vitality all effect how much damage is taken after hittype takes effect
//player attacks cannot do more than 9999 damage. //player attacks cannot do more than 9999 damage.
action.amount = (ushort) (finalAmount - CalculateDlvlModifier(dlvl) * (defender.GetMod((uint)Modifier.Defense) + 0.67 * defender.GetMod((uint)Modifier.Vitality))).Clamp(0, 9999); //VIT is turned into Defense at a 3:2 ratio in calculatestats, so don't need to do that here
double damageTakenPercent = 1 - (defender.GetMod(Modifier.DamageTakenDown) / 100.0);
action.amount = (ushort)(action.amount - CalculateDlvlModifier(dlvl) * (defender.GetMod((uint)Modifier.Defense))).Clamp(0, 9999);
action.amount = (ushort)(action.amount * damageTakenPercent).Clamp(0, 9999);
} }
public static void CalculateSpellDamageTaken(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{
short dlvl = (short)(defender.GetLevel() - attacker.GetLevel());
// todo: elemental resistances
//Patch 1.19:
//Magic Defense has been abolished and no longer appears in equipment attributes.
//The effect of elemental attributes has been changed to that of reducing damage from element-based attacks.
//http://kanican.livejournal.com/55370.html:
//elemental resistance stats are not actually related to resists (except for status effects), instead they impact damage taken
//dlvl, Defense, and Vitality all effect how much damage is taken after hittype takes effect
//player attacks cannot do more than 9999 damage.
double damageTakenPercent = 1 - (defender.GetMod(Modifier.DamageTakenDown) / 100.0);
action.amount = (ushort)(action.amount - CalculateDlvlModifier(dlvl) * (defender.GetMod((uint)Modifier.Defense) + 0.67 * defender.GetMod((uint)Modifier.Vitality))).Clamp(0, 9999);
action.amount = (ushort)(action.amount * damageTakenPercent).Clamp(0, 9999);
}
public static void CalculateBlockDamage(Character attacker, Character defender, BattleCommand skill, BattleAction action) public static void CalculateBlockDamage(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{ {
double percentBlocked = defender.GetMod((uint)Modifier.Block) * .2;//Every point of Block adds .2% to how much is blocked double percentBlocked;
percentBlocked += defender.GetMod((uint)Modifier.Vitality) * .1;//Every point of vitality adds .1% to how much is blocked
percentBlocked = 1 - percentBlocked; //Aegis boon forces a full block
action.amount = (ushort)(action.amount * percentBlocked); if (defender.statusEffects.HasStatusEffect(StatusEffectId.AegisBoon))
percentBlocked = 1.0;
else
{
//Is this a case where VIT gives Block?
percentBlocked = defender.GetMod((uint)Modifier.Block) * 0.002;//Every point of Block adds .2% to how much is blocked
percentBlocked += defender.GetMod((uint)Modifier.Vitality) * 0.001;//Every point of vitality adds .1% to how much is blocked
} }
//don't know crit formula action.amountMitigated = (ushort)(action.amount * percentBlocked);
action.amount = (ushort)(action.amount * (1.0 - percentBlocked));
}
//don't know exact crit bonus formula
public static void CalculateCritDamage(Character attacker, Character defender, BattleCommand skill, BattleAction action) public static void CalculateCritDamage(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{ {
short dlvl = (short)(defender.GetLevel() - attacker.GetLevel()); short dlvl = (short)(defender.GetLevel() - attacker.GetLevel());
@ -124,14 +151,15 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
// - Crit resilience // - Crit resilience
//bonus -= attacker.GetMod((uint)Modifier.CriticalResilience) * potencyModifier; //bonus -= attacker.GetMod((uint)Modifier.CriticalResilience) * potencyModifier;
//need to add something for bonus potency as a part of skill (ie thundara) //need to add something for bonus potency as a part of skill (ie thundara, which breaks the cap)
action.amount = (ushort)(action.amount * bonus.Clamp(1.15, 1.75));//min bonus of 115, max bonus of 175 action.amount = (ushort)(action.amount * bonus.Clamp(1.15, 1.75));//min bonus of 115, max bonus of 175
} }
public static void CalculateParryDamage(Character attacker, Character defender, BattleCommand skill, BattleAction action) public static void CalculateParryDamage(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{ {
double percentParry = .75; double percentParry = 0.75;
action.amountMitigated = (ushort)(action.amount * (1 - percentParry));
action.amount = (ushort)(action.amount * percentParry); action.amount = (ushort)(action.amount * percentParry);
} }
@ -140,80 +168,31 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
//Or we could have HitTypes for DoubleResist, TripleResist, and FullResist that get used here. //Or we could have HitTypes for DoubleResist, TripleResist, and FullResist that get used here.
public static void CalculateResistDamage(Character attacker, Character defender, BattleCommand skill, BattleAction action) public static void CalculateResistDamage(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{ {
double percentResist = .5; double percentResist = 0.5;
action.amountMitigated = (ushort)(action.amount * (1 - percentResist));
action.amount = (ushort)(action.amount * percentResist); action.amount = (ushort)(action.amount * percentResist);
} }
//Used for attacks and abilities like Jump that deal damage //It's weird that stoneskin is handled in C# and all other buffs are in scripts right now
public static ushort CalculateAttackDamage(Character attacker, Character defender, BattleAction action) //But it's because stoneskin acts like both a preaction and postaction buff in that it falls off after damage is dealt but impacts how much damage is dealt
public static void HandleStoneskin(Character defender, BattleAction action)
{ {
ushort damage = (ushort)100; var mitigation = Math.Min(action.amount, defender.GetMod(Modifier.Stoneskin));
if (attacker is Player p) action.amount = (ushort) (action.amount - mitigation).Clamp(0, 9999);
{ defender.SubtractMod((uint)Modifier.Stoneskin, mitigation);
var weapon = p.GetEquipment().GetItemAtSlot(Equipment.SLOT_MAINHAND);
if (weapon != null)
{
var weaponData = Server.GetItemGamedata(weapon.itemId);
//just some numbers from https://www.bluegartr.com/threads/107403-Stats-and-how-they-work/page24
damage += (ushort) (2.225 * (weaponData as WeaponItem).damagePower + (attacker.GetMod((uint) Modifier.Attack) * .38));
}
} }
// todo: handle all other crap before protect/stoneskin public static void DamageTarget(Character attacker, Character defender, BattleAction action, BattleActionContainer actionContainer= null)
// todo: handle crit etc
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Protect) || defender.statusEffects.HasStatusEffect(StatusEffectId.Protect2))
{
if (action != null)
action.effectId |= (uint)HitEffect.Protect;
}
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Stoneskin))
{
if (action != null)
action.effectId |= (uint)HitEffect.Stoneskin;
}
return damage;
}
public static ushort GetCriticalHitDamage(Character attacker, Character defender, BattleAction action)
{
ushort damage = action.amount;
// todo:
//
// action.effectId |= (uint)HitEffect.Critical;
//
return damage;
}
public static ushort CalculateSpellDamage(Character attacker, Character defender, BattleAction action)
{
ushort damage = 0;
// todo: handle all other crap before shell/stoneskin
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Shell))
{
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Shell))
{
}
}
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Stoneskin))
{
}
return damage;
}
public static void DamageTarget(Character attacker, Character defender, BattleAction action, DamageTakenType type, bool sendBattleAction = false)
{ {
if (defender != null) if (defender != null)
{ {
defender.DelHP((short)action.amount);
attacker.OnDamageDealt(defender, action, actionContainer);
defender.OnDamageTaken(attacker, action, actionContainer);
// todo: other stuff too // todo: other stuff too
if (defender is BattleNpc) if (defender is BattleNpc)
{ {
@ -225,66 +204,75 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
bnpc.hateContainer.UpdateHate(attacker, action.enmity); bnpc.hateContainer.UpdateHate(attacker, action.enmity);
bnpc.lastAttacker = attacker; bnpc.lastAttacker = attacker;
} }
defender.DelHP((short) action.amount);
defender.OnDamageTaken(attacker, action, type);
} }
} }
public static void DoAction(Character user, Character receiver, BattleAction action, DamageTakenType type = DamageTakenType.None) public static void HealTarget(Character caster, Character target, BattleAction action, BattleActionContainer actionContainer = null)
{ {
switch(action.battleActionType) if (target != null)
{ {
//split attack into phys/mag? target.AddHP(action.amount);
case (BattleActionType.AttackMagic)://not sure if these use different damage taken formulas
case (BattleActionType.AttackPhysical):
DamageTarget(user, receiver, action, type, false);
break;
case (BattleActionType.Heal):
receiver.AddHP(action.amount);
break;
}
target.statusEffects.CallLuaFunctionByFlag((uint) StatusEffectFlags.ActivateOnHealed, "onHealed", caster, target, action, actionContainer);
if ((type == DamageTakenType.Ability || type == DamageTakenType.Attack) && action.amount != 0)
{
receiver.AddTP(150);
user.AddTP(200);
} }
} }
/*
* Rate functions #region Rate Functions
*/
//How is accuracy actually calculated? //How is accuracy actually calculated?
public static double GetHitRate(Character attacker, Character defender, BattleCommand skill) public static double GetHitRate(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{ {
double hitRate = .80; double hitRate = 80.0;
//Certain skills have lower or higher accuracy rates depending on position/combo
return hitRate * (skill != null ? skill.accuracyModifier : 1); //Add raw hit rate buffs, subtract raw evade buffs, take into account skill's accuracy modifier.
double hitBuff = attacker.GetMod(Modifier.RawHitRate);
double evadeBuff = defender.GetMod(Modifier.RawEvadeRate);
float modifier = skill != null ? skill.accuracyModifier : 0;
hitRate += (hitBuff + modifier).Clamp(0, 100.0);
hitRate -= evadeBuff;
return hitRate.Clamp(0, 100.0);
} }
//Whats the parry formula? //Whats the parry formula?
public static double GetParryRate(Character attacker, Character defender, BattleCommand skill) public static double GetParryRate(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{ {
//Can't parry with shield, must be facing attacker //Can't parry with shield, can't parry rear attacks
if (defender.GetMod((uint)Modifier.HasShield) > 0 || !defender.IsFacing(attacker)) if (defender.GetMod((uint)Modifier.HasShield) != 0 || action.param == (byte) HitDirection.Rear)
return 0; return 0;
return .10; double parryRate = 10.0;
parryRate += defender.GetMod(Modifier.Parry) * 0.1;//.1% rate for every point of Parry
return parryRate + (defender.GetMod(Modifier.RawParryRate));
} }
public static double GetCritRate(Character attacker, Character defender, BattleCommand skill) public static double GetCritRate(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{ {
double critRate = 10;// .0016 * attacker.GetMod((uint)Modifier.CritRating);//Crit rating adds .16% per point if (action.actionType == ActionType.Status)
return Math.Min(critRate, .20);//Crit rate is capped at 20% return 0.0;
//using 10.0 for now since gear isn't working
double critRate = 10.0;// 0.16 * attacker.GetMod((uint)Modifier.CritRating);//Crit rating adds .16% per point
//Add additional crit rate from skill
//Should this be a raw percent or a flat crit raitng? the wording on skills/buffs isn't clear.
critRate += 0.16 * (skill != null ? skill.bonusCritRate : 0);
return critRate + attacker.GetMod(Modifier.RawCritRate);
} }
//http://kanican.livejournal.com/55370.html //http://kanican.livejournal.com/55370.html
// todo: figure that out // todo: figure that out
public static double GetResistRate(Character attacker, Character defender, BattleCommand skill) public static double GetResistRate(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{ {
// todo: add elemental stuff
//Can only resist spells?
if (action.commandType != CommandType.Spell && action.actionProperty <= ActionProperty.Projectile)
return 0.0;
return .95; return 15.0 + defender.GetMod(Modifier.RawResistRate);
} }
//Block Rate follows 4 simple rules: //Block Rate follows 4 simple rules:
@ -292,30 +280,34 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
//(2) Every point in "Block Rate" gives +0.2% rate //(2) Every point in "Block Rate" gives +0.2% rate
//(3) True block proc rate is capped at 75%. No clue on a possible floor. //(3) True block proc rate is capped at 75%. No clue on a possible floor.
//(4) The baseline rate is based on dLVL only(mob stats play no role). The baseline rate is summarized in this raw data sheet: https://imgbox.com/aasLyaJz //(4) The baseline rate is based on dLVL only(mob stats play no role). The baseline rate is summarized in this raw data sheet: https://imgbox.com/aasLyaJz
public static double GetBlockRate(Character attacker, Character defender, BattleCommand skill) public static double GetBlockRate(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{ {
//Shields are required to block. //Shields are required to block and can't block from rear.
if (defender.GetMod((uint)Modifier.HasShield) == 0)//|| !defender.IsFacing(attacker)) if (defender.GetMod((uint)Modifier.HasShield) == 0 || action.param == (byte)HitDirection.Rear)
return 0; return 0;
short dlvl = (short) (attacker.GetLevel() - defender.GetLevel()); short dlvl = (short)(defender.GetLevel() - attacker.GetLevel());
double blockRate = (-2.5 * dlvl) - 5; // Base block rate double blockRate = (2.5 * dlvl) + 5; // Base block rate
blockRate += attacker.GetMod((uint) Modifier.Dexterity) * .1;// .1% for every dex
blockRate += attacker.GetMod((uint) Modifier.BlockRate) * .2;// .2% for every block rate //Is this one of those thing where DEX gives block rate and this would be taking DEX into account twice?
return Math.Min(blockRate, 25); blockRate += defender.GetMod((uint)Modifier.Dexterity) * 0.1;// .1% for every dex
blockRate += defender.GetMod((uint)Modifier.BlockRate) * 0.2;// .2% for every block rate
return Math.Min(blockRate, 25.0) + defender.GetMod((uint)Modifier.RawBlockRate);
} }
#endregion
/*
* HitType helpers. Used for determining if attacks are hits, crits, blocks, etc. and changing their damage based on that
*/
public static bool TryCrit(Character attacker, Character defender, BattleCommand skill, BattleAction action) public static bool TryCrit(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{ {
if (Program.Random.NextDouble() < GetCritRate(attacker, defender, skill)) if ((Program.Random.NextDouble() * 100) <= action.critRate)
{ {
action.hitType = HitType.Crit; action.hitType = HitType.Crit;
CalculateCritDamage(attacker, defender, skill, action); CalculateCritDamage(attacker, defender, skill, action);
if(skill != null)
skill.actionCrit = true;
return true; return true;
} }
@ -324,7 +316,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
public static bool TryResist(Character attacker, Character defender, BattleCommand skill, BattleAction action) public static bool TryResist(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{ {
if (Program.Random.NextDouble() < GetResistRate(attacker, defender, skill)) if ((Program.Random.NextDouble() * 100) <= action.resistRate)
{ {
action.hitType = HitType.Resist; action.hitType = HitType.Resist;
CalculateResistDamage(attacker, defender, skill, action); CalculateResistDamage(attacker, defender, skill, action);
@ -336,10 +328,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
public static bool TryBlock(Character attacker, Character defender, BattleCommand skill, BattleAction action) public static bool TryBlock(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{ {
if (Program.Random.NextDouble() < GetBlockRate(attacker, defender, skill)) if ((Program.Random.NextDouble() * 100) <= action.blockRate)
{ {
action.hitType = HitType.Block; action.hitType = HitType.Block;
defender.SetProc((int)HitType.Block);
CalculateBlockDamage(attacker, defender, skill, action); CalculateBlockDamage(attacker, defender, skill, action);
return true; return true;
} }
@ -349,10 +340,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
public static bool TryParry(Character attacker, Character defender, BattleCommand skill, BattleAction action) public static bool TryParry(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{ {
if (Program.Random.NextDouble() < GetParryRate(attacker, defender, skill)) if ((Program.Random.NextDouble() * 100) <= action.parryRate)
{ {
action.hitType = HitType.Parry; action.hitType = HitType.Parry;
defender.SetProc((int)HitType.Parry);
CalculateParryDamage(attacker, defender, skill, action); CalculateParryDamage(attacker, defender, skill, action);
return true; return true;
} }
@ -363,12 +353,12 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
//TryMiss instead of tryHit because hits are the default and don't change damage //TryMiss instead of tryHit because hits are the default and don't change damage
public static bool TryMiss(Character attacker, Character defender, BattleCommand skill, BattleAction action) public static bool TryMiss(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{ {
if (Program.Random.NextDouble() > GetHitRate(attacker, defender, skill)) if ((Program.Random.NextDouble() * 100) >= GetHitRate(attacker, defender, skill, action))
{ {
action.hitType = HitType.Miss; action.hitType = (ushort)HitType.Miss;
//On misses, the entire amount is considered mitigated
action.amountMitigated = action.amount;
action.amount = 0; action.amount = 0;
defender.SetProc((int)HitType.Evade);
attacker.SetProc((int)HitType.Miss);
return true; return true;
} }
return false; return false;
@ -377,82 +367,167 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
/* /*
* Hit Effecthelpers. Different types of hit effects hits use some flags for different things, so they're split into physical, magical, heal, and status * Hit Effecthelpers. Different types of hit effects hits use some flags for different things, so they're split into physical, magical, heal, and status
*/ */
public static void CalcHitType(Character caster, Character target, BattleCommand skill, BattleAction action) public static void DoAction(Character caster, Character target, BattleCommand skill, BattleAction action, BattleActionContainer actionContainer = null)
{ {
//Might be a simpler way to do this? switch (action.actionType)
switch(action.battleActionType)
{ {
case (BattleActionType.AttackPhysical): case (ActionType.Physical):
SetHitEffectPhysical(caster, target, skill, action); FinishActionPhysical(caster, target, skill, action, actionContainer);
break; break;
case (BattleActionType.AttackMagic): case (ActionType.Magic):
SetHitEffectMagical(caster, target, skill, action); FinishActionSpell(caster, target, skill, action, actionContainer);
break; break;
case (BattleActionType.Heal): case (ActionType.Heal):
SetHitEffectHeal(caster, target, skill, action); FinishActionHeal(caster, target, skill, action, actionContainer);
break; break;
case (BattleActionType.Status): case (ActionType.Status):
SetHitEffectStatus(caster, target, skill, action); FinishActionStatus(caster, target, skill, action, actionContainer);
break; break;
} }
} }
public static void SetHitEffectPhysical(Character attacker, Character defender, BattleCommand skill, BattleAction action) //Determine the hit type, set the hit effect, modify damage based on stoneskin and hit type, hit target
public static void FinishActionPhysical(Character attacker, Character defender, BattleCommand skill, BattleAction action, BattleActionContainer actionContainer = null)
{ {
//Determine the hittype of the action and change amount of damage it does based on that //Figure out the hit type and change damage depending on hit type
if (!TryMiss(attacker, defender, skill, action)) if (!TryMiss(attacker, defender, skill, action))
if (!TryCrit(attacker, defender, skill, action)) {
if (!TryBlock(attacker, defender, skill, action)) //Handle Stoneskin here because it seems like stoneskin mitigates damage done before taking into consideration crit/block/parry damage reductions.
TryParry(attacker, defender, skill, action); //This is based on the fact that a 0 damage attack due to stoneskin will heal for 0 with Aegis Boon, meaning Aegis Boon didn't mitigate any damage
HandleStoneskin(defender, action);
//Crits can't be blocked (is this true for Aegis Boon and Divine Veil?) or parried so they are checked first.
if (!TryCrit(attacker, defender, skill, action))
//Block and parry order don't really matter because if you can block you can't parry and vice versa
if (!TryBlock(attacker, defender, skill, action))
if(!TryParry(attacker, defender, skill, action))
//Finally if it's none of these, the attack was a hit
action.hitType = HitType.Hit;
}
//Actions have different text ids depending on whether they're a part of a multi-hit ws or not.
Dictionary<HitType, ushort> textIds = SingleHitTypeTextIds;
//If this is the first hit of a multi hit command, add the "You use [command] on [target]" action
//Needs to be done here because certain buff messages appear before it.
if (skill != null && skill.numHits > 1)
{
if (action.hitNum == 1)
actionContainer?.AddAction(new BattleAction(attacker.actorId, 30441, 0));
textIds = MultiHitTypeTextIds;
}
//Set the correct textId
action.worldMasterTextId = textIds[action.hitType];
//Set the hit effect
SetHitEffectPhysical(attacker, defender, skill, action, actionContainer);
//Modify damage based on defender's stats
CalculatePhysicalDamageTaken(attacker, defender, skill, action);
actionContainer.AddAction(action);
action.enmity = (ushort) (action.enmity * (skill != null ? skill.enmityModifier : 1));
//Damage the target
DamageTarget(attacker, defender, action, actionContainer);
}
public static void FinishActionSpell(Character attacker, Character defender, BattleCommand skill, BattleAction action, BattleActionContainer actionContainer = null)
{
//Determine the hit type of the action
if (!TryMiss(attacker, defender, skill, action))
{
HandleStoneskin(defender, action);
if (!TryCrit(attacker, defender, skill, action))
if (!TryResist(attacker, defender, skill, action))
action.hitType = HitType.Hit;
}
//There are no multi-hit spells
action.worldMasterTextId = SingleHitTypeTextIds[action.hitType];
//Set the hit effect
SetHitEffectSpell(attacker, defender, skill, action);
HandleStoneskin(defender, action);
CalculateSpellDamageTaken(attacker, defender, skill, action);
actionContainer.AddAction(action);
DamageTarget(attacker, defender, action, actionContainer);
}
public static void FinishActionHeal(Character attacker, Character defender, BattleCommand skill, BattleAction action, BattleActionContainer actionContainer = null)
{
//Set the hit effect
SetHitEffectHeal(attacker, defender, skill, action);
actionContainer.AddAction(action);
HealTarget(attacker, defender, action, actionContainer);
}
public static void FinishActionStatus(Character attacker, Character defender, BattleCommand skill, BattleAction action, BattleActionContainer actionContainer = null)
{
//Set the hit effect
SetHitEffectStatus(attacker, defender, skill, action);
TryStatus(attacker, defender, skill, action, actionContainer, false);
actionContainer.AddAction(action);
}
public static void SetHitEffectPhysical(Character attacker, Character defender, BattleCommand skill, BattleAction action, BattleActionContainer actionContainer)
{
var hitEffect = HitEffect.HitEffectType; var hitEffect = HitEffect.HitEffectType;
HitType hitType = action.hitType;
//Don't know what recoil is actually based on, just guessing //Don't know what recoil is actually based on, just guessing
//Crit is 2 and 3 together //Crit is 2 and 3 together
if (action.hitType == HitType.Crit) if (hitType == HitType.Crit)
hitEffect |= HitEffect.CriticalHit; hitEffect |= HitEffect.CriticalHit;
else else
{ {
//It's not clear what recoil level is based on for physical attacks
double percentDealt = (100.0 * (action.amount / defender.GetMaxHP())); double percentDealt = (100.0 * (action.amount / defender.GetMaxHP()));
if (percentDealt > 5.0) if (percentDealt > 5.0)
hitEffect |= HitEffect.RecoilLv2; hitEffect |= HitEffect.RecoilLv2;
else if (percentDealt > 10) else if (percentDealt > 10)
hitEffect |= HitEffect.RecoilLv3; hitEffect |= HitEffect.RecoilLv3;
} }
action.worldMasterTextId = HitTypeTextIds[action.hitType];
hitEffect |= HitTypeEffects[action.hitType];
if (skill != null && skill.isCombo && action.hitType > HitType.Evade) hitEffect |= HitTypeEffects[hitType];
//For combos that land, add the combo effect
if (skill != null && skill.isCombo && hitType > HitType.Evade && hitType != HitType.Evade)
hitEffect |= (HitEffect)(skill.comboStep << 15); hitEffect |= (HitEffect)(skill.comboStep << 15);
//if attack hit the target, take into account protective status effects //if attack hit the target, take into account protective status effects
if (action.hitType >= HitType.Parry) if (hitType >= HitType.Parry)
{ {
//Protect / Shell only show on physical/ magical attacks respectively. //Protect / Shell only show on physical/ magical attacks respectively.
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Protect)) if (defender.statusEffects.HasStatusEffect(StatusEffectId.Protect))
if (action != null) if (action != null)
hitEffect |= HitEffect.Protect; hitEffect |= HitEffect.Protect;
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Stoneskin)) if (defender.statusEffects.HasStatusEffect(StatusEffectId.Stoneskin))
if (action != null) if (action != null)
hitEffect |= HitEffect.Stoneskin; hitEffect |= HitEffect.Stoneskin;
} }
action.effectId = (uint)hitEffect; action.effectId = (uint)hitEffect;
} }
public static void SetHitEffectMagical(Character attacker, Character defender, BattleCommand skill, BattleAction action) public static void SetHitEffectSpell(Character attacker, Character defender, BattleCommand skill, BattleAction action, BattleActionContainer actionContainer = null)
{ {
//Determine the hit type of the action
if (!TryMiss(attacker, defender, skill, action))
if (!TryCrit(attacker, defender, skill, action))
TryResist(attacker, defender, skill, action);
var hitEffect = HitEffect.MagicEffectType; var hitEffect = HitEffect.MagicEffectType;
HitType hitType = action.hitType;
//Recoil levels for spells are a bit different than physical. Recoil levels are used for resists. //Recoil levels for spells are a bit different than physical. Recoil levels are used for resists.
//Lv1 is for larger resists, Lv2 is for smaller resists and Lv3 is for no resists. Crit is still used for crits //Lv1 is for larger resists, Lv2 is for smaller resists and Lv3 is for no resists. Crit is still used for crits
if (action.hitType == HitType.Resist) if (hitType == HitType.Resist)
{ {
//todo: calculate resist levels and figure out what the difference between Lv1 and 2 in retail was. For now assuming a full resist with 0 damage dealt is Lv1, all other resists Lv2 //todo: calculate resist levels and figure out what the difference between Lv1 and 2 in retail was. For now assuming a full resist with 0 damage dealt is Lv1, all other resists Lv2
if (action.amount == 0) if (action.amount == 0)
@ -460,26 +535,22 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
else else
hitEffect |= HitEffect.RecoilLv2; hitEffect |= HitEffect.RecoilLv2;
} }
else if (action.hitType == HitType.Crit)
hitEffect |= HitEffect.Crit;
else else
hitEffect |= HitEffect.RecoilLv3; hitEffect |= HitEffect.RecoilLv3;
action.worldMasterTextId = HitTypeTextIds[action.hitType]; hitEffect |= HitTypeEffects[hitType];
hitEffect |= HitTypeEffects[action.hitType];
if (skill != null && skill.isCombo) if (skill != null && skill.isCombo)
hitEffect |= (HitEffect)(skill.comboStep << 15); hitEffect |= (HitEffect)(skill.comboStep << 15);
//if attack hit the target, take into account protective status effects //if attack hit the target, take into account protective status effects
if (action.hitType >= HitType.Block) if (hitType >= HitType.Block)
{ {
//Protect / Shell only show on physical/ magical attacks respectively. //Protect / Shell only show on physical/ magical attacks respectively.
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Shell)) if (defender.statusEffects.HasStatusEffect(StatusEffectId.Shell))
if (action != null) if (action != null)
hitEffect |= HitEffect.Shell; hitEffect |= HitEffect.Shell;
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Stoneskin)) if (defender.statusEffects.HasStatusEffect(StatusEffectId.Stoneskin))
if (action != null) if (action != null)
hitEffect |= HitEffect.Stoneskin; hitEffect |= HitEffect.Stoneskin;
@ -503,12 +574,8 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
{ {
var hitEffect = (uint)HitEffect.StatusEffectType | skill.statusId; var hitEffect = (uint)HitEffect.StatusEffectType | skill.statusId;
action.effectId = hitEffect; action.effectId = hitEffect;
}
public static int CalculateSpellDamage(Character attacker, Character defender, BattleCommand spell) action.hitType = HitType.Hit;
{
// todo: spell formulas and stuff (stoneskin, mods, stats, etc)
return 69;
} }
public static uint CalculateSpellCost(Character caster, Character target, BattleCommand spell) public static uint CalculateSpellCost(Character caster, Character target, BattleCommand spell)
@ -527,6 +594,50 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
return scaledCost; return scaledCost;
} }
//IsAdditional is needed because additional actions may be required for some actions' effects
//For instance, Goring Blade's bleed effect requires another action so the first action can still show damage numbers
//Sentinel doesn't require an additional action because it doesn't need to show those numbers
//this is stupid
public static void TryStatus(Character caster, Character target, BattleCommand skill, BattleAction action, BattleActionContainer battleActions, bool isAdditional = true)
{
double rand = Program.Random.NextDouble();
//Statuses only land for non-resisted attacks and attacks that hit
if (skill != null && skill.statusId != 0 && (action.hitType > HitType.Evade && action.hitType != HitType.Resist) && rand < skill.statusChance)
{
StatusEffect effect = Server.GetWorldManager().GetStatusEffect(skill.statusId);
//Because combos might change duration or tier
if (effect != null)
{
effect.SetDuration(skill.statusDuration);
effect.SetTier(skill.statusTier);
effect.SetMagnitude(skill.statusMagnitude);
effect.SetOwner(target);
if (target.statusEffects.AddStatusEffect(effect, caster))
{
//If we need an extra action to show the status text
if (isAdditional)
battleActions.AddAction(target.actorId, 30328, skill.statusId | (uint) HitEffect.StatusEffectType);
}
else
action.worldMasterTextId = 32002;//Is this right?
}
else
{
//until all effects are scripted and added to db just doing this
if (target.statusEffects.AddStatusEffect(skill.statusId, skill.statusTier, skill.statusMagnitude, skill.statusDuration, 3000))
{
//If we need an extra action to show the status text
if (isAdditional)
battleActions.AddAction(target.actorId, 30328, skill.statusId | (uint) HitEffect.StatusEffectType);
}
else
action.worldMasterTextId = 32002;//Is this right?
}
}
}
//Convert a HitDirection to a BattleCommandPositionBonus. Basically just combining left/right into flank //Convert a HitDirection to a BattleCommandPositionBonus. Basically just combining left/right into flank
public static BattleCommandPositionBonus ConvertHitDirToPosition(HitDirection hitDir) public static BattleCommandPositionBonus ConvertHitDirToPosition(HitDirection hitDir)
{ {
@ -548,44 +659,168 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
return position; return position;
} }
//IsAdditional is needed because additional actions may be required for some actions' effects
//For instance, Goring Blade's bleed effect requires another action so the first action can still show damage numbers #region experience helpers
//Sentinel doesn't require an additional action because it doesn't need to show those numbers //See 1.19 patch notes for exp info.
public static void TryStatus(Character caster, Character target, BattleCommand skill, BattleAction action, bool isAdditional = true) public static ushort GetBaseEXP(Player player, BattleNpc mob)
{ {
double rand = Program.Random.NextDouble(); //The way EXP seems to work for most enemies is that it gets the lower character's level, gets the base exp for that level, then uses dlvl to modify that exp
(caster as Player).SendMessage(0x20, "", rand.ToString()); //Less than -19 dlvl gives 0 exp and no message is sent.
if (skill != null && action.amount < target.GetHP() && skill.statusId != 0 && action.hitType > HitType.Evade && rand < skill.statusChance) //This equation doesn't seem to work for certain bosses or NMs.
{ //Some enemies might give less EXP? Unsure on this. It seems like there might have been a change in base exp amounts after 1.19
StatusEffect effect = Server.GetWorldManager().GetStatusEffect(skill.statusId);
//Because combos might change duration or tier //Example:
if (effect != null) //Level 50 in a party kills a level 45 enemy
{ //Base exp is 400, as that's the base EXP for level 45
effect.SetDuration(skill.statusDuration); //That's multiplied by the dlvl modifier for -5, which is 0.5625, which gives 225
effect.SetTier(skill.statusTier); //That's then multiplied by the party modifier, which seems to be 0.667 regardless of party size, which gives 150
effect.SetOwner(target); //150 is then modified by bonus experience from food, rested exp, links, and chains
if (target.statusEffects.AddStatusEffect(effect, caster))
{ int dlvl = mob.GetLevel() - player.GetLevel();
//If we need an extra action to show the status text if (dlvl <= -20)
if (isAdditional) return 0;
action.AddStatusAction(target.actorId, skill.statusId);
} int baseLevel = Math.Min(player.GetLevel(), mob.GetLevel());
ushort baseEXP = BASEEXP[baseLevel];
double dlvlModifier = 1.0;
//There's 2 functions depending on if the dlvl is positive or negative.
if (dlvl >= 0)
//I'm not sure if this caps out at some point. This is correct up to at least +9 dlvl though.
dlvlModifier += 0.2 * dlvl;
else else
action.worldMasterTextId = 32002;//Is this right? //0.1x + 0.0025x^2
} dlvlModifier += 0.1 * dlvl + 0.0025 * (dlvl * dlvl);
else
{ //The party modifier isn't clear yet. It seems like it might just be 0.667 for any number of members in a group, but the 1.19 notes say it's variable
if (target.statusEffects.AddStatusEffect(skill.statusId, 1, 3000, skill.statusDuration, skill.statusTier)) //There also seem to be some cases where it simply doesn't apply but it isn't obvious if that's correct or when it applies if it is correct
{ double partyModifier = player.currentParty.GetMemberCount() == 1 ? 1.0 : 0.667;
//If we need an extra action to show the status text
if (isAdditional) baseEXP = (ushort) (baseEXP * dlvlModifier * partyModifier);
action.AddStatusAction(target.actorId, skill.statusId);
} return baseEXP;
else
action.worldMasterTextId = 32002;//Is this right?
} }
//Gets the EXP bonus when enemies link
public static byte GetLinkBonus(ushort linkCount)
{
byte bonus = 0;
switch (linkCount)
{
case (0):
break;
case (1):
bonus = 25;
break;
case (2):
bonus = 50;
break;
case (3):
bonus = 75;
break;
case (4):
default:
bonus = 100;
break;
}
return bonus;
}
//Gets EXP chain bonus for Attacker fighting Defender
//Official text on EXP Chains: An EXP Chain occurs when players consecutively defeat enemies of equal or higher level than themselves within a specific amount of time.
//Assuming this means that there is no bonus for enemies below player's level and EXP chains are specific to the person, not party
public static byte GetChainBonus(ushort tier)
{
byte bonus = 0;
switch (tier)
{
case (0):
break;
case (1):
bonus = 20;
break;
case (2):
bonus = 25;
break;
case (3):
bonus = 30;
break;
case (4):
bonus = 40;
break;
default:
bonus = 50;
break;
}
return bonus;
}
public static byte GetChainTimeLimit(ushort tier)
{
byte timeLimit = 0;
switch (tier)
{
case (0):
timeLimit = 100;
break;
case (1):
timeLimit = 80;
break;
case (2):
timeLimit = 60;
break;
case (3):
timeLimit = 20;
break;
default:
timeLimit = 10;
break;
}
return timeLimit;
}
//Calculates bonus EXP for Links and Chains
public static void AddBattleBonusEXP(Player attacker, BattleNpc defender, BattleActionContainer actionContainer)
{
ushort baseExp = GetBaseEXP(attacker, defender);
//Only bother calculating the rest if there's actually exp to be gained.
//0 exp sends no message
if (baseExp > 0)
{
int totalBonus = 0;//GetMod(Modifier.bonusEXP)
var linkCount = defender.GetMobMod(MobModifier.LinkCount);
totalBonus += GetLinkBonus((byte)Math.Min(linkCount, 255));
StatusEffect effect = attacker.statusEffects.GetStatusEffectById((uint)StatusEffectId.EXPChain);
ushort expChainNumber = 0;
uint timeLimit = 100;
if (effect != null)
{
expChainNumber = effect.GetTier();
timeLimit = (uint)(GetChainTimeLimit(expChainNumber));
actionContainer?.AddEXPAction(new BattleAction(attacker.actorId, 33919, 0, expChainNumber, (byte)timeLimit));
}
totalBonus += GetChainBonus(expChainNumber);
StatusEffect newChain = Server.GetWorldManager().GetStatusEffect((uint)StatusEffectId.EXPChain);
newChain.SetDuration(timeLimit);
newChain.SetTier((byte)(Math.Min(expChainNumber + 1, 255)));
attacker.statusEffects.AddStatusEffect(newChain, attacker, true, true);
actionContainer?.AddEXPActions(attacker.AddExp(baseExp, (byte)attacker.GetClass(), (byte)(totalBonus.Min(255))));
} }
} }
#endregion
} }
} }

View File

@ -143,7 +143,7 @@ namespace FFXIVClassic_Map_Server.Actors
} }
} }
} }
npcWork.hateType = 2; npcWork.hateType = 3;
var propPacketUtil = new ActorPropertyPacketUtil("npcWork/hate", this); var propPacketUtil = new ActorPropertyPacketUtil("npcWork/hate", this);
propPacketUtil.AddProperty("npcWork.hateType"); propPacketUtil.AddProperty("npcWork.hateType");
return propPacketUtil.Done()[0]; return propPacketUtil.Done()[0];
@ -191,8 +191,23 @@ namespace FFXIVClassic_Map_Server.Actors
public override bool CanCast(Character target, BattleCommand spell) public override bool CanCast(Character target, BattleCommand spell)
{ {
// todo: // todo:
if (target == null)
{
// Target does not exist.
return false; return false;
} }
if (Utils.Distance(positionX, positionY, positionZ, target.positionX, target.positionY, target.positionZ) > spell.range)
{
// The target is out of range.
return false;
}
if (!IsValidTarget(target, spell.mainTarget) || !spell.IsValidMainTarget(this, target))
{
// error packet is set in IsValidTarget
return false;
}
return true;
}
public override bool CanWeaponSkill(Character target, BattleCommand skill) public override bool CanWeaponSkill(Character target, BattleCommand skill)
{ {
@ -256,7 +271,7 @@ namespace FFXIVClassic_Map_Server.Actors
updateFlags |= ActorUpdateFlags.AllNpc; updateFlags |= ActorUpdateFlags.AllNpc;
} }
public override void Die(DateTime tick) public override void Die(DateTime tick, BattleActionContainer actionContainer = null)
{ {
if (IsAlive()) if (IsAlive())
{ {
@ -270,24 +285,25 @@ namespace FFXIVClassic_Map_Server.Actors
{ {
//I think this is, or should be odne in DoBattleAction. Packet capture had the message in the same packet as an attack //I think this is, or should be odne in DoBattleAction. Packet capture had the message in the same packet as an attack
// <actor> defeat/defeats <target> // <actor> defeat/defeats <target>
//((Player)lastAttacker).QueuePacket(BattleActionX01Packet.BuildPacket(lastAttacker.actorId, 0, 0, new BattleAction(actorId, 30108, 0))); actionContainer?.AddEXPAction(new BattleAction(actorId, 30108, 0));
if (lastAttacker.currentParty != null && lastAttacker.currentParty is Party) if (lastAttacker.currentParty != null && lastAttacker.currentParty is Party)
{ {
foreach (var memberId in ((Party)lastAttacker.currentParty).members) foreach (var memberId in ((Party)lastAttacker.currentParty).members)
{ {
var partyMember = zone.FindActorInArea<Player>(memberId); var partyMember = zone.FindActorInArea<Character>(memberId);
// onDeath(monster, player, killer) // onDeath(monster, player, killer)
lua.LuaEngine.CallLuaBattleFunction(this, "onDeath", this, partyMember, lastAttacker); lua.LuaEngine.CallLuaBattleFunction(this, "onDeath", this, partyMember, lastAttacker);
if (partyMember is Player)
((Player)partyMember).AddExp(1500, (byte)partyMember.GetClass(), 5); // todo: add actual experience calculation and exp bonus values.
if (partyMember is Player player)
BattleUtils.AddBattleBonusEXP(player, this, actionContainer);
} }
} }
else else
{ {
// onDeath(monster, player, killer) // onDeath(monster, player, killer)
lua.LuaEngine.CallLuaBattleFunction(this, "onDeath", this, lastAttacker, lastAttacker); lua.LuaEngine.CallLuaBattleFunction(this, "onDeath", this, lastAttacker, lastAttacker);
((Player)lastAttacker).QueuePacket(BattleActionX01Packet.BuildPacket(lastAttacker.actorId, 0, 0, new BattleAction(actorId, 30108, 0))); //((Player)lastAttacker).QueuePacket(BattleActionX01Packet.BuildPacket(lastAttacker.actorId, 0, 0, new BattleAction(actorId, 30108, 0)));
} }
} }
positionUpdates?.Clear(); positionUpdates?.Clear();
@ -413,6 +429,10 @@ namespace FFXIVClassic_Map_Server.Actors
this.bnpcId = id; this.bnpcId = id;
} }
public Int64 GetMobMod(MobModifier mobMod)
{
return GetMobMod((uint)mobMod);
}
public Int64 GetMobMod(uint mobModId) public Int64 GetMobMod(uint mobModId)
{ {
@ -430,10 +450,11 @@ namespace FFXIVClassic_Map_Server.Actors
mobModifiers.Add((MobModifier)mobModId, val); mobModifiers.Add((MobModifier)mobModId, val);
} }
public override void OnDamageTaken(Character attacker, BattleAction action, DamageTakenType damageTakenType) public override void OnDamageTaken(Character attacker, BattleAction action, BattleActionContainer actionContainer = null)
{ {
if (GetMobMod((uint)MobModifier.DefendScript) != 0) if (GetMobMod((uint)MobModifier.DefendScript) != 0)
lua.LuaEngine.CallLuaBattleFunction(this, "onDamageTaken", this, attacker, action.amount, (uint)damageTakenType); lua.LuaEngine.CallLuaBattleFunction(this, "onDamageTaken", this, attacker, action.amount);
base.OnDamageTaken(attacker, action, actionContainer);
} }
} }
} }

View File

@ -33,6 +33,8 @@ namespace FFXIVClassic_Map_Server.actors.chara.npc
CallForHelp = 22, // actor with this id outside of target's party with this can attack me CallForHelp = 22, // actor with this id outside of target's party with this can attack me
FreeForAll = 23, // any actor can attack me FreeForAll = 23, // any actor can attack me
Roams = 24, // Do I walk around? Roams = 24, // Do I walk around?
RoamDelay = 25 // What is the delay between roam ticks RoamDelay = 25, // What is the delay between roam ticks
Linked = 26, // Did I get aggroed via linking?
LinkCount = 27 // How many BattleNPCs got linked with me
} }
} }

View File

@ -610,7 +610,6 @@ namespace FFXIVClassic_Map_Server.Actors
packet.ReplaceActorID(actorId); packet.ReplaceActorID(actorId);
var packets = packet.GetSubpackets(); var packets = packet.GetSubpackets();
QueuePackets(packets); QueuePackets(packets);
} }
catch (Exception e) catch (Exception e)
@ -1776,10 +1775,29 @@ namespace FFXIVClassic_Map_Server.Actors
} }
if ((updateFlags & ActorUpdateFlags.Stats) != 0)
{
var propPacketUtil = new ActorPropertyPacketUtil("charaWork/battleParameter", this);
for (uint i = 0; i < 35; i++)
{
if (GetMod(i) != charaWork.battleTemp.generalParameter[i])
{
charaWork.battleTemp.generalParameter[i] = (short)GetMod(i);
propPacketUtil.AddProperty($"charaWork.battleTemp.generalParameter[{i}]");
}
}
QueuePackets(propPacketUtil.Done());
}
base.PostUpdate(tick, packets); base.PostUpdate(tick, packets);
} }
public override void Die(DateTime tick) public override void Die(DateTime tick, BattleActionContainer actionContainer = null)
{ {
// todo: death timer // todo: death timer
aiContainer.InternalDie(tick, 60); aiContainer.InternalDie(tick, 60);
@ -2219,17 +2237,20 @@ namespace FFXIVClassic_Map_Server.Actors
} }
//Handles exp being added, does not handle figuring out exp bonus from buffs or skill/link chains or any of that //Handles exp being added, does not handle figuring out exp bonus from buffs or skill/link chains or any of that
public void AddExp(int exp, byte classId, int bonusPercent = 0) //Returns BattleActions that can be sent to display the EXP gained number and level ups
public List<BattleAction> AddExp(int exp, byte classId, byte bonusPercent = 0)
{ {
List<BattleAction> actionList = new List<BattleAction>();
exp += (int) Math.Ceiling((exp * bonusPercent / 100.0f)); exp += (int) Math.Ceiling((exp * bonusPercent / 100.0f));
//You earn [exp](+[bonusPercent]%) experience point(s).
SendGameMessage(this, Server.GetWorldManager().GetActor(), 33934, 0x44, this, 0, 0, 0, 0, 0, 0, 0, 0, 0, exp, "", bonusPercent); //33935: You earn [exp] (+[bonusPercent]%) experience points.
actionList.Add(new BattleAction(actorId, 33935, 0, (ushort)exp, bonusPercent));
bool leveled = false; bool leveled = false;
int diff = MAXEXP[GetLevel() - 1] - charaWork.battleSave.skillPoint[classId - 1]; int diff = MAXEXP[GetLevel() - 1] - charaWork.battleSave.skillPoint[classId - 1];
//While there is enough experience to level up, keep leveling up, unlocking skills and removing experience from exp until we don't have enough to level up //While there is enough experience to level up, keep leveling up, unlocking skills and removing experience from exp until we don't have enough to level up
while (exp >= diff && GetLevel() < charaWork.battleSave.skillLevelCap[classId]) while (exp >= diff && GetLevel() < charaWork.battleSave.skillLevelCap[classId])
{ {
//Level up //Level up
LevelUp(classId); LevelUp(classId);
leveled = true; leveled = true;
@ -2263,9 +2284,12 @@ namespace FFXIVClassic_Map_Server.Actors
QueuePackets(expPropertyPacket.Done()); QueuePackets(expPropertyPacket.Done());
Database.SetExp(this, classId, charaWork.battleSave.skillPoint[classId - 1]); Database.SetExp(this, classId, charaWork.battleSave.skillPoint[classId - 1]);
return actionList;
} }
public void LevelUp(byte classId) //Increaess level of current class and equips new abilities earned at that level
public void LevelUp(byte classId, List<BattleAction> actionList = null)
{ {
if (charaWork.battleSave.skillLevel[classId - 1] < charaWork.battleSave.skillLevelCap[classId]) if (charaWork.battleSave.skillLevel[classId - 1] < charaWork.battleSave.skillLevelCap[classId])
{ {
@ -2273,8 +2297,10 @@ namespace FFXIVClassic_Map_Server.Actors
charaWork.battleSave.skillLevel[classId - 1]++; charaWork.battleSave.skillLevel[classId - 1]++;
charaWork.parameterSave.state_mainSkillLevel++; charaWork.parameterSave.state_mainSkillLevel++;
SendGameMessage(this, Server.GetWorldManager().GetActor(), 33909, 0x44, this, 0, 0, 0, 0, 0, 0, 0, 0, 0, (int) GetLevel()); //33909: You gain level [level]
//If there's an ability that unlocks at this level, equip it. actionList?.Add(new BattleAction(actorId, 33909, 0, (ushort) charaWork.battleSave.skillLevel[classId - 1]));
//If there's any abilites that unlocks at this level, equip them.
List<uint> commandIds = Server.GetWorldManager().GetBattleCommandIdByLevel(classId, GetLevel()); List<uint> commandIds = Server.GetWorldManager().GetBattleCommandIdByLevel(classId, GetLevel());
foreach(uint commandId in commandIds) foreach(uint commandId in commandIds)
{ {
@ -2282,6 +2308,9 @@ namespace FFXIVClassic_Map_Server.Actors
byte jobId = ConvertClassIdToJobId(classId); byte jobId = ConvertClassIdToJobId(classId);
if (jobId != classId) if (jobId != classId)
EquipAbilityInFirstOpenSlot(jobId, commandId, false); EquipAbilityInFirstOpenSlot(jobId, commandId, false);
//33926: You learn [command].
actionList?.Add(new BattleAction(actorId, 33926, commandId));
} }
} }
} }
@ -2392,11 +2421,52 @@ namespace FFXIVClassic_Map_Server.Actors
SetMod((uint)Modifier.AttackType, damageAttribute); SetMod((uint)Modifier.AttackType, damageAttribute);
SetMod((uint)Modifier.AttackDelay, attackDelay); SetMod((uint)Modifier.AttackDelay, attackDelay);
SetMod((uint)Modifier.HitCount, hitCount); SetMod((uint)Modifier.HitCount, hitCount);
//These stats all correlate in a 3:2 fashion
//It seems these stats don't actually increase their respective stats. The magic stats do, however
AddMod((uint)Modifier.Attack, (long)(GetMod(Modifier.Strength) * 0.667));
AddMod((uint)Modifier.Accuracy, (long)(GetMod(Modifier.Dexterity) * 0.667));
AddMod((uint)Modifier.Defense, (long)(GetMod(Modifier.Vitality) * 0.667));
//These stats correlate in a 4:1 fashion. (Unsure if MND is accurate but it would make sense for it to be)
AddMod((uint)Modifier.MagicAttack, (long)((float)GetMod(Modifier.Intelligence) * 0.25));
AddMod((uint)Modifier.MagicAccuracy, (long)((float)GetMod(Modifier.Mind) * 0.25));
AddMod((uint)Modifier.MagicHeal, (long)((float)GetMod(Modifier.Mind) * 0.25));
AddMod((uint)Modifier.MagicEvasion, (long)((float)GetMod(Modifier.Piety) * 0.25));
AddMod((uint)Modifier.MagicEnfeeblingPotency, (long)((float)GetMod(Modifier.Piety) * 0.25));
//VIT correlates to HP in a 1:1 fashion
AddMod((uint)Modifier.Hp, (long)((float)Modifier.Vitality));
CalculateTraitMods();
} }
public void SetCurrentJob(ushort jobId)
public bool HasTrait(ushort id)
{ {
currentJob = jobId; BattleTrait trait = Server.GetWorldManager().GetBattleTrait(id);
BroadcastPacket(SetCurrentJobPacket.BuildPacket(actorId, jobId), true);
return HasTrait(trait);
}
public bool HasTrait(BattleTrait trait)
{
return (trait != null) && (trait.job == GetClass()) && (trait.level <= GetLevel());
}
public void CalculateTraitMods()
{
var traitIds = Server.GetWorldManager().GetAllBattleTraitIdsForClass((byte) GetClass());
foreach(var traitId in traitIds)
{
var trait = Server.GetWorldManager().GetBattleTrait(traitId);
if(HasTrait(trait))
{
AddMod(trait.modifier, trait.bonus);
}
}
} }
} }
} }

View File

@ -535,7 +535,7 @@ namespace FFXIVClassic_Map_Server.dataobjects
damagePower = reader.GetInt16("damagePower"); damagePower = reader.GetInt16("damagePower");
damageInterval = reader.GetFloat("damageInterval"); damageInterval = reader.GetFloat("damageInterval");
ammoVirtualDamagePower = reader.GetInt16("ammoVirtualDamagePower"); ammoVirtualDamagePower = reader.GetInt16("ammoVirtualDamagePower");
dps = damagePower / damageInterval;// this is wrong for bows, might need to store this in db because dps is used for weaponskill damage dps = (damagePower + ammoVirtualDamagePower) / damageInterval;
} }
} }

View File

@ -176,8 +176,7 @@ namespace FFXIVClassic_Map_Server.lua
public static int CallLuaStatusEffectFunction(Character actor, StatusEffect effect, string functionName, params object[] args) public static int CallLuaStatusEffectFunction(Character actor, StatusEffect effect, string functionName, params object[] args)
{ {
// todo: this is stupid, load the actual effect name from db table // todo: this is stupid, load the actual effect name from db table
var name = ((StatusEffectId)effect.GetStatusEffectId()).ToString().ToLower(); string path = $"./scripts/effects/{effect.GetName()}.lua";
string path = $"./scripts/effects/{name}.lua";
if (File.Exists(path)) if (File.Exists(path))
{ {

View File

@ -22,7 +22,9 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
//Not setting RecoilLv2 or RecoilLv3 results in the weaker RecoilLv1. //Not setting RecoilLv2 or RecoilLv3 results in the weaker RecoilLv1.
//These are the recoil animations that play on the target, ranging from weak to strong. //These are the recoil animations that play on the target, ranging from weak to strong.
//The recoil that gets set was likely based on the percentage of HP lost from the attack. //The recoil that gets set was likely based on the percentage of HP lost from the attack.
//These also have a visual effect with heals but in reverse. RecoilLv1 has a large effect, Lv3 has none. Crit is very large //These also have a visual effect with heals and spells but in reverse. RecoilLv1 has a large effect, Lv3 has none. Crit is very large
//For spells they represent resists. Lv0 is a max resist, Lv3 is no resist. Crit is still used for crits.
//Heals used the same effects sometimes but it isn't clear what for, it seems random? Possibly something like a trait proccing or even just a bug
RecoilLv1 = 0, RecoilLv1 = 0,
RecoilLv2 = 1 << 0, RecoilLv2 = 1 << 0,
RecoilLv3 = 1 << 1, RecoilLv3 = 1 << 1,
@ -61,6 +63,8 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
// Required for heal text to be blue, not sure if that's all it's used for // Required for heal text to be blue, not sure if that's all it's used for
Heal = 1 << 8, Heal = 1 << 8,
MP = 1 << 9, //Causes "MP" text to appear when used with MagicEffectType. | with Heal to make text blue
TP = 1 << 10,//Causes "TP" text to appear when used with MagicEffectType. | with Heal to make text blue
//If only HitEffect1 is set out of the hit effects, the "Evade!" pop-up text triggers along with the evade visual. //If only HitEffect1 is set out of the hit effects, the "Evade!" pop-up text triggers along with the evade visual.
//If no hit effects are set, the "Miss!" pop-up is triggered and no hit visual is played. //If no hit effects are set, the "Miss!" pop-up is triggered and no hit visual is played.
@ -76,9 +80,9 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
Miss = 0, Miss = 0,
Evade = HitEffect1, Evade = HitEffect1,
Hit = HitEffect1 | HitEffect2, Hit = HitEffect1 | HitEffect2,
Crit = HitEffect3,
Parry = Hit | HitEffect3, Parry = Hit | HitEffect3,
Block = HitEffect4, Block = HitEffect4,
Crit = HitEffect3,
//Knocks you back away from the attacker. //Knocks you back away from the attacker.
KnockbackLv1 = HitEffect4 | HitEffect2 | HitEffect1, KnockbackLv1 = HitEffect4 | HitEffect2 | HitEffect1,
@ -138,74 +142,192 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
Crit = 6 Crit = 6
} }
public enum BattleActionType //Type of action
public enum ActionType : ushort
{ {
None = 0, None = 0,
AttackPhysical = 1, Physical = 1,
AttackMagic = 2, Magic = 2,
Heal = 3, Heal = 3,
Status = 4 Status = 4
} }
//There's are two columns in gamecommand that are for action property and action element respectively and both have percentages next to them
//the percentages are for what percent that property or element factors into the attack. Astral and Umbral are always 33% because they are both 3 elments combined
//ActionProperty and ActionElement are slightly different. Property defines whta type of attack it is, and 11-13 are used for "sonic, breath, neutral". Neutral is always used for magic
//For Element 11-13 are used for astral, umbral, and healing magic.
//Right now we aren't actually using these but when things like resists get better defined we'll have to
public enum ActionProperty : ushort
{
None = 0,
Slashing = 1,
Piercing = 2,
Blunt = 3,
Projectile = 4,
Fire = 5,
Ice = 6,
Wind = 7,
Earth = 8,
Lightning = 9,
Water = 10,
//These I'm not sure about. Check gameCommand.csv
Astral = 11,
Umbral = 12,
Heal = 13
}
/*
public enum ActionProperty : ushort
{
None = 0,
Slashing = 1,
Piercing = 2,
Blunt = 3,
Projectile = 4,
Fire = 5,
Ice = 6,
Wind = 7,
Earth = 8,
Lightning = 9,
Water = 10,
Sonic = 11,
Breath = 12,
Neutral = 13,
Astral = 14,
Umbral = 15
}
public enum ActionElement : ushort
{
None = 0,
Slashing = 1,
Piercing = 2,
Blunt = 3,
Projectile = 4,
Fire = 5,
Ice = 6,
Wind = 7,
Earth = 8,
Lightning = 9,
Water = 10,
//These I'm not sure about. Check gameCommand.csv
Astral = 11,
Umbral = 12,
Heal = 13
}*/
class BattleAction class BattleAction
{ {
public uint targetId; public uint targetId;
public ushort amount; public ushort amount;
public ushort amountMitigated; //Amount that got blocked/evaded or resisted
public ushort enmity; //Seperate from amount for abilities that cause a different amount of enmity than damage public ushort enmity; //Seperate from amount for abilities that cause a different amount of enmity than damage
public ushort worldMasterTextId; public ushort worldMasterTextId;
public uint effectId; //Impact effect, damage/heal/status numbers or name public uint effectId; //Impact effect, damage/heal/status numbers or name
public byte param; //Which side the battle action is coming from public byte param; //Which side the battle action is coming from
public byte hitNum; //Which hit in a sequence of hits this is public byte hitNum; //Which hit in a sequence of hits this is
public HitType hitType;
//Need a list of actions for commands that may both deal damage and inflict status effects
public List<BattleAction> actionsList;
/// <summary> /// <summary>
/// this field is not actually part of the packet struct /// these fields are not actually part of the packet struct
/// </summary> /// </summary>
public uint animation; public uint animation;
public BattleActionType battleActionType; public CommandType commandType; //What type of command was used (ie weaponskill, ability, etc)
public ActionProperty actionProperty; //Damage type of the action
public ActionType actionType; //Type of this action (ie physical, magic, heal)
public HitType hitType;
public BattleAction(uint targetId, ushort worldMasterTextId, uint effectId, ushort amount = 0, byte param = 0, byte unknown = 1) //Rates, I'm not sure if these need to be stored like this but with the way some buffs work maybe they do?
//Makes things like Blindside easy at least.
public double parryRate = 0.0;
public double blockRate = 0.0;
public double resistRate = 0.0;
public double hitRate = 0.0;
public double critRate = 0.0;
public BattleAction(uint targetId, ushort worldMasterTextId, uint effectId, ushort amount = 0, byte param = 0, byte hitNum = 1)
{ {
this.targetId = targetId; this.targetId = targetId;
this.worldMasterTextId = worldMasterTextId; this.worldMasterTextId = worldMasterTextId;
this.effectId = effectId; this.effectId = effectId;
this.amount = amount; this.amount = amount;
this.param = param; this.param = param;
this.hitNum = unknown; this.hitNum = hitNum;
this.hitType = HitType.Hit; this.hitType = HitType.Hit;
this.enmity = amount; this.enmity = amount;
this.actionsList = new List<BattleAction>(); this.commandType = (byte) CommandType.None;
this.battleActionType = BattleActionType.None;
actionsList.Add(this);
} }
public void AddStatusAction(uint targetId, uint effectId) public BattleAction(uint targetId, BattleCommand command, byte param = 0, byte hitNum = 1)
{ {
actionsList.Add(new BattleAction(targetId, 30328, effectId | (uint) HitEffect.StatusEffectType)); this.targetId = targetId;
this.worldMasterTextId = command.worldMasterTextId;
this.param = param;
this.hitNum = hitNum;
this.commandType = command.commandType;
this.actionProperty = command.actionProperty;
this.actionType = command.actionType;
} }
public void AddHealAction(uint targetId, ushort amount) //Order of what (probably) happens when a skill is used:
//Buffs that alter things like recast times or that only happen once per skill usage like Power Surge are activated
//Script calculates damage and handles any special requirements
//Rates are calculated
//Buffs that impact indiviudal hits like Blindside or Blood for Blood are activated
//The final hit type is determined
//Stoneskin takes damage
//Final damage amount is calculated using the hit type and defender's stats
//Buffs that activate or respond to damage like Rampage. Stoneskin gets removed AFTER damage if it falls off.
//Additional effects that are a part of the skill itself or weapon in case of auto attacks take place like status effects
//Certain buffs that alter the whole skill fall off (Resonance, Excruciate)
public void DoAction(Character caster, Character target, BattleCommand skill, BattleActionContainer battleActions)
{ {
var a = new BattleAction(targetId, 30320, (uint)(HitEffect.MagicEffectType | HitEffect.RecoilLv3 | HitEffect.Heal), amount); //First calculate rates for hit/block/etc
actionsList.Add(a); CalcRates(caster, target, skill);
//Next, modify those rates based on preaction buffs
//Still not sure how we shouldh andle these
PreAction(caster, target, skill, battleActions);
BattleUtils.DoAction(caster, target, skill, this, battleActions);
} }
public void CalcHitType(Character caster, Character target, BattleCommand skill)
//Calculate the chance of hitting/critting/etc
public void CalcRates(Character caster, Character target, BattleCommand skill)
{ {
BattleUtils.CalcHitType(caster, target, skill, this); hitRate = BattleUtils.GetHitRate(caster, target, skill, this);
critRate = BattleUtils.GetCritRate(caster, target, skill, this);
blockRate = BattleUtils.GetBlockRate(caster, target, skill, this);
parryRate = BattleUtils.GetParryRate(caster, target, skill, this);
resistRate = BattleUtils.GetResistRate(caster, target, skill, this);
} }
public void TryStatus(Character caster, Character target, BattleCommand skill, bool isAdditional = true) //These are buffs that activate before the action hits. Usually they change things like hit or crit rates or damage
public void PreAction(Character caster, Character target, BattleCommand skill, BattleActionContainer battleActions)
{ {
BattleUtils.TryStatus(caster, target, skill, this, isAdditional); target.statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnPreactionTarget, "onPreAction", caster, target, skill, this, battleActions);
caster.statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnPreactionCaster, "onPreAction", caster, target, skill, this, battleActions);
} }
public List<BattleAction> GetAllActions() //Try and apply a status effect
public void TryStatus(Character caster, Character target, BattleCommand skill, BattleActionContainer battleActions, bool isAdditional = true)
{ {
return actionsList; BattleUtils.TryStatus(caster, target, skill, this, battleActions, isAdditional);
}
public ushort GetHitType()
{
return (ushort)hitType;
} }
} }
} }

View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FFXIVClassic_Map_Server.packets.send.actor.battle
{
class BattleActionContainer
{
private List<BattleAction> actionsList = new List<BattleAction>();
//EXP messages are always the last mesages in battlea ction packets, so they get appended after all the rest of the actions are done.
private List<BattleAction> expActionList = new List<BattleAction>();
public BattleActionContainer()
{
}
public void AddAction(uint targetId, ushort worldMasterTextId, uint effectId, ushort amount = 0, byte param = 0, byte hitNum = 0)
{
AddAction(new BattleAction(targetId, worldMasterTextId, effectId, amount, param, hitNum));
}
//Just to make scripting simpler
public void AddMPAction(uint targetId, ushort worldMasterTextId, ushort amount)
{
uint effectId = (uint) (HitEffect.MagicEffectType | HitEffect.MP | HitEffect.Heal);
AddAction(targetId, worldMasterTextId, effectId, amount);
}
public void AddHPAction(uint targetId, ushort worldMasterTextId, ushort amount)
{
uint effectId = (uint)(HitEffect.MagicEffectType | HitEffect.Heal);
AddAction(targetId, worldMasterTextId, effectId, amount);
}
public void AddTPAction(uint targetId, ushort worldMasterTextId, ushort amount)
{
uint effectId = (uint)(HitEffect.MagicEffectType | HitEffect.TP);
AddAction(targetId, worldMasterTextId, effectId, amount);
}
public void AddAction(BattleAction action)
{
if (action != null)
actionsList.Add(action);
}
public void AddActions(List<BattleAction> actions)
{
actionsList.AddRange(actions);
}
public void AddEXPAction(BattleAction action)
{
expActionList.Add(action);
}
public void AddEXPActions(List<BattleAction> actionList)
{
expActionList.AddRange(actionList);
}
public void CombineLists()
{
actionsList.AddRange(expActionList);
}
public List<BattleAction> GetList()
{
return actionsList;
}
}
}

View File

@ -33,13 +33,12 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
binWriter.Seek(0x20, SeekOrigin.Begin); binWriter.Seek(0x20, SeekOrigin.Begin);
binWriter.Write((UInt32)1); //Num actions (always 1 for this) binWriter.Write((UInt32)1); //Num actions (always 1 for this)
binWriter.Write((UInt16)commandId); binWriter.Write((UInt16)commandId);
binWriter.Write((UInt16)0x810); //? binWriter.Write((UInt16)0x801); //?
binWriter.Write((UInt32)action.targetId); binWriter.Write((UInt32)action.targetId);
binWriter.Write((UInt16)action.amount); binWriter.Write((UInt16)action.amount);
binWriter.Write((UInt16)action.worldMasterTextId); binWriter.Write((UInt16)action.worldMasterTextId);
Program.Log.Info(action.worldMasterTextId);
binWriter.Write((UInt32)action.effectId); binWriter.Write((UInt32)action.effectId);
binWriter.Write((Byte)action.param); binWriter.Write((Byte)action.param);

View File

@ -104,7 +104,9 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
binWriter.Seek(0x78, SeekOrigin.Begin); binWriter.Seek(0x78, SeekOrigin.Begin);
for (int i = 0; i < max; i++) for (int i = 0; i < max; i++)
{
binWriter.Write((UInt32)actionList[listOffset + i].effectId); binWriter.Write((UInt32)actionList[listOffset + i].effectId);
}
binWriter.Seek(0xA0, SeekOrigin.Begin); binWriter.Seek(0xA0, SeekOrigin.Begin);
for (int i = 0; i < max; i++) for (int i = 0; i < max; i++)

View File

@ -31,9 +31,9 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
//Missing... last value is float, string in here as well? //Missing... last value is float, string in here as well?
binWriter.Seek(0x20, SeekOrigin.Begin); binWriter.Seek(0x20, SeekOrigin.Begin);
binWriter.Write((UInt32)actionList.Length); //Num actions binWriter.Write((UInt32)max); //Num actions
binWriter.Write((UInt16)commandId); binWriter.Write((UInt16)commandId);
binWriter.Write((UInt16)0x810); //? binWriter.Write((UInt16)0x818); //?
binWriter.Seek(0x28, SeekOrigin.Begin); binWriter.Seek(0x28, SeekOrigin.Begin);
for (int i = 0; i < max; i++) for (int i = 0; i < max; i++)
@ -86,9 +86,9 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
//Missing... last value is float, string in here as well? //Missing... last value is float, string in here as well?
binWriter.Seek(0x20, SeekOrigin.Begin); binWriter.Seek(0x20, SeekOrigin.Begin);
binWriter.Write((UInt32)actionList.Count); //Num actions binWriter.Write((UInt32)max); //Num actions
binWriter.Write((UInt16)commandId); binWriter.Write((UInt16)commandId);
binWriter.Write((UInt16)0x810); //? binWriter.Write((UInt16)0x818); //?
binWriter.Seek(0x28, SeekOrigin.Begin); binWriter.Seek(0x28, SeekOrigin.Begin);
for (int i = 0; i < max; i++) for (int i = 0; i < max; i++)

View File

@ -34,27 +34,25 @@ end;
--For abilities that inflict statuses, like aegis boon or taunt --For abilities that inflict statuses, like aegis boon or taunt
function onStatusAbilityFinish(caster, target, skill, action) function onStatusAbilityFinish(caster, target, skill, action)
action.battleActionType = BattleActionType.Status; --action.CalcHitType(caster, target, skill);
action.CalcHitType(caster, target, skill); action.DoAction(caster, target, skill);
action.TryStatus(caster, target, skill, false); action.TryStatus(caster, target, skill, false);
return action.amount; return action.amount;
end; end;
function onAttackAbilityFinish(caster, target, skill, action) function onAttackAbilityFinish(caster, target, skill, action)
action.battleActionType = BattleActionType.AttackPhysical;
local damage = math.random(50, 150); local damage = math.random(50, 150);
action.amount = damage; action.amount = damage;
action.CalcHitType(caster, target, skill); action.DoAction(caster, target, skill);
return action.amount; return action.amount;
end; end;
function onHealAbilityFinish(caster, target, skill, action) function onHealAbilityFinish(caster, target, skill, action)
action.battleActionType = BattleActionType.Heal;
local amount = math.random(150, 250); local amount = math.random(150, 250);
action.amount = amount; action.amount = amount;
action.CalcHitType(caster, target, skill); action.DoAction(caster, target, skill);
action.TryStatus(caster, target, skill, true); action.TryStatus(caster, target, skill, true);
return action.amount; return action.amount;
end; end;

View File

@ -1,8 +1,64 @@
BattleActionType = CommandType =
{ {
None = 0, None = 0,
AttackPhysical = 1, AutoAttack = 1,
AttackMagic = 2, Weaponskill = 2,
Ability = 3,
Spell = 4
}
ActionType =
{
None = 0,
Physical = 1,
Magic = 2,
Heal = 3, Heal = 3,
Status = 4 Status = 4
} }
ActionProperty =
{
None = 0,
Physical = 1,
Magic = 2,
Heal = 4,
Status = 8,
Ranged = 16
}
DamageTakenType =
{
None,
Attack,
Magic,
Weaponskill,
Ability
}
HitDirection =
{
None = 0,
Front = 1,
Right = 2,
Rear = 4,
Left = 8
}
HitType =
{
Miss = 0,
Evade = 1,
Parry = 2,
Block = 3,
Resist = 4,
Hit = 5,
Crit = 6
}
TargetFindAOEType =
{
None = 0,
Circle = 1,
Cone = 2,
Box = 3
}

View File

@ -16,5 +16,4 @@ local attackMagicHandlers = {
function onEventStarted(player, command, triggerName, arg1, arg2, arg3, arg4, targetActor, arg5, arg6, arg7, arg8) function onEventStarted(player, command, triggerName, arg1, arg2, arg3, arg4, targetActor, arg5, arg6, arg7, arg8)
player.Ability(command.actorId, targetActor); player.Ability(command.actorId, targetActor);
player:endEvent(); player:endEvent();
end end

View File

@ -10,9 +10,9 @@ Switches between active and passive mode states
function onEventStarted(player, command, triggerName) function onEventStarted(player, command, triggerName)
if (player.newMainState == 0x0000) then if (player.currentMainState == 0x0000) then
player.Engage(0, 0x0002); player.Engage(0, 0x0002);
elseif (player.newMainState == 0x0002) then elseif (player.currentMainState == 0x0002) then
player.Disengage(0x0000); player.Disengage(0x0000);
end end
player:endEvent(); player:endEvent();

View File

@ -0,0 +1,6 @@
function onEventStarted(player, caller, commandRequest, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)
player:SetCurrentJob(17);
player:EndEvent();
end

View File

@ -70,7 +70,7 @@ function onEventStarted(player, equipAbilityWidget, triggername, slot, commandid
ability = worldManager.GetBattleCommand(commandid); ability = worldManager.GetBattleCommand(commandid);
--Is the ability a part of the player's current class? --Is the ability a part of the player's current class?
--This check isn't correct because of jobs having different ids --This check isn't correct because of jobs having different ids
local classId = player:GetClass(); local classId = player:GetCurrentClassOrJob();
local jobId = player:ConvertClassIdToJobId(classId); local jobId = player:ConvertClassIdToJobId(classId);
if(ability.job == classId or ability.job == jobId) then if(ability.job == classId or ability.job == jobId) then

View File

@ -0,0 +1,7 @@
function onEventStarted(player, command, triggerName, arg1, arg2, arg3, arg4, targetActor, arg5, arg6, arg7, arg8)
player.Cast(command.actorId, targetActor);
player:endEvent();
end

View File

@ -0,0 +1,18 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27164: Swift Aegis Boon
if caster.HasTrait(27164) then
ability.recastTimeMs = ability.recastTimeMs - 15000;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,22 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
ability.statusMagnitude = 4;
--27242: Enhanced Barrage: Adds an additional attack to barrage ( 4 -> 5 )
if caster.HasTrait(27242) then
ability.statusMagnitude = 5;
end
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27205: Enhanced Berserk: Increases the effect of Berserk by 20%
if caster.HasTrait(27205) then
ability.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27121: Enhanced Blindside
if caster.HasTrait(27121) then
ability.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,43 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27362: Enhanced Blissful Mind
if caster.HasTrait(27362) then
ability.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--Blissful Mind
--223228: Blissful Mind
--223242: Fully Blissful Mind
local buff = caster.statusEffects.GetStatusEffectById(223228) or caster.statusEffects.GetStatusEffectById(223242);
--If we have a buff then Blissful Mind removes that buff and restores MP. Otherwise, it adds the Blissful Mind effect
if buff ~= nil then
local amount = buff.GetExtra();
local remAction = caster.statusEffects.RemoveStatusEffectForBattleAction(buff, 30329);
caster.AddMP(amount);
actionContainer.AddMPAction(caster.actorId, 30321, amount);
actionContainer.AddAction(remAction);
else
--Blissful mind takes 25% of CURRENT HP and begins storing MP up to that point, at which point the buff changes to indicate its full
local amount = caster.GetHP() * 0.25;
caster.DelHP(amount);
skill.statusMagnitude = amount;
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end
end;

View File

@ -0,0 +1,24 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27283: Enhanced Blood for Blood: Increases damage dealt to enemies by B4B by 25%
if caster.HasTrait(27283) then
ability.statusTier = 2;
end
--27284: Swift Blood for Blood: Reduces recast time of B4B by 15 seconds
if caster.HasTrait(27284) then
ability.recastTimeMs = ability.recastTimeMs - 15000;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27202: Swift Bloodbath
if caster.HasTrait(27202) then
ability.recastTimeMs = ability.recastTimeMs - 15000;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,18 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27245: Swift Chameleon
if caster.HasTrait(27245) then
ability.recastTimeMs = ability.recastTimeMs - 60000;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
target.hateContainer.UpdateHate(caster, -840);
end;

View File

@ -0,0 +1,15 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--8032701: Fighter's Gauntlets: Reduces Collusion cooldown by 10 seconds
if caster.GetEquipment().GetItemAtSlot(14).itemId == 8032701 then
skill.recastTimeMs = skill.recastTimeMs - 10000;
end
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,23 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--This is for the "Cover" effect the caster receives.
local coverTier = 1
--8032701: Gallant Surcoat: Enhances Cover
if caster.GetEquipment().GetItemAtSlot(10).itemId == 8032701 then
coverTier = 2;
end
actionContainer.AddAction(caster.statusEffects.AddStatusForBattleAction(223063, coverTier));
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27320: Swift Dark Seal
if caster.HasTrait(27320) then
ability.recastTimeMs = ability.recastTimeMs - 30000;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27244: Enhanced Decoy: Renders Decoy capable of evading melee attacks
if caster.HasTrait(27244) then
ability.statusId = 223238;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -9,6 +9,5 @@ function onAbilityStart(caster, target, skill)
return 0; return 0;
end; end;
function onAbilityFinish(caster, target, skill, action) function onSkillFinish(caster, target, skill, action)
return onStatusAbilityFinish(caster, target, skill, action);
end; end;

View File

@ -0,0 +1,20 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--8051401: Gallant Cuisses
if caster.GetEquipment().GetItemAtSlot(14).itemId == 8051401 then
ability.statusTier = 2;
end
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -9,6 +9,8 @@ function onAbilityStart(caster, target, ability)
return 0; return 0;
end; end;
function onAbilityFinish(caster, target, skill, action) function onSkillFinish(caster, target, skill, action, actionContainer)
return onAttackAbilityFinish(caster, target, skill, action); action.amount = skill.basePotency;
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end; end;

View File

@ -0,0 +1,24 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--Need a better way to do this
for i = 223212,223217 do
local remAction = caster.statusEffects.RemoveStatusEffectForBattleAction(i, 30329)
if remAction ~= nil then
actionContainer.AddAction(remAction);
skill.statusTier = 2;
break;
end
end
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,16 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--How to do enmity?
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27203: Enhanced Outmaneuver
if caster.HasTrait(27203) then
ability.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27321: Enhanced Excruciate: Increases critical rate bonus from Excruciate.
if caster.HasTrait(27321) then
ability.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27123: Enhanced Featherfoot
if caster.HasTrait(27123) then
ability.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27124: Enhanced Fists of Earth
if caster.HasTrait(27125) then
ability.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27124: Enhanced Fists of Fire
if caster.HasTrait(27124) then
ability.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,27 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27161: Enhanced Flash: Adds Blind effect to flash
if caster.HasTrait(27161) then
ability.statusChance = 1;
end
--27162: Enhanced Flash II: Expands Flash to affect enemies near target
if caster.HasTrait(27162) then
ability.aoeTarget = TargetFindAOEType.Circle;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
action.enmity = 400;
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27201: Swift Foresight
if caster.HasTrait(27201) then
ability.recastTimeMs = ability.recastTimeMs - 15000;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,28 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27245: Swift Chameleon
if caster.HasTrait(27245) then
ability.recastTimeMs = ability.recastTimeMs - 60000;
end
return 0;
end;
--Get all targets with hate on caster and spread 1140 enmity between them.
function onSkillFinish(caster, target, skill, action, actionContainer)
--[[
local enemies = caster.GetTargetsWithHate()
local enmity = 1140 / enemies.Count
for enemy in enemies do
enemy.hateContainer.updateHate(enmity);
end]]
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27240: Enhanced Hawks Eye
--Increases accuracy gained by 50%. (Hawks Eye normally gives 12.5% of your accuracy, Traited it gives 18.75%)
if caster.HasTrait(27240) then
ability.statusTier = 2
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -6,9 +6,24 @@ function onAbilityPrepare(caster, target, ability)
end; end;
function onAbilityStart(caster, target, ability) function onAbilityStart(caster, target, ability)
--27280: Enhanced Invigorate: Increases duration of Invigorate by 15 seconds
if caster.HasTrait(27280) then
ability.statusDuration = ability.statusDuration + 15;
end
--Drachen Mail: Increases Invigorate TP tick from 100 to 120.
local magnitude = 100;
--8032704: Drachen Mail
if caster.GetEquipment().GetItemAtSlot(10).itemId == 8032704 then
magnitude = 120;
end
ability.statusMagnitude = magnitude;
return 0; return 0;
end; end;
function onAbilityFinish(caster, target, ability, action) function onSkillFinish(caster, target, skill, action, actionContainer)
return onStatusAbilityFinish(caster, target, ability, action) --DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end; end;

View File

@ -9,6 +9,9 @@ function onAbilityStart(caster, target, ability)
return 0; return 0;
end; end;
function onAbilityFinish(caster, target, skill, action) function onSkillFinish(caster, target, skill, action, actionContainer)
return onAttackAbilityFinish(caster, target, skill, action); action.amount = skill.basePotency;
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end; end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27285: Enhanced Keen Flurry: Reduces recast time of WS used during KF by 50%
if caster.HasTrait(27285) then
ability.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,53 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27282: Enhanced Life Surge: Increases effect of Life Surge by 20%
if caster.HasTrait(27282) then
ability.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--Need a better way to do this
--223212: Power Surge I
--223213: Power Surge II
--223212: Power Surge III
--No message is sent when PS is removed by Life Surge
caster.statusEffects.RemoveStatusEffect(223212, true);
caster.statusEffects.RemoveStatusEffect(223213, true);
caster.statusEffects.RemoveStatusEffect(223214, true);
--Using this ability moves to the next LS buff
local removeId = 0;
--223215: Life Surge I
--223216: Life Surge II
--223217: Life Surge III
if caster.statusEffects.HasStatusEffect(223215) then
removeId = 223215;
skill.statusId = 223216;
skill.statusTier = 2;
elseif caster.statusEffects.HasStatusEffect(223216) then
removeId = 223216;
skill.statusId = 223217;
skill.statusTier = 3;
elseif caster.statusEffects.HasStatusEffect(223217) then
effect = caster.statusEffects.GetStatusEffectById(223217)
effect.RefreshTime();
skill.statusId = 223217;
end
if not (removeId == 0) then
--caster.statusEffects.RemoveStatusEffect(removeId, true);
caster.statusEffects.ReplaceEffect(caster.statusEffects.GetStatusEffectById(removeId), skill.statusId, skill.statusTier, skill.statusMagnitude, skill.statusDuration);
end
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -9,6 +9,8 @@ function onAbilityStart(caster, target, ability)
return 0; return 0;
end; end;
function onAbilityFinish(caster, target, skill, action) function onSkillFinish(caster, target, skill, action, actionContainer)
return onAttackAbilityFinish(caster, target, skill, action); action.amount = skill.basePotency;
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end; end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27322: Swift Dark Seal
if caster.HasTrait(27322) then
ability.recastTimeMs = ability.recastTimeMs - 30000;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27164: Enhanced Outmaneuver
if caster.HasTrait(27164) then
ability.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27323: Enhanced Parsimony: Increases MP gained from Parsimony by 25%
if caster.HasTrait(27323) then
ability.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,24 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27281: Enhanced Power Surge: Increases effect of Power Surge by 50%
if caster.HasTrait(27281) then
ability.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--Need a better way to do this
actionContainer.AddAction(caster.statusEffects.RemoveStatusEffectForBattleAction(223215));
actionContainer.AddAction(caster.statusEffects.RemoveStatusEffectForBattleAction(223216));
actionContainer.AddAction(caster.statusEffects.RemoveStatusEffectForBattleAction(223217));
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,21 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27200: Enhanced Provoke: Adds Attack Down effect to Provoke.
if caster.HasTrait(27200) then
ability.statusChance = 1.0;
end
return 0;
end;
--http://forum.square-enix.com/ffxiv/threads/47393-Tachi-s-Guide-to-Paladin-%28post-1.22b%29
function onSkillFinish(caster, target, skill, action, actionContainer)
action.enmity = 750;
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,29 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--QS gives 300 TP by default.
skill.statusMagnitude = 300;
--I'm assuming that with raging strikes, that increases to 500.
--and traited that increases again to 750 (or 450 without RS)
if caster.statusEffects.HasStatusEffect(223221) then
actionContainer.AddAction(caster.statusEffects.RemoveStatusEffectForBattleAction(223221));
skill.statusMagnitude = 500;
end
--27241: Enhanced Quelling Strike: Increases TP gained from QS by 50%
if caster.HasTrait(27241) then
skill.statusMagnitude = skill.statusMagnitude * 1.5;
end
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27243: Enhanced Raging Strike: Increases effect of Raging Strike by 50%
if caster.HasTrait(27241) then
ability.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27204: Enhanced Rampage
if caster.HasTrait(27204) then
ability.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,27 @@
require("global");
require("ability");
require("battleutils")
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27163: Enhanced Rampart:Expands rampart to affect party members
if caster.HasTrait(27163) then
ability.aoeType = TargetFindAOEType.Circle;
end
return 0;
end;
--http://forum.square-enix.com/ffxiv/threads/47393-Tachi-s-Guide-to-Paladin-%28post-1.22b%29
--180 enmity per member that has enmity on the current enemy
--Need to figure out enmity system
function onSkillFinish(caster, target, skill, action, actionContainer)
action.enmity = 180;
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27360: Swift Sacred Prism: Reduces recast by 30 seconds
if caster.HasTrait(27360) then
ability.recastTimeMs = ability.recastTimeMs - 30000;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -1,14 +1,39 @@
require("global"); require("global");
require("ability"); require("modifiers");
--require("ability");
function onAbilityPrepare(caster, target, ability) function onAbilityPrepare(caster, target, ability)
return 0; return 0;
end; end;
function onAbilityStart(caster, target, ability) function onAbilityStart(caster, target, ability)
return 0; return 0;
end; end;
function onAbilityFinish(caster, target, ability, action) --http://forum.square-enix.com/ffxiv/threads/51208-2nd-wind-modifier
return onHealAbilityFinish(caster, target, ability, action) --The primary modifier for SW is class level.
--There are three other factors that contribute to SW:
-- PGL's SW trait, which increases potency by 25%.
-- A bonus from INT (2INT=1HP)
-- An additional random integer (580 at level 50. +/- 3%)
function onSkillFinish(caster, target, skill, action, actionContainer)
--Base formula isn't quit known yet
local amount = 100;
--Heals can vary by up to ~3.5% in either direction
amount = math.Clamp(amount * (0.965 + (math.rand() * 7.0)), 0, 9999);
--PGL gets an INT bonus for Second Wind
if caster.GetClass() == 2 then
amount = amount + caster.GetMod(modifiersGlobal.Intelligence) / 2;
end;
--27120: Enhanced Second Wind
if caster.HasTrait(27120) then
amount = amount * 1.25;
end;
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end; end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27160: Enhanced Sentinel
if caster.HasTrait(27160) then
ability.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27361: Swift Shroud of Saints
if caster.HasTrait(27361) then
ability.recastTimeMs = ability.recastTimeMs - 60000;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,19 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--27122: Swift Taunt: Reduces recast time by 15 seconds.
if caster.HasTrait(27121) then
ability.recastTimeMs = ability.recastTimeMs - 15000;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,20 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--Is this before or after status is gained?
--Will probably need to switch to a flag for this because it might include more than just these 3 effects.
actionContainer.AddAction(caster.statusEffects.RemoveStatusEffectForBattleAction(228011));
actionContainer.AddAction(caster.statusEffects.RemoveStatusEffectForBattleAction(228013));
actionContainer.AddAction(caster.statusEffects.RemoveStatusEffectForBattleAction(228021));
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,22 @@
require("global");
require("ability");
require("battleutils")
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
--8032703: Fighter's Cuirass: Enhances Vengeance
if caster.GetEquipment().GetItemAtSlot(13).itemId == 8032703 then
skill.statusTier = 2;
end
return 0;
end;
function onSkillFinish(caster, target, skill, action, actionContainer)
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end;

View File

@ -0,0 +1,29 @@
require("global");
properties = {
permissions = 0,
parameters = "sssss",
description =
[[
Adds experience <qty> to player or <targetname>.
!giveexp <qty> |
!giveexp <qty> <targetname> |
]],
}
function onTrigger(player, argc, commandId, animationId, textId, effectId, amount)
local sender = "[battleaction] ";
if player then
cid = tonumber(commandId) or 0;
aid = tonumber(animationId) or 0;
tid = tonumber(textId) or 0;
print(effectId)
eid = tonumber(effectId) or 0;
amt = tonumber(amount) or 0;
player:DoBattleActionAnimation(cid, aid, tid, eid, amt);
else
print(sender.."unable to add experience, ensure player name is valid.");
end;
end;

View File

@ -31,4 +31,5 @@ function onTrigger(player, argc, slot, wId, eId, vId, cId)
else else
player:SendMessage(messageID, sender, "No parameters sent! Usage: "..properties.description); player:SendMessage(messageID, sender, "No parameters sent! Usage: "..properties.description);
end; end;
end; end;

View File

@ -0,0 +1,24 @@
require("global");
properties = {
permissions = 0,
parameters = "s",
description =
[[
Changes appearance for equipment with given parameters.
!graphic <slot> <wID> <eID> <vID> <vID>
]],
}
function onTrigger(player, argc, size)
local messageID = MESSAGE_TYPE_SYSTEM_ERROR;
local sender = "[setappearance] ";
s = tonumber(size) or 0;
if player and player.target then
player.target.appearanceIds[0] = s;
player.target.zone.BroadcastPacketAroundActor(player.target, player.target.CreateAppearancePacket());
end;
end;

View File

@ -0,0 +1,27 @@
require("global");
properties = {
permissions = 0,
parameters = "s",
description =
[[
Changes appearance for equipment with given parameters.
!graphic <slot> <wID> <eID> <vID> <vID>
]],
}
function onTrigger(player, argc, state)
local messageID = MESSAGE_TYPE_SYSTEM_ERROR;
local sender = "[setstate] ";
max = tonumber(state) or 0;
for s = 0, max do
if player and player.target then
player.target:ChangeState(s);
wait(0.8);
player:SendMessage(0x20, "", "state: "..s);
end;
end
end;

View File

@ -1,4 +1,5 @@
require("global"); require("global");
require("modifiers");
properties = { properties = {
permissions = 0, permissions = 0,
@ -149,16 +150,18 @@ function onTrigger(player, argc, width, height, blockCount)
local zone = pos[4]; local zone = pos[4];
local w = tonumber(width) or 0; local w = tonumber(width) or 0;
local h = tonumber(height) or 0; local h = tonumber(height) or 0;
local blocks = tonumber(blockCount) or 0;
printf("%f %f %f", x, y, z); printf("%f %f %f", x, y, z);
--local x, y, z = player.GetPos(); --local x, y, z = player.GetPos();
for i = 0, blockCount do for i = 0, blocks do
for i = 0, w do for i = 0, w do
for j = 0, h do for j = 0, h do
local actor = player.GetZone().SpawnActor(2104001, 'ass', x + (i - (w / 2) * 3), y, z + (j - (h / 2) * 3), rot, 0, 0, true); local actor = player.GetZone().SpawnActor(2104001, 'ass', x + (i - (w / 2) * 3), y, z + (j - (h / 2) * 3), rot, 0, 0, true);
actor.ChangeNpcAppearance(1001149) actor.ChangeNpcAppearance(1001149);
actor.SetLevel(50); actor.SetMaxHP(10000);
actor.SetHP(10000);
actor.SetMod(modifiersGlobal.HasShield, 1);
end end
--actor.FollowTarget(player, 3.2);
end end
x = x + 500 x = x + 500

View File

@ -2,20 +2,18 @@ require("global");
properties = { properties = {
permissions = 0, permissions = 0,
parameters = "sss", parameters = "",
description = description =
[[ [[
Set movement speed for player. Enter no value to reset to default. Get the amount of actors in this zone.
!speed <run> | !zonecount
!speed <stop> <walk> <run> |
]] ]]
} }
function onTrigger(player, argc, stop, walk, run) function onTrigger(player, argc)
local message = tostring(player.zone.GetAllActors().Count); local message = tostring(player.zone.GetAllActors().Count);
player.SendMessage(0x20, "", message); player.SendMessage(0x20, "", message);
end end

View File

@ -9,6 +9,14 @@ function onMagicStart(caster, target, spell)
return 0; return 0;
end; end;
function onMagicFinish(caster, target, spell, action) function onSkillFinish(caster, target, skill, action, actionContainer)
magic.onMagicFinish(caster, target, spell, action) --calculate damage
action.amount = skill.basePotency;
action.statusMagnitude = 15;
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
--Try to apply status effect
action.TryStatus(caster, target, skill, actionContainer, true);
end; end;

View File

@ -15,13 +15,19 @@ function onCombo(caster, target, spell)
spell.potency = spell.potency * 1.5; spell.potency = spell.potency * 1.5;
end; end;
function onMagicFinish(caster, target, spell, action) function onSkillFinish(caster, target, skill, action, actionContainer)
local damage = math.random(10, 100);
--Dispels an effect on each target. --Dispels an effect on each target.
local effects = target.statusEffects.GetStatusEffectsByFlag(16); --lose on dispel local effects = target.statusEffects.GetStatusEffectsByFlag2(16); --lose on dispel
if effects != nil then if effects != nil then
target.statusEffects.RemoveStatusEffect(effects[0]); target.statusEffects.RemoveStatusEffect(effects[0]);
end; end;
return damage;
--calculate damage
action.amount = skill.basePotency;
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
--Try to apply status effect
action.TryStatus(caster, target, skill, actionContainer, true);
end; end;

View File

@ -9,6 +9,7 @@ function onMagicStart(caster, target, spell)
return 0; return 0;
end; end;
function onMagicFinish(caster, target, spell, action) function onSkillFinish(caster, target, skill, action, actionContainer)
magic.onBuffMagicFinish(caster, target, spell, action) --DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end; end;

View File

@ -9,6 +9,13 @@ function onMagicStart(caster, target, spell)
return 0; return 0;
end; end;
function onMagicFinish(caster, target, spell, action) function onSkillFinish(caster, target, skill, action, actionContainer)
return magic.onMagicFinish(caster, target, spell, action) --calculate damage
action.amount = skill.basePotency;
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
--Try to apply status effect
action.TryStatus(caster, target, skill, actionContainer, true);
end; end;

View File

@ -8,6 +8,13 @@ function onMagicStart(caster, target, spell)
return 0; return 0;
end; end;
function onMagicFinish(caster, target, spell, action) function onSkillFinish(caster, target, skill, action, actionContainer)
magic.onMagicFinish(caster, target, spell, action) --calculate damage
action.amount = skill.basePotency;
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
--Try to apply status effect
action.TryStatus(caster, target, skill, actionContainer, true);
end; end;

View File

@ -14,6 +14,13 @@ function onCombo(caster, target, spell)
end; end;
function onMagicFinish(caster, target, spell, action) function onSkillFinish(caster, target, skill, action, actionContainer)
magic.onMagicFinish(caster, target, spell, action) --calculate damage
action.amount = skill.basePotency;
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
--Try to apply status effect
action.TryStatus(caster, target, skill, actionContainer, true);
end; end;

View File

@ -9,6 +9,13 @@ function onMagicStart(caster, target, spell)
return 0; return 0;
end; end;
function onMagicFinish(caster, target, spell, action) function onSkillFinish(caster, target, skill, action, actionContainer)
magic.onCureMagicFinish(caster, target, spell, action) --http://forum.square-enix.com/ffxiv/threads/41900-White-Mage-A-Guide
--2.5 HP per Healing Magic Potency
--0.5 HP per MND
--this is WITH WHM AF chest, don't know formula without AF. AF seems to increase healing by 7-10%?
action.amount = 2.5 * caster.GetMod(modifiersGlobal.MagicHeal) + 0.5 * (caster.GetMod(modifiersGlobal.Mind));
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end; end;

View File

@ -5,11 +5,15 @@ function onMagicPrepare(caster, target, spell)
return 0; return 0;
end; end;
--Idea: add way to sort list of targets by hp here?
function onMagicStart(caster, target, spell) function onMagicStart(caster, target, spell)
return 0; return 0;
end; end;
--http://forum.square-enix.com/ffxiv/threads/41900-White-Mage-A-Guide read function onSkillFinish(caster, target, skill, action, actionContainer)
function onMagicFinish(caster, target, spell, action) --calculate damage
magic.onCureMagicFinish(caster, target, spell, action) action.amount = skill.basePotency;
--DoAction handles rates, buffs, dealing damage
action.DoAction(caster, target, skill, actionContainer);
end; end;

Some files were not shown because too many files have changed in this diff Show More