1
Fork 0
mirror of https://bitbucket.org/Ioncannon/project-meteor-server.git synced 2025-04-25 06:07:46 +00:00

Combat changes and bug fixes

Added the combo and proc systems
Added scripts for most weaponskill and spells as well as some abilities and status effects
Added support for multihit attacks
Added AbilityState for abilities
Added hiteffects that change based on an attack's parameters
Added positionals

Changed how targeting works for battlecommands

Fixed bug that occurred when moving or swapping hotbar commands
Fixed bug that occurred when losing status effects
This commit is contained in:
yogurt 2018-02-15 13:20:46 -06:00
parent 837c7a9223
commit b8d6a943aa
175 changed files with 4361 additions and 1213 deletions

View file

@ -964,7 +964,7 @@ namespace FFXIVClassic_Map_Server
var effect = Server.GetWorldManager().GetStatusEffect(id); var effect = Server.GetWorldManager().GetStatusEffect(id);
if (effect != null) if (effect != null)
{ {
effect.SetDurationMs(duration); effect.SetDuration(duration);
effect.SetMagnitude(magnitude); effect.SetMagnitude(magnitude);
effect.SetTickMs(tick); effect.SetTickMs(tick);
effect.SetTier(tier); effect.SetTier(tier);
@ -1276,8 +1276,7 @@ namespace FFXIVClassic_Map_Server
} }
public static void EquipAbility(Player player, byte classId, ushort hotbarSlot, uint commandId, uint recastTime) public static void EquipAbility(Player player, byte classId, ushort hotbarSlot, uint commandId, uint recastTime)
{ {
//2700083201 is where abilities start. 2700083200 is for unequipping abilities. Trying to put this in the hotbar will crash the game, need to put 0 instead if (commandId > 0)
if (commandId > 2700083200)
{ {
using (MySqlConnection conn = new MySqlConnection( using (MySqlConnection conn = new MySqlConnection(
String.Format("Server={0}; Port={1}; Database={2}; UID={3}; Password={4}", String.Format("Server={0}; Port={1}; Database={2}; UID={3}; Password={4}",
@ -1321,7 +1320,7 @@ namespace FFXIVClassic_Map_Server
UnequipAbility(player, hotbarSlot); UnequipAbility(player, hotbarSlot);
} }
//Unequipping is done by sending an equip packet with 2700083200 as the ability and the hotbar slot of the action being unequipped //Unequipping is done by sending an equip packet with 0xA0F00000 as the ability and the hotbar slot of the action being unequipped
public static void UnequipAbility(Player player, ushort hotbarSlot) public static void UnequipAbility(Player player, ushort hotbarSlot)
{ {
using (MySqlConnection conn = new MySqlConnection( using (MySqlConnection conn = new MySqlConnection(
@ -1338,8 +1337,6 @@ namespace FFXIVClassic_Map_Server
MySqlCommand cmd; MySqlCommand cmd;
string query = ""; string query = "";
//Drop
List<Tuple<ushort, uint>> hotbarList = new List<Tuple<ushort, uint>>();
query = @" query = @"
DELETE FROM characters_hotbar DELETE FROM characters_hotbar
WHERE characterId = @charId AND classId = @classId AND hotbarSlot = @hotbarSlot WHERE characterId = @charId AND classId = @classId AND hotbarSlot = @hotbarSlot
@ -1393,13 +1390,13 @@ namespace FFXIVClassic_Map_Server
{ {
int hotbarSlot = reader.GetUInt16("hotbarSlot"); int hotbarSlot = reader.GetUInt16("hotbarSlot");
uint commandId = reader.GetUInt32("commandId"); uint commandId = reader.GetUInt32("commandId");
player.charaWork.command[hotbarSlot + player.charaWork.commandBorder] = commandId | 0xA0F00000; player.charaWork.command[hotbarSlot + player.charaWork.commandBorder] = 0xA0F00000 | commandId;
player.charaWork.commandCategory[hotbarSlot + player.charaWork.commandBorder] = 1; player.charaWork.commandCategory[hotbarSlot + player.charaWork.commandBorder] = 1;
player.charaWork.parameterSave.commandSlot_recastTime[hotbarSlot] = reader.GetUInt32("recastTime"); player.charaWork.parameterSave.commandSlot_recastTime[hotbarSlot] = reader.GetUInt32("recastTime");
//Recast timer //Recast timer
BattleCommand ability = Server.GetWorldManager().GetBattleCommand((ushort)(commandId)); BattleCommand ability = Server.GetWorldManager().GetBattleCommand((ushort)(commandId));
player.charaWork.parameterTemp.maxCommandRecastTime[hotbarSlot] = (ushort) (ability != null ? ability.recastTimeSeconds : 1); player.charaWork.parameterTemp.maxCommandRecastTime[hotbarSlot] = (ushort) (ability != null ? ability.maxRecastTimeSeconds : 1);
} }
} }
} }
@ -1448,7 +1445,7 @@ namespace FFXIVClassic_Map_Server
while (reader.Read()) while (reader.Read())
{ {
if (slot != reader.GetUInt16("hotbarSlot")) if (slot != reader.GetUInt16("hotbarSlot"))
return slot; break;
slot++; slot++;
} }
@ -2189,7 +2186,7 @@ namespace FFXIVClassic_Map_Server
{ {
conn.Open(); conn.Open();
var query = @"SELECT id, name, flags, overwrite FROM server_statuseffects;"; var query = @"SELECT id, name, flags, overwrite, tickMs FROM server_statuseffects;";
MySqlCommand cmd = new MySqlCommand(query, conn); MySqlCommand cmd = new MySqlCommand(query, conn);
@ -2201,8 +2198,9 @@ namespace FFXIVClassic_Map_Server
var name = reader.GetString("name"); var name = reader.GetString("name");
var flags = reader.GetUInt32("flags"); var flags = reader.GetUInt32("flags");
var overwrite = reader.GetByte("overwrite"); var overwrite = reader.GetByte("overwrite");
var tickMs = reader.GetUInt32("tickMs");
var effect = new StatusEffect(id, name, flags, overwrite); var effect = new StatusEffect(id, name, flags, overwrite, tickMs);
lua.LuaEngine.LoadStatusEffectScript(effect);
effects.Add(id, effect); effects.Add(id, effect);
} }
} }
@ -2231,7 +2229,7 @@ namespace FFXIVClassic_Map_Server
string queries = ""; string queries = "";
foreach (var effect in player.statusEffects.GetStatusEffects()) foreach (var effect in player.statusEffects.GetStatusEffects())
{ {
var duration = effect.GetDurationMs() + effect.GetStartTime().Millisecond - Program.Tick.Millisecond; var duration = effect.GetDuration() + effect.GetStartTime().Second - Program.Tick.Second;
queries += Environment.NewLine + $"REPLACE INTO characters_statuseffect(characterId, statusId, magnitude, duration, tick, tier, extra) VALUES ({player.actorId}, {effect.GetStatusEffectId()}, {effect.GetMagnitude()}, {duration}, {effect.GetTickMs()}, {effect.GetTier()}, {effect.GetExtra()});"; queries += Environment.NewLine + $"REPLACE INTO characters_statuseffect(characterId, statusId, magnitude, duration, tick, tier, extra) VALUES ({player.actorId}, {effect.GetStatusEffectId()}, {effect.GetMagnitude()}, {duration}, {effect.GetTickMs()}, {effect.GetTier()}, {effect.GetExtra()});";
} }
@ -2259,10 +2257,11 @@ namespace FFXIVClassic_Map_Server
{ {
try try
{ {
int count = 0;
conn.Open(); conn.Open();
var query = ("SELECT `id`, name, classJob, lvl, requirements, validTarget, aoeType, aoeRange, aoeTarget, numHits, positionBonus, procRequirement, `range`, buffDuration, debuffDuration, " + 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 FROM server_battle_commands;"); "castType, castTime, recastTime, mpCost, tpCost, animationType, effectAnimation, modelAnimation, animationDuration, battleAnimation, validUser, comboId1, comboId2, comboStep, accuracyMod, worldMasterTextId FROM server_battle_commands;");
MySqlCommand cmd = new MySqlCommand(query, conn); MySqlCommand cmd = new MySqlCommand(query, conn);
@ -2277,17 +2276,21 @@ namespace FFXIVClassic_Map_Server
battleCommand.job = reader.GetByte("classJob"); battleCommand.job = reader.GetByte("classJob");
battleCommand.level = reader.GetByte("lvl"); battleCommand.level = reader.GetByte("lvl");
battleCommand.requirements = (BattleCommandRequirements)reader.GetUInt16("requirements"); battleCommand.requirements = (BattleCommandRequirements)reader.GetUInt16("requirements");
battleCommand.mainTarget = (ValidTarget)reader.GetByte("mainTarget");
battleCommand.validTarget = (ValidTarget)reader.GetByte("validTarget"); battleCommand.validTarget = (ValidTarget)reader.GetByte("validTarget");
battleCommand.aoeType = (TargetFindAOEType)reader.GetByte("aoeType"); battleCommand.aoeType = (TargetFindAOEType)reader.GetByte("aoeType");
battleCommand.basePotency = reader.GetUInt16("basePotency");
battleCommand.numHits = reader.GetByte("numHits"); battleCommand.numHits = reader.GetByte("numHits");
battleCommand.positionBonus = (BattleCommandPositionBonus)reader.GetByte("positionBonus"); battleCommand.positionBonus = (BattleCommandPositionBonus)reader.GetByte("positionBonus");
battleCommand.procRequirement = (BattleCommandProcRequirement)reader.GetByte("procRequirement"); battleCommand.procRequirement = (BattleCommandProcRequirement)reader.GetByte("procRequirement");
battleCommand.range = reader.GetInt32("range"); battleCommand.range = reader.GetInt32("range");
battleCommand.debuffDurationSeconds = reader.GetUInt32("debuffDuration"); battleCommand.statusId = reader.GetUInt32("statusId");
battleCommand.buffDurationSeconds = reader.GetUInt32("buffDuration"); battleCommand.statusDuration = reader.GetUInt32("statusDuration");
battleCommand.statusChance = reader.GetFloat("statusChance");
battleCommand.castType = reader.GetByte("castType"); battleCommand.castType = reader.GetByte("castType");
battleCommand.castTimeSeconds = reader.GetUInt32("castTime"); battleCommand.castTimeMs = reader.GetUInt32("castTime");
battleCommand.recastTimeSeconds = reader.GetUInt32("recastTime"); battleCommand.maxRecastTimeSeconds = reader.GetUInt32("recastTime");
battleCommand.recastTimeMs = battleCommand.maxRecastTimeSeconds * 1000;
battleCommand.mpCost = reader.GetUInt16("mpCost"); battleCommand.mpCost = reader.GetUInt16("mpCost");
battleCommand.tpCost = reader.GetUInt16("tpCost"); battleCommand.tpCost = reader.GetUInt16("tpCost");
battleCommand.animationType = reader.GetByte("animationType"); battleCommand.animationType = reader.GetByte("animationType");
@ -2300,6 +2303,12 @@ namespace FFXIVClassic_Map_Server
battleCommand.battleAnimation = reader.GetUInt32("battleAnimation"); battleCommand.battleAnimation = reader.GetUInt32("battleAnimation");
battleCommand.validUser = (BattleCommandValidUser)reader.GetByte("validUser"); battleCommand.validUser = (BattleCommandValidUser)reader.GetByte("validUser");
battleCommand.comboNextCommandId[0] = reader.GetInt32("comboId1");
battleCommand.comboNextCommandId[1] = reader.GetInt32("comboId2");
battleCommand.comboStep = reader.GetInt16("comboStep");
battleCommand.accuracyModifier = reader.GetFloat("accuracyMod");
battleCommand.worldMasterTextId = reader.GetUInt16("worldMasterTextId");
lua.LuaEngine.LoadBattleCommandScript(battleCommand, "weaponskill");
battleCommandDict.Add(id, battleCommand); battleCommandDict.Add(id, battleCommand);
Tuple<byte, short> tuple = Tuple.Create<byte, short>(battleCommand.job, battleCommand.level); Tuple<byte, short> tuple = Tuple.Create<byte, short>(battleCommand.job, battleCommand.level);
@ -2312,8 +2321,11 @@ namespace FFXIVClassic_Map_Server
List<uint> list = new List<uint>() { id | 0xA0F00000 }; List<uint> list = new List<uint>() { id | 0xA0F00000 };
battleCommandIdByLevel.Add(tuple, list); battleCommandIdByLevel.Add(tuple, list);
} }
count++;
} }
} }
Program.Log.Info(String.Format("Loaded {0} battle commands.", count));
} }
catch (MySqlException e) catch (MySqlException e)
{ {

View file

@ -95,6 +95,7 @@
<Compile Include="actors\chara\ai\HateContainer.cs" /> <Compile Include="actors\chara\ai\HateContainer.cs" />
<Compile Include="actors\chara\ai\helpers\PathFind.cs" /> <Compile Include="actors\chara\ai\helpers\PathFind.cs" />
<Compile Include="actors\chara\ai\BattleCommand.cs" /> <Compile Include="actors\chara\ai\BattleCommand.cs" />
<Compile Include="actors\chara\ai\state\AbilityState.cs" />
<Compile Include="actors\chara\ai\state\AttackState.cs" /> <Compile Include="actors\chara\ai\state\AttackState.cs" />
<Compile Include="actors\chara\ai\state\DeathState.cs" /> <Compile Include="actors\chara\ai\state\DeathState.cs" />
<Compile Include="actors\chara\ai\state\DespawnState.cs" /> <Compile Include="actors\chara\ai\state\DespawnState.cs" />

View file

@ -364,11 +364,13 @@ namespace FFXIVClassic_Map_Server.Actors
return classParams; return classParams;
} }
//character's newMainState kind of messes with this
public void ChangeState(ushort newState) public void ChangeState(ushort newState)
{ {
//if (newState != currentMainState) if (newState != currentMainState)
{ {
currentMainState = newState; currentMainState = newState;
updateFlags |= (ActorUpdateFlags.State | ActorUpdateFlags.Position); updateFlags |= (ActorUpdateFlags.State | ActorUpdateFlags.Position);
} }
} }
@ -403,6 +405,8 @@ namespace FFXIVClassic_Map_Server.Actors
if (positionUpdates != null && positionUpdates.Count > 0) if (positionUpdates != null && positionUpdates.Count > 0)
{ {
var pos = positionUpdates[0]; var pos = positionUpdates[0];
if (pos != null)
{
oldPositionX = positionX; oldPositionX = positionX;
oldPositionY = positionY; oldPositionY = positionY;
oldPositionZ = positionZ; oldPositionZ = positionZ;
@ -415,8 +419,9 @@ namespace FFXIVClassic_Map_Server.Actors
zone.UpdateActorPosition(this); zone.UpdateActorPosition(this);
//Program.Server.GetInstance().mLuaEngine.OnPath(actor, position, positionUpdates) //Program.Server.GetInstance().mLuaEngine.OnPath(actor, position, positionUpdates)
}
positionUpdates.Remove(pos); positionUpdates.Remove(pos);
} }
packets.Add(CreatePositionUpdatePacket()); packets.Add(CreatePositionUpdatePacket());
} }

View file

@ -141,6 +141,7 @@ namespace FFXIVClassic_Map_Server.Actors
public void RemoveActorFromZone(Actor actor) public void RemoveActorFromZone(Actor actor)
{ {
if (actor != null)
lock (mActorList) lock (mActorList)
{ {
mActorList.Remove(actor.actorId); mActorList.Remove(actor.actorId);
@ -487,7 +488,6 @@ namespace FFXIVClassic_Map_Server.Actors
return null; return null;
uint zoneId; uint zoneId;
if (this is PrivateArea) if (this is PrivateArea)
zoneId = ((PrivateArea)this).GetParentZone().actorId; zoneId = ((PrivateArea)this).GetParentZone().actorId;
else else
@ -500,8 +500,9 @@ 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(3000); npc.SetMaxHP(30000);
//npc.SetHP(3000); npc.SetHP(30000);
npc.ResetMoveSpeeds();
AddActorToZone(npc); AddActorToZone(npc);

View file

@ -125,13 +125,18 @@ namespace FFXIVClassic_Map_Server.Actors
public ushort newMainState; public ushort newMainState;
public float spawnX, spawnY, spawnZ; public float spawnX, spawnY, spawnZ;
//I needed some values I could reuse for random stuff, delete later
public int extraInt;
public uint extraUint;
public float extraFloat;
protected Dictionary<string, UInt64> tempVars = new Dictionary<string, UInt64>(); protected Dictionary<string, UInt64> tempVars = new Dictionary<string, UInt64>();
public Character(uint actorID) : base(actorID) public Character(uint actorID) : base(actorID)
{ {
//Init timer array to "notimer" //Init timer array to "notimer"
for (int i = 0; i < charaWork.statusShownTime.Length; i++) for (int i = 0; i < charaWork.statusShownTime.Length; i++)
charaWork.statusShownTime[i] = 0xFFFFFFFF; charaWork.statusShownTime[i] = 0;
this.statusEffects = new StatusEffectContainer(this); this.statusEffects = new StatusEffectContainer(this);
@ -191,11 +196,14 @@ namespace FFXIVClassic_Map_Server.Actors
var propPacketUtil = new ActorPropertyPacketUtil("charaWork/status", this); var propPacketUtil = new ActorPropertyPacketUtil("charaWork/status", this);
var i = 0; var i = 0;
foreach (var effect in statusEffects.GetStatusEffects()) foreach (var effect in statusEffects.GetStatusEffects())
{
if (!effect.GetHidden())
{ {
propPacketUtil.AddProperty($"charaWork.statusShownTime[{i}]"); propPacketUtil.AddProperty($"charaWork.statusShownTime[{i}]");
propPacketUtil.AddProperty(String.Format("charaWork.statusShownTime[{0}]", i)); propPacketUtil.AddProperty(String.Format("charaWork.statusShownTime[{0}]", i));
i++; i++;
} }
}
return propPacketUtil.Done(); return propPacketUtil.Done();
} }
@ -230,6 +238,7 @@ namespace FFXIVClassic_Map_Server.Actors
{ {
int currentIndex = 0; int currentIndex = 0;
//AoE abilities only ever hit 16 people, so we probably won't need this loop anymore //AoE abilities only ever hit 16 people, so we probably won't need this loop anymore
//Apparently aoe are limited to 8?
while (true) while (true)
{ {
if (actions.Length - currentIndex >= 10) if (actions.Length - currentIndex >= 10)
@ -255,7 +264,7 @@ namespace FFXIVClassic_Map_Server.Actors
while (true) while (true)
{ {
if (actions.Count - currentIndex >= 18) if (actions.Count - currentIndex >= 10)
zone.BroadcastPacketAroundActor(this, BattleActionX18Packet.BuildPacket(actorId, animationId, commandId, actions, ref currentIndex)); zone.BroadcastPacketAroundActor(this, BattleActionX18Packet.BuildPacket(actorId, animationId, commandId, actions, ref currentIndex));
else if (actions.Count - currentIndex > 1) else if (actions.Count - currentIndex > 1)
zone.BroadcastPacketAroundActor(this, BattleActionX10Packet.BuildPacket(actorId, animationId, commandId, actions, ref currentIndex)); zone.BroadcastPacketAroundActor(this, BattleActionX10Packet.BuildPacket(actorId, animationId, commandId, actions, ref currentIndex));
@ -585,6 +594,8 @@ namespace FFXIVClassic_Map_Server.Actors
} }
public void AddTP(int tp) public void AddTP(int tp)
{
if (IsAlive())
{ {
charaWork.parameterTemp.tp = (short)((charaWork.parameterTemp.tp + tp).Clamp(0, 3000)); charaWork.parameterTemp.tp = (short)((charaWork.parameterTemp.tp + tp).Clamp(0, 3000));
tpBase = (ushort)charaWork.parameterTemp.tp; tpBase = (ushort)charaWork.parameterTemp.tp;
@ -593,6 +604,7 @@ namespace FFXIVClassic_Map_Server.Actors
if (tpBase >= 1000) if (tpBase >= 1000)
lua.LuaEngine.GetInstance().OnSignal("tpOver1000"); lua.LuaEngine.GetInstance().OnSignal("tpOver1000");
} }
}
public void DelHP(int hp) public void DelHP(int hp)
{ {
@ -609,14 +621,9 @@ namespace FFXIVClassic_Map_Server.Actors
AddTP(-tp); AddTP(-tp);
} }
public void CalculateBaseStats() virtual public void CalculateBaseStats()
{ {
// todo: apply mods and shit here, get race/level/job and shit // todo: apply mods and shit here, get race/level/job and shit
}
public void RecalculateStats()
{
uint hpMod = (uint) GetMod((uint)Modifier.Hp); uint hpMod = (uint) GetMod((uint)Modifier.Hp);
if (hpMod != 0) if (hpMod != 0)
{ {
@ -644,6 +651,14 @@ namespace FFXIVClassic_Map_Server.Actors
} }
// todo: recalculate stats and crap // todo: recalculate stats and crap
updateFlags |= ActorUpdateFlags.HpTpMp; updateFlags |= ActorUpdateFlags.HpTpMp;
SetMod((uint)Modifier.HitCount, 1);
}
public void RecalculateStats()
{
//CalculateBaseStats();
} }
public void SetStat(uint statId, uint val) public void SetStat(uint statId, uint val)
@ -664,57 +679,65 @@ namespace FFXIVClassic_Map_Server.Actors
public virtual void OnAttack(State state, BattleAction action, ref BattleAction error) public virtual void OnAttack(State state, BattleAction action, ref BattleAction error)
{ {
// todo: change animation based on equipped weapon
action.effectId |= (uint)HitEffect.HitVisual1; // melee
var target = state.GetTarget(); var target = state.GetTarget();
// todo: change animation based on equipped weapon
// todo: get hitrate and shit, handle protect effect and whatever // todo: get hitrate and shit, handle protect effect and whatever
if (BattleUtils.TryAttack(this, target, action, ref error)) if (BattleUtils.TryAttack(this, target, action, ref error))
{ {
action.amount = BattleUtils.CalculateAttackDamage(this, target, action);
//var packet = BattleActionX01Packet.BuildPacket(owner.actorId, owner.actorId, target.actorId, (uint)0x19001000, (uint)0x8000604, (ushort)0x765D, (ushort)BattleActionX01PacketCommand.Attack, (ushort)damage, (byte)0x1); //var packet = BattleActionX01Packet.BuildPacket(owner.actorId, owner.actorId, target.actorId, (uint)0x19001000, (uint)0x8000604, (ushort)0x765D, (ushort)BattleActionX01PacketCommand.Attack, (ushort)damage, (byte)0x1);
} }
// todo: call onAttack/onDamageTaken // todo: call onAttack/onDamageTaken
BattleUtils.DamageTarget(this, target, action, DamageTakenType.Attack); BattleUtils.DamageTarget(this, target, action, DamageTakenType.Attack);
AddTP(115); AddTP(200);
target.AddTP(100); target.AddTP(100);
} }
public virtual void OnCast(State state, BattleAction[] actions, ref BattleAction[] errors) public virtual void OnCast(State state, BattleAction[] actions, BattleCommand spell, ref BattleAction[] errors)
{ {
var spell = ((MagicState)state).GetSpell();
// damage is handled in script // damage is handled in script
var spellCost = spell.CalculateCost((uint)this.GetLevel()); var spellCost = spell.CalculateMpCost(this);
this.DelMP(spellCost); // mpCost can be set in script e.g. if caster has something for free spells this.DelMP(spellCost); // mpCost can be set in script e.g. if caster has something for free spells
foreach (BattleAction action in actions) foreach (BattleAction action in actions)
{
if (zone.FindActorInArea<Character>(action.targetId) is Character chara) if (zone.FindActorInArea<Character>(action.targetId) is Character chara)
BattleUtils.DamageTarget(this, chara, action, DamageTakenType.Magic); {
//BattleUtils.HandleHitType(this, chara, action);
BattleUtils.DoAction(this, chara, action, DamageTakenType.Magic);
}
}
lua.LuaEngine.GetInstance().OnSignal("spellUsed"); lua.LuaEngine.GetInstance().OnSignal("spellUsed");
} }
public virtual void OnWeaponSkill(State state, BattleAction[] actions, ref BattleAction[] errors) public virtual void OnWeaponSkill(State state, BattleAction[] actions, BattleCommand skill, ref BattleAction[] errors)
{ {
var skill = ((WeaponSkillState)state).GetWeaponSkill();
// damage is handled in script // damage is handled in script
this.DelTP(skill.tpCost);
foreach (BattleAction action in actions) foreach (BattleAction action in actions)
{
//Should we just store the character insteado f having to find it again?
if (zone.FindActorInArea<Character>(action.targetId) is Character chara) if (zone.FindActorInArea<Character>(action.targetId) is Character chara)
BattleUtils.DamageTarget(this, chara, action, DamageTakenType.Weaponskill); {
BattleUtils.DoAction(this, chara, action, DamageTakenType.Weaponskill);
}
}
this.DelTP(skill.tpCost);
//Do procs reset on weaponskills?
ResetProcs();
lua.LuaEngine.GetInstance().OnSignal("weaponskillUsed"); lua.LuaEngine.GetInstance().OnSignal("weaponskillUsed");
} }
public virtual void OnAbility(State state, BattleAction[] actions, ref BattleAction[] errors) public virtual void OnAbility(State state, BattleAction[] actions, BattleCommand ability, ref BattleAction[] errors)
{ {
if (target is BattleNpc)
((BattleNpc)target).lastAttacker = this;
foreach (var action in actions) foreach (var action in actions)
zone.FindActorInArea<BattleNpc>(action.targetId)?.OnDamageTaken(this, action, DamageTakenType.Ability); {
if (zone.FindActorInArea<Character>(action.targetId) is Character chara)
{
BattleUtils.DoAction(this, chara, action, DamageTakenType.Ability);
}
}
} }
public virtual void OnSpawn() public virtual void OnSpawn()
@ -810,6 +833,97 @@ namespace FFXIVClassic_Map_Server.Actors
} }
#endregion lua helpers #endregion lua helpers
#endregion ai stuff #endregion ai stuff
//Reset procs. Only send packet if any procs were actually reset.
//This assumes you can't use weaponskills between getting a proc and using the procced ability
public void ResetProcs()
{
var propPacketUtil = new ActorPropertyPacketUtil("charaWork/timingCommand", this);
bool shouldSend = false;
for (int i = 0; i < 4; i++)
{
if (charaWork.battleTemp.timingCommandFlag[i])
{
shouldSend = true;
charaWork.battleTemp.timingCommandFlag[i] = false;
propPacketUtil.AddProperty($"charaWork.battleTemp.timingCommandFlag[{i}]");
}
}
if (shouldSend && this is Player player)
player.QueuePackets(propPacketUtil.Done());
}
//Set given proc to true and send packet if this is a player
// todo: hidden status effects for timing when the procs fall off
public void SetProc(int procId, bool val = true)
{
charaWork.battleTemp.timingCommandFlag[procId] = val;
uint effectId = (uint)StatusEffectId.EvadeProc + (uint)procId;
//If a proc just occurred, add a hidden effect effect
if (val)
{
StatusEffect procEffect = Server.GetWorldManager().GetStatusEffect(effectId);
procEffect.SetDuration(5000);
statusEffects.AddStatusEffect(procEffect, this, true, true);
string procFunc = "";
//is there any reason we need this
switch(procId)
{
case (0):
procFunc = "onEvade";
break;
case (1):
procFunc = "onBlock";
break;
case (2):
procFunc = "onParry";
break;
case (3):
procFunc = "onMiss";
break;
}
//lua.LuaEngine.CallLuaBattleFunction(this, procFunc, this);
}
//Otherwise we're reseting a proc, remove the status
else
{
statusEffects.RemoveStatusEffect(statusEffects.GetStatusEffectById((uint)effectId));
}
if (this is Player player)
{
var propPacketUtil = new ActorPropertyPacketUtil("charaWork/timingCommand", this);
propPacketUtil.AddProperty($"charaWork.battleTemp.timingCommandFlag[{procId}]");
player.QueuePackets(propPacketUtil.Done());
}
}
public HitDirection GetHitDirection(Actor target)
{
//Get between taget's position and our position
double angle = Vector3.GetAngle(target.GetPosAsVector3(), GetPosAsVector3());
//Add to the target's rotation, mod by 2pi. This is the angle relative to where the target is looking
//Actor's rotation is 0 degrees on their left side, rotate it by 45 degrees so that quadrants line up with sides
angle = (angle + target.rotation - (.25 * Math.PI)) % (2 * Math.PI);
//Make positive
if (angle < 0)
angle = angle + (2 * Math.PI);
//Get the side we're on. 0 is front, 1 is right, 2 is rear, 3 is left
var side = (int) (angle / (.5 * Math.PI)) % 4;
return (HitDirection) (1 << side);
}
//Called when this character evades attacker's action
public void OnEvade(Character attacker, BattleAction action)
{
} }
} }
}

View file

@ -8,6 +8,7 @@ namespace FFXIVClassic_Map_Server.actors.chara
{ {
enum Modifier : UInt32 enum Modifier : UInt32
{ {
None = 0, None = 0,
Hp = 1, Hp = 1,
HpPercent = 2, HpPercent = 2,
@ -54,5 +55,11 @@ namespace FFXIVClassic_Map_Server.actors.chara
Raise = 41, Raise = 41,
MinimumHpLock = 42, // hp cannot fall below this value MinimumHpLock = 42, // hp cannot fall below this value
AttackType = 43, // slashing, piercing, etc
BlockRate = 44,
Block = 45,
CritRating = 46,
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
} }
} }

View file

@ -35,9 +35,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
actionQueue = new ActionQueue(owner); actionQueue = new ActionQueue(owner);
} }
public void UpdateLastActionTime() public void UpdateLastActionTime(uint delay = 0)
{ {
lastActionTime = DateTime.Now; lastActionTime = DateTime.Now.AddSeconds(delay);
} }
public DateTime GetLastActionTime() public DateTime GetLastActionTime()
@ -62,6 +62,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
controller.Update(tick); controller.Update(tick);
State top; State top;
while (states.Count > 0 && (top = states.Peek()).Update(tick)) while (states.Count > 0 && (top = states.Peek()).Update(tick))
{ {
if (top == GetCurrentState()) if (top == GetCurrentState())
@ -330,7 +331,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
{ {
if (CanChangeState()) if (CanChangeState())
{ {
ChangeState(new AbilityState(owner, target, (ushort)abilityId));
} }
} }

View file

@ -8,6 +8,7 @@ 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;
using FFXIVClassic_Map_Server.actors.chara.ai.utils; using FFXIVClassic_Map_Server.actors.chara.ai.utils;
using MoonSharp.Interpreter;
namespace FFXIVClassic_Map_Server.actors.chara.ai namespace FFXIVClassic_Map_Server.actors.chara.ai
{ {
@ -38,10 +39,10 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
public enum BattleCommandProcRequirement : byte public enum BattleCommandProcRequirement : byte
{ {
None, None,
Evade = 0x01, Miss,
Block = 0x02, Evade,
Parry = 0x04, Parry,
Miss = 0x08 Block
} }
public enum BattleCommandValidUser : byte public enum BattleCommandValidUser : byte
@ -51,6 +52,16 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
Monster Monster
} }
public enum BattleCommandCastType : ushort
{
None,
Weaponskill = 1,
Weaponskill2 = 2,
BlackMagic = 3,
WhiteMagic = 4,
SongMagic = 8
}
class BattleCommand class BattleCommand
{ {
public ushort id; public ushort id;
@ -58,28 +69,41 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
public byte job; public byte job;
public byte level; public byte level;
public BattleCommandRequirements requirements; public BattleCommandRequirements requirements;
public ValidTarget validTarget; public ValidTarget mainTarget; //what the skill has to be used on. ie self for flare, enemy for ring of talons even though both are self-centere aoe
public TargetFindAOEType aoeType; public ValidTarget validTarget; //what type of character the skill can hit
public TargetFindAOETarget aoeTarget; public TargetFindAOEType aoeType; //shape of aoe
public byte numHits; public TargetFindAOETarget aoeTarget; //where the center of the aoe is (target/self)
public BattleCommandPositionBonus positionBonus; public byte numHits; //amount of hits in the skill
public BattleCommandProcRequirement procRequirement; public BattleCommandPositionBonus positionBonus; //bonus for front/flank/rear
public int range; public BattleCommandProcRequirement procRequirement;//if the skill requires a block/parry/evade before using
public uint debuffDurationSeconds; public int range; //max distance to use skill
public uint buffDurationSeconds;
public byte castType; public uint statusId; //id of statuseffect that the skill might inflict
public uint castTimeSeconds; public uint statusDuration; //duration of statuseffect in milliseconds
public uint recastTimeSeconds; public float statusChance; //percent chance of status landing, 0-1.0. Usually 1.0 for buffs
public byte castType; //casting animation, 2 for blm, 3 for whm, 8 for brd
public uint castTimeMs; //cast time in milliseconds
public uint recastTimeMs; //recast time in milliseconds
public uint maxRecastTimeSeconds; //maximum recast time in seconds
public ushort mpCost; public ushort mpCost;
public ushort tpCost; public ushort tpCost;
public byte animationType; public byte animationType;
public ushort effectAnimation; public ushort effectAnimation;
public ushort modelAnimation; public ushort modelAnimation;
public ushort animationDurationSeconds; public ushort animationDurationSeconds;
public uint battleAnimation; public uint battleAnimation;
public ushort worldMasterTextId; public ushort worldMasterTextId;
public int aoeRange; 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 short comboStep; //Where in a combo string this skill is
public byte statusTier; //tief of status to put on target
public ushort basePotency; //damage variable
public float enmityModifier; //multiples by damage done to get final enmity
public float accuracyModifier; //modifies accuracy
public bool isCombo;
public lua.LuaScript script; //cached script
public TargetFind targetFind; public TargetFind targetFind;
public BattleCommandValidUser validUser; public BattleCommandValidUser validUser;
@ -89,6 +113,12 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
this.id = id; this.id = id;
this.name = name; this.name = name;
this.range = 0; this.range = 0;
this.enmityModifier = 1;
this.accuracyModifier = 1;
this.statusTier = 1;
this.statusChance = 50;
this.recastTimeMs = (uint) maxRecastTimeSeconds * 1000;
this.isCombo = false;
} }
public BattleCommand Clone() public BattleCommand Clone()
@ -96,17 +126,31 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return (BattleCommand)MemberwiseClone(); return (BattleCommand)MemberwiseClone();
} }
public int CallLuaFunction(Character chara, string functionName, params object[] args)
{
if (script != null && !script.Globals.Get(functionName).IsNil())
{
DynValue res = new DynValue();
res = script.Call(script.Globals.Get(functionName), args);
if (res != null)
return (int)res.Number;
}
return -1;
}
public bool IsSpell() public bool IsSpell()
{ {
return mpCost != 0 || castTimeSeconds != 0; return mpCost != 0 || castTimeMs != 0;
} }
public bool IsInstantCast() public bool IsInstantCast()
{ {
return castTimeSeconds == 0; return castTimeMs == 0;
} }
public bool IsValidTarget(Character user, Character target) //Checks whether the skill can be used on the given target
public bool IsValidMainTarget(Character user, Character target)
{ {
targetFind = new TargetFind(user); targetFind = new TargetFind(user);
@ -130,24 +174,36 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
32517 can only be performed on a friendly target. 32517 can only be performed on a friendly target.
32518 cannot be performed on an enemy. 32518 cannot be performed on an enemy.
32519 can only be performed on an enemy, 32519 can only be performed on an enemy,
32556 unable to execute [weaponskill]. Conditions for use are not met.
*/ */
// cant target dead // cant target dead
if ((validTarget & (ValidTarget.Corpse | ValidTarget.CorpseOnly)) == 0 && target.IsDead()) if ((mainTarget & (ValidTarget.Corpse | ValidTarget.CorpseOnly)) == 0 && target.IsDead())
{ {
// cannot be perfomed on // cannot be perfomed on
if (user is Player) if (user is Player)
((Player)user).SendGameMessage(Server.GetWorldManager().GetActor(), 32512, 0x20, (uint)id); ((Player)user).SendGameMessage(Server.GetWorldManager().GetActor(), 32512, 0x20, (uint)id);
return false; return false;
} }
if (level > user.charaWork.parameterSave.state_mainSkillLevel)
//level too high
if (level > user.GetLevel())
{ {
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;
} }
if (tpCost > user.GetTP()) //Proc requirement
if (procRequirement != BattleCommandProcRequirement.None && !user.charaWork.battleTemp.timingCommandFlag[(int) procRequirement - 1])
{
if (user is Player)
((Player)user).SendGameMessage(Server.GetWorldManager().GetActor(), 32556, 0x20, (uint)id);
return false;
}
//costs too much tp
if (CalculateTpCost(user) > user.GetTP())
{ {
if (user is Player) if (user is Player)
((Player)user).SendGameMessage(Server.GetWorldManager().GetActor(), 32546, 0x20, (uint)id); ((Player)user).SendGameMessage(Server.GetWorldManager().GetActor(), 32546, 0x20, (uint)id);
@ -155,7 +211,6 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
} }
// todo: calculate cost based on modifiers also (probably in BattleUtils) // todo: calculate cost based on modifiers also (probably in BattleUtils)
if (BattleUtils.CalculateSpellCost(user, target, this) > user.GetMP()) if (BattleUtils.CalculateSpellCost(user, target, this) > user.GetMP())
{ {
if (user is Player) if (user is Player)
@ -175,8 +230,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
} }
} }
// todo: i dont care to message for each scenario, just the most common ones.. // todo: i dont care to message for each scenario, just the most common ones..
if ((validTarget & ValidTarget.CorpseOnly) != 0) if ((mainTarget & ValidTarget.CorpseOnly) != 0)
{ {
if (target != null && target.IsAlive()) if (target != null && target.IsAlive())
{ {
@ -186,7 +242,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
} }
} }
if ((validTarget & ValidTarget.Enemy) != 0) if ((mainTarget & ValidTarget.Enemy) != 0)
{ {
if (target == user || target != null && if (target == user || target != null &&
user.allegiance == target.allegiance) user.allegiance == target.allegiance)
@ -197,21 +253,43 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
} }
} }
if ((validTarget & (ValidTarget.PartyMember | ValidTarget.Player | ValidTarget.Ally)) != 0) if ((mainTarget & ValidTarget.Ally) != 0)
{ {
if (target == null || target.allegiance != user.allegiance) if (target == null || target.allegiance != user.allegiance)
{ {
if (user is Player) if (user is Player)
((Player)user).SendGameMessage(Server.GetWorldManager().GetActor(), 32516, 0x20, (uint)id); ((Player)user).SendGameMessage(Server.GetWorldManager().GetActor(), 32517, 0x20, (uint)id);
return false; return false;
} }
} }
return targetFind.CanTarget(target, true, true, true);
if ((mainTarget & ValidTarget.PartyMember) != 0)
{
if (target == null || target.currentParty != user.currentParty)
{
if (user is Player)
((Player)user).SendGameMessage(Server.GetWorldManager().GetActor(), 32547, 0x20, (uint)id);
return false;
}
} }
public ushort CalculateCost(uint level) if ((mainTarget & ValidTarget.Player) != 0)
{
if (!(target is Player))
{
if (user is Player)
((Player)user).SendGameMessage(Server.GetWorldManager().GetActor(), 32517, 0x20, (uint)id);
return false;
}
}
return true;// targetFind.CanTarget(target, true, true, true); //this will be done later
}
public ushort CalculateMpCost(Character user)
{ {
// todo: use precalculated costs instead // todo: use precalculated costs instead
var level = user.GetLevel();
ushort cost = 0; ushort cost = 0;
if (level <= 10) if (level <= 10)
cost = (ushort)(100 + level * 10); cost = (ushort)(100 + level * 10);
@ -230,15 +308,59 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
else else
cost = (ushort)(8000 + (level - 70) * 500); cost = (ushort)(8000 + (level - 70) * 500);
if (mpCost != 0) //scale the mpcost by level
return (ushort)Math.Ceiling((cost * mpCost * 0.001)); cost = (ushort)Math.Ceiling((cost * mpCost * 0.001));
return mpCost != 0 ? (ushort)Math.Ceiling((cost * mpCost * 0.001)) : (ushort)0; //if user is player, check if spell is a part of combo
if (user is Player)
{
var player = user as Player;
if (player.playerWork.comboNextCommandId[0] == id || player.playerWork.comboNextCommandId[1] == id)
cost = (ushort)Math.Ceiling(cost * (1 - player.playerWork.comboCostBonusRate));
}
return mpCost != 0 ? cost : (ushort)0;
}
//Calculate TP cost taking into considerating the combo bonus rate for players
//Should this set tpCost or should it be called like CalculateMp where it gets calculated each time?
//Might cause issues with the delay between starting and finishing a WS
public ushort CalculateTpCost(Character user)
{
ushort tp = tpCost;
//Calculate tp cost
if (user is Player)
{
var player = user as Player;
if (player.playerWork.comboNextCommandId[0] == id || player.playerWork.comboNextCommandId[1] == id)
tp = (ushort)Math.Ceiling((float)tpCost * (1 - player.playerWork.comboCostBonusRate));
}
return tp;
} }
public List<Character> GetTargets() public List<Character> GetTargets()
{ {
return targetFind?.GetTargets<Character>(); return targetFind?.GetTargets<Character>();
} }
//Handles setting the correct target for self-targeted spells
public Character GetMainTarget(Character caster, Character target)
{
//If skill can only be used on self
if (mainTarget == ValidTarget.Self)
return caster;
//If skill can only be used on party members and the target is not a party member
else if (((mainTarget & ValidTarget.PartyMember) != 0) && (target.currentParty != caster.currentParty))
return caster;
//If skill can only be used on allys and the target is not an ally
else if (((mainTarget & ValidTarget.Ally) != 0) && (target.allegiance != caster.allegiance))
return caster;
//If skill can only be used on players and the target is not a player
else if (((mainTarget & ValidTarget.Player) != 0) && (!(target is Player)))
return caster;
return target;
}
} }
} }

View file

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MoonSharp.Interpreter;
namespace FFXIVClassic_Map_Server.actors.chara.ai namespace FFXIVClassic_Map_Server.actors.chara.ai
{ {
@ -322,24 +323,50 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
// custom effects here // custom effects here
// status for having procs fall off
EvadeProc = 253003,
BlockProc = 253004,
ParryProc = 253005,
MissProc = 253006
} }
[Flags] [Flags]
enum StatusEffectFlags : uint enum StatusEffectFlags : uint
{ {
None = 0x00, None = 0,
Silent = 0x01, // dont display effect loss message Silent = 1 << 0, // dont display effect loss message
LoseOnDeath = 0x02, // effects removed on death
LoseOnZoning = 0x04, // effects removed on zoning
LoseOnEsuna = 0x08, // effects which can be removed with esuna (debuffs)
LoseOnDispel = 0x10, // some buffs which player might be able to dispel from mob
LoseOnLogout = 0x20, // effects removed on logging out
LoseOnAttacking = 0x40, // effects removed when owner attacks another entity
LoseOnCasting = 0x80, // effects removed when owner starts casting
LoseOnDamageTaken = 0x100, // effects removed when owner takes damage
PreventAction = 0x200, // effects which prevent actions such as sleep/paralyze/petrify //Loss flags
Stealth = 0x400, // sneak/invis LoseOnDeath = 1 << 1, // effects removed on death
LoseOnZoning = 1 << 2, // effects removed on zoning
LoseOnEsuna = 1 << 3, // effects which can be removed with esuna (debuffs)
LoseOnDispel = 1 << 4, // some buffs which player might be able to dispel from mob
LoseOnLogout = 1 << 5, // effects removed on logging out
LoseOnAttacking = 1 << 6, // effects removed when owner attacks another entity
LoseOnCasting = 1 << 7, // effects removed when owner starts casting
LoseOnDamageTaken = 1 << 8, // effects removed when owner takes damage
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
ActivateOnAttack = 1 << 13,
ActivateOnSpell = 1 << 14,
ActivateOnDamageTaken = 1 << 15,
ActivateOnBlock = 1 << 16,
ActivateOnMiss = 1 << 17,
//Prevent flags. Sleep/stun/petrify/etc combine these
PreventSpell= 1 << 18, // effects which prevent using spells, such as silence
PreventWeaponSkill = 1 << 19, // effects which prevent using weaponskills, such as pacification
PreventAbility = 1 << 20, // effects which prevent using abilities, such as amnesia
PreventAttack = 1 << 21, // effects which prevent basic attacks
PreventMovement = 1 << 22, // 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
Stealth = 1 << 24, // sneak/invis
Stance = 1 << 25, // effects that do not have a timer
} }
enum StatusEffectOverwrite : byte enum StatusEffectOverwrite : byte
@ -360,7 +387,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
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 lastTick; // when did this effect last tick private DateTime lastTick; // when did this effect last tick
private uint durationMs; // how long should this effect last in ms 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 UInt64 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
@ -368,17 +395,19 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
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
private bool hidden = false;
public LuaScript script;
HitEffect animationEffect; HitEffect animationEffect;
public StatusEffect(Character owner, uint id, UInt64 magnitude, uint tickMs, uint durationMs, byte tier = 0) public StatusEffect(Character owner, uint id, UInt64 magnitude, uint tickMs, uint duration, byte tier = 0)
{ {
this.owner = owner; this.owner = owner;
this.source = owner; this.source = owner;
this.id = (StatusEffectId)id; this.id = (StatusEffectId)id;
this.magnitude = magnitude; this.magnitude = magnitude;
this.tickMs = tickMs; this.tickMs = tickMs;
this.durationMs = durationMs; this.duration = duration;
this.tier = tier; this.tier = tier;
this.startTime = DateTime.Now; this.startTime = DateTime.Now;
@ -392,7 +421,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
this.id = effect.id; this.id = effect.id;
this.magnitude = effect.magnitude; this.magnitude = effect.magnitude;
this.tickMs = effect.tickMs; this.tickMs = effect.tickMs;
this.durationMs = effect.durationMs; this.duration = effect.duration;
this.tier = effect.tier; this.tier = effect.tier;
this.startTime = effect.startTime; this.startTime = effect.startTime;
this.lastTick = effect.lastTick; this.lastTick = effect.lastTick;
@ -401,14 +430,16 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
this.flags = effect.flags; this.flags = effect.flags;
this.overwrite = effect.overwrite; this.overwrite = effect.overwrite;
this.extra = effect.extra; this.extra = effect.extra;
this.script = effect.script;
} }
public StatusEffect(uint id, string name, uint flags, uint overwrite) public StatusEffect(uint id, string name, uint flags, uint overwrite, uint tickMs)
{ {
this.id = (StatusEffectId)id; this.id = (StatusEffectId)id;
this.name = name; this.name = name;
this.flags = (StatusEffectFlags)flags; this.flags = (StatusEffectFlags)flags;
this.overwrite = (StatusEffectOverwrite)overwrite; this.overwrite = (StatusEffectOverwrite)overwrite;
this.tickMs = tickMs;
} }
// return true when duration has elapsed // return true when duration has elapsed
@ -420,13 +451,28 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
LuaEngine.CallLuaStatusEffectFunction(this.owner, this, "onTick", this.owner, this); LuaEngine.CallLuaStatusEffectFunction(this.owner, this, "onTick", this.owner, this);
} }
if (durationMs != 0 && (tick - startTime).TotalMilliseconds >= durationMs) if (duration != 0xFFFFFFFF && (tick - startTime).TotalSeconds >= duration)
{ {
return true; return true;
} }
return false; return false;
} }
public int CallLuaFunction(Character chara, string functionName, params object[] args)
{
DynValue res = new DynValue();
if (!script.Globals.Get(functionName).IsNil())
{
res = script.Call(script.Globals.Get(functionName), args);
if (res != null)
return (int)res.Number;
}
return -1;
}
public Character GetOwner() public Character GetOwner()
{ {
return owner; return owner;
@ -457,9 +503,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return name; return name;
} }
public uint GetDurationMs() public uint GetDuration()
{ {
return durationMs; return duration;
} }
public uint GetTickMs() public uint GetTickMs()
@ -497,6 +543,11 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return silent; return silent;
} }
public bool GetHidden()
{
return hidden;
}
public void SetStartTime(DateTime time) public void SetStartTime(DateTime time)
{ {
this.startTime = time; this.startTime = time;
@ -523,9 +574,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
this.magnitude = magnitude; this.magnitude = magnitude;
} }
public void SetDurationMs(uint durationMs) public void SetDuration(uint duration)
{ {
this.durationMs = durationMs; this.duration = duration;
} }
public void SetTickMs(uint tickMs) public void SetTickMs(uint tickMs)
@ -558,6 +609,11 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
this.silent = silent; this.silent = silent;
} }
public void SetHidden(bool hidden)
{
this.hidden = hidden;
}
public void SetAnimation(uint hitEffect) public void SetAnimation(uint hitEffect)
{ {
animationEffect = (HitEffect)hitEffect; animationEffect = (HitEffect)hitEffect;

View file

@ -9,6 +9,7 @@ using FFXIVClassic_Map_Server.lua;
using FFXIVClassic_Map_Server.actors.area; using FFXIVClassic_Map_Server.actors.area;
using FFXIVClassic_Map_Server.packets.send; using FFXIVClassic_Map_Server.packets.send;
using FFXIVClassic_Map_Server.packets.send.actor; using FFXIVClassic_Map_Server.packets.send.actor;
using FFXIVClassic_Map_Server.packets.send.actor.battle;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using FFXIVClassic_Map_Server.utils; using FFXIVClassic_Map_Server.utils;
@ -61,32 +62,47 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
return effects.ContainsKey((uint)id); return effects.ContainsKey((uint)id);
} }
public bool AddStatusEffect(uint id, UInt64 magnitude, double tickMs, double durationMs, byte tier = 0) public bool AddStatusEffect(uint id, UInt64 magnitude, uint tickMs, uint duration, byte tier = 0)
{ {
return AddStatusEffect(new StatusEffect(this.owner, id, magnitude, (uint)(tickMs * 1000), (uint)(durationMs * 1000), tier), owner); var se = Server.GetWorldManager().GetStatusEffect(id);
if (se != null)
{
se.SetDuration(duration);
se.SetStartTime(DateTime.Now);
se.SetOwner(owner);
}
return AddStatusEffect(se ?? new StatusEffect(this.owner, id, magnitude, tickMs, duration, tier), owner);
} }
public bool AddStatusEffect(StatusEffect newEffect, Character source, bool silent = false) public bool AddStatusEffect(StatusEffect newEffect, Character source, bool silent = false, bool hidden = false)
{ {
/* /*
worldMasterTextId worldMasterTextId
32001 [@2B([@IF($E4($EB(1),$EB(2)),you,[@IF($E9(7),[@SHEETEN(xtx/displayName,2,$E9(7),1,1)],$EB(2))])])] [@IF($E4($EB(1),$EB(2)),resist,resists)] the effect of [@SHEET(xtx/status,$E8(11),3)]. 32001 [@2B([@IF($E4($EB(1),$EB(2)),you,[@IF($E9(7),[@SHEETEN(xtx/displayName,2,$E9(7),1,1)],$EB(2))])])] [@IF($E4($EB(1),$EB(2)),resist,resists)] the effect of [@SHEET(xtx/status,$E8(11),3)].
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;
if (effect != null) if (effect != null)
{ {
var overwritable = effect.GetOverwritable(); var overwritable = effect.GetOverwritable();
canOverwrite = (overwritable == (uint)StatusEffectOverwrite.Always) || canOverwrite = (overwritable == (uint)StatusEffectOverwrite.Always) ||
(overwritable == (uint)StatusEffectOverwrite.GreaterOnly && (effect.GetDurationMs() < newEffect.GetDurationMs() || effect.GetMagnitude() < newEffect.GetMagnitude())) || (overwritable == (uint)StatusEffectOverwrite.GreaterOnly && (effect.GetDuration() < newEffect.GetDuration() || effect.GetMagnitude() < newEffect.GetMagnitude())) ||
(overwritable == (uint)StatusEffectOverwrite.GreaterOrEqualTo && (effect.GetDurationMs() <= newEffect.GetDurationMs() || effect.GetMagnitude() <= newEffect.GetMagnitude())); (overwritable == (uint)StatusEffectOverwrite.GreaterOrEqualTo && (effect.GetDuration() <= newEffect.GetDuration() || effect.GetMagnitude() <= newEffect.GetMagnitude()));
} }
if (canOverwrite || effect == null) if (canOverwrite || effect == null)
{ {
// send packet to client with effect added message // send packet to client with effect added message
if (!silent || !effect.GetSilent() || (effect.GetFlags() & (uint)StatusEffectFlags.Silent) == 0) if (effect != null && (!silent || !effect.GetSilent() || (effect.GetFlags() & (uint)StatusEffectFlags.Silent) == 0))
{ {
// todo: send packet to client with effect added message // todo: send packet to client with effect added message
} }
@ -95,16 +111,36 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
if (canOverwrite) if (canOverwrite)
effects.Remove(newEffect.GetStatusEffectId()); effects.Remove(newEffect.GetStatusEffectId());
newEffect.SetStartTime(DateTime.Now);
newEffect.SetOwner(owner);
if (effects.Count < MAX_EFFECTS) if (effects.Count < MAX_EFFECTS)
{ {
if(newEffect.script != null)
newEffect.CallLuaFunction(this.owner, "onGain", this.owner, newEffect);
else
LuaEngine.CallLuaStatusEffectFunction(this.owner, newEffect, "onGain", this.owner, newEffect);
effects.Add(newEffect.GetStatusEffectId(), newEffect); effects.Add(newEffect.GetStatusEffectId(), newEffect);
newEffect.SetSilent(silent); newEffect.SetSilent(silent);
newEffect.SetHidden(hidden);
if (!newEffect.GetHidden())
{ {
var index = Array.IndexOf(effects.Values.ToArray(), newEffect); int index = 0;
if (owner.charaWork.status.Contains(newEffect.GetStatusId()))
index = Array.IndexOf(owner.charaWork.status, newEffect.GetStatusId());
else
index = Array.IndexOf(owner.charaWork.status, (ushort) 0);
owner.charaWork.status[index] = newEffect.GetStatusId(); owner.charaWork.status[index] = newEffect.GetStatusId();
owner.charaWork.statusShownTime[index] = Utils.UnixTimeStampUTC() + (newEffect.GetDurationMs() / 1000);
//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
uint time = (newEffect.GetFlags() & (uint) StatusEffectFlags.Stance) == 0 ? Utils.UnixTimeStampUTC() + (newEffect.GetDuration()) : 0xFFFFFFFF;
owner.charaWork.statusShownTime[index] = time;
this.owner.zone.BroadcastPacketAroundActor(this.owner, SetActorStatusPacket.BuildPacket(this.owner.actorId, (ushort)index, (ushort)newEffect.GetStatusId())); this.owner.zone.BroadcastPacketAroundActor(this.owner, SetActorStatusPacket.BuildPacket(this.owner.actorId, (ushort)index, (ushort)newEffect.GetStatusId()));
} }
{ {
owner.zone.BroadcastPacketsAroundActor(owner, owner.GetActorStatusPackets()); owner.zone.BroadcastPacketsAroundActor(owner, owner.GetActorStatusPackets());
} }
@ -117,24 +153,29 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
public void RemoveStatusEffect(StatusEffect effect, bool silent = false) public void RemoveStatusEffect(StatusEffect effect, bool silent = false)
{ {
if (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 // todo: send packet to client with effect added message
} }
//hidden effects not in charawork
if(!effect.GetHidden())
{ {
var index = Array.IndexOf(effects.Values.ToArray(), effect); var index = Array.IndexOf(owner.charaWork.status, effect.GetStatusId());
owner.charaWork.status[index] = 0; owner.charaWork.status[index] = 0;
owner.charaWork.statusShownTime[index] = uint.MaxValue; owner.charaWork.statusShownTime[index] = 0;
this.owner.zone.BroadcastPacketAroundActor(this.owner, SetActorStatusPacket.BuildPacket(owner.actorId, (ushort)index, (ushort)0)); this.owner.zone.BroadcastPacketAroundActor(this.owner, SetActorStatusPacket.BuildPacket(owner.actorId, (ushort)index, (ushort)0));
} }
// function onLose(actor, effect) // function onLose(actor, effect)
LuaEngine.CallLuaStatusEffectFunction(this.owner, effect, "onLose", this.owner, effect);
effects.Remove(effect.GetStatusEffectId()); effects.Remove(effect.GetStatusEffectId());
if(effect.script != null)
effect.CallLuaFunction(owner, "onLose", owner, effect);
else
LuaEngine.CallLuaStatusEffectFunction(this.owner, effect, "onLose", this.owner, effect);
owner.RecalculateStats(); owner.RecalculateStats();
sendUpdate = true; sendUpdate = true;
} }

View file

@ -53,7 +53,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
} }
//Only move if owner isn't dead and is either too far away from their spawn point or is meant to roam //Only move if owner isn't dead and is either too far away from their spawn point or is meant to roam
else if (!owner.IsDead() && (owner.isMovingToSpawn || owner.GetMobMod((uint)MobModifier.Roams) > 0)) if (!owner.IsDead() && (owner.isMovingToSpawn || owner.GetMobMod((uint)MobModifier.Roams) > 0))
{ {
DoRoamTick(tick); DoRoamTick(tick);
} }
@ -83,7 +83,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
{ {
foreach (var chara in owner.zone.GetActorsAroundActor<Character>(owner, 50)) foreach (var chara in owner.zone.GetActorsAroundActor<Character>(owner, 50))
{ {
if (owner.allegiance == chara.allegiance) if (chara.allegiance == owner.allegiance)
continue; continue;
if (owner.aiContainer.pathFind.AtPoint() && owner.detectionType != DetectionType.None) if (owner.aiContainer.pathFind.AtPoint() && owner.detectionType != DetectionType.None)
@ -119,8 +119,6 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
if (owner.GetState() != SetActorStatePacket.MAIN_STATE_ACTIVE) if (owner.GetState() != SetActorStatePacket.MAIN_STATE_ACTIVE)
owner.ChangeState(SetActorStatePacket.MAIN_STATE_ACTIVE); owner.ChangeState(SetActorStatePacket.MAIN_STATE_ACTIVE);
owner.moveState = 2;
//owner.SetMod((uint)Modifier.Speed, 5);
lastActionTime = DateTime.Now; lastActionTime = DateTime.Now;
battleStartTime = lastActionTime; battleStartTime = lastActionTime;
// todo: adjust cooldowns with modifiers // todo: adjust cooldowns with modifiers
@ -138,7 +136,6 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
{ {
var target = owner.target; var target = owner.target;
base.Disengage(); base.Disengage();
owner.statusEffects.RemoveStatusEffectsByFlags((uint)StatusEffectFlags.LoseOnDeath, true);
// todo: // todo:
lastActionTime = lastUpdate.AddSeconds(5); lastActionTime = lastUpdate.AddSeconds(5);
owner.isMovingToSpawn = true; owner.isMovingToSpawn = true;
@ -213,13 +210,13 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
Disengage(); Disengage();
return; return;
} }
owner.SetMod((uint)Modifier.Speed, 5);
if ((tick - lastCombatTickScript).TotalSeconds > 3)//Program.Random.Next(10, 15))
Move();
if ((tick - lastCombatTickScript).TotalSeconds > 2)
{ {
lua.LuaEngine.CallLuaBattleFunction(owner, "onCombatTick", owner, owner.target, Utils.UnixTimeStampUTC(tick), contentGroupCharas); Move();
//if (owner.aiContainer.CanChangeState())
//owner.aiContainer.WeaponSkill(owner.zone.FindActorInArea<Character>(owner.target.actorId), 27155);
//lua.LuaEngine.CallLuaBattleFunction(owner, "onCombatTick", owner, owner.target, Utils.UnixTimeStampUTC(tick), contentGroupCharas);
lastCombatTickScript = tick; lastCombatTickScript = tick;
} }
} }
@ -239,7 +236,6 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
var targetPos = new Vector3(owner.target.positionX, owner.target.positionY, owner.target.positionZ); var targetPos = new Vector3(owner.target.positionX, owner.target.positionY, owner.target.positionZ);
var distance = Utils.Distance(owner.positionX, owner.positionY, owner.positionZ, targetPos.X, targetPos.Y, targetPos.Z); var distance = Utils.Distance(owner.positionX, owner.positionY, owner.positionZ, targetPos.X, targetPos.Y, targetPos.Z);
if (distance > owner.GetAttackRange() - 0.2f || owner.aiContainer.CanFollowPath()) if (distance > owner.GetAttackRange() - 0.2f || owner.aiContainer.CanFollowPath())
{ {
if (CanMoveForward(distance)) if (CanMoveForward(distance))
@ -276,12 +272,13 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
{ {
FaceTarget(); FaceTarget();
} }
lastRoamUpdate = DateTime.Now;
} }
protected void FaceTarget() protected void FaceTarget()
{ {
// todo: check if stunned etc // todo: check if stunned etc
if (owner.statusEffects.HasStatusEffectsByFlag(StatusEffectFlags.PreventAction)) if (owner.statusEffects.HasStatusEffectsByFlag(StatusEffectFlags.PreventTurn) )
{ {
} }
else else
@ -391,6 +388,8 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
} }
public override void ChangeTarget(Character target) public override void ChangeTarget(Character target)
{
if (target != owner.target)
{ {
owner.target = target; owner.target = target;
owner.currentLockedTarget = target?.actorId ?? Actor.INVALID_ACTORID; owner.currentLockedTarget = target?.actorId ?? Actor.INVALID_ACTORID;
@ -403,3 +402,4 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.controllers
} }
} }
} }
}

View file

@ -175,6 +175,11 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
if (masterTarget != null) if (masterTarget != null)
targets.Add(masterTarget); targets.Add(masterTarget);
if (aoeType != TargetFindAOEType.None)
{
AddAllInRange(target, withPet);
}
/*
if (aoeType != TargetFindAOEType.None) if (aoeType != TargetFindAOEType.None)
{ {
if (IsPlayer(owner)) if (IsPlayer(owner))
@ -229,7 +234,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
AddAllInHateList(); AddAllInHateList();
} }
} }
} }*/
if (targets.Count > 16) if (targets.Count > 16)
targets.RemoveRange(16, targets.Count - 16); targets.RemoveRange(16, targets.Count - 16);
} }
@ -311,6 +318,18 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai
} }
} }
private void AddAllInRange(Character target, bool withPet)
{
int dist = (int)maxDistance;
var actors = owner.zone.GetActorsAroundActor<Character>(target, dist);
foreach (Character actor in actors)
{
AddTarget(actor, false);
}
}
private void AddAllInHateList() private void AddAllInHateList()
{ {
if (!(owner is BattleNpc)) if (!(owner is BattleNpc))
@ -342,6 +361,12 @@ 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)
return false;
if ((validTarget & ValidTarget.PartyMember) != 0 && target.currentParty != owner.currentParty)
return false;
if ((validTarget & ValidTarget.NPC) != 0 && target.isStatic) if ((validTarget & ValidTarget.NPC) != 0 && target.isStatic)
return false; return false;

View file

@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FFXIVClassic.Common;
using FFXIVClassic_Map_Server.Actors;
using FFXIVClassic_Map_Server.packets.send.actor;
using FFXIVClassic_Map_Server.packets.send.actor.battle;
using FFXIVClassic_Map_Server.packets.send;
namespace FFXIVClassic_Map_Server.actors.chara.ai.state
{
class AbilityState : State
{
private BattleCommand skill;
public AbilityState(Character owner, Character target, ushort skillId) :
base(owner, target)
{
this.startTime = DateTime.Now;
this.skill = Server.GetWorldManager().GetBattleCommand(skillId);
var returnCode = lua.LuaEngine.CallLuaBattleCommandFunction(owner, skill, "ability", "onAbilityPrepare", owner, target, skill);
this.target = skill.GetMainTarget(owner, target);
if (returnCode == 0)
{
OnStart();
}
else
{
errorResult = null;
interrupt = true;
}
}
public override void OnStart()
{
var returnCode = lua.LuaEngine.CallLuaBattleCommandFunction(owner, skill, "ability", "onAbilityStart", owner, target, skill);
if (returnCode != 0)
{
interrupt = true;
errorResult = new BattleAction(owner.actorId, (ushort)(returnCode == -1 ? 32558 : returnCode), 0);
}
else
{
//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;
}
}
}
public override bool Update(DateTime tick)
{
if (skill != null)
{
TryInterrupt();
if (interrupt)
{
OnInterrupt();
return true;
}
// todo: check weapon delay/haste etc and use that
var actualCastTime = skill.castTimeMs;
if ((tick - startTime).Milliseconds >= skill.castTimeMs)
{
OnComplete();
return true;
}
return false;
}
return true;
}
public override void OnInterrupt()
{
// todo: send paralyzed/sleep message etc.
if (errorResult != null)
{
owner.DoBattleAction(skill.id, errorResult.animation, errorResult);
errorResult = null;
}
}
public override void OnComplete()
{
bool hitTarget = false;
skill.targetFind.FindWithinArea(target, skill.validTarget, skill.aoeTarget);
isCompleted = true;
var targets = skill.targetFind.GetTargets();
List<BattleAction> actions = new List<BattleAction>();
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()
{
if (interrupt)
return;
if (owner.statusEffects.HasStatusEffectsByFlag((uint)StatusEffectFlags.PreventAbility))
{
// todo: sometimes paralyze can let you attack, get random percentage of actually letting you attack
var list = owner.statusEffects.GetStatusEffectsByFlag((uint)StatusEffectFlags.PreventAbility);
uint effectId = 0;
if (list.Count > 0)
{
// todo: actually check proc rate/random chance of whatever effect
effectId = list[0].GetStatusEffectId();
}
interrupt = true;
return;
}
interrupt = !CanUse();
}
private bool CanUse()
{
return skill.IsValidMainTarget(owner, target);
}
public BattleCommand GetWeaponSkill()
{
return skill;
}
public override void Cleanup()
{
owner.aiContainer.UpdateLastActionTime(skill.animationDurationSeconds);
}
}
}

View file

@ -82,7 +82,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
public override void OnComplete() public override void OnComplete()
{ {
BattleAction action = new BattleAction(target.actorId, 0x765D, (uint) HitEffect.Hit, 0, (byte) HitDirection.None); //BattleAction action = new BattleAction(target.actorId, 0x765D, (uint) HitEffect.Hit, 0, (byte) HitDirection.None);
errorResult = null; errorResult = null;
// todo: implement auto attack damage bonus in Character.OnAttack // todo: implement auto attack damage bonus in Character.OnAttack
@ -99,16 +99,35 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
* The above damage bonus also applies to Shot attacks by archers. * The above damage bonus also applies to Shot attacks by archers.
*/ */
// handle paralyze/intimidate/sleep/whatever in Character.OnAttack // handle paralyze/intimidate/sleep/whatever in Character.OnAttack
owner.OnAttack(this, action, ref errorResult);
owner.DoBattleAction((ushort)BattleActionX01PacketCommand.Attack, action.animation, errorResult == null ? action : errorResult);
List<BattleAction> actions = new List<BattleAction>();
var i = 0;
for (int hitNum = 0; hitNum < owner.GetMod((uint) Modifier.HitCount); hitNum++)
{
BattleAction action = new BattleAction(target.actorId, 0x765D, (uint)HitEffect.Hit, 100, (byte)HitDirection.None, (byte) hitNum);
action.battleActionType = BattleActionType.AttackPhysical;
// evasion, miss, dodge, etc to be handled in script, calling helpers from scripts/weaponskills.lua
// temporary evade/miss/etc function to test hit effects
utils.BattleUtils.CalcHitType(owner, target, null, action);
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.OnAttack(this, actions[0], ref errorResult);
owner.DoBattleAction(22104, 0x19001000, actions);
target.SetMod((uint) Modifier.MinimumHpLock, 0);
} }
public override void TryInterrupt() public override void TryInterrupt()
{ {
if (owner.statusEffects.HasStatusEffectsByFlag((uint)StatusEffectFlags.PreventAction)) if (owner.statusEffects.HasStatusEffectsByFlag((uint)StatusEffectFlags.PreventAttack))
{ {
// todo: sometimes paralyze can let you attack, calculate proc rate // todo: sometimes paralyze can let you attack, calculate proc rate
var list = owner.statusEffects.GetStatusEffectsByFlag((uint)StatusEffectFlags.PreventAction); var list = owner.statusEffects.GetStatusEffectsByFlag((uint)StatusEffectFlags.PreventAttack);
uint statusId = 0; uint statusId = 0;
if (list.Count > 0) if (list.Count > 0)
{ {
@ -124,7 +143,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
private bool IsAttackReady() private bool IsAttackReady()
{ {
// todo: this enforced delay should really be changed if it's not retail.. // todo: this enforced delay should really be changed if it's not retail..
return Program.Tick >= attackTime && Program.Tick >= owner.aiContainer.GetLastActionTime().AddSeconds(1); return Program.Tick >= attackTime && Program.Tick >= owner.aiContainer.GetLastActionTime();
} }
private bool CanAttack() private bool CanAttack()

View file

@ -15,9 +15,10 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
: base(owner, null) : base(owner, null)
{ {
owner.Disengage(); owner.Disengage();
//owner.ChangeState(SetActorStatePacket.MAIN_STATE_DEAD); owner.ChangeState(SetActorStatePacket.MAIN_STATE_DEAD);
var deathStatePacket = SetActorStatePacket.BuildPacket(owner.actorId, SetActorStatePacket.MAIN_STATE_DEAD, owner.currentSubState); owner.statusEffects.RemoveStatusEffectsByFlags((uint)StatusEffectFlags.LoseOnDeath, true);
owner.zone.BroadcastPacketAroundActor(owner, deathStatePacket); //var deathStatePacket = SetActorStatePacket.BuildPacket(owner.actorId, SetActorStatePacket.MAIN_STATE_DEAD2, owner.currentSubState);
//owner.zone.BroadcastPacketAroundActor(owner, deathStatePacket);
canInterrupt = false; canInterrupt = false;
startTime = tick; startTime = tick;
despawnTime = startTime.AddSeconds(timeToFadeOut); despawnTime = startTime.AddSeconds(timeToFadeOut);

View file

@ -27,7 +27,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
if (owner.IsDead()) if (owner.IsDead())
return true; return true;
if (!owner.statusEffects.HasStatusEffectsByFlag(StatusEffectFlags.PreventAction)) if (!owner.statusEffects.HasStatusEffectsByFlag(StatusEffectFlags.PreventMovement))
return true; return true;
} }

View file

@ -26,7 +26,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
this.spell = Server.GetWorldManager().GetBattleCommand(spellId); this.spell = Server.GetWorldManager().GetBattleCommand(spellId);
var returnCode = lua.LuaEngine.CallLuaBattleCommandFunction(owner, spell, "magic", "onMagicPrepare", owner, target, spell); var returnCode = lua.LuaEngine.CallLuaBattleCommandFunction(owner, spell, "magic", "onMagicPrepare", owner, target, spell);
if (returnCode == 0 && owner.CanCast(target, spell)) this.target = spell.GetMainTarget(owner, target);
if (returnCode == 0 && owner.CanCast(this.target, spell))
{ {
OnStart(); OnStart();
} }
@ -51,16 +53,28 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
// todo: check within attack range // todo: check within attack range
float[] baseCastDuration = { 1.0f, 0.25f }; float[] baseCastDuration = { 1.0f, 0.25f };
float spellSpeed = spell.castTimeSeconds; //Check combo stuff here because combos can impact spell cast times
float spellSpeed = spell.castTimeMs;
//There are no positional spells, so just check onCombo, need to check first because certain spells change aoe type/accuracy
//If owner is a player and the spell being used is part of the current combo
if (spell.comboStep == 1 || ((owner is Player p) && (p.playerWork.comboNextCommandId[0] == spell.id || p.playerWork.comboNextCommandId[1] == spell.id)))
{
lua.LuaEngine.CallLuaBattleCommandFunction(owner, spell, "magic", "onCombo", owner, target, spell);
spell.isCombo = true;
}
if (!spell.IsInstantCast())
{
// command casting duration // command casting duration
if (owner is Player) if (owner is Player)
{ {
// todo: modify spellSpeed based on modifiers and stuff // todo: modify spellSpeed based on modifiers and stuff
((Player)owner).SendStartCastbar(spell.id, Utils.UnixTimeStampUTC(DateTime.Now.AddSeconds(spellSpeed))); ((Player)owner).SendStartCastbar(spell.id, Utils.UnixTimeStampUTC(DateTime.Now.AddMilliseconds(spellSpeed)));
}
owner.SendChant(0xf, 0x0);
owner.DoBattleAction(spell.id, (uint) 0x6F000000 | spell.castType, new BattleAction(target.actorId, 30128, 1, 0, 1)); //You begin casting (6F000002: BLM, 6F000003: WHM, 0x6F000008: BRD)
} }
owner.SendChant(0xF, 0x0);
owner.DoBattleAction(spell.id, 0x6F000002, new BattleAction(target.actorId, 30128, 1, 0, 1)); //You begin casting (6F000002: BLM, 6F000003: WHM)
} }
} }
@ -77,9 +91,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
} }
// todo: check weapon delay/haste etc and use that // todo: check weapon delay/haste etc and use that
var actualCastTime = spell.castTimeSeconds; var actualCastTime = spell.castTimeMs;
if ((tick - startTime).TotalSeconds >= spell.castTimeSeconds) if ((tick - startTime).TotalMilliseconds >= spell.castTimeMs)
{ {
OnComplete(); OnComplete();
return true; return true;
@ -102,24 +116,58 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
public override void OnComplete() public override void OnComplete()
{ {
//How do combos/hitdirs work for aoe abilities or does that not matter for aoe?
HitDirection hitDir = owner.GetHitDirection(target);
bool hitTarget = false;
spell.targetFind.FindWithinArea(target, spell.validTarget, spell.aoeTarget); spell.targetFind.FindWithinArea(target, spell.validTarget, spell.aoeTarget);
isCompleted = true; isCompleted = true;
var targets = spell.targetFind.GetTargets(); var targets = spell.targetFind.GetTargets();
BattleAction[] actions = new BattleAction[targets.Count];
var i = 0;
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) foreach (var chara in targets)
{ {
var action = new BattleAction(chara.actorId, spell.worldMasterTextId, spell.battleAnimation, 0, (byte)HitDirection.None, 1); for (int hitNum = 0; hitNum < spell.numHits; hitNum++)
action.amount = (ushort)lua.LuaEngine.CallLuaBattleCommandFunction(owner, spell, "magic", "onMagicFinish", owner, chara, spell, action); {
actions[i++] = action; 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
var errors = (BattleAction[])actions.Clone();
owner.OnCast(this, actions, ref errors);
owner.DoBattleAction(spell.id, spell.battleAnimation, actions);
// 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()
@ -127,10 +175,10 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
if (interrupt) if (interrupt)
return; return;
if (owner.statusEffects.HasStatusEffectsByFlag((uint)StatusEffectFlags.PreventAction)) if (owner.statusEffects.HasStatusEffectsByFlag((uint)StatusEffectFlags.PreventSpell))
{ {
// todo: sometimes paralyze can let you attack, get random percentage of actually letting you attack // todo: sometimes paralyze can let you attack, get random percentage of actually letting you attack
var list = owner.statusEffects.GetStatusEffectsByFlag((uint)StatusEffectFlags.PreventAction); var list = owner.statusEffects.GetStatusEffectsByFlag((uint)StatusEffectFlags.PreventSpell);
uint effectId = 0; uint effectId = 0;
if (list.Count > 0) if (list.Count > 0)
{ {
@ -154,7 +202,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
private bool CanCast() private bool CanCast()
{ {
return owner.CanCast(target, spell) && spell.IsValidTarget(owner, target) && !HasMoved(); return owner.CanCast(target, spell) && spell.IsValidMainTarget(owner, target) && !HasMoved();
} }
private bool HasMoved() private bool HasMoved()
@ -170,7 +218,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
{ {
((Player)owner).SendEndCastbar(); ((Player)owner).SendEndCastbar();
} }
owner.aiContainer.UpdateLastActionTime(); owner.aiContainer.UpdateLastActionTime(spell.animationDurationSeconds);
} }
public BattleCommand GetSpell() public BattleCommand GetSpell()

View file

@ -15,11 +15,12 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
{ {
private BattleCommand skill; private BattleCommand skill;
private HitDirection hitDirection;
public WeaponSkillState(Character owner, Character target, ushort skillId) : public WeaponSkillState(Character owner, Character target, ushort skillId) :
base(owner, target) base(owner, target)
{ {
this.startTime = DateTime.Now; this.startTime = DateTime.Now;
//this.target = skill.targetFind.
this.skill = Server.GetWorldManager().GetBattleCommand(skillId); this.skill = Server.GetWorldManager().GetBattleCommand(skillId);
var returnCode = lua.LuaEngine.CallLuaBattleCommandFunction(owner, skill, "weaponskill", "onSkillPrepare", owner, target, skill); var returnCode = lua.LuaEngine.CallLuaBattleCommandFunction(owner, skill, "weaponskill", "onSkillPrepare", owner, target, skill);
@ -46,6 +47,37 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
else else
{ {
owner.LookAt(target); owner.LookAt(target);
hitDirection = owner.GetHitDirection(target);
//Do positionals and combo effects first because these can influence accuracy and amount of targets/numhits, which influence the rest of the steps
//If there is no positon required or if the position bonus should be activated
if ((skill.positionBonus & utils.BattleUtils.ConvertHitDirToPosition(hitDirection)) == skill.positionBonus)
{
//If there is a position bonus
if (skill.positionBonus != BattleCommandPositionBonus.None)
//lua.LuaEngine.CallLuaBattleCommandFunction(owner, skill, "weaponskill", "onPositional", owner, target, skill);
skill.CallLuaFunction(owner, "onPositional", owner, target, skill);
//Combo stuff
if (owner is Player p)
{
//If skill is part of owner's class/job, it can be used in a combo
if (skill.job == p.GetClass() || skill.job == p.GetCurrentClassOrJob())
{
//If owner is a player and the skill being used is part of the current combo
if (p.playerWork.comboNextCommandId[0] == skill.id || p.playerWork.comboNextCommandId[1] == skill.id)
{
lua.LuaEngine.CallLuaBattleCommandFunction(owner, skill, "weaponskill", "onCombo", owner, target, skill);
skill.CallLuaFunction(owner, "onCombo", owner, target, skill);
skill.isCombo = true;
}
//or if this just the start of a combo
else if (skill.comboStep == 1)
skill.isCombo = true;
}
}
}
} }
} }
@ -62,9 +94,9 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
} }
// todo: check weapon delay/haste etc and use that // todo: check weapon delay/haste etc and use that
var actualCastTime = skill.castTimeSeconds; var actualCastTime = skill.castTimeMs;
if ((tick - startTime).TotalSeconds >= skill.castTimeSeconds) if ((tick - startTime).Milliseconds >= skill.castTimeMs)
{ {
OnComplete(); OnComplete();
return true; return true;
@ -86,28 +118,55 @@ 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(); var targets = skill.targetFind.GetTargets();
BattleAction[] actions = new BattleAction[targets.Count]; //Need a variable size list of actions because status effects may add extra actions
//BattleAction[] actions = new BattleAction[targets.Count * skill.numHits];
List<BattleAction> actions = new List<BattleAction>();
List<StatusEffect> effects = owner.statusEffects.GetStatusEffectsByFlag((uint)StatusEffectFlags.ActivateOnAttack);
var i = 0; //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) foreach (var chara in targets)
{ {
var action = new BattleAction(chara.actorId, skill.worldMasterTextId, (uint)HitEffect.Hit, 0, 1, 1); for (int hitNum = 0; hitNum < skill.numHits; hitNum++)
// evasion, miss, dodge, etc to be handled in script, calling helpers from scripts/weaponskills.lua {
action.amount = (ushort)lua.LuaEngine.CallLuaBattleCommandFunction(owner, skill, "weaponskill", "onSkillFinish", owner, target, skill, action); var action = new BattleAction(chara.actorId, skill.worldMasterTextId, 0, 0, (byte) hitDirection, 1);
actions[i++] = action; //For older versions this will need to be in the database for magic weaponskills
chara.Engage(chara.actorId, 1); 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 // todo: this is fuckin stupid, probably only need *one* error packet, not an error for each action
var errors = (BattleAction[])actions.Clone(); BattleAction[] errors = (BattleAction[]) actions.ToArray().Clone();
owner.OnWeaponSkill(this, actions, ref errors); owner.OnWeaponSkill(this, actions.ToArray(), skill, ref errors);
owner.DoBattleAction(skill.id, skill.battleAnimation, actions); owner.DoBattleAction(skill.id, skill.battleAnimation, actions);
owner.statusEffects.RemoveStatusEffectsByFlags((uint) StatusEffectFlags.LoseOnAttacking);
//Now that we know if we hit the target we can check if the combo continues
if (owner is Player player)
if (skill.isCombo && hitTarget)
player.SetCombos(skill.comboNextCommandId);
else
player.SetCombos();
} }
public override void TryInterrupt() public override void TryInterrupt()
@ -115,10 +174,10 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
if (interrupt) if (interrupt)
return; return;
if (owner.statusEffects.HasStatusEffectsByFlag((uint)StatusEffectFlags.PreventAction)) if (owner.statusEffects.HasStatusEffectsByFlag((uint)StatusEffectFlags.PreventWeaponSkill))
{ {
// todo: sometimes paralyze can let you attack, get random percentage of actually letting you attack // todo: sometimes paralyze can let you attack, get random percentage of actually letting you attack
var list = owner.statusEffects.GetStatusEffectsByFlag((uint)StatusEffectFlags.PreventAction); var list = owner.statusEffects.GetStatusEffectsByFlag((uint)StatusEffectFlags.PreventWeaponSkill);
uint effectId = 0; uint effectId = 0;
if (list.Count > 0) if (list.Count > 0)
{ {
@ -134,7 +193,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
private bool CanUse() private bool CanUse()
{ {
return owner.CanWeaponSkill(target, skill) && skill.IsValidTarget(owner, target); return owner.CanWeaponSkill(target, skill) && skill.IsValidMainTarget(owner, target);
} }
public BattleCommand GetWeaponSkill() public BattleCommand GetWeaponSkill()
@ -144,7 +203,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.state
public override void Cleanup() public override void Cleanup()
{ {
owner.aiContainer.UpdateLastActionTime(); owner.aiContainer.UpdateLastActionTime(skill.animationDurationSeconds);
} }
} }
} }

View file

@ -7,22 +7,161 @@ using System.Threading.Tasks;
using FFXIVClassic_Map_Server.Actors; 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.dataobjects;
using FFXIVClassic.Common; using FFXIVClassic.Common;
namespace FFXIVClassic_Map_Server.actors.chara.ai.utils namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
{ {
static class BattleUtils static class BattleUtils
{ {
public static Dictionary<HitType, ushort> HitTypeTextIds = new Dictionary<HitType, ushort>()
{
{ HitType.Miss, 30311 },
{ HitType.Evade, 30310 },
{ HitType.Parry, 30308 },
{ HitType.Block, 30306 },
{ HitType.Resist, 30306 },//I can't find what the actual textid for resists is
{ HitType.Hit, 30301 },
{ HitType.Crit, 30302 }
};
public static Dictionary<HitType, HitEffect> HitTypeEffects = new Dictionary<HitType, HitEffect>()
{
{ HitType.Miss, 0 },
{ HitType.Evade, HitEffect.Evade },
{ HitType.Parry, HitEffect.Parry },
{ HitType.Block, HitEffect.Block },
{ HitType.Resist, HitEffect.RecoilLv1 },//Probably don't need this, resists are handled differently to the rest
{ HitType.Hit, HitEffect.Hit },
{ HitType.Crit, HitEffect.Crit }
};
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)
{ {
// todo: get hit rate, hit count, set hit effect // todo: get hit rate, hit count, set hit effect
action.effectId |= (uint)(HitEffect.RecoilLv2 | HitEffect.Hit | HitEffect.HitVisual1); //action.effectId |= (uint)(HitEffect.RecoilLv2 | HitEffect.Hit | HitEffect.HitVisual1);
return true; return true;
} }
private static double CalculateDlvlModifier(short dlvl)
{
//this is just a really, really simplified version of the graph from http://kanican.livejournal.com/55915.html
//actual formula is definitely more complicated
//I'm going to assum these formulas are linear, and they're clamped so the modifier never goes below 0.
double modifier = 0;
if (dlvl >= 0)
modifier = (.35 * dlvl) + .225;
else
modifier = (.01 * dlvl) + .25;
return modifier.Clamp(0, 1);
}
//Damage calculations
//Calculate damage of action
//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)
{
short dlvl = (short)(defender.GetLevel() - attacker.GetLevel());
switch (action.hitType)
{
//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
//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);
}
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
percentBlocked += defender.GetMod((uint)Modifier.Vitality) * .1;//Every point of vitality adds .1% to how much is blocked
percentBlocked = 1 - percentBlocked;
action.amount = (ushort)(action.amount * percentBlocked);
}
//don't know crit formula
public static void CalculateCritDamage(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{
short dlvl = (short)(defender.GetLevel() - attacker.GetLevel());
double bonus = (.04 * (dlvl * dlvl)) - 2 * dlvl;
bonus += 1.20;
double potencyModifier = (-.075 * dlvl) + 1.73;
// + potency bonus
//bonus += attacker.GetMod((uint) Modifier.CriticalPotency) * potencyModifier;
// - Crit resilience
//bonus -= attacker.GetMod((uint)Modifier.CriticalResilience) * potencyModifier;
//need to add something for bonus potency as a part of skill (ie thundara)
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)
{
double percentParry = .75;
action.amount = (ushort)(action.amount * percentParry);
}
//There are 3 or 4 tiers of resist that are flat 25% decreases in damage.
//It's possible we could just calculate the damage at the same time as we determine the hit type (the same goes for the rest of the hit types)
//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)
{
double percentResist = .5;
action.amount = (ushort)(action.amount * percentResist);
}
//Used for attacks and abilities like Jump that deal damage
public static ushort CalculateAttackDamage(Character attacker, Character defender, BattleAction action) public static ushort CalculateAttackDamage(Character attacker, Character defender, BattleAction action)
{ {
ushort damage = (ushort)(Program.Random.Next(10) * 10); ushort damage = (ushort)100;
if (attacker is Player p)
{
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 // todo: handle all other crap before protect/stoneskin
@ -60,23 +199,18 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Shell)) if (defender.statusEffects.HasStatusEffect(StatusEffectId.Shell))
{ {
// todo: shell probably only shows on magic..
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Shell)) if (defender.statusEffects.HasStatusEffect(StatusEffectId.Shell))
{ {
if (action != null)
action.effectId |= (uint)HitEffect.Shell;
} }
} }
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Stoneskin)) if (defender.statusEffects.HasStatusEffect(StatusEffectId.Stoneskin))
{ {
if (action != null)
action.effectId |= (uint)HitEffect.Stoneskin;
} }
return damage; return damage;
} }
public static void DamageTarget(Character attacker, Character defender, BattleAction action, DamageTakenType type) public static void DamageTarget(Character attacker, Character defender, BattleAction action, DamageTakenType type, bool sendBattleAction = false)
{ {
if (defender != null) if (defender != null)
{ {
@ -88,7 +222,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
{ {
bnpc.hateContainer.AddBaseHate(attacker); bnpc.hateContainer.AddBaseHate(attacker);
} }
bnpc.hateContainer.UpdateHate(attacker, action.amount); bnpc.hateContainer.UpdateHate(attacker, action.enmity);
bnpc.lastAttacker = attacker; bnpc.lastAttacker = attacker;
} }
defender.DelHP((short) action.amount); defender.DelHP((short) action.amount);
@ -96,6 +230,281 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
} }
} }
public static void DoAction(Character user, Character receiver, BattleAction action, DamageTakenType type = DamageTakenType.None)
{
switch(action.battleActionType)
{
//split attack into phys/mag?
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;
}
if ((type == DamageTakenType.Ability || type == DamageTakenType.Attack) && action.amount != 0)
{
receiver.AddTP(150);
user.AddTP(200);
}
}
/*
* Rate functions
*/
//How is accuracy actually calculated?
public static double GetHitRate(Character attacker, Character defender, BattleCommand skill)
{
double hitRate = .80;
//Certain skills have lower or higher accuracy rates depending on position/combo
return hitRate * (skill != null ? skill.accuracyModifier : 1);
}
//Whats the parry formula?
public static double GetParryRate(Character attacker, Character defender, BattleCommand skill)
{
//Can't parry with shield, must be facing attacker
if (defender.GetMod((uint)Modifier.HasShield) > 0 || !defender.IsFacing(attacker))
return 0;
return .10;
}
public static double GetCritRate(Character attacker, Character defender, BattleCommand skill)
{
double critRate = 10;// .0016 * attacker.GetMod((uint)Modifier.CritRating);//Crit rating adds .16% per point
return Math.Min(critRate, .20);//Crit rate is capped at 20%
}
//http://kanican.livejournal.com/55370.html
// todo: figure that out
public static double GetResistRate(Character attacker, Character defender, BattleCommand skill)
{
return .95;
}
//Block Rate follows 4 simple rules:
//(1) Every point in DEX gives +0.1% 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.
//(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)
{
//Shields are required to block.
if (defender.GetMod((uint)Modifier.HasShield) == 0)//|| !defender.IsFacing(attacker))
return 0;
short dlvl = (short) (attacker.GetLevel() - defender.GetLevel());
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
return Math.Min(blockRate, 25);
}
/*
* 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)
{
if (Program.Random.NextDouble() < GetCritRate(attacker, defender, skill))
{
action.hitType = HitType.Crit;
CalculateCritDamage(attacker, defender, skill, action);
return true;
}
return false;
}
public static bool TryResist(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{
if (Program.Random.NextDouble() < GetResistRate(attacker, defender, skill))
{
action.hitType = HitType.Resist;
CalculateResistDamage(attacker, defender, skill, action);
return true;
}
return false;
}
public static bool TryBlock(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{
if (Program.Random.NextDouble() < GetBlockRate(attacker, defender, skill))
{
action.hitType = HitType.Block;
defender.SetProc((int)HitType.Block);
CalculateBlockDamage(attacker, defender, skill, action);
return true;
}
return false;
}
public static bool TryParry(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{
if (Program.Random.NextDouble() < GetParryRate(attacker, defender, skill))
{
action.hitType = HitType.Parry;
defender.SetProc((int)HitType.Parry);
CalculateParryDamage(attacker, defender, skill, action);
return true;
}
return false;
}
//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)
{
if (Program.Random.NextDouble() > GetHitRate(attacker, defender, skill))
{
action.hitType = HitType.Miss;
action.amount = 0;
defender.SetProc((int)HitType.Evade);
attacker.SetProc((int)HitType.Miss);
return true;
}
return false;
}
/*
* 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)
{
//Might be a simpler way to do this?
switch(action.battleActionType)
{
case (BattleActionType.AttackPhysical):
SetHitEffectPhysical(caster, target, skill, action);
break;
case (BattleActionType.AttackMagic):
SetHitEffectMagical(caster, target, skill, action);
break;
case (BattleActionType.Heal):
SetHitEffectHeal(caster, target, skill, action);
break;
case (BattleActionType.Status):
SetHitEffectStatus(caster, target, skill, action);
break;
}
}
public static void SetHitEffectPhysical(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{
//Determine the hittype of the action and change amount of damage it does based on that
if (!TryMiss(attacker, defender, skill, action))
if (!TryCrit(attacker, defender, skill, action))
if (!TryBlock(attacker, defender, skill, action))
TryParry(attacker, defender, skill, action);
var hitEffect = HitEffect.HitEffectType;
//Don't know what recoil is actually based on, just guessing
//Crit is 2 and 3 together
if (action.hitType == HitType.Crit)
hitEffect |= HitEffect.CriticalHit;
else
{
double percentDealt = (100.0 * (action.amount / defender.GetMaxHP()));
if (percentDealt > 5.0)
hitEffect |= HitEffect.RecoilLv2;
else if(percentDealt > 10)
hitEffect |= HitEffect.RecoilLv3;
}
action.worldMasterTextId = HitTypeTextIds[action.hitType];
hitEffect |= HitTypeEffects[action.hitType];
if (skill != null && skill.isCombo && action.hitType > HitType.Evade)
hitEffect |= (HitEffect)(skill.comboStep << 15);
//if attack hit the target, take into account protective status effects
if (action.hitType >= HitType.Parry)
{
//Protect / Shell only show on physical/ magical attacks respectively.
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Protect))
if (action != null)
hitEffect |= HitEffect.Protect;
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Stoneskin))
if (action != null)
hitEffect |= HitEffect.Stoneskin;
}
action.effectId = (uint) hitEffect;
}
public static void SetHitEffectMagical(Character attacker, Character defender, BattleCommand skill, BattleAction action)
{
//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;
//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
if (action.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
if (action.amount == 0)
hitEffect |= HitEffect.RecoilLv1;
else
hitEffect |= HitEffect.RecoilLv2;
}
else if (action.hitType == HitType.Crit)
hitEffect |= HitEffect.Crit;
else
hitEffect |= HitEffect.RecoilLv3;
action.worldMasterTextId = HitTypeTextIds[action.hitType];
hitEffect |= HitTypeEffects[action.hitType];
if (skill != null && skill.isCombo)
hitEffect |= (HitEffect)(skill.comboStep << 15);
//if attack hit the target, take into account protective status effects
if (action.hitType >= HitType.Block)
{
//Protect / Shell only show on physical/ magical attacks respectively.
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Shell))
if (action != null)
hitEffect |= HitEffect.Shell;
if (defender.statusEffects.HasStatusEffect(StatusEffectId.Stoneskin))
if (action != null)
hitEffect |= HitEffect.Stoneskin;
}
action.effectId = (uint)hitEffect;
}
public static void SetHitEffectHeal(Character caster, Character receiver, BattleCommand skill, BattleAction action)
{
var hitEffect = HitEffect.MagicEffectType | HitEffect.Heal;
//Heals use recoil levels in some way as well. Possibly for very low health clutch heals or based on percentage of current health healed (not max health).
// todo: figure recoil levels out for heals
hitEffect |= HitEffect.RecoilLv3;
//do heals crit?
action.effectId = (uint)hitEffect;
}
public static void SetHitEffectStatus(Character caster, Character receiver, BattleCommand skill, BattleAction action)
{
var hitEffect = (uint) HitEffect.StatusEffectType | skill.statusId;
action.effectId = hitEffect;
}
public static int CalculateSpellDamage(Character attacker, Character defender, BattleCommand spell) public static int CalculateSpellDamage(Character attacker, Character defender, BattleCommand spell)
{ {
// todo: spell formulas and stuff (stoneskin, mods, stats, etc) // todo: spell formulas and stuff (stoneskin, mods, stats, etc)
@ -104,7 +513,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
public static uint CalculateSpellCost(Character caster, Character target, BattleCommand spell) public static uint CalculateSpellCost(Character caster, Character target, BattleCommand spell)
{ {
var scaledCost = spell.CalculateCost((uint)caster.charaWork.parameterSave.state_mainSkillLevel); var scaledCost = spell.CalculateMpCost(caster);
// todo: calculate cost for mob/player // todo: calculate cost for mob/player
if (caster is BattleNpc) if (caster is BattleNpc)
@ -117,5 +526,66 @@ namespace FFXIVClassic_Map_Server.actors.chara.ai.utils
} }
return scaledCost; return scaledCost;
} }
//Convert a HitDirection to a BattleCommandPositionBonus. Basically just combining left/right into flank
public static BattleCommandPositionBonus ConvertHitDirToPosition(HitDirection hitDir)
{
BattleCommandPositionBonus position = BattleCommandPositionBonus.None;
switch(hitDir)
{
case (HitDirection.Front):
position = BattleCommandPositionBonus.Front;
break;
case (HitDirection.Right):
case (HitDirection.Left):
position = BattleCommandPositionBonus.Flank;
break;
case (HitDirection.Rear):
position = BattleCommandPositionBonus.Rear;
break;
}
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
//Sentinel doesn't require an additional action because it doesn't need to show those numbers
public static void TryStatus(Character caster, Character target, BattleCommand skill, BattleAction action, bool isAdditional = true)
{
double rand = Program.Random.NextDouble();
(caster as Player).SendMessage(0x20, "", rand.ToString());
if (skill != null && action.amount < target.GetHP() && skill.statusId != 0 && action.hitType > HitType.Evade && 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.SetOwner(target);
if (target.statusEffects.AddStatusEffect(effect, caster))
{
//If we need an extra action to show the status text
if (isAdditional)
action.AddStatusAction(target.actorId, skill.statusId);
}
else
action.worldMasterTextId = 32002;//Is this right?
}
else
{
if (target.statusEffects.AddStatusEffect(skill.statusId, 1, 3000, skill.statusDuration, skill.statusTier))
{
//If we need an extra action to show the status text
if (isAdditional)
action.AddStatusAction(target.actorId, skill.statusId);
}
else
action.worldMasterTextId = 32002;//Is this right?
}
}
}
} }
} }

View file

@ -118,34 +118,33 @@ namespace FFXIVClassic_Map_Server.Actors
return subpackets; return subpackets;
} }
//This might need more work
//I think there migh be something that ties mobs to parties
//and the client checks if any mobs are tied to the current party
//and bases the color on that. Adding mob to party obviously doesn't work
//Based on depictionjudge script:
//HATE_TYPE_NONE is for passive
//HATE_TYPE_ENGAGED is for aggroed mobs
//HATE_TYPE_ENGAGED_PARTY is for claimed mobs, client uses occupancy group to determine if mob is claimed by player's party
//for now i'm just going to assume that occupancygroup will be BattleNpc's currentparties when they're in combat,
//so if party isn't null, they're claimed.
public SubPacket GetHateTypePacket(Player player) public SubPacket GetHateTypePacket(Player player)
{ {
npcWork.hateType = 1; npcWork.hateType = NpcWork.HATE_TYPE_NONE;
if (player != null) if (player != null)
{ {
if (aiContainer.IsEngaged()) if (aiContainer.IsEngaged())
{ {
npcWork.hateType = 2; npcWork.hateType = NpcWork.HATE_TYPE_ENGAGED;
}
if (player.actorId == this.currentLockedTarget) if (this.currentParty != null)
{ {
npcWork.hateType = NpcWork.HATE_TYPE_ENGAGED_PARTY; npcWork.hateType = NpcWork.HATE_TYPE_ENGAGED_PARTY;
} }
else if (player.currentParty != null)
{
foreach (var memberId in ((Party)player.currentParty).members)
{
if (this.currentLockedTarget == memberId)
{
npcWork.hateType = NpcWork.HATE_TYPE_ENGAGED_PARTY;
break;
} }
} }
} npcWork.hateType = 2;
} var propPacketUtil = new ActorPropertyPacketUtil("npcWork/hate", this);
var propPacketUtil = new ActorPropertyPacketUtil("npcWork", this);
propPacketUtil.AddProperty("npcWork.hateType"); propPacketUtil.AddProperty("npcWork.hateType");
return propPacketUtil.Done()[0]; return propPacketUtil.Done()[0];
} }
@ -198,7 +197,7 @@ namespace FFXIVClassic_Map_Server.Actors
public override bool CanWeaponSkill(Character target, BattleCommand skill) public override bool CanWeaponSkill(Character target, BattleCommand skill)
{ {
// todo: // todo:
return false; return true;
} }
public override bool CanUseAbility(Character target, BattleCommand ability) public override bool CanUseAbility(Character target, BattleCommand ability)
@ -359,18 +358,18 @@ namespace FFXIVClassic_Map_Server.Actors
lua.LuaEngine.CallLuaBattleFunction(this, "onAttack", this, state.GetTarget(), action.amount); lua.LuaEngine.CallLuaBattleFunction(this, "onAttack", this, state.GetTarget(), action.amount);
} }
public override void OnCast(State state, BattleAction[] actions, ref BattleAction[] errors) public override void OnCast(State state, BattleAction[] actions, BattleCommand spell, ref BattleAction[] errors)
{ {
base.OnCast(state, actions, ref errors); base.OnCast(state, actions, spell, ref errors);
if (GetMobMod((uint)MobModifier.SpellScript) != 0) if (GetMobMod((uint)MobModifier.SpellScript) != 0)
foreach (var action in actions) foreach (var action in actions)
lua.LuaEngine.CallLuaBattleFunction(this, "onCast", this, zone.FindActorInArea<Character>(action.targetId), ((MagicState)state).GetSpell(), action); lua.LuaEngine.CallLuaBattleFunction(this, "onCast", this, zone.FindActorInArea<Character>(action.targetId), ((MagicState)state).GetSpell(), action);
} }
public override void OnAbility(State state, BattleAction[] actions, ref BattleAction[] errors) public override void OnAbility(State state, BattleAction[] actions, BattleCommand ability, ref BattleAction[] errors)
{ {
base.OnAbility(state, actions, ref errors); base.OnAbility(state, actions, ability, ref errors);
/* /*
if (GetMobMod((uint)MobModifier.AbilityScript) != 0) if (GetMobMod((uint)MobModifier.AbilityScript) != 0)
@ -379,9 +378,9 @@ namespace FFXIVClassic_Map_Server.Actors
*/ */
} }
public override void OnWeaponSkill(State state, BattleAction[] actions, ref BattleAction[] errors) public override void OnWeaponSkill(State state, BattleAction[] actions, BattleCommand skill, ref BattleAction[] errors)
{ {
base.OnWeaponSkill(state, actions, ref errors); base.OnWeaponSkill(state, actions, skill, ref errors);
if (GetMobMod((uint)MobModifier.WeaponSkillScript) != 0) if (GetMobMod((uint)MobModifier.WeaponSkillScript) != 0)
foreach (var action in actions) foreach (var action in actions)

View file

@ -245,7 +245,7 @@ namespace FFXIVClassic_Map_Server.Actors
//Status Times //Status Times
for (int i = 0; i < charaWork.statusShownTime.Length; i++) for (int i = 0; i < charaWork.statusShownTime.Length; i++)
{ {
if (charaWork.statusShownTime[i] != 0xFFFFFFFF) if (charaWork.statusShownTime[i] != 0)
propPacketUtil.AddProperty(String.Format("charaWork.statusShownTime[{0}]", i)); propPacketUtil.AddProperty(String.Format("charaWork.statusShownTime[{0}]", i));
} }

View file

@ -163,6 +163,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.player
owner.QueuePacket(InventoryEndChangePacket.BuildPacket(owner.actorId)); owner.QueuePacket(InventoryEndChangePacket.BuildPacket(owner.actorId));
list[slot] = item; list[slot] = item;
owner.CalculateBaseStats();// RecalculateStats();
} }
public void ToggleDBWrite(bool flag) public void ToggleDBWrite(bool flag)
@ -189,6 +190,7 @@ namespace FFXIVClassic_Map_Server.actors.chara.player
owner.QueuePacket(InventoryEndChangePacket.BuildPacket(owner.actorId)); owner.QueuePacket(InventoryEndChangePacket.BuildPacket(owner.actorId));
list[slot] = null; list[slot] = null;
owner.RecalculateStats();
} }
private void SendEquipmentPackets(ushort equipSlot, InventoryItem item) private void SendEquipmentPackets(ushort equipSlot, InventoryItem item)

View file

@ -26,6 +26,7 @@ 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 FFXIVClassic_Map_Server.actors.chara.ai.state; using FFXIVClassic_Map_Server.actors.chara.ai.state;
using FFXIVClassic_Map_Server.actors.chara.npc; using FFXIVClassic_Map_Server.actors.chara.npc;
using FFXIVClassic_Map_Server.actors.chara;
namespace FFXIVClassic_Map_Server.Actors namespace FFXIVClassic_Map_Server.Actors
{ {
@ -228,6 +229,7 @@ namespace FFXIVClassic_Map_Server.Actors
this.aiContainer = new AIContainer(this, new PlayerController(this), null, new TargetFind(this)); this.aiContainer = new AIContainer(this, new PlayerController(this), null, new TargetFind(this));
allegiance = CharacterTargetingAllegiance.Player; allegiance = CharacterTargetingAllegiance.Player;
CalculateBaseStats();
} }
public List<SubPacket> Create0x132Packets() public List<SubPacket> Create0x132Packets()
@ -361,7 +363,7 @@ namespace FFXIVClassic_Map_Server.Actors
//Status Times //Status Times
for (int i = 0; i < charaWork.statusShownTime.Length; i++) for (int i = 0; i < charaWork.statusShownTime.Length; i++)
{ {
if (charaWork.statusShownTime[i] != 0xFFFFFFFF) if (charaWork.statusShownTime[i] != 0)
propPacketUtil.AddProperty(String.Format("charaWork.statusShownTime[{0}]", i)); propPacketUtil.AddProperty(String.Format("charaWork.statusShownTime[{0}]", i));
} }
@ -614,6 +616,7 @@ namespace FFXIVClassic_Map_Server.Actors
catch (Exception e) catch (Exception e)
{ {
this.SendMessage(SendMessagePacket.MESSAGE_TYPE_SYSTEM_ERROR, "[SendPacket]", "Unable to send packet."); this.SendMessage(SendMessagePacket.MESSAGE_TYPE_SYSTEM_ERROR, "[SendPacket]", "Unable to send packet.");
this.SendMessage(SendMessagePacket.MESSAGE_TYPE_SYSTEM_ERROR, "[SendPacket]", e.Message);
} }
} }
@ -1705,20 +1708,22 @@ namespace FFXIVClassic_Map_Server.Actors
Party partyGroup = (Party) currentParty; Party partyGroup = (Party) currentParty;
for (int i = 0; i < partyGroup.members.Count; i++) partyGroup.RemoveMember(actorId);
{
if (partyGroup.members[i] == actorId) //for (int i = 0; i < partyGroup.members.Count; i++)
{ //{
partyGroup.members.RemoveAt(i); // if (partyGroup.members[i] == actorId)
break; // {
} // partyGroup.members.RemoveAt(i);
} // break;
// }
//}
//currentParty.members.Remove(this); //currentParty.members.Remove(this);
if (partyGroup.members.Count == 0) if (partyGroup.members.Count == 0)
Server.GetWorldManager().NoMembersInParty((Party)currentParty); Server.GetWorldManager().NoMembersInParty((Party)currentParty);
currentParty = null; //currentParty = new Party(0, actorId);
} }
public void IssueChocobo(byte appearanceId, string nameResponse) public void IssueChocobo(byte appearanceId, string nameResponse)
@ -1842,27 +1847,23 @@ namespace FFXIVClassic_Map_Server.Actors
//If the class we're equipping for is the current class, we can just look at charawork.command //If the class we're equipping for is the current class, we can just look at charawork.command
if(classId == charaWork.parameterSave.state_mainSkill[0]) if(classId == charaWork.parameterSave.state_mainSkill[0])
{
hotbarSlot = FindFirstCommandSlotById(0); hotbarSlot = FindFirstCommandSlotById(0);
}
//Otherwise, we need to check the database. //Otherwise, we need to check the database.
else else
{
hotbarSlot = (ushort) (Database.FindFirstCommandSlot(this, classId) + charaWork.commandBorder); hotbarSlot = (ushort) (Database.FindFirstCommandSlot(this, classId) + charaWork.commandBorder);
}
EquipAbility(classId, commandId, hotbarSlot, printMessage); EquipAbility(classId, commandId, hotbarSlot, printMessage);
} }
//Add commandId to classId's hotbar at hotbarSlot. //Add commandId to classId's hotbar at hotbarSlot.
//If classId is not the current class, do it in the database //If classId is not the current class, do it in the database
//hotbarSlot is 32-indexed //hotbarSlot starts at 32
public void EquipAbility(byte classId, uint commandId, ushort hotbarSlot, bool printMessage = true) public void EquipAbility(byte classId, uint commandId, ushort hotbarSlot, bool printMessage = true)
{ {
var ability = Server.GetWorldManager().GetBattleCommand(commandId); var ability = Server.GetWorldManager().GetBattleCommand(commandId);
uint trueCommandId = commandId | 0xA0F00000; uint trueCommandId = 0xA0F00000 + commandId;
ushort lowHotbarSlot = (ushort)(hotbarSlot - charaWork.commandBorder); ushort lowHotbarSlot = (ushort)(hotbarSlot - charaWork.commandBorder);
ushort maxRecastTime = (ushort)ability.recastTimeSeconds; ushort maxRecastTime = (ushort)(ability != null ? ability.maxRecastTimeSeconds : 5);
uint recastEnd = Utils.UnixTimeStampUTC() + maxRecastTime; uint recastEnd = Utils.UnixTimeStampUTC() + maxRecastTime;
List<ushort> slotsToUpdate = new List<ushort>(); List<ushort> slotsToUpdate = new List<ushort>();
@ -1889,6 +1890,7 @@ namespace FFXIVClassic_Map_Server.Actors
//hotbarSlot 1 and 2 are 32-indexed. //hotbarSlot 1 and 2 are 32-indexed.
public void SwapAbilities(ushort hotbarSlot1, ushort hotbarSlot2) public void SwapAbilities(ushort hotbarSlot1, ushort hotbarSlot2)
{ {
//0 indexed hotbar slots for saving to database and recast timers
uint lowHotbarSlot1 = (ushort)(hotbarSlot1 - charaWork.commandBorder); uint lowHotbarSlot1 = (ushort)(hotbarSlot1 - charaWork.commandBorder);
uint lowHotbarSlot2 = (ushort)(hotbarSlot2 - charaWork.commandBorder); uint lowHotbarSlot2 = (ushort)(hotbarSlot2 - charaWork.commandBorder);
@ -1907,9 +1909,11 @@ namespace FFXIVClassic_Map_Server.Actors
charaWork.parameterTemp.maxCommandRecastTime[lowHotbarSlot2] = recastMax; charaWork.parameterTemp.maxCommandRecastTime[lowHotbarSlot2] = recastMax;
charaWork.parameterSave.commandSlot_recastTime[lowHotbarSlot2] = recastEnd; charaWork.parameterSave.commandSlot_recastTime[lowHotbarSlot2] = recastEnd;
//Save changes //Save changes to both slots
Database.EquipAbility(this, charaWork.parameterSave.state_mainSkill[0], (ushort)(lowHotbarSlot1), charaWork.command[hotbarSlot1], charaWork.parameterSave.commandSlot_recastTime[lowHotbarSlot1]); Database.EquipAbility(this, GetCurrentClassOrJob(), (ushort)(lowHotbarSlot1), 0xA0F00000 ^ charaWork.command[hotbarSlot1], charaWork.parameterSave.commandSlot_recastTime[lowHotbarSlot1]);
Database.EquipAbility(this, GetCurrentClassOrJob(), (ushort)(lowHotbarSlot2), 0xA0F00000 ^ charaWork.command[hotbarSlot2], charaWork.parameterSave.commandSlot_recastTime[lowHotbarSlot2]);
//Update slots on client
List<ushort> slotsToUpdate = new List<ushort>(); List<ushort> slotsToUpdate = new List<ushort>();
slotsToUpdate.Add(hotbarSlot1); slotsToUpdate.Add(hotbarSlot1);
slotsToUpdate.Add(hotbarSlot2); slotsToUpdate.Add(hotbarSlot2);
@ -1926,7 +1930,7 @@ namespace FFXIVClassic_Map_Server.Actors
slotsToUpdate.Add(trueHotbarSlot); slotsToUpdate.Add(trueHotbarSlot);
if(printMessage) if(printMessage)
SendGameMessage(Server.GetWorldManager().GetActor(), 30604, 0x20, 0, commandId ^ 0xA0F00000); SendGameMessage(Server.GetWorldManager().GetActor(), 30604, 0x20, 0, 0xA0F00000 ^ commandId);
UpdateHotbar(slotsToUpdate); UpdateHotbar(slotsToUpdate);
} }
@ -1952,10 +1956,10 @@ namespace FFXIVClassic_Map_Server.Actors
return firstSlot; return firstSlot;
} }
private void UpdateHotbarTimer(uint commandId, uint recastTimeSeconds) private void UpdateHotbarTimer(uint commandId, uint recastTimeMs)
{ {
ushort slot = FindFirstCommandSlotById(commandId); ushort slot = FindFirstCommandSlotById(commandId);
charaWork.parameterSave.commandSlot_recastTime[slot - charaWork.commandBorder] = Utils.UnixTimeStampUTC(DateTime.Now.AddSeconds(recastTimeSeconds)); charaWork.parameterSave.commandSlot_recastTime[slot - charaWork.commandBorder] = Utils.UnixTimeStampUTC(DateTime.Now.AddMilliseconds(recastTimeMs));
var slots = new List<ushort>(); var slots = new List<ushort>();
slots.Add(slot); slots.Add(slot);
UpdateRecastTimers(slots); UpdateRecastTimers(slots);
@ -2123,7 +2127,7 @@ namespace FFXIVClassic_Map_Server.Actors
SendGameMessage(Server.GetWorldManager().GetActor(), 32539, 0x20, (uint)spell.id); SendGameMessage(Server.GetWorldManager().GetActor(), 32539, 0x20, (uint)spell.id);
return false; return false;
} }
if (!IsValidTarget(target, spell.validTarget) || !spell.IsValidTarget(this, target)) if (!IsValidTarget(target, spell.mainTarget) || !spell.IsValidMainTarget(this, target))
{ {
// error packet is set in IsValidTarget // error packet is set in IsValidTarget
return false; return false;
@ -2141,29 +2145,36 @@ namespace FFXIVClassic_Map_Server.Actors
SendGameMessage(Server.GetWorldManager().GetActor(), 32535, 0x20, (uint)skill.id); SendGameMessage(Server.GetWorldManager().GetActor(), 32535, 0x20, (uint)skill.id);
return false; return false;
} }
if (target == null) if (target == null)
{ {
// Target does not exist. // Target does not exist.
SendGameMessage(Server.GetWorldManager().GetActor(), 32511, 0x20, (uint)skill.id); SendGameMessage(Server.GetWorldManager().GetActor(), 32511, 0x20, (uint)skill.id);
return false; return false;
} }
if (Utils.Distance(positionX, positionY, positionZ, target.positionX, target.positionY, target.positionZ) > skill.range) if (Utils.Distance(positionX, positionY, positionZ, target.positionX, target.positionY, target.positionZ) > skill.range)
{ {
// The target is out of range. // The target is out of range.
SendGameMessage(Server.GetWorldManager().GetActor(), 32539, 0x20, (uint)skill.id); SendGameMessage(Server.GetWorldManager().GetActor(), 32539, 0x20, (uint)skill.id);
return false; return false;
} }
if (!IsValidTarget(target, skill.validTarget) || !skill.IsValidTarget(this, target))
if (!IsValidTarget(target, skill.validTarget) || !skill.IsValidMainTarget(this, target))
{ {
// error packet is set in IsValidTarget // error packet is set in IsValidTarget
return false; return false;
} }
return true; return true;
} }
public override void OnAttack(State state, BattleAction action, ref BattleAction error) public override void OnAttack(State state, BattleAction action, ref BattleAction error)
{ {
var target = state.GetTarget();
base.OnAttack(state, action, ref error); base.OnAttack(state, action, ref error);
// todo: switch based on main weap (also probably move this anim assignment somewhere else) // todo: switch based on main weap (also probably move this anim assignment somewhere else)
action.animation = 0x19001000; action.animation = 0x19001000;
if (error == null) if (error == null)
@ -2171,42 +2182,39 @@ namespace FFXIVClassic_Map_Server.Actors
// melee attack animation // melee attack animation
//action.animation = 0x19001000; //action.animation = 0x19001000;
} }
var target = state.GetTarget();
if (target is BattleNpc) if (target is BattleNpc)
{ {
((BattleNpc)target).hateContainer.UpdateHate(this, action.amount); ((BattleNpc)target).hateContainer.UpdateHate(this, action.enmity);
} }
LuaEngine.GetInstance().OnSignal("playerAttack"); LuaEngine.GetInstance().OnSignal("playerAttack");
} }
public override void OnCast(State state, BattleAction[] actions, ref BattleAction[] errors) public override void OnCast(State state, BattleAction[] actions, BattleCommand spell, ref BattleAction[] errors)
{ {
// todo: update hotbar timers to skill's recast time (also needs to be done on class change or equip crap) // todo: update hotbar timers to skill's recast time (also needs to be done on class change or equip crap)
base.OnCast(state, actions, ref errors); base.OnCast(state, actions, spell, ref errors);
var spell = ((MagicState)state).GetSpell();
// todo: should just make a thing that updates the one slot cause this is dumb as hell // todo: should just make a thing that updates the one slot cause this is dumb as hell
UpdateHotbarTimer(spell.id, spell.recastTimeSeconds); UpdateHotbarTimer(spell.id, spell.recastTimeMs);
LuaEngine.GetInstance().OnSignal("spellUse"); //LuaEngine.GetInstance().OnSignal("spellUse");
} }
public override void OnWeaponSkill(State state, BattleAction[] actions, ref BattleAction[] errors) public override void OnWeaponSkill(State state, BattleAction[] actions, BattleCommand skill, ref BattleAction[] errors)
{ {
// todo: update hotbar timers to skill's recast time (also needs to be done on class change or equip crap) // todo: update hotbar timers to skill's recast time (also needs to be done on class change or equip crap)
base.OnWeaponSkill(state, actions, ref errors); base.OnWeaponSkill(state, actions, skill, ref errors);
var skill = ((WeaponSkillState)state).GetWeaponSkill();
// todo: should just make a thing that updates the one slot cause this is dumb as hell // todo: should just make a thing that updates the one slot cause this is dumb as hell
UpdateHotbarTimer(skill.id, skill.recastTimeSeconds); UpdateHotbarTimer(skill.id, skill.recastTimeMs);
// todo: this really shouldnt be called on each ws? // todo: this really shouldnt be called on each ws?
lua.LuaEngine.CallLuaBattleFunction(this, "onWeaponSkill", this, state.GetTarget(), skill); lua.LuaEngine.CallLuaBattleFunction(this, "onWeaponSkill", this, state.GetTarget(), skill);
LuaEngine.GetInstance().OnSignal("weaponskillUse"); LuaEngine.GetInstance().OnSignal("weaponskillUse");
} }
public override void OnAbility(State state, BattleAction[] actions, ref BattleAction[] errors) public override void OnAbility(State state, BattleAction[] actions, BattleCommand ability, ref BattleAction[] errors)
{ {
base.OnAbility(state, actions, ref errors); base.OnAbility(state, actions, ability, ref errors);
UpdateHotbarTimer(ability.id, ability.recastTimeMs);
LuaEngine.GetInstance().OnSignal("abilityUse"); LuaEngine.GetInstance().OnSignal("abilityUse");
} }
@ -2307,6 +2315,7 @@ namespace FFXIVClassic_Map_Server.Actors
currentJob = jobId; currentJob = jobId;
BroadcastPacket(SetCurrentJobPacket.BuildPacket(actorId, jobId), true); BroadcastPacket(SetCurrentJobPacket.BuildPacket(actorId, jobId), true);
Database.LoadHotbar(this); Database.LoadHotbar(this);
SendCharaExpInfo();
} }
//Gets the id of the player's current job. If they aren't a job, gets the id of their class //Gets the id of the player's current job. If they aren't a job, gets the id of their class
@ -2328,5 +2337,66 @@ namespace FFXIVClassic_Map_Server.Actors
updateFlags |= ActorUpdateFlags.HpTpMp; updateFlags |= ActorUpdateFlags.HpTpMp;
} }
public void SetCombos(int comboId1 = 0, int comboId2 = 0)
{
SetCombos(new int[] { comboId1, comboId2 });
}
public void SetCombos(int[] comboIds)
{
Array.Copy(comboIds, playerWork.comboNextCommandId, 2);
//If we're starting or continuing a combo chain, add the status effect and combo cost bonus
if (comboIds[0] != 0)
{
StatusEffect comboEffect = new StatusEffect(this, (uint) StatusEffectId.Combo, 1, 0, 13);
comboEffect.SetOverwritable(1);
statusEffects.AddStatusEffect(comboEffect, this, true);
playerWork.comboCostBonusRate = 1;
}
//Otherwise we're ending a combo, remove the status
else
{
statusEffects.RemoveStatusEffect(statusEffects.GetStatusEffectById((uint) StatusEffectId.Combo));
playerWork.comboCostBonusRate = 0;
}
ActorPropertyPacketUtil comboPropertyPacket = new ActorPropertyPacketUtil("playerWork/combo", this);
comboPropertyPacket.AddProperty($"playerWork.comboCostBonusRate");
comboPropertyPacket.AddProperty($"playerWork.comboNextCommandId[{0}]");
comboPropertyPacket.AddProperty($"playerWork.comboNextCommandId[{1}]");
QueuePackets(comboPropertyPacket.Done());
}
public override void CalculateBaseStats()
{
base.CalculateBaseStats();
//Add weapon property mod
var equip = GetEquipment();
var mainHandItem = equip.GetItemAtSlot(Equipment.SLOT_MAINHAND);
var damageAttribute = 0;
var attackDelay = 3000;
var hitCount = 1;
GetAttackDelayMs();
if (mainHandItem != null)
{
var mainHandWeapon = (Server.GetItemGamedata(mainHandItem.itemId) as WeaponItem);
damageAttribute = mainHandWeapon.damageAttributeType1;
attackDelay = (int) (mainHandWeapon.damageInterval * 1000);
hitCount = mainHandWeapon.frequency;
}
var hasShield = equip.GetItemAtSlot(Equipment.SLOT_OFFHAND) != null ? 1 : 0;
SetMod((uint)Modifier.HasShield, hasShield);
SetMod((uint)Modifier.AttackType, damageAttribute);
SetMod((uint)Modifier.AttackDelay, attackDelay);
SetMod((uint)Modifier.HitCount, hitCount);
}
public void SetCurrentJob(ushort jobId)
{
currentJob = jobId;
BroadcastPacket(SetCurrentJobPacket.BuildPacket(actorId, jobId), true);
}
} }
} }

View file

@ -17,6 +17,7 @@ namespace FFXIVClassic_Map_Server.actors.group
public MonsterParty(ulong groupIndex, uint[] initialMonsterMembers) public MonsterParty(ulong groupIndex, uint[] initialMonsterMembers)
: base(groupIndex) : base(groupIndex)
{ {
if(initialMonsterMembers != null)
for (int i = 0; i < initialMonsterMembers.Length; i++) for (int i = 0; i < initialMonsterMembers.Length; i++)
monsterMembers.Add(initialMonsterMembers[i]); monsterMembers.Add(initialMonsterMembers[i]);
} }

View file

@ -81,6 +81,8 @@ namespace FFXIVClassic_Map_Server.actors.group
{ {
members.Remove(memberId); members.Remove(memberId);
SendGroupPacketsAll(members); SendGroupPacketsAll(members);
if (members.Count == 0)
Server.GetWorldManager().NoMembersInParty(this);
} }
} }
} }

View file

@ -479,7 +479,7 @@ namespace FFXIVClassic_Map_Server.dataobjects
public readonly short craftMagicProcessing; public readonly short craftMagicProcessing;
public readonly short harvestPotency; public readonly short harvestPotency;
public readonly short harvestLimit; public readonly short harvestLimit;
public readonly byte frequency; public readonly byte frequency; // hit count, 2 for h2h weapons
public readonly short rate; public readonly short rate;
public readonly short magicRate; public readonly short magicRate;
public readonly short craftProcessControl; public readonly short craftProcessControl;
@ -488,7 +488,7 @@ namespace FFXIVClassic_Map_Server.dataobjects
public readonly short magicCritical; public readonly short magicCritical;
public readonly short parry; public readonly short parry;
public readonly int damageAttributeType1; public readonly int damageAttributeType1; // 1 slashing, 2 piercing, 3 blunt, 4 projectile
public readonly float damageAttributeValue1; public readonly float damageAttributeValue1;
public readonly int damageAttributeType2; public readonly int damageAttributeType2;
public readonly float damageAttributeValue2; public readonly float damageAttributeValue2;
@ -498,6 +498,7 @@ namespace FFXIVClassic_Map_Server.dataobjects
public readonly short damagePower; public readonly short damagePower;
public readonly float damageInterval; public readonly float damageInterval;
public readonly short ammoVirtualDamagePower; public readonly short ammoVirtualDamagePower;
public readonly float dps;
public WeaponItem(MySqlDataReader reader) public WeaponItem(MySqlDataReader reader)
: base(reader) : base(reader)
@ -534,6 +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
} }
} }

View file

@ -19,7 +19,6 @@ namespace FFXIVClassic_Map_Server.dataobjects
public void QueuePacket(SubPacket subpacket) public void QueuePacket(SubPacket subpacket)
{ {
//Temporary fix for r0
if(SendPacketQueue.Count == 1000) if(SendPacketQueue.Count == 1000)
FlushQueuedSendPackets(); FlushQueuedSendPackets();

View file

@ -247,7 +247,7 @@ namespace FFXIVClassic_Map_Server.lua
Program.Log.Error($"LuaEngine.CallLuaBattleCommandFunction [{functionName}] {e.Message}"); Program.Log.Error($"LuaEngine.CallLuaBattleCommandFunction [{functionName}] {e.Message}");
} }
DynValue res = new DynValue(); DynValue res = new DynValue();
DynValue r = script.Globals.Get(functionName); // DynValue r = script.Globals.Get(functionName);
if (!script.Globals.Get(functionName).IsNil()) if (!script.Globals.Get(functionName).IsNil())
{ {
@ -259,6 +259,82 @@ namespace FFXIVClassic_Map_Server.lua
return -1; return -1;
} }
public static void LoadBattleCommandScript(BattleCommand command, string folder)
{
string path = $"./scripts/commands/{folder}/{command.name}.lua";
if (File.Exists(path))
{
var script = LoadGlobals();
try
{
script.DoFile(path);
}
catch (Exception e)
{
Program.Log.Error($"LuaEngine.CallLuaBattleCommandFunction {e.Message}");
}
command.script = script;
}
else
{
path = $"./scripts/commands/{folder}/default.lua";
//Program.Log.Error($"LuaEngine.CallLuaBattleCommandFunction [{command.name}] Unable to find script {path}");
var script = LoadGlobals();
try
{
script.DoFile(path);
}
catch (Exception e)
{
Program.Log.Error($"LuaEngine.CallLuaBattleCommandFunction {e.Message}");
}
command.script = script;
}
}
public static void LoadStatusEffectScript(StatusEffect effect)
{
string path = $"./scripts/effects/{effect.GetName()}.lua";
if (File.Exists(path))
{
var script = LoadGlobals();
try
{
script.DoFile(path);
}
catch (Exception e)
{
Program.Log.Error($"LuaEngine.CallLuaBattleCommandFunction {e.Message}");
}
effect.script = script;
}
else
{
path = $"./scripts/effects/default.lua";
//Program.Log.Error($"LuaEngine.CallLuaBattleCommandFunction [{command.name}] Unable to find script {path}");
var script = LoadGlobals();
try
{
script.DoFile(path);
}
catch (Exception e)
{
Program.Log.Error($"LuaEngine.CallLuaBattleCommandFunction {e.Message}");
}
effect.script = script;
}
}
public static string GetScriptPath(Actor target) public static string GetScriptPath(Actor target)
{ {
if (target is Player) if (target is Player)

View file

@ -1,5 +1,10 @@
using FFXIVClassic.Common; using FFXIVClassic.Common;
using System; using System;
using System.Collections.Generic;
using FFXIVClassic_Map_Server.actors.chara.ai;
using FFXIVClassic_Map_Server.actors.chara.ai.utils;
using FFXIVClassic_Map_Server.Actors;
using FFXIVClassic_Map_Server.packets.send.actor.battle;
namespace FFXIVClassic_Map_Server.packets.send.actor.battle namespace FFXIVClassic_Map_Server.packets.send.actor.battle
{ {
@ -9,13 +14,18 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
{ {
//All HitEffects have the last byte 0x8 //All HitEffects have the last byte 0x8
HitEffectType = 8 << 24, HitEffectType = 8 << 24,
//Status effects use 32 << 24
StatusEffectType = 32 << 24,
//Magic effects use 48 << 24
MagicEffectType = 48 << 24,
//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.
RecoilLv1 = 0 | HitEffectType, //These also have a visual effect with heals but in reverse. RecoilLv1 has a large effect, Lv3 has none. Crit is very large
RecoilLv2 = 1 << 0 | HitEffectType, RecoilLv1 = 0,
RecoilLv3 = 1 << 1 | HitEffectType, RecoilLv2 = 1 << 0,
RecoilLv3 = 1 << 1,
//Setting both recoil flags triggers the "Critical!" pop-up text and hit visual effect. //Setting both recoil flags triggers the "Critical!" pop-up text and hit visual effect.
CriticalHit = RecoilLv2 | RecoilLv3, CriticalHit = RecoilLv2 | RecoilLv3,
@ -24,10 +34,19 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
//Mixing these flags together will yield different results. //Mixing these flags together will yield different results.
//Each visual likely relates to a specific weapon. //Each visual likely relates to a specific weapon.
//Ex: HitVisual4 flag alone appears to be the visual and sound effect for hand-to-hand attacks. //Ex: HitVisual4 flag alone appears to be the visual and sound effect for hand-to-hand attacks.
HitVisual1 = 1 << 2 | HitEffectType,
HitVisual2 = 1 << 3 | HitEffectType, //HitVisual is probably based on attack property.
HitVisual3 = 1 << 4 | HitEffectType, //HitVisual1 is for slashing attacks
HitVisual4 = 1 << 5 | HitEffectType, //HitVisual2 is for piercing attacks
//HitVisual1 | Hitvisual2 is for blunt attacks
//HitVisual3 is for projectile attacks
//Basically take the attack property of a weapon and shift it left 2
//For auto attacks attack property is weapon's damageAttributeType1
//Still not totally sure how this works with weaponskills or what hitvisual4 or the other combinations are for
HitVisual1 = 1 << 2,
HitVisual2 = 1 << 3,
HitVisual3 = 1 << 4,
HitVisual4 = 1 << 5,
//An additional visual effect that plays on the target when attacked if: //An additional visual effect that plays on the target when attacked if:
//The attack is physical and they have the protect buff on. //The attack is physical and they have the protect buff on.
@ -40,19 +59,27 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
Shell = 1 << 7 | HitEffectType, Shell = 1 << 7 | HitEffectType,
ProtectShellSpecial = Protect | Shell, ProtectShellSpecial = Protect | Shell,
//Unknown = 1 << 8, -- Not sure what this flag does. // Required for heal text to be blue, not sure if that's all it's used for
Heal = 1 << 8,
//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.
HitEffect1 = 1 << 9 | HitEffectType, HitEffect1 = 1 << 9,
HitEffect2 = 1 << 10 | HitEffectType, //Plays the standard hit visual effect, but with no sound if used alone. HitEffect2 = 1 << 10, //Plays the standard hit visual effect, but with no sound if used alone.
Hit = HitEffect1 | HitEffect2, //A standard hit effect with sound effect. HitEffect3 = 1 << 11, //Yellow effect, crit?
HitEffect3 = 1 << 11 | HitEffectType, HitEffect4 = 1 << 12, //Plays the blocking animation
HitEffect4 = 1 << 12 | HitEffectType, HitEffect5 = 1 << 13,
HitEffect5 = 1 << 13 | HitEffectType,
GustyHitEffect = HitEffect3 | HitEffect2, GustyHitEffect = HitEffect3 | HitEffect2,
GreenTintedHitEffect = HitEffect4 | HitEffect1, GreenTintedHitEffect = HitEffect4 | HitEffect1,
//For specific animations
Miss = 0,
Evade = HitEffect1,
Hit = HitEffect1 | HitEffect2,
Parry = Hit | HitEffect3,
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,
KnockbackLv2 = HitEffect4 | HitEffect3, KnockbackLv2 = HitEffect4 | HitEffect3,
@ -80,10 +107,10 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
//A special effect when performing appropriate skill combos in succession. //A special effect when performing appropriate skill combos in succession.
//Ex: Thunder (SkillCombo1 Effect) -> Thundara (SkillCombo2 Effect) -> Thundaga (SkillCombo3 Effect) //Ex: Thunder (SkillCombo1 Effect) -> Thundara (SkillCombo2 Effect) -> Thundaga (SkillCombo3 Effect)
//Special Note: SkillCombo4 was never actually used in 1.0 since combos only chained up to 3 times maximum. //Special Note: SkillCombo4 was never actually used in 1.0 since combos only chained up to 3 times maximum.
SkillCombo1 = 1 << 15 | HitEffectType, SkillCombo1 = 1 << 15,
SkillCombo2 = 1 << 16 | HitEffectType, SkillCombo2 = 1 << 16,
SkillCombo3 = SkillCombo1 | SkillCombo2, SkillCombo3 = SkillCombo1 | SkillCombo2,
SkillCombo4 = 1 << 17 | HitEffectType SkillCombo4 = 1 << 17
//Flags beyond here are unknown/untested. //Flags beyond here are unknown/untested.
} }
@ -100,19 +127,45 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
Left = 1 << 3 Left = 1 << 3
} }
public enum HitType : ushort
{
Miss = 0,
Evade = 1,
Parry = 2,
Block = 3,
Resist = 4,
Hit = 5,
Crit = 6
}
public enum BattleActionType
{
None = 0,
AttackPhysical = 1,
AttackMagic = 2,
Heal = 3,
Status = 4
}
class BattleAction class BattleAction
{ {
public uint targetId; public uint targetId;
public ushort amount; public ushort amount;
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; public uint effectId; //Impact effect, damage/heal/status numbers or name
public byte param; public byte param; //Which side the battle action is coming from
public byte unknown; 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 /// this field is not actually part of the packet struct
/// </summary> /// </summary>
public uint animation; public uint animation;
public BattleActionType battleActionType;
public BattleAction(uint targetId, ushort worldMasterTextId, uint effectId, ushort amount = 0, byte param = 0, byte unknown = 1) public BattleAction(uint targetId, ushort worldMasterTextId, uint effectId, ushort amount = 0, byte param = 0, byte unknown = 1)
{ {
@ -121,7 +174,38 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
this.effectId = effectId; this.effectId = effectId;
this.amount = amount; this.amount = amount;
this.param = param; this.param = param;
this.unknown = unknown; this.hitNum = unknown;
this.hitType = HitType.Hit;
this.enmity = amount;
this.actionsList = new List<BattleAction>();
this.battleActionType = BattleActionType.None;
actionsList.Add(this);
}
public void AddStatusAction(uint targetId, uint effectId)
{
actionsList.Add(new BattleAction(targetId, 30328, effectId | (uint) HitEffect.StatusEffectType));
}
public void AddHealAction(uint targetId, ushort amount)
{
var a = new BattleAction(targetId, 30320, (uint)(HitEffect.MagicEffectType | HitEffect.RecoilLv3 | HitEffect.Heal), amount);
actionsList.Add(a);
}
public void CalcHitType(Character caster, Character target, BattleCommand skill)
{
BattleUtils.CalcHitType(caster, target, skill, this);
}
public void TryStatus(Character caster, Character target, BattleCommand skill, bool isAdditional = true)
{
BattleUtils.TryStatus(caster, target, skill, this, isAdditional);
}
public List<BattleAction> GetAllActions()
{
return actionsList;
} }
} }
} }

View file

@ -39,14 +39,14 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
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);
binWriter.Write((Byte)1); //? binWriter.Write((Byte)1); //?
} }
} }
new SubPacket(OPCODE, sourceActorId, data).DebugPrintSubPacket();
return new SubPacket(OPCODE, sourceActorId, data); return new SubPacket(OPCODE, sourceActorId, data);
} }
} }

View file

@ -57,7 +57,7 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
binWriter.Seek(0xAA, SeekOrigin.Begin); binWriter.Seek(0xAA, SeekOrigin.Begin);
for (int i = 0; i < max; i++) for (int i = 0; i < max; i++)
binWriter.Write((Byte)actionList[listOffset + i].unknown); binWriter.Write((Byte)actionList[listOffset + i].hitNum);
listOffset += max; listOffset += max;
} }
@ -112,7 +112,7 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
binWriter.Seek(0xAA, SeekOrigin.Begin); binWriter.Seek(0xAA, SeekOrigin.Begin);
for (int i = 0; i < max; i++) for (int i = 0; i < max; i++)
binWriter.Write((Byte)actionList[listOffset + i].unknown); binWriter.Write((Byte)actionList[listOffset + i].hitNum);
listOffset += max; listOffset += max;
} }

View file

@ -57,7 +57,7 @@ namespace FFXIVClassic_Map_Server.packets.send.actor.battle
binWriter.Seek(0x112, SeekOrigin.Begin); binWriter.Seek(0x112, SeekOrigin.Begin);
for (int i = 0; i < max; i++) for (int i = 0; i < max; i++)
binWriter.Write((Byte)actionList[listOffset + i].unknown); binWriter.Write((Byte)actionList[listOffset + i].hitNum);
listOffset += max; listOffset += max;
} }
@ -86,33 +86,33 @@ 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)max); //Num actions binWriter.Write((UInt32)actionList.Count); //Num actions
binWriter.Write((UInt16)commandId); binWriter.Write((UInt16)commandId);
binWriter.Write((UInt16)0x810); //? binWriter.Write((UInt16)0x810); //?
binWriter.Seek(0x58, SeekOrigin.Begin); binWriter.Seek(0x28, SeekOrigin.Begin);
for (int i = 0; i < max; i++) for (int i = 0; i < max; i++)
binWriter.Write((UInt32)actionList[listOffset + i].targetId); binWriter.Write((UInt32)actionList[listOffset + i].targetId);
binWriter.Seek(0xA0, SeekOrigin.Begin); binWriter.Seek(0x70, SeekOrigin.Begin);
for (int i = 0; i < max; i++) for (int i = 0; i < max; i++)
binWriter.Write((UInt16)actionList[listOffset + i].amount); binWriter.Write((UInt16)actionList[listOffset + i].amount);
binWriter.Seek(0xC4, SeekOrigin.Begin); binWriter.Seek(0x94, SeekOrigin.Begin);
for (int i = 0; i < max; i++) for (int i = 0; i < max; i++)
binWriter.Write((UInt16)actionList[listOffset + i].worldMasterTextId); binWriter.Write((UInt16)actionList[listOffset + i].worldMasterTextId);
binWriter.Seek(0xE8, SeekOrigin.Begin); binWriter.Seek(0xB8, 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(0x130, SeekOrigin.Begin); binWriter.Seek(0x100, SeekOrigin.Begin);
for (int i = 0; i < max; i++) for (int i = 0; i < max; i++)
binWriter.Write((Byte)actionList[listOffset + i].param); binWriter.Write((Byte)actionList[listOffset + i].param);
binWriter.Seek(0x142, SeekOrigin.Begin); binWriter.Seek(0x112, SeekOrigin.Begin);
for (int i = 0; i < max; i++) for (int i = 0; i < max; i++)
binWriter.Write((Byte)actionList[listOffset + i].unknown); binWriter.Write((Byte)actionList[listOffset + i].hitNum);
listOffset += max; listOffset += max;
} }

60
data/scripts/ability.lua Normal file
View file

@ -0,0 +1,60 @@
-- todo: add enums for status effects in global.lua
require("global")
require("battleutils")
--[[
statId - see BattleTemp.cs
modifier - Modifier.Intelligence, Modifier.Mind (see Modifier.cs)
multiplier -
]]
function HandleHealingSkill(caster, target, skill, action, statId, modifierId, multiplier, baseAmount)
potency = potency or 1.0;
healAmount = baseAmount;
-- todo: shit based on mnd
local mind = caster.GetMod(Modifier.Mind);
end;
function HandleAttackSkill(caster, target, skill, action, statId, modifierId, multiplier, baseAmount)
-- todo: actually handle this
damage = baseAmount or math.random(1,10) * 10;
return damage;
end;
function HandleStoneskin(caster, target, skill, action, statId, modifierId, damage)
--[[
if target.statusEffects.HasStatusEffect(StatusEffect.Stoneskin) then
-- todo: damage reduction
return true;
end;
]]
return false;
end;
--For abilities that inflict statuses, like aegis boon or taunt
function onStatusAbilityFinish(caster, target, skill, action)
action.battleActionType = BattleActionType.Status;
action.CalcHitType(caster, target, skill);
action.TryStatus(caster, target, skill, false);
return action.amount;
end;
function onAttackAbilityFinish(caster, target, skill, action)
action.battleActionType = BattleActionType.AttackPhysical;
local damage = math.random(50, 150);
action.amount = damage;
action.CalcHitType(caster, target, skill);
return action.amount;
end;
function onHealAbilityFinish(caster, target, skill, action)
action.battleActionType = BattleActionType.Heal;
local amount = math.random(150, 250);
action.amount = amount;
action.CalcHitType(caster, target, skill);
action.TryStatus(caster, target, skill, true);
return action.amount;
end;

View file

@ -34,14 +34,17 @@ function allyGlobal.onDespawn(ally)
end end
--tryAggro serves the same purpose for now, keeping this around just in case
function allyGlobal.HelpPlayers(ally, contentGroupCharas, pickRandomTarget) function allyGlobal.HelpPlayers(ally, contentGroupCharas, pickRandomTarget)
print("helpPlayers");
if contentGroupCharas and not ally.IsEngaged() then if contentGroupCharas and not ally.IsEngaged() then
print("contentGroup exists");
for chara in contentGroupCharas do for chara in contentGroupCharas do
print("looping");
if chara then if chara then
-- probably a player, or another ally -- probably a player, or another ally
-- todo: queue support actions, heal, try pull hate off player etc -- todo: queue support actions, heal, try pull hate off player etc
if chara:IsPlayer() then if chara.IsPlayer() then
print("chara is a player");
-- do stuff -- do stuff
if not ally.IsEngaged() then if not ally.IsEngaged() then
if chara.IsEngaged() then if chara.IsEngaged() then
@ -51,6 +54,7 @@ function allyGlobal.HelpPlayers(ally, contentGroupCharas, pickRandomTarget)
end end
elseif chara.IsMonster() and chara.IsEngaged() then elseif chara.IsMonster() and chara.IsEngaged() then
if not ally.IsEngaged() then if not ally.IsEngaged() then
print("Engaging monster that is engaged");
allyGlobal.EngageTarget(ally, chara, nil); allyGlobal.EngageTarget(ally, chara, nil);
break; break;
end end
@ -60,7 +64,6 @@ function allyGlobal.HelpPlayers(ally, contentGroupCharas, pickRandomTarget)
end end
end end
--Iterate over characters in contentGroup, if a player is in combat, assist them.
function allyGlobal.tryAggro(ally, contentGroupCharas) function allyGlobal.tryAggro(ally, contentGroupCharas)
local count = 0; local count = 0;
if contentGroupCharas and not ally.IsEngaged() then if contentGroupCharas and not ally.IsEngaged() then
@ -76,14 +79,12 @@ function allyGlobal.tryAggro(ally, contentGroupCharas)
break; break;
end end
end end
--[[
elseif contentGroupCharas[i].IsMonster() and contentGroupCharas[i].IsEngaged() then elseif contentGroupCharas[i].IsMonster() and contentGroupCharas[i].IsEngaged() then
if not ally.IsEngaged() then if not ally.IsEngaged() then
print("Engaging monster that is engaged"); print("Engaging monster that is engaged");
allyGlobal.EngageTarget(ally, contentGroupCharas[i], nil); allyGlobal.EngageTarget(ally, contentGroupCharas[i], nil);
break; break;
end]] end
end end
end end
end end
@ -109,6 +110,7 @@ function allyGlobal.EngageTarget(ally, target, contentGroupCharas)
end end
end end
elseif target then elseif target then
print("Engaging");
ally.Engage(target) ally.Engage(target)
ally.hateContainer.AddBaseHate(target); ally.hateContainer.AddBaseHate(target);
end end

175
data/scripts/battlenpc.lua Normal file
View file

@ -0,0 +1,175 @@
local initClassItems, initRaceItems;
function onBeginLogin(player)
--New character, set the initial quest
if (player:GetPlayTime(false) == 0) then
initialTown = player:GetInitialTown();
if (initialTown == 1 and player:HasQuest(110001) == false) then
player:AddQuest(110001);
player:SetHomePoint(1280001);
elseif (initialTown == 2 and player:HasQuest(110005) == false) then
player:AddQuest(110005);
player:SetHomePoint(1280061);
elseif (initialTown == 3 and player:HasQuest(110009) == false) then
player:AddQuest(110009);
player:SetHomePoint(1280031);
end
end
--For Opening. Set Director and reset position incase d/c
if (player:HasQuest(110001) == true and player:GetZoneID() == 193) then
director = player:GetZone():CreateDirector("OpeningDirector", false);
player:AddDirector(director);
director:StartDirector(true);
player:SetLoginDirector(director);
player:KickEvent(director, "noticeEvent", true);
player.positionX = 0.016;
player.positionY = 10.35;
player.positionZ = -36.91;
player.rotation = 0.025;
player:GetQuest(110001):ClearQuestData();
player:GetQuest(110001):ClearQuestFlags();
elseif (player:HasQuest(110005) == true and player:GetZoneID() == 166) then
director = player:GetZone():CreateDirector("OpeningDirector", false);
player:AddDirector(director);
director:StartDirector(false);
player:SetLoginDirector(director);
player:KickEvent(director, "noticeEvent", true);
player.positionX = 369.5434;
player.positionY = 4.21;
player.positionZ = -706.1074;
player.rotation = -1.26721;
player:GetQuest(110005):ClearQuestData();
player:GetQuest(110005):ClearQuestFlags();
elseif (player:HasQuest(110009) == true and player:GetZoneID() == 184) then
--director = player:GetZone():CreateDirector("OpeningDirector", false);
--player:AddDirector(director);
--director:StartDirector(false);
--player:SetLoginDirector(director);
--player:KickEvent(director, "noticeEvent", true);
--
player.positionX = 5.364327;
player.positionY = 196.0;
player.positionZ = 133.6561;
player.rotation = -2.849384;
player:GetQuest(110009):ClearQuestData();
player:GetQuest(110009):ClearQuestFlags();
end
end
function onLogin(player)
if (player:GetPlayTime(false) == 0) then
player:SendMessage(0x1D,"",">PlayTime == 0, new player!");
initClassItems(player);
initRaceItems(player);
player:SavePlayTime();
end
end
function initClassItems(player)
local slotTable;
local invSlotTable;
--DoW
if (player.charaWork.parameterSave.state_mainSkill[0] == 2) then --PUG
player:GetInventory(0):AddItem({4020001, 8030701, 8050728, 8080601, 8090307});
player:GetEquipment():SetEquipment({0, 10, 12, 14, 15},{0, 1, 2, 3, 4});
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 3) then --GLA
player:GetInventory(0):AddItem({4030010, 8031120, 8050245, 8080601, 8090307});
player:GetEquipment():SetEquipment({0, 10, 12, 14, 15},{0, 1, 2, 3, 4});
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 4) then --MRD
player:GetInventory(0):AddItem({4040001, 8011001, 8050621, 8070346, 8090307});
player:GetEquipment():SetEquipment({0, 8, 12, 13, 15},{0, 1, 2, 3, 4});
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 7) then --ARC
player:GetInventory(0):AddItem({4070001, 8030601, 8050622, 8080601, 8090307});
player:GetEquipment():SetEquipment({0, 10, 12, 14, 15},{0, 1, 2, 3, 4});
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 8) then --LNC
player:GetInventory(0):AddItem({4080201, 8030801, 8051015, 8080501, 8090307});
player:GetEquipment():SetEquipment({0, 10, 12, 14, 15},{0, 1, 2, 3, 4});
--DoM
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 22) then --THM
player:GetInventory(0):AddItem({5020001, 8030245, 8050346, 8080346, 8090208});
player:GetEquipment():SetEquipment({0, 10, 12, 14, 15},{0, 1, 2, 3, 4});
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 23) then --CNJ
player:GetInventory(0):AddItem({5030101, 8030445, 8050031, 8080246, 8090208});
player:GetEquipment():SetEquipment({0, 10, 12, 14, 15},{0, 1, 2, 3, 4});
--DoH
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 29) then --
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 30) then --
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 31) then --
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 32) then --
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 33) then --
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 34) then --
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 35) then --
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 36) then --
--DoL
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 39) then --MIN
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 40) then --BTN
elseif (player.charaWork.parameterSave.state_mainSkill[0] == 41) then --FSH
end
end
function initRaceItems(player)
if (player.playerWork.tribe == 1) then --Hyur Midlander Male
player:GetInventory(0):AddItem(8040001);
player:GetInventory(0):AddItem(8060001);
elseif (player.playerWork.tribe == 2) then --Hyur Midlander Female
player:GetInventory(0):AddItem(8040002);
player:GetInventory(0):AddItem(8060002);
elseif (player.playerWork.tribe == 3) then --Hyur Highlander Male
player:GetInventory(0):AddItem(8040003);
player:GetInventory(0):AddItem(8060003);
elseif (player.playerWork.tribe == 4) then --Elezen Wildwood Male
player:GetInventory(0):AddItem(8040004);
player:GetInventory(0):AddItem(8060004);
elseif (player.playerWork.tribe == 5) then --Elezen Wildwood Female
player:GetInventory(0):AddItem(8040006);
player:GetInventory(0):AddItem(8060006);
elseif (player.playerWork.tribe == 6) then --Elezen Duskwight Male
player:GetInventory(0):AddItem(8040005);
player:GetInventory(0):AddItem(8060005);
elseif (player.playerWork.tribe == 7) then --Elezen Duskwight Female
player:GetInventory(0):AddItem(8040007);
player:GetInventory(0):AddItem(8060007);
elseif (player.playerWork.tribe == 8) then --Lalafell Plainsfolk Male
player:GetInventory(0):AddItem(8040008);
player:GetInventory(0):AddItem(8060008);
elseif (player.playerWork.tribe == 9) then --Lalafell Plainsfolk Female
player:GetInventory(0):AddItem(8040010);
player:GetInventory(0):AddItem(8060010);
elseif (player.playerWork.tribe == 10) then --Lalafell Dunesfolk Male
player:GetInventory(0):AddItem(8040009);
player:GetInventory(0):AddItem(8060009);
elseif (player.playerWork.tribe == 11) then --Lalafell Dunesfolk Female
player:GetInventory(0):AddItem(8040011);
player:GetInventory(0):AddItem(8060011);
elseif (player.playerWork.tribe == 12) then --Miqo'te Seekers of the Sun
player:GetInventory(0):AddItem(8040012);
player:GetInventory(0):AddItem(8060012);
elseif (player.playerWork.tribe == 13) then --Miqo'te Seekers of the Moon
player:GetInventory(0):AddItem(8040013);
player:GetInventory(0):AddItem(8060013);
elseif (player.playerWork.tribe == 14) then --Roegadyn Sea Wolf
player:GetInventory(0):AddItem(8040014);
player:GetInventory(0):AddItem(8060014);
elseif (player.playerWork.tribe == 15) then --Roegadyn Hellsguard
player:GetInventory(0):AddItem(8040015);
player:GetInventory(0):AddItem(8060015);
end
player:GetEquipment():SetEquipment({9, 11},{5,6});
end

View file

@ -0,0 +1,8 @@
BattleActionType =
{
None = 0,
AttackPhysical = 1,
AttackMagic = 2,
Heal = 3,
Status = 4
}

View file

@ -14,14 +14,6 @@ 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)
print(command.actorId)
--Are they in active mode?
if (player:GetState() != 2) then
player:SendGameMessage(GetWorldMaster(), 32503, 0x20);
player:endEvent();
return;
end
player.Ability(command.actorId, targetActor); player.Ability(command.actorId, targetActor);
player:endEvent(); player:endEvent();

View file

@ -0,0 +1,5 @@
require("global")
function onEventStarted(player, command, triggerName, arg1, arg2, arg3, arg4, targetActor, arg5, arg6, arg7, arg8)
end

View file

@ -0,0 +1,20 @@
require ("global")
require ("utils")
--[[
AttackWeaponSkill Script
Finds the correct weaponskill subscript to fire when a weaponskill actor is activated.
--]]
local attackMagicHandlers = {
}
function onEventStarted(player, command, triggerName, arg1, arg2, arg3, arg4, targetActor, arg5, arg6, arg7, arg8)
player.Ability(command.actorId, targetActor);
player:endEvent();
end

View file

@ -0,0 +1,5 @@
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,5 @@
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,26 @@
require ("global")
require ("utils")
--[[
AttackWeaponSkill Script
Finds the correct weaponskill subscript to fire when a weaponskill actor is activated.
--]]
function onEventStarted(player, command, triggerName, arg1, arg2, arg3, arg4, targetActor, arg5, arg6, arg7, arg8)
--Are they in active mode?
if (player:GetState() != 2) then
player:SendGameMessage(GetWorldMaster(), 32503, 0x20);
player:endEvent();
return;
end
if not player.aiContainer.IsEngaged() then
player.Engage(targetActor);
end;
player.WeaponSkill(command.actorId, targetActor);
player:endEvent();
end;

View file

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

View file

@ -61,7 +61,7 @@ function onEventStarted(player, equipAbilityWidget, triggername, slot, commandid
player:SwapAbilities(oldSlot, slot + player.charaWork.commandBorder); player:SwapAbilities(oldSlot, slot + player.charaWork.commandBorder);
else else
local tslot = slot + player.charaWork.commandBorder; local tslot = slot + player.charaWork.commandBorder;
player:EquipAbility(player.GetJob(), commandid, tslot, true); player:EquipAbility(player.GetCurrentClassOrJob(), commandid, tslot, true);
end end
--Unequip --Unequip
@ -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:GetJob(); local classId = player:GetClass();
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

@ -145,6 +145,12 @@ function equipItem(player, equipSlot, item)
--Item Equipped message --Item Equipped message
player:SendGameMessage(player, worldMaster, 30601, 0x20, equipSlot+1, item.itemId, item.quality, 0, 0, 1); player:SendGameMessage(player, worldMaster, 30601, 0x20, equipSlot+1, item.itemId, item.quality, 0, 0, 1);
--Load gearset for new class and begin class change
if (classId ~= nil) then
loadGearset(player, classId);
player:DoClassChange(classId);
end
player:GetEquipment():Equip(equipSlot, item); player:GetEquipment():Equip(equipSlot, item);
if (equipSlot == EQUIPSLOT_MAINHAND and gItem:IsNailWeapon() == false) then graphicSlot = GRAPHICSLOT_MAINHAND; if (equipSlot == EQUIPSLOT_MAINHAND and gItem:IsNailWeapon() == false) then graphicSlot = GRAPHICSLOT_MAINHAND;
@ -171,13 +177,6 @@ function equipItem(player, equipSlot, item)
player:GraphicChange(GRAPHICSLOT_R_EAR, item); player:GraphicChange(GRAPHICSLOT_R_EAR, item);
player:GraphicChange(GRAPHICSLOT_L_EAR, item); player:GraphicChange(GRAPHICSLOT_L_EAR, item);
end end
--Load gearset for new class and begin class change
if (classId ~= nil) then
loadGearset(player, classId);
player:DoClassChange(classId);
end
end end
end end

View file

@ -0,0 +1,15 @@
require ("global")
require ("utils")
--[[
AttackWeaponSkill Script
Finds the correct weaponskill subscript to fire when a weaponskill actor is activated.
--]]
function onEventStarted(player, command, triggerName, arg1, arg2, arg3, arg4, targetActor, arg5, arg6, arg7, arg8)
player.Ability(command.actorId, targetActor);
player:endEvent();
end;

View file

@ -0,0 +1,19 @@
require ("global")
require ("utils")
--[[
AttackWeaponSkill Script
Finds the correct weaponskill subscript to fire when a weaponskill actor is activated.
--]]
local attackMagicHandlers = {
}
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,14 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, skill)
return 0;
end;
function onAbilityStart(caster, target, skill)
return 0;
end;
function onAbilityFinish(caster, target, skill, action)
return onStatusAbilityFinish(caster, target, skill, action);
end;

View file

@ -0,0 +1,14 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
return 0;
end;
function onAbilityFinish(caster, target, skill, action)
return onAttackAbilityFinish(caster, target, skill, action);
end;

View file

@ -0,0 +1,14 @@
require("global");
require("Ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
return 0;
end;
function onAbilityFinish(caster, target, ability, action)
return onStatusAbilityFinish(caster, target, ability, action)
end;

View file

@ -0,0 +1,14 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
return 0;
end;
function onAbilityFinish(caster, target, skill, action)
return onAttackAbilityFinish(caster, target, skill, action);
end;

View file

@ -0,0 +1,14 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
return 0;
end;
function onAbilityFinish(caster, target, skill, action)
return onAttackAbilityFinish(caster, target, skill, action);
end;

View file

@ -0,0 +1,14 @@
require("global");
require("ability");
function onAbilityPrepare(caster, target, ability)
return 0;
end;
function onAbilityStart(caster, target, ability)
return 0;
end;
function onAbilityFinish(caster, target, ability, action)
return onHealAbilityFinish(caster, target, ability, action)
end;

View file

@ -0,0 +1,29 @@
require("global");
properties = {
permissions = 0,
parameters = "sss",
description =
[[
Adds target to party
]]
}
function onTrigger(player, argc)
local sender = "[addtoparty] ";
if player then
if player.target then
print("hi")
local id = player.target.actorId
print("hi")
player.currentParty:AddMember(id);
player.target.currentParty = player.currentParty;
print("hi")
else
print(sender.." no target")
end
else
print(sender.." no player");
end;
end;

View file

@ -11,7 +11,7 @@ Equips <commandid> in the first open slot without checking if you can.
} }
function onTrigger(player, argc, commandid) function onTrigger(player, argc, commandid)
local sender = "[givegil] "; local sender = "[eaction] ";
print(commandid); print(commandid);
if name then if name then

View file

@ -21,11 +21,14 @@ function onTrigger(player, argc, slot, wId, eId, vId, cId)
cId = tonumber(cId) or 0; cId = tonumber(cId) or 0;
if player and argc > 0 then if player and argc > 0 then
if argc > 2 then
player:GraphicChange(slot, wId, eId, vId, cId); player:GraphicChange(slot, wId, eId, vId, cId);
player:SendAppearance();
player:SendMessage(messageID, sender, string.format("Changing appearance on slot %u", slot)); player:SendMessage(messageID, sender, string.format("Changing appearance on slot %u", slot));
else
player:GraphicChange(slot, wId);
end
player:SendAppearance();
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,25 @@
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, appearanceId)
local messageID = MESSAGE_TYPE_SYSTEM_ERROR;
local sender = "[setappearance] ";
app = tonumber(appearanceId) or 0;
player:SendMessage(messageID, sender, string.format("appearance %u", app));
if player and player.target then
player.target.ChangeNpcAppearance(app);
player:SendMessage(messageID, sender, string.format("appearance %u", app));
end;
end;

View file

@ -0,0 +1,21 @@
require("global");
properties = {
permissions = 0,
parameters = "sss",
description =
[[
Adds experience <qty> to player or <targetname>.
!giveexp <qty> |
!giveexp <qty> <targetname> |
]],
}
function onTrigger(player, argc, jobId)
local sender = "[setjob] ";
jobId = tonumber(jobId)
if player then
player:SetCurrentJob(jobId);
end;
end;

View file

@ -0,0 +1,18 @@
require("global");
properties = {
permissions = 0,
parameters = "sss",
description =
[[
Adds experience <qty> to player or <targetname>.
!giveexp <qty> |
!giveexp <qty> <targetname> |
]],
}
function onTrigger(player, argc, procid)
local sender = "[giveexp] ";
local pid = tonumber(procid)
player:SetProc(pid, true);
end;

View file

@ -0,0 +1,27 @@
require("global");
properties = {
permissions = 0,
parameters = "sss",
description =
[[
Sets player or <targetname>'s maximum tp to <tp> and heals them to full.
!setmaxtp <tp> |
!setmaxtp <tp> <targetname>
]],
}
function onTrigger(player, argc, tp)
local sender = "[setmaxtp] ";
if player then
tp = tonumber(tp) or 0;
location = INVENTORY_CURRENCY;
player:SetTP(tp);
else
print(sender.."unable to add experience, ensure player name is valid.");
end;
end;

View file

@ -6,7 +6,7 @@ properties = {
description = "Spawns a actor", description = "Spawns a actor",
} }
function onTrigger(player, argc, actorClassId) function onTrigger(player, argc, actorClassId, width, height)
if (actorClassId == nil) then if (actorClassId == nil) then
player:SendMessage(0x20, "", "No actor class id provided."); player:SendMessage(0x20, "", "No actor class id provided.");
@ -24,7 +24,16 @@ function onTrigger(player, argc, actorClassId)
if (actorClassId ~= nil) then if (actorClassId ~= nil) then
zone = player:GetZone(); zone = player:GetZone();
actor = zone:SpawnActor(actorClassId, "test", pos[0], pos[1], pos[2], pos[3]); local w = tonumber(width) or 0;
local h = tonumber(height) or 0;
printf("%f %f %f", x, y, z);
--local x, y, z = player.GetPos();
for i = 0, w do
for j = 0, h do
actor = zone:SpawnActor(actorClassId, "test", pos[0] + (i - (w / 2) * 3), pos[1], pos[2] + (j - (h / 2) * 3), pos[3]);
actor.SetAppearance(1001149)
end
end
end end
if (actor == nil) then if (actor == nil) then

View file

@ -128,7 +128,7 @@ function onTrigger(player, argc, id, level, weight)
end; end;
]] ]]
function onTrigger(player, argc, skillName, level) function onTrigger(player, argc, width, height, blockCount)
local messageId = MESSAGE_TYPE_SYSTEM_ERROR; local messageId = MESSAGE_TYPE_SYSTEM_ERROR;
local sender = "yolo"; local sender = "yolo";
@ -147,24 +147,24 @@ function onTrigger(player, argc, skillName, level)
local z = tonumber(pos[2]); local z = tonumber(pos[2]);
local rot = tonumber(pos[3]); local rot = tonumber(pos[3]);
local zone = pos[4]; local zone = pos[4];
local w = tonumber(width) or 0;
local h = tonumber(height) 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 = 1, 1 do for i = 0, blockCount do
for i = 0, w do
local actor = player.GetZone().SpawnActor(2104001, 'ass', x, y, z, rot, 0, 0, true ); 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);
if player.currentContentGroup then actor.ChangeNpcAppearance(1001149)
player.currentContentGroup:AddMember(actor.actorId) actor.SetLevel(50);
end end
--actor.FollowTarget(player, 3.2); --actor.FollowTarget(player, 3.2);
end; end
x = x + 500
end
return; return;
end end
level = tonumber(level) or 1;
if player then
player.SendMessage(messageId, sender, string.format("name %s | cost %d | level %u", skillName, calculateCommandCost(player, skillName, level), level));
end;
end; end;
function calculateCommandCost(player, skillName, level) function calculateCommandCost(player, skillName, level)

View file

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

View file

@ -0,0 +1,14 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
function onMagicFinish(caster, target, spell, action)
magic.onMagicFinish(caster, target, spell, action)
end;

View file

@ -0,0 +1,27 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
--Increased damage and conversion to single target
function onCombo(caster, target, spell)
spell.aoeType = 0;
spell.potency = spell.potency * 1.5;
end;
function onMagicFinish(caster, target, spell, action)
local damage = math.random(10, 100);
--Dispels an effect on each target.
local effects = target.statusEffects.GetStatusEffectsByFlag(16); --lose on dispel
if effects != nil then
target.statusEffects.RemoveStatusEffect(effects[0]);
end;
return damage;
end;

View file

@ -0,0 +1,14 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
function onMagicFinish(caster, target, spell, action)
magic.onBuffMagicFinish(caster, target, spell, action)
end;

View file

@ -10,15 +10,5 @@ function onMagicStart(caster, target, spell)
end; end;
function onMagicFinish(caster, target, spell, action) function onMagicFinish(caster, target, spell, action)
local damage = math.random(10, 100); return magic.onMagicFinish(caster, target, spell, action)
-- todo: populate a global script with statuses and modifiers
action.worldMasterTextId = 0x765D;
-- todo: populate a global script with statuses and modifiers
-- magic.HandleAttackMagic(caster, target, spell, action)
-- action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
return damage;
end; end;

View file

@ -9,13 +9,5 @@ function onMagicStart(caster, target, spell)
end; end;
function onMagicFinish(caster, target, spell, action) function onMagicFinish(caster, target, spell, action)
local damage = math.random(10, 100); magic.onMagicFinish(caster, target, spell, action)
action.worldMasterTextId = 0x765D;
-- todo: populate a global script with statuses and modifiers
-- magic.HandleAttackMagic(caster, target, spell, action)
action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
return damage;
end; end;

View file

@ -9,16 +9,11 @@ function onMagicStart(caster, target, spell)
return 0; return 0;
end; end;
function onMagicFinish(caster, target, spell, action) --Increased damage with lesser current hp
local damage = math.random(1000, 2500); function onCombo(caster, target, spell)
-- todo: populate a global script with statuses and modifiers end;
action.worldMasterTextId = 0x765D;
function onMagicFinish(caster, target, spell, action)
-- todo: populate a global script with statuses and modifiers magic.onMagicFinish(caster, target, spell, action)
-- magic.HandleAttackMagic(caster, target, spell, action)
-- action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
return damage;
end; end;

View file

@ -0,0 +1,14 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
function onMagicFinish(caster, target, spell, action)
magic.onCureMagicFinish(caster, target, spell, action)
end;

View file

@ -0,0 +1,15 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
--http://forum.square-enix.com/ffxiv/threads/41900-White-Mage-A-Guide read
function onMagicFinish(caster, target, spell, action)
magic.onCureMagicFinish(caster, target, spell, action)
end;

View file

@ -0,0 +1,14 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
function onMagicFinish(caster, target, spell, action)
magic.onCureMagicFinish(caster, target, spell, action)
end;

View file

@ -0,0 +1,14 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
function onMagicFinish(caster, target, spell, action)
magic.onCureMagicFinish(caster, target, spell, action)
end;

View file

@ -9,16 +9,11 @@ function onMagicStart(caster, target, spell)
return 0; return 0;
end; end;
function onMagicFinish(caster, target, spell, action) --Increased Damage and reduced recast time in place of stun
local damage = math.random(10, 100); function onCombo(caster, target, spell)
spell.castTimeMs = spell.castTimeMs / 2;
-- todo: populate a global script with statuses and modifiers end;
action.worldMasterTextId = 0x765D;
function onMagicFinish(caster, target, spell, action)
-- todo: populate a global script with statuses and modifiers magic.onMagicFinish(caster, target, spell, action)
-- magic.HandleAttackMagic(caster, target, spell, action)
-- action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
return damage;
end; end;

View file

@ -9,16 +9,11 @@ function onMagicStart(caster, target, spell)
return 0; return 0;
end; end;
function onMagicFinish(caster, target, spell, action) --Increased critical damage
local damage = math.random(1000, 2500); function onCombo(caster, target, spell)
spell.castTimeMs = spell.castTimeMs / 2;
-- todo: populate a global script with statuses and modifiers end;
action.worldMasterTextId = 0x765D;
function onMagicFinish(caster, target, spell, action)
-- todo: populate a global script with statuses and modifiers magic.onMagicFinish(caster, target, spell, action)
-- magic.HandleAttackMagic(caster, target, spell, action)
-- action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
return damage;
end; end;

View file

@ -10,18 +10,5 @@ function onMagicStart(caster, target, spell)
end; end;
function onMagicFinish(caster, target, spell, action) function onMagicFinish(caster, target, spell, action)
local damage = math.random(10, 100); magic.onMagicFinish(caster, target, spell, action)
-- todo: populate a global script with statuses and modifiers
action.worldMasterTextId = 0x765D;
-- todo: populate a global script with statuses and modifiers
-- magic.HandleAttackMagic(caster, target, spell, action)
-- action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
if target.hateContainer then
target.hateContainer.UpdateHate(caster, damage);
end;
return damage;
end; end;

View file

@ -10,15 +10,7 @@ function onMagicStart(caster, target, spell)
end; end;
function onMagicFinish(caster, target, spell, action) function onMagicFinish(caster, target, spell, action)
local damage = math.random(1000, 2500); --increase potency based on proximity to target
-- todo: populate a global script with statuses and modifiers magic.onMagicFinish(caster, target, spell, action)
action.worldMasterTextId = 0x765D;
-- todo: populate a global script with statuses and modifiers
-- magic.HandleAttackMagic(caster, target, spell, action)
-- action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
return damage;
end; end;

View file

@ -10,15 +10,5 @@ function onMagicStart(caster, target, spell)
end; end;
function onMagicFinish(caster, target, spell, action) function onMagicFinish(caster, target, spell, action)
local damage = math.random(10, 100); magic.onMagicFinish(caster, target, spell, action)
-- todo: populate a global script with statuses and modifiers
action.worldMasterTextId = 0x765D;
-- todo: populate a global script with statuses and modifiers
-- magic.HandleAttackMagic(caster, target, spell, action)
-- action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
return damage;
end; end;

View file

@ -10,15 +10,5 @@ function onMagicStart(caster, target, spell)
end; end;
function onMagicFinish(caster, target, spell, action) function onMagicFinish(caster, target, spell, action)
local damage = math.random(1000, 2500); magic.onMagicFinish(caster, target, spell, action)
-- todo: populate a global script with statuses and modifiers
action.worldMasterTextId = 0x765D;
-- todo: populate a global script with statuses and modifiers
-- magic.HandleAttackMagic(caster, target, spell, action)
-- action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
return damage;
end; end;

View file

@ -0,0 +1,21 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
function onMagicFinish(caster, target, spell, action)
spell.statusId = 228011;
spell.statusDuration = 25;
spell.statusChance = 1.0;
magic.onCureMagicFinish(caster, target, spell, action)
if caster != target then
action.AddHealAction(caster.actorId, (action.amount / 2));
end
end;

View file

@ -0,0 +1,14 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
function onMagicFinish(caster, target, spell, action)
magic.onBuffMagicFinish(caster, target, spell, action)
end;

View file

@ -0,0 +1,14 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
function onMagicFinish(caster, target, spell, action)
magic.onBuffMagicFinish(caster, target, spell, action)
end;

View file

@ -0,0 +1,14 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
function onMagicFinish(caster, target, spell, action)
magic.onStatusMagicFinish(caster, target, spell, action)
end;

View file

@ -0,0 +1,14 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
function onMagicFinish(caster, target, spell, action)
magic.onMagicFinish(caster, target, spell, action)
end;

View file

@ -0,0 +1,14 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
function onMagicFinish(caster, target, spell, action)
magic.onStatusMagicFinish(caster, target, spell, action)
end;

View file

@ -0,0 +1,14 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
function onMagicFinish(caster, target, spell, action)
magic.onStatusMagicFinish(caster, target, spell, action)
end;

View file

@ -0,0 +1,14 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
function onMagicFinish(caster, target, spell, action)
magic.onMagicFinish(caster, target, spell, action)
end;

View file

@ -0,0 +1,20 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
--Increased damage and conversion to single target
function onCombo(caster, target, spell)
spell.aoeType = 0;
spell.potency = spell.potency * 1.5;
end;
function onMagicFinish(caster, target, spell, action)
magic.onMagicFinish(caster, target, spell, action)
end;

View file

@ -0,0 +1,14 @@
require("global");
require("magic");
function onMagicPrepare(caster, target, spell)
return 0;
end;
function onMagicStart(caster, target, spell)
return 0;
end;
function onMagicFinish(caster, target, spell, action)
magic.onStatusMagicFinish(caster, target, spell, action)
end;

View file

@ -8,14 +8,11 @@ function onMagicStart(caster, target, spell)
return 0; return 0;
end; end;
function onMagicFinish(caster, target, spell, action) --Increased critical damage
local damage = math.random(10, 100); function onCombo(caster, target, spell)
spell.critDamageModifier = 1.5;
action.worldMasterTextId = 0x765D; end;
-- todo: populate a global script with statuses and modifiers function onMagicFinish(caster, target, spell, action)
-- magic.HandleAttackMagic(caster, target, spell, action) magic.onMagicFinish(caster, target, spell, action)
action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
return damage;
end; end;

View file

@ -8,14 +8,13 @@ function onMagicStart(caster, target, spell)
return 0; return 0;
end; end;
function onMagicFinish(caster, target, spell, action) --Increased Damage and reduced recast time in place of stun
local damage = math.random(10, 100); function onCombo(caster, target, spell)
spell.statusChance = 0;
action.worldMasterTextId = 0x765D; spell.basePotency = spell.basePotency * 1.5;
spell.recastTimeMs = spell.recastTimeMs / 2;
-- todo: populate a global script with statuses and modifiers end;
-- magic.HandleAttackMagic(caster, target, spell, action)
action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636); function onMagicFinish(caster, target, spell, action)
magic.onMagicFinish(caster, target, spell, action)
return damage;
end; end;

View file

@ -9,13 +9,5 @@ function onMagicStart(caster, target, spell)
end; end;
function onMagicFinish(caster, target, spell, action) function onMagicFinish(caster, target, spell, action)
local damage = math.random(10, 100); magic.onMagicFinish(caster, target, spell, action)
action.worldMasterTextId = 0x765D;
-- todo: populate a global script with statuses and modifiers
-- magic.HandleAttackMagic(caster, target, spell, action)
action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
return damage;
end; end;

View file

@ -0,0 +1,19 @@
require("global");
require("weaponskill");
function onSkillPrepare(caster, target, skill)
return 0;
end;
function onSkillStart(caster, target, skill)
return 0;
end;
--Chance to inflict slow
function onCombo(caster, target, skill)
skill.statusChance = 0.50;
end;
function onSkillFinish(caster, target, skill, action)
return weaponskill.onSkillFinish(caster, target, skill, action);
end;

View file

@ -0,0 +1,20 @@
require("global");
require("weaponskill");
function onSkillPrepare(caster, target, skill)
return 0;
end;
function onSkillStart(caster, target, skill)
return 0;
end;
--Inflicts additional damage when bleed ends
--Note for later, going to set bleed tier to 2, when bleed get scripted, check if tier is 2 and add additional damage at the end
function onCombo(caster, target, skill)
skill.statusTier = 2;
end;
function onSkillFinish(caster, target, skill, action)
return weaponskill.onSkillFinish(caster, target, skill, action);
end;

View file

@ -0,0 +1,19 @@
require("global");
require("weaponskill");
function onSkillPrepare(caster, target, skill)
return 0;
end;
function onSkillStart(caster, target, skill)
return 0;
end;
--Increased damage
function onPositional(caster, target, skill)
skill.basePotency = skill.basePotency * 1.5;
end;
function onSkillFinish(caster, target, skill, action)
return weaponskill.onSkillFinish(caster, target, skill, action);
end;

View file

@ -1,4 +1,5 @@
require("global"); require("global");
require("weaponskill");
function onSkillPrepare(caster, target, skill) function onSkillPrepare(caster, target, skill)
return 0; return 0;
@ -8,19 +9,11 @@ function onSkillStart(caster, target, skill)
return 0; return 0;
end; end;
--Increased crit hit rating
function onCombo(caster, target, skill)
skill.critRateModifier = 1.5;
end;
function onSkillFinish(caster, target, skill, action) function onSkillFinish(caster, target, skill, action)
local damage = math.random(100, 200); return weaponskill.onSkillFinish(caster, target, skill, action);
-- todo: populate a global script with statuses and modifiers
action.worldMasterTextId = 0x765D;
-- todo: populate a global script with statuses and modifiers
-- magic.HandleAttackMagic(caster, target, spell, action)
-- action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
--action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
if target.hateContainer then
target.hateContainer.UpdateHate(caster, damage);
end;
return damage;
end; end;

View file

@ -0,0 +1,25 @@
require("global");
require("weaponskill");
function onSkillPrepare(caster, target, skill)
return 0;
end;
function onSkillStart(caster, target, skill)
return 0;
end;
--Chance to inflict blind on flank
function onPositional(caster, target, skill)
skill.statusChance = 0.50;
skill.statusDuration = 10;
end;
function onCombo(caster, target, skill)
skill.basePotency = skill.basePotency * 1.5;
end;
function onSkillFinish(caster, target, skill, action)
return weaponskill.onSkillFinish(caster, target, skill, action);
end;

View file

@ -1,4 +1,5 @@
require("global"); require("global");
require("weaponskill");
function onSkillPrepare(caster, target, skill) function onSkillPrepare(caster, target, skill)
return 0; return 0;
@ -9,18 +10,5 @@ function onSkillStart(caster, target, skill)
end; end;
function onSkillFinish(caster, target, skill, action) function onSkillFinish(caster, target, skill, action)
local damage = math.random(100, 200); return weaponskill.onSkillFinish(caster, target, skill, action);
-- todo: populate a global script with statuses and modifiers
action.worldMasterTextId = 0x765D;
-- todo: populate a global script with statuses and modifiers
-- magic.HandleAttackMagic(caster, target, spell, action)
-- action.effectId = bit32.bxor(0x8000000, spell.effectAnimation, 15636);
action.effectId = bit32.bxor(0x8000000, skill.effectAnimation, 15636);
if target.hateContainer then
target.hateContainer.UpdateHate(caster, damage);
end;
return damage;
end; end;

View file

@ -0,0 +1,23 @@
require("global");
require("weaponskill");
function onSkillPrepare(caster, target, skill)
return 0;
end;
function onSkillStart(caster, target, skill)
return 0;
end;
--Dispel
--Does dispel have a text id?
function onCombo(caster, target, skill)
local effects = target.statusEffects.GetStatusEffectsByFlag(16); --lose on dispel
if effects != nil then
target.statusEffects.RemoveStatusEffect(effects[0]);
end;
end;
function onSkillFinish(caster, target, skill, action)
return weaponskill.onSkillFinish(caster, target, skill, action);
end;

View file

@ -0,0 +1,19 @@
require("global");
require("weaponskill");
function onSkillPrepare(caster, target, skill)
return 0;
end;
function onSkillStart(caster, target, skill)
return 0;
end;
--Increased paralysis duration
function onCombo(caster, target, skill)
skill.statusDuration = skill.statusDuration * 2;
end;
function onSkillFinish(caster, target, skill, action)
return weaponskill.onSkillFinish(caster, target, skill, action);
end;

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