/* =========================================================================== Copyright (C) 2015-2019 Project Meteor Dev Team This file is part of Project Meteor Server. Project Meteor Server is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Project Meteor Server is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with Project Meteor Server. If not, see . =========================================================================== */ using Meteor.Common; using Meteor.Map.actors.chara.player; using Meteor.Map.actors.group; using Meteor.Map.Actors.Chara; using Meteor.Map.dataobjects; using Meteor.Map.packets.send.actor; using Meteor.Map.packets.send.actor.inventory; using Meteor.Map.utils; using Meteor.Map.actors.chara.ai; using System; using System.Collections.Generic; using Meteor.Map.actors.chara; using Meteor.Map.packets.send.actor.battle; using Meteor.Map.actors.chara.ai.state; using Meteor.Map.actors.chara.ai.utils; using Meteor.Map.actors.chara.npc; namespace Meteor.Map.Actors { /// Which Character types am I friendly with enum CharacterTargetingAllegiance { /// Friendly to BattleNpcs BattleNpcs, /// Friendly to Players Player } enum DamageTakenType { None, Attack, Magic, Weaponskill, Ability } class Character : Actor { public const int CLASSID_PUG = 2; public const int CLASSID_GLA = 3; public const int CLASSID_MRD = 4; public const int CLASSID_ARC = 7; public const int CLASSID_LNC = 8; public const int CLASSID_THM = 22; public const int CLASSID_CNJ = 23; public const int CLASSID_CRP = 29; public const int CLASSID_BSM = 30; public const int CLASSID_ARM = 31; public const int CLASSID_GSM = 32; public const int CLASSID_LTW = 33; public const int CLASSID_WVR = 34; public const int CLASSID_ALC = 35; public const int CLASSID_CUL = 36; public const int CLASSID_MIN = 39; public const int CLASSID_BTN = 40; public const int CLASSID_FSH = 41; public const int SIZE = 0; public const int COLORINFO = 1; public const int FACEINFO = 2; public const int HIGHLIGHT_HAIR = 3; public const int VOICE = 4; public const int MAINHAND = 5; public const int OFFHAND = 6; public const int SPMAINHAND = 7; public const int SPOFFHAND = 8; public const int THROWING = 9; public const int PACK = 10; public const int POUCH = 11; public const int HEADGEAR = 12; public const int BODYGEAR = 13; public const int LEGSGEAR = 14; public const int HANDSGEAR = 15; public const int FEETGEAR = 16; public const int WAISTGEAR = 17; public const int NECKGEAR = 18; public const int L_EAR = 19; public const int R_EAR = 20; public const int R_WRIST = 21; public const int L_WRIST = 22; public const int R_RINGFINGER = 23; public const int L_RINGFINGER = 24; public const int R_INDEXFINGER = 25; public const int L_INDEXFINGER = 26; public const int UNKNOWN = 27; public bool isStatic = false; public bool isMovingToSpawn = false; public bool isAutoAttackEnabled = true; public uint modelId; public uint[] appearanceIds = new uint[28]; public uint animationId = 0; public uint currentTarget = Actor.INVALID_ACTORID; public uint currentLockedTarget = Actor.INVALID_ACTORID; public uint currentActorIcon = 0; public Work work = new Work(); public CharaWork charaWork = new CharaWork(); public Group currentParty = null; public ContentGroup currentContentGroup = null; //public DateTime lastAiUpdate; public AIContainer aiContainer; public StatusEffectContainer statusEffects; public CharacterTargetingAllegiance allegiance; public Pet pet; private Dictionary modifiers = new Dictionary(); protected ushort hpBase, hpMaxBase, mpBase, mpMaxBase, tpBase; protected BattleTemp baseStats = new BattleTemp(); public ushort currentJob; public ushort newMainState; public float spawnX, spawnY, spawnZ; //I needed some values I could reuse for random stuff, delete later public int extraInt; public uint extraUint; public float extraFloat; protected Dictionary tempVars = new Dictionary(); //Inventory protected Dictionary itemPackages = new Dictionary(); protected ReferencedItemPackage equipment; public Character(uint actorID) : base(actorID) { //Init timer array to "notimer" for (int i = 0; i < charaWork.statusShownTime.Length; i++) charaWork.statusShownTime[i] = 0; this.statusEffects = new StatusEffectContainer(this); // todo: move this somewhere more appropriate // todo: base this on equip and shit SetMod((uint)Modifier.AttackRange, 3); SetMod((uint)Modifier.Delay, (Program.Random.Next(30, 60) * 100)); SetMod((uint)Modifier.MovementSpeed, (uint)moveSpeeds[2]); spawnX = positionX; spawnY = positionY; spawnZ = positionZ; } public SubPacket CreateAppearancePacket() { SetActorAppearancePacket setappearance = new SetActorAppearancePacket(modelId, appearanceIds); return setappearance.BuildPacket(actorId); } public SubPacket CreateInitStatusPacket() { return (SetActorStatusAllPacket.BuildPacket(actorId, charaWork.status)); } public SubPacket CreateSetActorIconPacket() { return SetActorIconPacket.BuildPacket(actorId, currentActorIcon); } public SubPacket CreateSubStatePacket() { return SetActorSubStatePacket.BuildPacket(actorId, currentSubState); } public void SetQuestGraphic(Player player, int graphicNum) { player.QueuePacket(SetActorQuestGraphicPacket.BuildPacket(actorId, graphicNum)); } public void SetCurrentContentGroup(ContentGroup group) { if (group != null) charaWork.currentContentGroup = group.GetTypeId(); else charaWork.currentContentGroup = 0; currentContentGroup = group; ActorPropertyPacketUtil propPacketUtil = new ActorPropertyPacketUtil("charaWork/currentContentGroup", this); propPacketUtil.AddProperty("charaWork.currentContentGroup"); zone.BroadcastPacketsAroundActor(this, propPacketUtil.Done()); } //This logic isn't correct, order of GetStatusEffects() is not necessarily the same as the actual effects in game. Also sending every time at once isn't needed public List GetActorStatusPackets() { var propPacketUtil = new ActorPropertyPacketUtil("charaWork/status", this); var i = 0; foreach (var effect in statusEffects.GetStatusEffects()) { if (!effect.GetHidden()) { propPacketUtil.AddProperty(String.Format("charaWork.statusShownTime[{0}]", i)); propPacketUtil.AddProperty(String.Format("charaWork.statusShownTime[{0}]", i)); i++; } } return propPacketUtil.Done(); } public void PlayAnimation(uint animId, bool onlySelf = false) { if (onlySelf) { if (this is Player) ((Player)this).QueuePacket(PlayAnimationOnActorPacket.BuildPacket(actorId, animId)); } else zone.BroadcastPacketAroundActor(this, PlayAnimationOnActorPacket.BuildPacket(actorId, animId)); } public void DoBattleAction(ushort commandId, uint animationId) { zone.BroadcastPacketAroundActor(this, CommandResultX00Packet.BuildPacket(actorId, animationId, commandId)); } public void DoBattleAction(ushort commandId, uint animationId, CommandResult result) { zone.BroadcastPacketAroundActor(this, CommandResultX01Packet.BuildPacket(actorId, animationId, commandId, result)); } public void DoBattleAction(ushort commandId, uint animationId, CommandResult[] results) { int currentIndex = 0; while (true) { if (results.Length - currentIndex >= 10) zone.BroadcastPacketAroundActor(this, CommandResultX18Packet.BuildPacket(actorId, animationId, commandId, results, ref currentIndex)); else if (results.Length - currentIndex > 1) zone.BroadcastPacketAroundActor(this, CommandResultX10Packet.BuildPacket(actorId, animationId, commandId, results, ref currentIndex)); else if (results.Length - currentIndex == 1) { zone.BroadcastPacketAroundActor(this, CommandResultX01Packet.BuildPacket(actorId, animationId, commandId, results[currentIndex])); currentIndex++; } else break; } } public void DoBattleAction(ushort commandId, uint animationId, List results) { int currentIndex = 0; while (true) { if (results.Count - currentIndex >= 10) zone.BroadcastPacketAroundActor(this, CommandResultX18Packet.BuildPacket(actorId, animationId, commandId, results, ref currentIndex)); else if (results.Count - currentIndex > 1) zone.BroadcastPacketAroundActor(this, CommandResultX10Packet.BuildPacket(actorId, animationId, commandId, results, ref currentIndex)); else if (results.Count - currentIndex == 1) { zone.BroadcastPacketAroundActor(this, CommandResultX01Packet.BuildPacket(actorId, animationId, commandId, results[currentIndex])); currentIndex++; } else break; //Sending multiple packets at once causes some issues. Setting any combination of these to zero changes what breaks //animationId = 0; //If more than one packet is sent out, only send the animation once to avoid double playing. //commandId = 0; //sourceActorId = 0; } } #region ai stuff public void PathTo(float x, float y, float z, float stepSize = 0.70f, int maxPath = 40, float polyRadius = 0.0f) { if (aiContainer != null && aiContainer.pathFind != null) aiContainer.pathFind.PreparePath(x, y, z, stepSize, maxPath, polyRadius); } public void FollowTarget(Actor target, float stepSize = 1.2f, int maxPath = 25, float radius = 0.0f) { if (target != null) PathTo(target.positionX, target.positionY, target.positionZ, stepSize, maxPath, radius); } public double GetMod(Modifier modifier) { return GetMod((uint)modifier); } public double GetMod(uint modifier) { double res; if (modifiers.TryGetValue((Modifier)modifier, out res)) return res; return 0; } public void SetMod(uint modifier, double val) { if (modifiers.ContainsKey((Modifier)modifier)) modifiers[(Modifier)modifier] = val; else modifiers.Add((Modifier)modifier, val); if (modifier >= 3 && modifier <= 35) updateFlags |= ActorUpdateFlags.Stats; } public void AddMod(Modifier modifier, double val) { AddMod((uint)modifier, val); } public void AddMod(uint modifier, double val) { double newVal = GetMod(modifier) + val; SetMod(modifier, newVal); } public void SubtractMod(Modifier modifier, double val) { AddMod((uint)modifier, val); } public void SubtractMod(uint modifier, double val) { double newVal = GetMod(modifier) - val; SetMod(modifier, newVal); } public void MultiplyMod(Modifier modifier, double val) { MultiplyMod((uint)modifier, val); } public void MultiplyMod(uint modifier, double val) { double newVal = GetMod(modifier) * val; SetMod(modifier, newVal); } public void DivideMod(Modifier modifier, double val) { DivideMod((uint)modifier, val); } public void DivideMod(uint modifier, double val) { double newVal = GetMod(modifier) / val; SetMod(modifier, newVal); } public virtual void OnPath(Vector3 point) { //lua.LuaEngine.CallLuaBattleFunction(this, "onPath", this, point); updateFlags |= ActorUpdateFlags.Position; this.isAtSpawn = false; } public override void Update(DateTime tick) { } public override void PostUpdate(DateTime tick, List packets = null) { if (updateFlags != ActorUpdateFlags.None) { packets = packets ?? new List(); if ((updateFlags & ActorUpdateFlags.Appearance) != 0) { packets.Add(new SetActorAppearancePacket(modelId, appearanceIds).BuildPacket(actorId)); } if ((updateFlags & ActorUpdateFlags.State) != 0) { packets.Add(SetActorStatePacket.BuildPacket(actorId, currentMainState, 0x0)); packets.Add(CommandResultX00Packet.BuildPacket(actorId, 0x72000062, 0)); packets.Add(CommandResultX01Packet.BuildPacket(actorId, 0x7C000062, 21001, new CommandResult(actorId, 0, 1))); updateFlags &= ~ActorUpdateFlags.State; //DoBattleAction(21001, 0x7C000062, new BattleAction(this.actorId, 0, 1, 0, 0, 1)); //Attack Mode } if ((updateFlags & ActorUpdateFlags.SubState) != 0) { packets.Add(SetActorSubStatePacket.BuildPacket(actorId, currentSubState)); //packets.Add(CommandResultX00Packet.BuildPacket(actorId, 0x72000062, 0)); //packets.Add(CommandResultX01Packet.BuildPacket(actorId, 0x7C000062, 21001, new CommandResult(actorId, 0, 1))); updateFlags &= ~ActorUpdateFlags.SubState; //DoBattleAction(21001, 0x7C000062, new BattleAction(this.actorId, 0, 1, 0, 0, 1)); //Attack Mode } if ((updateFlags & ActorUpdateFlags.Status) != 0) { List statusPackets = statusEffects.GetStatusPackets(); packets.AddRange(statusPackets); statusPackets.Clear(); updateFlags &= ~ActorUpdateFlags.Status; } if ((updateFlags & ActorUpdateFlags.StatusTime) != 0) { packets.AddRange(statusEffects.GetStatusTimerPackets()); statusEffects.ResetPropPacketUtil(); updateFlags &= ~ActorUpdateFlags.StatusTime; } if ((updateFlags & ActorUpdateFlags.HpTpMp) != 0) { var propPacketUtil = new ActorPropertyPacketUtil("charaWork/stateAtQuicklyForAll", this); propPacketUtil.AddProperty("charaWork.parameterSave.hp[0]"); propPacketUtil.AddProperty("charaWork.parameterSave.hpMax[0]"); propPacketUtil.AddProperty("charaWork.parameterSave.mp"); propPacketUtil.AddProperty("charaWork.parameterSave.mpMax"); propPacketUtil.AddProperty("charaWork.parameterTemp.tp"); packets.AddRange(propPacketUtil.Done()); } base.PostUpdate(tick, packets); } } public virtual bool IsValidTarget(Character target, ValidTarget validTarget) { return !target.isStatic; } public virtual bool CanAttack() { return true; } public virtual bool CanUse(Character target, BattleCommand skill, CommandResult error = null) { return false; } public virtual uint GetAttackDelayMs() { return (uint)GetMod((uint)Modifier.Delay); } public virtual uint GetAttackRange() { return (uint)(GetMod((uint)Modifier.AttackRange) == 0 ? 3 : GetMod((uint)Modifier.AttackRange)); } public virtual bool Engage(uint targid = 0, ushort newMainState = 0xFFFF) { // todo: attack the things /*if (newMainState != 0xFFFF) { currentMainState = newMainState;// this.newMainState = newMainState; updateFlags |= ActorUpdateFlags.State; } else*/ if (aiContainer.CanChangeState()) { if (targid == 0) { if (currentTarget != Actor.INVALID_ACTORID) targid = currentTarget; else if (currentLockedTarget != Actor.INVALID_ACTORID) targid = currentLockedTarget; } //if (targid != 0) { aiContainer.Engage(zone.FindActorInArea(targid)); } } return false; } public virtual bool Engage(Character target) { aiContainer.Engage(target); return false; } public virtual bool Disengage(ushort newMainState = 0xFFFF) { /*if (newMainState != 0xFFFF) { currentMainState = newMainState;// this.newMainState = newMainState; updateFlags |= ActorUpdateFlags.State; } else*/ if (IsEngaged()) { aiContainer.Disengage(); return true; } return false; } public virtual void Cast(uint spellId, uint targetId = 0) { if (aiContainer.CanChangeState()) aiContainer.Cast(zone.FindActorInArea(targetId == 0 ? currentTarget : targetId), spellId); } public virtual void Ability(uint abilityId, uint targetId = 0) { if (aiContainer.CanChangeState()) aiContainer.Ability(zone.FindActorInArea(targetId == 0 ? currentTarget : targetId), abilityId); } public virtual void WeaponSkill(uint skillId, uint targetId = 0) { if (aiContainer.CanChangeState()) aiContainer.WeaponSkill(zone.FindActorInArea(targetId == 0 ? currentTarget : targetId), skillId); } public virtual void Spawn(DateTime tick) { aiContainer.Reset(); // todo: reset hp/mp/tp etc here ChangeState(SetActorStatePacket.MAIN_STATE_PASSIVE); SetHP((uint) GetMaxHP()); SetMP((uint) GetMaxMP()); RecalculateStats(); } //AdditionalActions is the list of actions that EXP/Chain messages are added to public virtual void Die(DateTime tick, CommandResultContainer actionContainer = null) { // todo: actual despawn timer aiContainer.InternalDie(tick, 10); ChangeSpeed(0.0f, 0.0f, 0.0f, 0.0f); } public virtual void Despawn(DateTime tick) { } public bool IsDead() { return !IsAlive(); } public bool IsAlive() { return !aiContainer.IsDead();// && GetHP() > 0; } public short GetHP() { // todo: return charaWork.parameterSave.hp[0]; } public short GetMaxHP() { return charaWork.parameterSave.hpMax[0]; } public short GetMP() { return charaWork.parameterSave.mp; } public ushort GetTP() { return tpBase; } public short GetMaxMP() { return charaWork.parameterSave.mpMax; } public byte GetMPP() { return (byte)((charaWork.parameterSave.mp / charaWork.parameterSave.mpMax) * 100); } public byte GetTPP() { return (byte)((tpBase / 3000) * 100); } public byte GetHPP() { return (byte)(charaWork.parameterSave.hp[0] == 0 ? 0 : (charaWork.parameterSave.hp[0] / (float) charaWork.parameterSave.hpMax[0]) * 100); } public void SetHP(uint hp) { charaWork.parameterSave.hp[0] = (short)hp; if (hp > charaWork.parameterSave.hpMax[0]) SetMaxHP(hp); updateFlags |= ActorUpdateFlags.HpTpMp; } public void SetMaxHP(uint hp) { charaWork.parameterSave.hpMax[0] = (short)hp; updateFlags |= ActorUpdateFlags.HpTpMp; } public void SetMP(uint mp) { charaWork.parameterSave.mp = (short)mp; if (mp > charaWork.parameterSave.mpMax) SetMaxMP(mp); updateFlags |= ActorUpdateFlags.HpTpMp; } public void SetMaxMP(uint mp) { charaWork.parameterSave.mp = (short)mp; updateFlags |= ActorUpdateFlags.HpTpMp; } // todo: the following functions are virtuals since we want to check hidden item bonuses etc on player for certain conditions public virtual void AddHP(int hp, CommandResultContainer resultContainer = null) { // dont wanna die ded, don't want to send update if hp isn't actually changed if (IsAlive() && hp != 0) { // todo: +/- hp and die // todo: battlenpcs probably have way more hp? var addHp = charaWork.parameterSave.hp[0] + hp; addHp = addHp.Clamp((short)GetMod((uint)Modifier.MinimumHpLock), charaWork.parameterSave.hpMax[0]); charaWork.parameterSave.hp[0] = (short)addHp; updateFlags |= ActorUpdateFlags.HpTpMp; if (charaWork.parameterSave.hp[0] < 1) Die(Program.Tick, resultContainer); } } public short GetClass() { return charaWork.parameterSave.state_mainSkill[0]; } public short GetLevel() { return charaWork.parameterSave.state_mainSkillLevel; } public void AddMP(int mp) { if (IsAlive() && mp != 0) { charaWork.parameterSave.mp = (short)(charaWork.parameterSave.mp + mp).Clamp(ushort.MinValue, charaWork.parameterSave.mpMax); // todo: check hidden effects and shit updateFlags |= ActorUpdateFlags.HpTpMp; } } public void AddTP(int tp) { if (IsAlive() && tp != 0) { var addTp = charaWork.parameterTemp.tp + tp; addTp = addTp.Clamp((int) GetMod(Modifier.MinimumTpLock), 3000); charaWork.parameterTemp.tp = (short) addTp; tpBase = (ushort)charaWork.parameterTemp.tp; updateFlags |= ActorUpdateFlags.HpTpMp; if (tpBase >= 1000) lua.LuaEngine.GetInstance().OnSignal("tpOver1000"); } } public void DelHP(int hp, CommandResultContainer resultContainer = null) { AddHP((short)-hp, resultContainer); } public void DelMP(int mp) { AddMP(-mp); } public void DelTP(int tp) { AddTP(-tp); } virtual public void CalculateBaseStats() { // todo: apply mods and shit here, get race/level/job and shit uint hpMod = (uint) GetMod((uint)Modifier.Hp); if (hpMod != 0) { SetMaxHP(hpMod); uint hpp = (uint) GetMod((uint) Modifier.HpPercent); uint hp = hpMod; if(hpp != 0) { hp = (uint) Math.Ceiling(((float)hpp / 100.0f) * hpMod); } SetHP(hp); } uint mpMod = (uint)GetMod((uint)Modifier.Mp); if (mpMod != 0) { SetMaxMP(mpMod); uint mpp = (uint)GetMod((uint)Modifier.MpPercent); uint mp = mpMod; if (mpp != 0) { mp = (uint)Math.Ceiling(((float)mpp / 100.0f) * mpMod); } SetMP(mp); } // todo: recalculate stats and crap updateFlags |= ActorUpdateFlags.HpTpMp; SetMod((uint)Modifier.HitCount, 1); } public void RecalculateStats() { //CalculateBaseStats(); } public void SetStat(uint statId, int val) { charaWork.battleTemp.generalParameter[statId] = (short)val; } public short GetStat(uint statId) { return charaWork.battleTemp.generalParameter[statId]; } public virtual float GetSpeed() { // todo: for battlenpc/player calculate speed return (float) GetMod((uint)Modifier.MovementSpeed); } public virtual void OnAttack(State state, CommandResult action, ref CommandResult error) { var target = state.GetTarget(); // todo: change animation based on equipped weapon // todo: get hitrate and shit, handle protect effect and whatever if (BattleUtils.TryAttack(this, target, action, ref error)) { //var packet = BattleActionX01Packet.BuildPacket(owner.actorId, owner.actorId, target.actorId, (uint)0x19001000, (uint)0x8000604, (ushort)0x765D, (ushort)BattleActionX01PacketCommand.Attack, (ushort)damage, (byte)0x1); } // todo: call onAttack/onDamageTaken //BattleUtils.DamageTarget(this, target, DamageTakenType.Attack, action); AddTP(200); target.AddTP(100); } public virtual void OnCast(State state, CommandResult[] actions, BattleCommand spell, ref CommandResult[] errors) { // damage is handled in script var spellCost = spell.CalculateMpCost(this); this.DelMP(spellCost); // mpCost can be set in script e.g. if caster has something for free spells foreach (CommandResult action in actions) { if (zone.FindActorInArea(action.targetId) is Character) { //BattleUtils.HandleHitType(this, chara, action); //BattleUtils.DoAction(this, chara, action, DamageTakenType.Magic); } } lua.LuaEngine.GetInstance().OnSignal("spellUsed"); } public virtual void OnWeaponSkill(State state, CommandResult[] actions, BattleCommand skill, ref CommandResult[] errors) { // damage is handled in script foreach (CommandResult action in actions) { //Should we just store the character insteado f having to find it again? if (zone.FindActorInArea(action.targetId) is Character) { //BattleUtils.DoAction(this, chara, action, DamageTakenType.Weaponskill); } } this.DelTP(skill.tpCost); } public virtual void OnAbility(State state, CommandResult[] actions, BattleCommand ability, ref CommandResult[] errors) { foreach (var action in actions) { if (zone.FindActorInArea(action.targetId) is Character) { //BattleUtils.DoAction(this, chara, action, DamageTakenType.Ability); } } } public virtual void OnSpawn() { } public virtual void OnDeath() { } public virtual void OnDespawn() { } public virtual void OnDamageDealt(Character defender, BattleCommand skill, CommandResult action, CommandResultContainer actionContainer = null) { switch (action.hitType) { case (HitType.Miss): OnMiss(defender, skill, action, actionContainer); break; case (HitType.Crit): OnCrit(defender, skill, action, actionContainer); OnHit(defender, skill, action, actionContainer); break; default: OnHit(defender, skill, action, actionContainer); break; } //TP is only gained from autoattacks and abilities if ((action.commandType == CommandType.AutoAttack || action.commandType == CommandType.Ability) && action.hitType != HitType.Miss) { //TP gained on an attack is usually 100 * delay. //Store TP seems to add .1% per point. double weaponDelay = GetMod(Modifier.Delay) / 1000.0; var storeTPPercent = 1 + (GetMod(Modifier.StoreTp) * 0.001); AddTP((int)(weaponDelay * 100 * storeTPPercent)); } } public virtual void OnDamageTaken(Character attacker, BattleCommand skill, CommandResult action, CommandResultContainer actionContainer = null) { switch (action.hitType) { case (HitType.Miss): OnEvade(attacker, skill, action, actionContainer); break; case (HitType.Parry): OnParry(attacker, skill, action, actionContainer); break; case (HitType.Block): OnBlock(attacker, skill, action, actionContainer); break; } statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnDamageTaken, "onDamageTaken", attacker, this, skill, action, actionContainer); //TP gain formula seems to be something like 5 * e ^ ( -0.667 * [defender's level] ) * damage taken, rounded up //This should be completely accurate at level 50, but isn't totally accurate at lower levels. //Don't know if store tp impacts this double tpModifier = 5 * Math.Pow(Math.E, (-0.0667 * GetLevel())); AddTP((int)Math.Ceiling(tpModifier * action.amount)); } public UInt64 GetTempVar(string name) { UInt64 retVal = 0; if (tempVars.TryGetValue(name, out retVal)) return retVal; return 0; } // cause lua is a dick public void SetTempVar(string name, uint val) { if (tempVars.ContainsKey(name)) tempVars[name] = val; } public void SetTempVar(string name, UInt64 val) { if (tempVars.ContainsKey(name)) tempVars[name] = val; } public void ResetTempVars() { tempVars.Clear(); } #region lua helpers public bool IsEngaged() { return aiContainer.IsEngaged(); } public bool IsPlayer() { return this is Player; } public bool IsMonster() { return this is BattleNpc; } public bool IsPet() { return this is Pet; } public bool IsAlly() { return this is Ally; } public bool IsDiscipleOfWar() { return GetClass() < CLASSID_THM; } public bool IsDiscipleOfMagic() { return GetClass() >= CLASSID_THM && currentJob < CLASSID_CRP; } public bool IsDiscipleOfHand() { return GetClass() >= CLASSID_CRP && currentJob < CLASSID_MIN; } public bool IsDiscipleOfLand() { return GetClass() >= CLASSID_MIN; } #endregion lua helpers #endregion ai stuff //Reset procs. Only send packet if any procs were actually reset. //This assumes you can't use weaponskills between getting a proc and using the procced ability public void ResetProcs() { var propPacketUtil = new ActorPropertyPacketUtil("charaWork/timingCommand", this); bool shouldSend = false; for (int i = 0; i < 4; i++) { if (charaWork.battleTemp.timingCommandFlag[i]) { shouldSend = true; charaWork.battleTemp.timingCommandFlag[i] = false; propPacketUtil.AddProperty(String.Format("charaWork.battleTemp.timingCommandFlag[{0}]", i)); } } if (shouldSend && this is Player) ((Player)this).QueuePackets(propPacketUtil.Done()); } //Set given proc to true and send packet if this is a player // todo: hidden status effects for timing when the procs fall off public void SetProc(int procId, bool val = true) { charaWork.battleTemp.timingCommandFlag[procId] = val; uint effectId = (uint)StatusEffectId.EvadeProc + (uint)procId; //If a proc just occurred, add a hidden effect effect if (val) { StatusEffect procEffect = Server.GetWorldManager().GetStatusEffect(effectId); procEffect.SetDuration(5); statusEffects.AddStatusEffect(procEffect, this); } //Otherwise we're reseting a proc, remove the status else { statusEffects.RemoveStatusEffect(statusEffects.GetStatusEffectById((uint)effectId)); } if (this is Player) { var propPacketUtil = new ActorPropertyPacketUtil("charaWork/timingCommand", this); propPacketUtil.AddProperty(String.Format("charaWork.battleTemp.timingCommandFlag[{0}]", procId)); ((Player)this).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, BattleCommand skill, CommandResult action, CommandResultContainer actionContainer = null) { SetProc((ushort)HitType.Evade); statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnEvade, "onEvade", attacker, this, skill, action, actionContainer); } //Called when this character blocks attacker's action public void OnBlock(Character attacker, BattleCommand skill, CommandResult action, CommandResultContainer actionContainer = null) { SetProc((ushort)HitType.Block); statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnBlock, "onBlock", attacker, this, skill, action, actionContainer); } //Called when this character parries attacker's action public void OnParry(Character attacker, BattleCommand skill, CommandResult action, CommandResultContainer actionContainer = null) { SetProc((ushort)HitType.Parry); statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnParry, "onParry", attacker, this, skill, action, actionContainer); } //Called when this character misses public void OnMiss(Character defender, BattleCommand skill, CommandResult action, CommandResultContainer actionContainer = null) { SetProc((ushort)HitType.Miss); statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnMiss, "onMiss", this, defender, skill, action, actionContainer); } public void OnHit(Character defender, BattleCommand skill, CommandResult action, CommandResultContainer actionContainer = null) { statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnHit, "onHit", this, defender, skill, action, actionContainer); } public void OnCrit(Character defender, BattleCommand skill, CommandResult action, CommandResultContainer actionContainer = null) { statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnCrit, "onCrit", this, defender, skill, action, actionContainer); } //The order of messages that appears after using a command is: //1. Cast start messages. (ie "You begin casting... ") //2. Messages from buffs that activate before the command actually starts, like Power Surge or Presence of Mind. (This may be wrong and these could be the same as 4.) //3. If the command is a multi-hit command, this is where the "You use [command] on [target]" message goes //Then, for each hit: //4. Buffs that activate before a command hits, like Blindside //5. The hit itself. For single hit commands this message is "Your [command] hits [target] for x damage" for multi hits it's "[Target] takes x points of damage" //6. Stoneskin falling off //6. Buffs that activate after a command hits, like Aegis Boon and Divine Veil //After all hits //7. If it's a multi-hit command there's a "{numhits]fold attack..." message or if all hits miss an "All attacks missed" message //8. Buffs that fall off after the skill ends, like Excruciate //For every target defeated: //8. Defeat message //9. EXP message //10. EXP chain message //folder is probably temporary until move to cached scripts is complete public void DoBattleCommand(BattleCommand command, string folder) { //List actions = new List(); CommandResultContainer actions = new CommandResultContainer(); var targets = command.targetFind.GetTargets(); bool hitTarget = false; if (targets.Count > 0) { statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnCommandStart, "onCommandStart", this, command, actions); foreach (var chara in targets) { ushort hitCount = 0; ushort totalDamage = 0; for (int hitNum = 1; hitNum <= command.numHits; hitNum++) { var action = new CommandResult(chara.actorId, command, (byte)GetHitDirection(chara), (byte) hitNum); //uncached script lua.LuaEngine.CallLuaBattleCommandFunction(this, command, folder, "onSkillFinish", this, chara, command, action, actions); //cached script //skill.CallLuaFunction(owner, "onSkillFinish", this, chara, command, action, actions); if (action.ActionLanded()) { hitTarget = true; hitCount++; totalDamage += action.amount; } } if (command.numHits > 1) { //30442: [hitCount]fold Attack! [chara] takes a total of totalDamage points of damage. //30450: All attacks miss! ushort textId = (ushort) (hitTarget ? 30442 : 30450); actions.AddAction(new CommandResult(chara.actorId, textId, 0, totalDamage, (byte)hitCount)); } } statusEffects.CallLuaFunctionByFlag((uint)StatusEffectFlags.ActivateOnCommandFinish, "onCommandFinish", this, command, actions); } else { actions.AddAction(new CommandResult(actorId, 30202, 0)); } DelMP(command.CalculateMpCost(this)); DelTP(command.CalculateTpCost(this)); //Now that we know if we hit the target we can check if the combo continues if (this is Player) { if (command.isCombo && hitTarget) ((Player)this).SetCombos(command.comboNextCommandId); //Only reset combo if the command is a spell or weaponskill, since abilities can be used between combo skills else if (command.commandType == CommandType.Spell || command.commandType == CommandType.WeaponSkill) ((Player)this).SetCombos(); } actions.CombineLists(); DoBattleAction(command.id, command.battleAnimation, actions.GetList()); } public List GetPartyMembersInRange(uint range) { TargetFind targetFind = new TargetFind(this); targetFind.SetAOEType(ValidTarget.Party, TargetFindAOEType.Circle, TargetFindAOETarget.Self, range, 0, 10, 0, 0); targetFind.FindWithinArea(this, ValidTarget.Party, TargetFindAOETarget.Self); return targetFind.GetTargets(); } #region Inventory public void SendItemPackage(Player player, uint id) { if (!itemPackages.ContainsKey((ushort)id)) return; player.QueuePacket(InventoryBeginChangePacket.BuildPacket(actorId, true)); itemPackages[(ushort)id].SendFullPackage(player); player.QueuePacket(InventoryEndChangePacket.BuildPacket(actorId)); } public void AddItem(uint catalogID) { AddItem(catalogID, 1); } public void AddItem(uint catalogID, int quantity) { AddItem(catalogID, quantity, 1); } public void AddItem(uint catalogID, int quantity, byte quality) { ushort itemPackage = GetPackageForItem(catalogID); if (itemPackages.ContainsKey(itemPackage)) { itemPackages[itemPackage].AddItem(catalogID, quantity, quality); } } public void AddItem(InventoryItem item) { ushort itemPackage = GetPackageForItem(item.GetItemData().catalogID); if (itemPackages.ContainsKey(itemPackage)) { itemPackages[itemPackage].AddItem(item); } } public void MoveItem(InventoryItem item, ushort destinationPackage) { ushort sourcePackage = item.itemPackage; if (!itemPackages.ContainsKey(sourcePackage) && !itemPackages.ContainsKey(destinationPackage)) return; itemPackages[sourcePackage].MoveItem(item, itemPackages[destinationPackage]); } public void RemoveItem(uint catalogID) { RemoveItem(catalogID, 1); } public void RemoveItem(uint catalogID, int quantity) { RemoveItem(catalogID, quantity, 1); } public void RemoveItem(uint catalogID, int quantity, byte quality) { ushort itemPackage = GetPackageForItem(catalogID); if (itemPackages.ContainsKey(itemPackage)) { itemPackages[itemPackage].RemoveItem(catalogID, quantity, quality); } } public void RemoveItemAtSlot(ushort itemPackage, ushort slot) { if (itemPackages.ContainsKey(itemPackage)) { itemPackages[itemPackage].RemoveItemAtSlot(slot); } } public void RemoveItem(InventoryItem item) { itemPackages[item.itemPackage].RemoveItem(item); } public bool HasItem(uint catalogID) { return HasItem(catalogID, 1); } public bool HasItem(uint catalogID, int minQuantity) { return HasItem(catalogID, minQuantity, 1); } public bool HasItem(uint catalogID, int minQuantity, byte quality) { ushort itemPackage = GetPackageForItem(catalogID); if (itemPackages.ContainsKey(itemPackage)) { return itemPackages[itemPackage].HasItem(catalogID, minQuantity, quality); } return false; } public bool HasItem(InventoryItem item) { ushort itemPackage = GetPackageForItem(item.GetItemData().catalogID); if (itemPackages.ContainsKey(itemPackage)) { //return itemPackages[itemPackage].HasItem(item); return false; //TODO FIX } else return false; } public InventoryItem GetItem(LuaUtils.ItemRefParam reference) { if (reference.actorId != actorId) return null; if (itemPackages.ContainsKey(reference.itemPackage)) { return itemPackages[reference.itemPackage].GetItemAtSlot(reference.slot); } return null; } public ItemPackage GetItemPackage(ushort package) { if (itemPackages.ContainsKey(package)) return itemPackages[package]; else return null; } public ushort GetPackageForItem(uint catalogID) { ItemData data = Server.GetItemGamedata(catalogID); if (data == null) return ItemPackage.NORMAL; else { if (data.IsMoney()) return ItemPackage.CURRENCY_CRYSTALS; else if (data.IsImportant()) return ItemPackage.KEYITEMS; else return ItemPackage.NORMAL; } } //public void removeItem(byUniqueId) //public void removeItem(byUniqueId, quantity) //public void removeItem(slot) //public void removeItem(slot, quantity) #endregion } }