forked from MagicBane/Server
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1354 lines
45 KiB
1354 lines
45 KiB
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . |
|
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· |
|
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ |
|
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ |
|
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ |
|
// Magicbane Emulator Project © 2013 - 2022 |
|
// www.magicbane.com |
|
|
|
package engine.gameManager; |
|
|
|
import engine.Enum.*; |
|
import engine.exception.MsgSendException; |
|
import engine.job.JobContainer; |
|
import engine.job.JobScheduler; |
|
import engine.jobs.AttackJob; |
|
import engine.jobs.DeferredPowerJob; |
|
import engine.math.Vector3fImmutable; |
|
import engine.net.DispatchMessage; |
|
import engine.net.client.ClientConnection; |
|
import engine.net.client.msg.*; |
|
import engine.objects.*; |
|
import engine.powers.DamageShield; |
|
import engine.powers.PowersBase; |
|
import engine.powers.effectmodifiers.AbstractEffectModifier; |
|
import engine.powers.effectmodifiers.WeaponProcEffectModifier; |
|
import engine.server.MBServerStatics; |
|
import org.pmw.tinylog.Logger; |
|
|
|
import java.util.HashSet; |
|
import java.util.concurrent.ConcurrentHashMap; |
|
import java.util.concurrent.ThreadLocalRandom; |
|
|
|
public enum CombatManager { |
|
|
|
COMBATMANAGER; |
|
|
|
public static int animation = 0; |
|
|
|
/** |
|
* Message sent by player to attack something. |
|
*/ |
|
|
|
public static void AttackTarget(PlayerCharacter playerCharacter, AbstractWorldObject target) { |
|
|
|
boolean swingOffhand = false; |
|
|
|
//check my weapon can I do an offhand attack |
|
|
|
Item weaponOff = playerCharacter.getCharItemManager().getEquipped().get(MBServerStatics.SLOT_OFFHAND); |
|
Item weaponMain = playerCharacter.getCharItemManager().getEquipped().get(MBServerStatics.SLOT_MAINHAND); |
|
|
|
// if you carry something in the offhand thats a weapon you get to swing it |
|
|
|
if (weaponOff != null) |
|
if (weaponOff.getItemBase().getType().equals(ItemType.WEAPON)) |
|
swingOffhand = true; |
|
|
|
// if you carry nothing in either hand you get to swing your offhand |
|
|
|
if (weaponOff == null && weaponMain == null) |
|
swingOffhand = true; |
|
|
|
|
|
//we always swing our mainhand if we are not on timer |
|
|
|
JobContainer main = playerCharacter.getTimers().get("Attack" + MBServerStatics.SLOT_MAINHAND); |
|
|
|
// no timers on the mainhand, lets submit a job to swing |
|
|
|
if (main == null) |
|
CombatManager.createTimer(playerCharacter, MBServerStatics.SLOT_MAINHAND, 1, true); // attack in 0.1 of a second |
|
|
|
/* |
|
only swing offhand if we have a weapon in it or are unarmed in both hands |
|
and no timers running |
|
*/ |
|
|
|
if (swingOffhand) { |
|
|
|
JobContainer off = playerCharacter.getTimers().get("Attack" + MBServerStatics.SLOT_OFFHAND); |
|
|
|
if (off == null) |
|
CombatManager.createTimer(playerCharacter, MBServerStatics.SLOT_OFFHAND, 1, true); // attack in 0.1 of a second |
|
} |
|
} |
|
|
|
public static void setAttackTarget(PetAttackMsg msg, ClientConnection origin) throws MsgSendException { |
|
|
|
PlayerCharacter player; |
|
Mob pet; |
|
int targetType; |
|
AbstractWorldObject target; |
|
|
|
if (TargetedActionMsg.un2cnt == 60 || TargetedActionMsg.un2cnt == 70) |
|
return; |
|
|
|
player = SessionManager.getPlayerCharacter(origin); |
|
|
|
if (player == null) |
|
return; |
|
|
|
pet = player.getPet(); |
|
|
|
if (pet == null) |
|
return; |
|
|
|
targetType = msg.getTargetType(); |
|
|
|
if (targetType == GameObjectType.PlayerCharacter.ordinal()) |
|
target = PlayerCharacter.getFromCache(msg.getTargetID()); |
|
else if (targetType == GameObjectType.Building.ordinal()) |
|
target = BuildingManager.getBuildingFromCache(msg.getTargetID()); |
|
else if (targetType == GameObjectType.Mob.ordinal()) |
|
target = Mob.getFromCache(msg.getTargetID()); |
|
else { |
|
pet.setCombatTarget(null); |
|
return; //not valid type to attack |
|
} |
|
|
|
if (pet.equals(target)) |
|
return; |
|
|
|
// quit of the combat target is already the current combat target |
|
// or there is no combat target |
|
|
|
if (target == null || target == pet.getCombatTarget()) |
|
return; |
|
|
|
//set sources target |
|
|
|
pet.setCombatTarget(target); |
|
|
|
//put in combat if not already |
|
|
|
if (!pet.isCombat()) |
|
pet.setCombat(true); |
|
|
|
//make character stand if sitting |
|
|
|
if (pet.isSit()) |
|
toggleSit(false, origin); |
|
|
|
} |
|
|
|
private static void removeAttackTimers(AbstractCharacter ac) { |
|
|
|
JobContainer main; |
|
JobContainer off; |
|
|
|
if (ac == null) |
|
return; |
|
|
|
main = ac.getTimers().get("Attack" + MBServerStatics.SLOT_MAINHAND); |
|
off = ac.getTimers().get("Attack" + MBServerStatics.SLOT_OFFHAND); |
|
|
|
if (main != null) |
|
JobScheduler.getInstance().cancelScheduledJob(main); |
|
|
|
ac.getTimers().remove("Attack" + MBServerStatics.SLOT_MAINHAND); |
|
|
|
if (off != null) |
|
JobScheduler.getInstance().cancelScheduledJob(off); |
|
|
|
ac.getTimers().remove("Attack" + MBServerStatics.SLOT_OFFHAND); |
|
|
|
ac.setCombatTarget(null); |
|
|
|
} |
|
|
|
/** |
|
* Begin Attacking |
|
*/ |
|
public static void doCombat(AbstractCharacter ac, int slot) { |
|
|
|
int ret = 0; |
|
|
|
if (ac == null) |
|
return; |
|
|
|
// Attempt to eat null targets until we can clean |
|
// up this unholy mess and refactor it into a thread. |
|
|
|
ret = attemptCombat(ac, slot); |
|
|
|
//handle pets |
|
if (ret < 2 && ac.getObjectType().equals(GameObjectType.Mob)) { |
|
|
|
Mob mob = (Mob) ac; |
|
|
|
if (mob.isPet()) |
|
return; |
|
} |
|
|
|
//ret values |
|
//0: not valid attack, fail attack |
|
//1: cannot attack, wrong hand |
|
//2: valid attack |
|
//3: cannot attack currently, continue checking |
|
|
|
if (ret == 0 || ret == 1) { |
|
|
|
//Could not attack, clear timer |
|
|
|
ConcurrentHashMap<String, JobContainer> timers = ac.getTimers(); |
|
|
|
if (timers != null) |
|
timers.remove("Attack" + slot); |
|
|
|
//clear combat target if not valid attack |
|
|
|
if (ret == 0) |
|
ac.setCombatTarget(null); |
|
|
|
} else if (ret == 3) //Failed but continue checking. reset timer |
|
createTimer(ac, slot, 5, false); |
|
} |
|
|
|
/** |
|
* Verify can attack target |
|
*/ |
|
private static int attemptCombat(AbstractCharacter abstractCharacter, int slot) { |
|
|
|
if (abstractCharacter == null) |
|
return 0; |
|
|
|
try { |
|
|
|
//Make sure player can attack |
|
|
|
PlayerBonuses bonus = abstractCharacter.getBonuses(); |
|
|
|
if (bonus != null && bonus.getBool(ModType.ImmuneToAttack, SourceType.None)) |
|
return 0; |
|
|
|
AbstractWorldObject target = abstractCharacter.getCombatTarget(); |
|
|
|
if (target == null) |
|
return 0; |
|
|
|
//target must be valid type |
|
|
|
if (AbstractWorldObject.IsAbstractCharacter(target)) { |
|
|
|
AbstractCharacter tar = (AbstractCharacter) target; |
|
|
|
//must be alive, attackable and in World |
|
|
|
if (!tar.isAlive()) |
|
return 0; |
|
else if (tar.isSafeMode()) |
|
return 0; |
|
else if (!tar.isActive()) |
|
return 0; |
|
|
|
if (target.getObjectType().equals(GameObjectType.PlayerCharacter) && abstractCharacter.getObjectType().equals(GameObjectType.PlayerCharacter) && abstractCharacter.getTimers().get("Attack" + slot) == null) |
|
if (!((PlayerCharacter) abstractCharacter).canSee((PlayerCharacter) target)) |
|
return 0; |
|
|
|
//must not be immune to all or immune to attack |
|
|
|
Resists res = tar.getResists(); |
|
bonus = tar.getBonuses(); |
|
|
|
if (bonus != null && !bonus.getBool(ModType.NoMod, SourceType.ImmuneToAttack)) |
|
if (res != null) |
|
if (res.immuneToAll() || res.immuneToAttacks()) |
|
return 0; |
|
} else if (target.getObjectType().equals(GameObjectType.Building)) { |
|
Building tar = (Building) target; |
|
|
|
// Cannot attack an invuln building |
|
|
|
if (tar.isVulnerable() == false) |
|
return 0; |
|
|
|
} else |
|
return 0; //only characters and buildings may be attacked |
|
|
|
//source must be in world and alive |
|
|
|
if (!abstractCharacter.isActive()) |
|
return 0; |
|
else if (!abstractCharacter.isAlive()) |
|
return 0; |
|
|
|
//make sure source is in combat mode |
|
|
|
if (!abstractCharacter.isCombat()) |
|
return 0; |
|
|
|
//See if either target is in safe zone |
|
|
|
if (abstractCharacter.getObjectType().equals(GameObjectType.PlayerCharacter) && target.getObjectType().equals(GameObjectType.PlayerCharacter)) |
|
if (((PlayerCharacter) abstractCharacter).inSafeZone() || ((PlayerCharacter) target).inSafeZone()) |
|
return 0; |
|
|
|
if (!(slot == MBServerStatics.SLOT_MAINHAND || slot == MBServerStatics.SLOT_OFFHAND)) |
|
return 0; |
|
|
|
if (abstractCharacter.getCharItemManager() == null) |
|
return 0; |
|
|
|
//get equippment |
|
|
|
ConcurrentHashMap<Integer, Item> equipped = abstractCharacter.getCharItemManager().getEquipped(); |
|
boolean hasNoWeapon = false; |
|
|
|
if (equipped == null) |
|
return 0; |
|
|
|
//get Weapon |
|
|
|
boolean isWeapon = true; |
|
Item weapon = equipped.get(slot); |
|
ItemBase wb = null; |
|
|
|
if (weapon == null) |
|
isWeapon = false; |
|
else { |
|
ItemBase ib = weapon.getItemBase(); |
|
|
|
if (ib == null || !ib.getType().equals(ItemType.WEAPON)) |
|
isWeapon = false; |
|
else |
|
wb = ib; |
|
} |
|
|
|
//no weapon, see if other hand has a weapon |
|
|
|
if (!isWeapon) |
|
if (slot == MBServerStatics.SLOT_MAINHAND) { |
|
|
|
//make sure offhand has weapon, not shield |
|
|
|
Item weaponOff = equipped.get(MBServerStatics.SLOT_OFFHAND); |
|
|
|
if (weaponOff != null) { |
|
ItemBase ib = weaponOff.getItemBase(); |
|
|
|
if (ib == null || !ib.getType().equals(ItemType.WEAPON)) |
|
hasNoWeapon = true; |
|
else |
|
return 1; //no need to attack with this hand |
|
|
|
} else |
|
hasNoWeapon = true; |
|
|
|
} else if (equipped.get(MBServerStatics.SLOT_MAINHAND) == null) |
|
return 1; //no need to attack with this hand |
|
|
|
//Source can attack. |
|
//NOTE Don't 'return;' beyond this point until timer created |
|
|
|
boolean attackFailure = (wb != null) && (wb.getRange() > 35f) && abstractCharacter.isMoving(); |
|
|
|
//Target can't attack on move with ranged weapons. |
|
//if not enough stamina, then skip attack |
|
|
|
if (wb == null) { |
|
if (abstractCharacter.getStamina() < 1) |
|
attackFailure = true; |
|
} else if (abstractCharacter.getStamina() < wb.getWeight()) |
|
attackFailure = true; |
|
|
|
//see if attacker is stunned. If so, stop here |
|
|
|
bonus = abstractCharacter.getBonuses(); |
|
|
|
if (bonus != null && bonus.getBool(ModType.Stunned, SourceType.None)) |
|
attackFailure = true; |
|
|
|
//Get Range of weapon |
|
|
|
float range; |
|
|
|
if (hasNoWeapon) |
|
range = MBServerStatics.NO_WEAPON_RANGE; |
|
else |
|
range = getWeaponRange(wb, bonus); |
|
|
|
if (abstractCharacter.getObjectType() == GameObjectType.Mob) { |
|
|
|
Mob minion = (Mob) abstractCharacter; |
|
|
|
if (minion.isSiege()) |
|
range = 300f; |
|
} |
|
|
|
//Range check. |
|
|
|
if (NotInRange(abstractCharacter, target, range)) { |
|
|
|
//target is in stealth and can't be seen by source |
|
|
|
if (target.getObjectType().equals(GameObjectType.PlayerCharacter) && abstractCharacter.getObjectType().equals(GameObjectType.PlayerCharacter)) |
|
if (!((PlayerCharacter) abstractCharacter).canSee((PlayerCharacter) target)) |
|
return 0; |
|
|
|
attackFailure = true; |
|
} |
|
|
|
//handle pet, skip timers (handled by AI) |
|
|
|
if (abstractCharacter.getObjectType().equals(GameObjectType.Mob)) { |
|
|
|
Mob mob = (Mob) abstractCharacter; |
|
|
|
if (mob.isPet()) { |
|
attack(abstractCharacter, target, weapon, wb, slot == MBServerStatics.SLOT_MAINHAND); |
|
return 2; |
|
} |
|
} |
|
|
|
//TODO Verify attacker has los (if not ranged weapon). |
|
|
|
if (!attackFailure) { |
|
|
|
if (hasNoWeapon || abstractCharacter.getObjectType().equals(GameObjectType.Mob)) |
|
createTimer(abstractCharacter, slot, 20, true); //2 second for no weapon |
|
else { |
|
int wepSpeed = (int) (wb.getSpeed()); |
|
|
|
if (weapon != null && weapon.getBonusPercent(ModType.WeaponSpeed, SourceType.None) != 0f) //add weapon speed bonus |
|
wepSpeed *= (1 + weapon.getBonus(ModType.WeaponSpeed, SourceType.None)); |
|
|
|
if (abstractCharacter.getBonuses() != null && abstractCharacter.getBonuses().getFloatPercentAll(ModType.AttackDelay, SourceType.None) != 0f) //add effects speed bonus |
|
wepSpeed *= (1 + abstractCharacter.getBonuses().getFloatPercentAll(ModType.AttackDelay, SourceType.None)); |
|
|
|
if (wepSpeed < 10) |
|
wepSpeed = 10; //Old was 10, but it can be reached lower with legit buffs,effects. |
|
|
|
createTimer(abstractCharacter, slot, wepSpeed, true); |
|
} |
|
|
|
attack(abstractCharacter, target, weapon, wb, slot == MBServerStatics.SLOT_MAINHAND); |
|
} else |
|
createTimer(abstractCharacter, slot, 5, false); // changed this to half a second to make combat attempts more aggressive than movement sync |
|
|
|
} catch (Exception e) { |
|
return 0; |
|
} |
|
return 2; |
|
} |
|
|
|
private static void createTimer(AbstractCharacter ac, int slot, int time, boolean success) { |
|
|
|
ConcurrentHashMap<String, JobContainer> timers = ac.getTimers(); |
|
|
|
if (timers != null) { |
|
AttackJob aj = new AttackJob(ac, slot, success); |
|
JobContainer job; |
|
job = JobScheduler.getInstance().scheduleJob(aj, (time * 100)); |
|
timers.put("Attack" + slot, job); |
|
} else { |
|
Logger.error("Unable to find Timers for Character " + ac.getObjectUUID()); |
|
} |
|
} |
|
|
|
/** |
|
* Attempt to attack target |
|
*/ |
|
private static void attack(AbstractCharacter attacker, AbstractWorldObject target, Item weapon, ItemBase wb, boolean mainHand) { |
|
|
|
float atr; |
|
int minDamage, maxDamage; |
|
int errorTrack = 0; |
|
|
|
try { |
|
|
|
if (attacker == null) |
|
return; |
|
|
|
if (target == null) |
|
return; |
|
|
|
if (mainHand) { |
|
atr = attacker.getAtrHandOne(); |
|
minDamage = attacker.getMinDamageHandOne(); |
|
maxDamage = attacker.getMaxDamageHandOne(); |
|
} else { |
|
atr = attacker.getAtrHandTwo(); |
|
minDamage = attacker.getMinDamageHandTwo(); |
|
maxDamage = attacker.getMaxDamageHandTwo(); |
|
} |
|
|
|
boolean tarIsRat = false; |
|
|
|
if (target.getObjectTypeMask() == MBServerStatics.MASK_RAT) |
|
tarIsRat = true; |
|
else if (target.getObjectType() == GameObjectType.PlayerCharacter) { |
|
|
|
PlayerCharacter pTar = (PlayerCharacter) target; |
|
|
|
for (Effect eff : pTar.getEffects().values()) |
|
if (eff.getPowerToken() == 429513599 || eff.getPowerToken() == 429415295) |
|
tarIsRat = true; |
|
} |
|
|
|
//Dont think we need to do this anymore. |
|
|
|
if (tarIsRat) |
|
if (attacker.getBonuses().getFloatPercentAll(ModType.Slay, SourceType.Rat) != 0) { //strip away current % dmg buffs then add with rat % |
|
|
|
float percent = 1 + attacker.getBonuses().getFloatPercentAll(ModType.Slay, SourceType.Rat); |
|
|
|
minDamage *= percent; |
|
maxDamage *= percent; |
|
} |
|
|
|
errorTrack = 1; |
|
|
|
//subtract stamina |
|
|
|
if (wb == null) |
|
attacker.modifyStamina(-0.5f, attacker, true); |
|
else { |
|
float stam = wb.getWeight() / 3; |
|
stam = (stam < 1) ? 1 : stam; |
|
attacker.modifyStamina(-(stam), attacker, true); |
|
} |
|
|
|
attacker.cancelOnAttackSwing(); |
|
|
|
errorTrack = 2; |
|
|
|
//set last time this player has attacked something. |
|
|
|
if (target.getObjectType().equals(GameObjectType.PlayerCharacter) && target.getObjectUUID() != attacker.getObjectUUID() && attacker.getObjectType() == GameObjectType.PlayerCharacter) { |
|
attacker.setTimeStamp("LastCombatPlayer", System.currentTimeMillis()); |
|
((PlayerCharacter) target).setTimeStamp("LastCombatPlayer", System.currentTimeMillis()); |
|
} else |
|
attacker.setTimeStamp("LastCombatMob", System.currentTimeMillis()); |
|
|
|
errorTrack = 3; |
|
|
|
//Get defense for target |
|
|
|
float defense; |
|
|
|
if (target.getObjectType().equals(GameObjectType.Building)) { |
|
|
|
if (BuildingManager.getBuildingFromCache(target.getObjectUUID()) == null) { |
|
attacker.setCombatTarget(null); |
|
return; |
|
} |
|
|
|
defense = 0; |
|
|
|
} else { |
|
AbstractCharacter tar = (AbstractCharacter) target; |
|
defense = tar.getDefenseRating(); |
|
handleRetaliate(tar, attacker); //Handle target attacking back if in combat and has no other target |
|
} |
|
|
|
errorTrack = 4; |
|
|
|
//Get hit chance |
|
|
|
int chance; |
|
float dif = atr - defense; |
|
|
|
if (dif > 100) |
|
chance = 94; |
|
else if (dif < -100) |
|
chance = 4; |
|
else |
|
chance = (int) ((0.45 * dif) + 49); |
|
|
|
errorTrack = 5; |
|
|
|
//calculate hit/miss |
|
|
|
int roll = ThreadLocalRandom.current().nextInt(100); |
|
DeferredPowerJob dpj = null; |
|
|
|
if (roll < chance) { |
|
|
|
if (attacker.getObjectType().equals(GameObjectType.PlayerCharacter)) |
|
updateAttackTimers((PlayerCharacter) attacker, target, true); |
|
|
|
boolean skipPassives = false; |
|
PlayerBonuses bonuses = attacker.getBonuses(); |
|
|
|
if (bonuses != null && bonuses.getBool(ModType.IgnorePassiveDefense, SourceType.None)) |
|
skipPassives = true; |
|
|
|
AbstractCharacter tarAc = null; |
|
|
|
if (AbstractWorldObject.IsAbstractCharacter(target)) |
|
tarAc = (AbstractCharacter) target; |
|
|
|
errorTrack = 6; |
|
|
|
// Apply Weapon power effect if any. don't try to apply twice if |
|
// dual wielding. Perform after passive test for sync purposes. |
|
|
|
if (attacker.getObjectType().equals(GameObjectType.PlayerCharacter) && (mainHand || wb.isTwoHanded())) { |
|
|
|
dpj = ((PlayerCharacter) attacker).getWeaponPower(); |
|
|
|
if (dpj != null) { |
|
|
|
PlayerBonuses bonus = attacker.getBonuses(); |
|
float attackRange = getWeaponRange(wb, bonus); |
|
dpj.attack(target, attackRange); |
|
|
|
if (dpj.getPower() != null && (dpj.getPowerToken() == -1851459567 || dpj.getPowerToken() == -1851489518)) |
|
((PlayerCharacter) attacker).setWeaponPower(dpj); |
|
} |
|
} |
|
|
|
//check to apply second backstab. |
|
|
|
if (attacker.getObjectType().equals(GameObjectType.PlayerCharacter) && !mainHand) { |
|
|
|
dpj = ((PlayerCharacter) attacker).getWeaponPower(); |
|
|
|
if (dpj != null && dpj.getPower() != null && (dpj.getPowerToken() == -1851459567 || dpj.getPowerToken() == -1851489518)) { |
|
float attackRange = getWeaponRange(wb, bonuses); |
|
dpj.attack(target, attackRange); |
|
} |
|
} |
|
|
|
errorTrack = 7; |
|
|
|
//Hit, check if passive kicked in |
|
|
|
boolean passiveFired = false; |
|
|
|
if (!skipPassives && tarAc != null) { |
|
|
|
if (target.getObjectType().equals(GameObjectType.PlayerCharacter)) { |
|
|
|
//Handle Block passive |
|
|
|
if (testPassive(attacker, tarAc, "Block") && canTestBlock(attacker, target)) { |
|
|
|
if (!target.isAlive()) |
|
return; |
|
|
|
sendPassiveDefenseMessage(attacker, wb, target, MBServerStatics.COMBAT_SEND_BLOCK, dpj, mainHand); |
|
passiveFired = true; |
|
} |
|
|
|
//Handle Parry passive |
|
|
|
if (!passiveFired) |
|
if (canTestParry(attacker, target) && testPassive(attacker, tarAc, "Parry")) { |
|
|
|
if (!target.isAlive()) |
|
return; |
|
|
|
sendPassiveDefenseMessage(attacker, wb, target, MBServerStatics.COMBAT_SEND_PARRY, dpj, mainHand); |
|
passiveFired = true; |
|
} |
|
|
|
} |
|
|
|
errorTrack = 8; |
|
|
|
//Handle Dodge passive |
|
|
|
if (!passiveFired) |
|
if (testPassive(attacker, tarAc, "Dodge")) { |
|
|
|
if (!target.isAlive()) |
|
return; |
|
|
|
sendPassiveDefenseMessage(attacker, wb, target, MBServerStatics.COMBAT_SEND_DODGE, dpj, mainHand); |
|
passiveFired = true; |
|
} |
|
} |
|
|
|
//return if passive (Block, Parry, Dodge) fired |
|
|
|
if (passiveFired) |
|
return; |
|
|
|
errorTrack = 9; |
|
|
|
//Hit and no passives |
|
//if target is player, set last attack timestamp |
|
|
|
if (target.getObjectType().equals(GameObjectType.PlayerCharacter)) |
|
updateAttackTimers((PlayerCharacter) target, attacker, false); |
|
|
|
//Get damage Type |
|
|
|
DamageType damageType; |
|
|
|
if (wb != null) |
|
damageType = wb.getDamageType(); |
|
else if (attacker.getObjectType().equals(GameObjectType.Mob) && ((Mob) attacker).isSiege()) |
|
damageType = DamageType.Siege; |
|
else |
|
damageType = DamageType.Crush; |
|
|
|
errorTrack = 10; |
|
|
|
//Get target resists |
|
|
|
Resists resists = null; |
|
|
|
if (tarAc != null) |
|
resists = tarAc.getResists(); |
|
else if (target.getObjectType().equals(GameObjectType.Building)) |
|
resists = ((Building) target).getResists(); |
|
|
|
//make sure target is not immune to damage type; |
|
|
|
if (resists != null && resists.immuneTo(damageType)) { |
|
sendCombatMessage(attacker, target, 0f, wb, dpj, mainHand); |
|
return; |
|
} |
|
|
|
errorTrack = 11; |
|
|
|
//Calculate Damage done |
|
|
|
float damage; |
|
|
|
if (wb != null) |
|
damage = calculateDamage(attacker, tarAc, minDamage, maxDamage, damageType, resists); |
|
else |
|
damage = calculateDamage(attacker, tarAc, minDamage, maxDamage, damageType, resists); |
|
|
|
float d = 0f; |
|
|
|
errorTrack = 12; |
|
|
|
//Subtract Damage from target's health |
|
|
|
if (tarAc != null) { |
|
|
|
if (tarAc.isSit()) |
|
damage *= 2.5f; //increase damage if sitting |
|
|
|
if (tarAc.getHealth() > 0) |
|
d = tarAc.modifyHealth(-damage, attacker, false); |
|
|
|
} else if (target.getObjectType().equals(GameObjectType.Building)) { |
|
|
|
if (BuildingManager.getBuildingFromCache(target.getObjectUUID()) == null) { |
|
attacker.setCombatTarget(null); |
|
return; |
|
} |
|
|
|
if (target.getHealth() > 0) |
|
d = ((Building) target).modifyHealth(-damage, attacker); |
|
} |
|
|
|
errorTrack = 13; |
|
|
|
//Test to see if any damage needs done to weapon or armor |
|
|
|
testItemDamage(attacker, target, weapon, wb); |
|
|
|
// if target is dead, we got the killing blow, remove attack timers on our weapons |
|
|
|
if (tarAc != null && !tarAc.isAlive()) |
|
removeAttackTimers(attacker); |
|
|
|
//test double death fix |
|
|
|
if (d != 0) |
|
sendCombatMessage(attacker, target, damage, wb, dpj, mainHand); //send damage message |
|
|
|
errorTrack = 14; |
|
|
|
//handle procs |
|
|
|
if (weapon != null && tarAc != null && tarAc.isAlive()) { |
|
|
|
ConcurrentHashMap<String, Effect> effects = weapon.getEffects(); |
|
|
|
for (Effect eff : effects.values()) { |
|
if (eff == null) |
|
continue; |
|
|
|
HashSet<AbstractEffectModifier> aems = eff.getEffectModifiers(); |
|
|
|
if (aems != null) { |
|
for (AbstractEffectModifier aem : aems) { |
|
|
|
if (!tarAc.isAlive()) |
|
break; |
|
|
|
if (aem instanceof WeaponProcEffectModifier) { |
|
|
|
int procChance = ThreadLocalRandom.current().nextInt(100); |
|
|
|
if (procChance < MBServerStatics.PROC_CHANCE) |
|
((WeaponProcEffectModifier) aem).applyProc(attacker, target); |
|
|
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
errorTrack = 15; |
|
|
|
//handle damage shields |
|
|
|
if (attacker.isAlive() && tarAc != null && tarAc.isAlive()) |
|
handleDamageShields(attacker, tarAc, damage); |
|
|
|
} else { |
|
|
|
// Apply Weapon power effect if any. |
|
// don't try to apply twice if dual wielding. |
|
|
|
if (attacker.getObjectType().equals(GameObjectType.PlayerCharacter) && (mainHand || wb.isTwoHanded())) { |
|
dpj = ((PlayerCharacter) attacker).getWeaponPower(); |
|
|
|
if (dpj != null) { |
|
|
|
PowersBase wp = dpj.getPower(); |
|
|
|
if (wp.requiresHitRoll() == false) { |
|
PlayerBonuses bonus = attacker.getBonuses(); |
|
float attackRange = getWeaponRange(wb, bonus); |
|
dpj.attack(target, attackRange); |
|
} else |
|
((PlayerCharacter) attacker).setWeaponPower(null); |
|
} |
|
} |
|
|
|
if (target.getObjectType() == GameObjectType.Mob) |
|
((Mob) target).handleDirectAggro(attacker); |
|
|
|
errorTrack = 17; |
|
|
|
//miss, Send miss message |
|
|
|
sendCombatMessage(attacker, target, 0f, wb, dpj, mainHand); |
|
|
|
//if attacker is player, set last attack timestamp |
|
|
|
if (attacker.getObjectType().equals(GameObjectType.PlayerCharacter)) |
|
updateAttackTimers((PlayerCharacter) attacker, target, true); |
|
} |
|
|
|
errorTrack = 18; |
|
|
|
//cancel effects that break on attack or attackSwing |
|
attacker.cancelOnAttack(); |
|
|
|
} catch (Exception e) { |
|
Logger.error(attacker.getName() + ' ' + errorTrack + ' ' + e); |
|
} |
|
} |
|
|
|
public static boolean canTestParry(AbstractCharacter ac, AbstractWorldObject target) { |
|
|
|
if (ac == null || target == null || !AbstractWorldObject.IsAbstractCharacter(target)) |
|
return false; |
|
|
|
AbstractCharacter tar = (AbstractCharacter) target; |
|
|
|
CharacterItemManager acItem = ac.getCharItemManager(); |
|
CharacterItemManager tarItem = tar.getCharItemManager(); |
|
|
|
if (acItem == null || tarItem == null) |
|
return false; |
|
|
|
Item acMain = acItem.getItemFromEquipped(1); |
|
Item acOff = acItem.getItemFromEquipped(2); |
|
Item tarMain = tarItem.getItemFromEquipped(1); |
|
Item tarOff = tarItem.getItemFromEquipped(2); |
|
|
|
return !isRanged(acMain) && !isRanged(acOff) && !isRanged(tarMain) && !isRanged(tarOff); |
|
} |
|
|
|
public static boolean canTestBlock(AbstractCharacter ac, AbstractWorldObject target) { |
|
|
|
if (ac == null || target == null || !AbstractWorldObject.IsAbstractCharacter(target)) |
|
return false; |
|
|
|
AbstractCharacter tar = (AbstractCharacter) target; |
|
|
|
CharacterItemManager acItem = ac.getCharItemManager(); |
|
CharacterItemManager tarItem = tar.getCharItemManager(); |
|
|
|
if (acItem == null || tarItem == null) |
|
return false; |
|
|
|
Item tarOff = tarItem.getItemFromEquipped(2); |
|
|
|
if (tarOff == null) |
|
return false; |
|
|
|
return tarOff.getItemBase().isShield() != false; |
|
} |
|
|
|
private static boolean isRanged(Item item) { |
|
|
|
if (item == null) |
|
return false; |
|
|
|
ItemBase ib = item.getItemBase(); |
|
|
|
if (ib == null) |
|
return false; |
|
|
|
if (ib.getType().equals(ItemType.WEAPON) == false) |
|
return false; |
|
|
|
return ib.getRange() > MBServerStatics.RANGED_WEAPON_RANGE; |
|
|
|
} |
|
|
|
private static float calculateDamage(AbstractCharacter source, AbstractCharacter target, float minDamage, float maxDamage, DamageType damageType, Resists resists) { |
|
|
|
//get range between min and max |
|
|
|
float range = maxDamage - minDamage; |
|
|
|
//Damage is calculated twice to average a more central point |
|
|
|
float damage = ThreadLocalRandom.current().nextFloat() * range; |
|
damage = (damage + (ThreadLocalRandom.current().nextFloat() * range)) * .5f; |
|
|
|
//put it back between min and max |
|
|
|
damage += minDamage; |
|
|
|
//calculate resists in if any |
|
|
|
if (resists != null) |
|
return resists.getResistedDamage(source, target, damageType, damage, 0); |
|
else |
|
return damage; |
|
} |
|
|
|
private static void sendPassiveDefenseMessage(AbstractCharacter source, ItemBase wb, AbstractWorldObject target, int passiveType, DeferredPowerJob dpj, boolean mainHand) { |
|
|
|
int swingAnimation = getSwingAnimation(wb, dpj, mainHand); |
|
|
|
if (dpj != null) |
|
if (PowersManager.AnimationOverrides.containsKey(dpj.getAction().getEffectID())) |
|
swingAnimation = PowersManager.AnimationOverrides.get(dpj.getAction().getEffectID()); |
|
|
|
TargetedActionMsg cmm = new TargetedActionMsg(source, swingAnimation, target, passiveType); |
|
DispatchMessage.sendToAllInRange(target, cmm); |
|
|
|
} |
|
|
|
private static void sendCombatMessage(AbstractCharacter source, AbstractWorldObject target, float damage, ItemBase wb, DeferredPowerJob dpj, boolean mainHand) { |
|
|
|
int swingAnimation = getSwingAnimation(wb, dpj, mainHand); |
|
|
|
if (dpj != null) |
|
if (PowersManager.AnimationOverrides.containsKey(dpj.getAction().getEffectID())) |
|
swingAnimation = PowersManager.AnimationOverrides.get(dpj.getAction().getEffectID()); |
|
|
|
if (source.getObjectType() == GameObjectType.PlayerCharacter) |
|
for (Effect eff : source.getEffects().values()) |
|
if (eff.getPower() != null && (eff.getPower().getToken() == 429506943 || eff.getPower().getToken() == 429408639 || eff.getPower().getToken() == 429513599 || eff.getPower().getToken() == 429415295)) |
|
swingAnimation = 0; |
|
|
|
TargetedActionMsg cmm = new TargetedActionMsg(source, target, damage, swingAnimation); |
|
DispatchMessage.sendToAllInRange(target, cmm); |
|
} |
|
|
|
public static int getSwingAnimation(ItemBase wb, DeferredPowerJob dpj, boolean mainHand) { |
|
int token = 0; |
|
|
|
if (dpj != null) |
|
token = (dpj.getPower() != null) ? dpj.getPower().getToken() : 0; |
|
|
|
if (token == 563721004) //kick animation |
|
return 79; |
|
|
|
if (CombatManager.animation != 0) |
|
return CombatManager.animation; |
|
|
|
if (wb == null) |
|
return 75; |
|
|
|
if (mainHand) { |
|
if (wb.getAnimations().size() > 0) { |
|
|
|
int animation; |
|
|
|
int random = ThreadLocalRandom.current().nextInt(wb.getAnimations().size()); |
|
|
|
try { |
|
animation = wb.getAnimations().get(random); |
|
return animation; |
|
} catch (Exception e) { |
|
Logger.error(e.getMessage()); |
|
return wb.getAnimations().get(0); |
|
} |
|
|
|
} else if (wb.getOffHandAnimations().size() > 0) { |
|
|
|
int animation; |
|
int random = ThreadLocalRandom.current().nextInt(wb.getOffHandAnimations().size()); |
|
|
|
try { |
|
animation = wb.getOffHandAnimations().get(random); |
|
return animation; |
|
} catch (Exception e) { |
|
Logger.error(e.getMessage()); |
|
return wb.getOffHandAnimations().get(0); |
|
} |
|
} |
|
} else { |
|
if (wb.getOffHandAnimations().size() > 0) { |
|
int animation; |
|
int random = ThreadLocalRandom.current().nextInt(wb.getOffHandAnimations().size()); |
|
|
|
try { |
|
animation = wb.getOffHandAnimations().get(random); |
|
return animation; |
|
} catch (Exception e) { |
|
Logger.error(e.getMessage()); |
|
return wb.getOffHandAnimations().get(0); |
|
|
|
} |
|
} else if (wb.getAnimations().size() > 0) { |
|
|
|
int animation; |
|
int random = ThreadLocalRandom.current().nextInt(wb.getAnimations().size()); |
|
|
|
try { |
|
animation = wb.getAnimations().get(random); |
|
return animation; |
|
} catch (Exception e) { |
|
Logger.error(e.getMessage()); |
|
return wb.getAnimations().get(0); |
|
|
|
} |
|
|
|
} |
|
} |
|
|
|
|
|
String required = wb.getSkillRequired(); |
|
String mastery = wb.getMastery(); |
|
|
|
if (required.equals("Unarmed Combat")) |
|
return 75; |
|
else if (required.equals("Sword")) { |
|
|
|
if (wb.isTwoHanded()) |
|
return 105; |
|
else |
|
return 98; |
|
|
|
} else if (required.equals("Staff") || required.equals("Pole Arm")) { |
|
return 85; |
|
} else if (required.equals("Spear")) { |
|
return 92; |
|
} else if (required.equals("Hammer") || required.equals("Axe")) { |
|
if (wb.isTwoHanded()) { |
|
return 105; |
|
} else if (mastery.equals("Throwing")) { |
|
return 115; |
|
} else { |
|
return 100; |
|
} |
|
} else if (required.equals("Dagger")) { |
|
if (mastery.equals("Throwing")) { |
|
return 117; |
|
} else { |
|
return 81; |
|
} |
|
} else if (required.equals("Crossbow")) { |
|
return 110; |
|
} else if (required.equals("Bow")) { |
|
return 109; |
|
} else if (wb.isTwoHanded()) { |
|
return 105; |
|
} else { |
|
return 100; |
|
} |
|
} |
|
|
|
private static boolean testPassive(AbstractCharacter source, AbstractCharacter target, String type) { |
|
|
|
float chance = target.getPassiveChance(type, source.getLevel(), true); |
|
|
|
if (chance == 0f) |
|
return false; |
|
|
|
//max 75% chance of passive to fire |
|
|
|
if (chance > 75f) |
|
chance = 75f; |
|
|
|
int roll = ThreadLocalRandom.current().nextInt(100); |
|
|
|
return roll < chance; |
|
|
|
} |
|
|
|
private static void updateAttackTimers(PlayerCharacter pc, AbstractWorldObject target, boolean attack) { |
|
|
|
//Set Attack Timers |
|
|
|
if (target.getObjectType().equals(GameObjectType.PlayerCharacter)) |
|
pc.setLastPlayerAttackTime(); |
|
} |
|
|
|
public static float getWeaponRange(ItemBase weapon, PlayerBonuses bonus) { |
|
|
|
float rangeMod = 1.0f; |
|
|
|
if (weapon == null) |
|
return 0f; |
|
|
|
if (bonus != null) |
|
rangeMod += bonus.getFloatPercentAll(ModType.WeaponRange, SourceType.None); |
|
|
|
return weapon.getRange() * rangeMod; |
|
} |
|
|
|
public static void toggleCombat(ToggleCombatMsg msg, ClientConnection origin) { |
|
toggleCombat(msg.toggleCombat(), origin); |
|
} |
|
|
|
public static void toggleCombat(SetCombatModeMsg msg, ClientConnection origin) { |
|
toggleCombat(msg.getToggle(), origin); |
|
} |
|
|
|
public static void toggleCombat(boolean toggle, ClientConnection origin) { |
|
|
|
PlayerCharacter pc = SessionManager.getPlayerCharacter(origin); |
|
|
|
if (pc == null) |
|
return; |
|
|
|
pc.setCombat(toggle); |
|
|
|
if (!toggle) // toggle is move it to false so clear combat target |
|
pc.setCombatTarget(null); //clear last combat target |
|
|
|
UpdateStateMsg rwss = new UpdateStateMsg(); |
|
rwss.setPlayer(pc); |
|
DispatchMessage.dispatchMsgToInterestArea(pc, rwss, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, false, false); |
|
} |
|
|
|
public static void toggleSit(boolean toggle, ClientConnection origin) { |
|
|
|
PlayerCharacter pc = SessionManager.getPlayerCharacter(origin); |
|
|
|
if (pc == null) |
|
return; |
|
|
|
pc.setSit(toggle); |
|
|
|
UpdateStateMsg rwss = new UpdateStateMsg(); |
|
rwss.setPlayer(pc); |
|
DispatchMessage.dispatchMsgToInterestArea(pc, rwss, DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false); |
|
} |
|
|
|
public static boolean NotInRange(AbstractCharacter ac, AbstractWorldObject target, float range) { |
|
|
|
Vector3fImmutable sl = ac.getLoc(); |
|
Vector3fImmutable tl = target.getLoc(); |
|
|
|
//add Hitbox to range. |
|
|
|
range += (calcHitBox(ac) + calcHitBox(target)); |
|
|
|
float magnitudeSquared = tl.distanceSquared(sl); |
|
|
|
return magnitudeSquared > range * range; |
|
|
|
} |
|
|
|
//Called when character takes damage. |
|
public static void handleRetaliate(AbstractCharacter target, AbstractCharacter attacker) { |
|
|
|
if (attacker == null || target == null) |
|
return; |
|
|
|
if (attacker.equals(target)) |
|
return; |
|
|
|
if (target.isMoving() && target.getObjectType().equals(GameObjectType.PlayerCharacter)) |
|
return; |
|
|
|
if (!target.isAlive() || !attacker.isAlive()) |
|
return; |
|
|
|
boolean isCombat = target.isCombat(); |
|
|
|
//If target in combat and has no target, then attack back |
|
|
|
AbstractWorldObject awoCombTar = target.getCombatTarget(); |
|
|
|
if ((target.isCombat() && awoCombTar == null) || (isCombat && awoCombTar != null && (!awoCombTar.isAlive() || target.isCombat() && NotInRange(target, awoCombTar, target.getRange()))) || (target != null && target.getObjectType() == GameObjectType.Mob && ((Mob) target).isSiege())) |
|
if (target.getObjectType().equals(GameObjectType.PlayerCharacter)) { // we are in combat with no valid target |
|
|
|
PlayerCharacter pc = (PlayerCharacter) target; |
|
target.setCombatTarget(attacker); |
|
pc.setLastTarget(attacker.getObjectType(), attacker.getObjectUUID()); |
|
|
|
if (target.getTimers() != null) |
|
if (!target.getTimers().containsKey("Attack" + MBServerStatics.SLOT_MAINHAND)) |
|
CombatManager.AttackTarget((PlayerCharacter) target, target.getCombatTarget()); |
|
} |
|
|
|
//Handle pet retaliate if assist is on and pet doesn't have a target. |
|
|
|
if (target.getObjectType().equals(GameObjectType.PlayerCharacter)) { |
|
|
|
Mob pet = ((PlayerCharacter) target).getPet(); |
|
|
|
if (pet != null && pet.assist && pet.getCombatTarget() == null) |
|
pet.setCombatTarget(attacker); |
|
} |
|
|
|
//Handle Mob Retaliate. |
|
|
|
if (target.getObjectType() == GameObjectType.Mob) { |
|
|
|
Mob attackedMobile = (Mob) target; |
|
|
|
//handle minion informing his captain of an attack |
|
|
|
if (attackedMobile.agentType.equals(AIAgentType.GUARDMINION) && attackedMobile.guardCaptain != null && attackedMobile.guardCaptain.isAlive()) { |
|
|
|
if (attackedMobile.guardCaptain.combatTarget == null) |
|
attackedMobile.guardCaptain.setCombatTarget(attacker); |
|
|
|
} |
|
|
|
// Mobile already has a target; don't switch. |
|
|
|
if (attackedMobile.getCombatTarget() != null && !attackedMobile.isSiege()) |
|
return; |
|
|
|
attackedMobile.setCombatTarget(attacker); |
|
|
|
} |
|
} |
|
|
|
public static void handleDamageShields(AbstractCharacter ac, AbstractCharacter target, float damage) { |
|
|
|
if (ac == null || target == null) |
|
return; |
|
|
|
PlayerBonuses bonuses = target.getBonuses(); |
|
|
|
if (bonuses != null) { |
|
|
|
ConcurrentHashMap<AbstractEffectModifier, DamageShield> damageShields = bonuses.getDamageShields(); |
|
float total = 0; |
|
|
|
for (DamageShield ds : damageShields.values()) { |
|
|
|
//get amount to damage back |
|
|
|
float amount; |
|
|
|
if (ds.usePercent()) |
|
amount = damage * ds.getAmount() / 100; |
|
else |
|
amount = ds.getAmount(); |
|
|
|
//get resisted damage for damagetype |
|
|
|
Resists resists = ac.getResists(); |
|
|
|
if (resists != null) |
|
amount = resists.getResistedDamage(target, ac, ds.getDamageType(), amount, 0); |
|
|
|
total += amount; |
|
} |
|
|
|
if (total > 0) { |
|
|
|
//apply Damage back |
|
|
|
ac.modifyHealth(-total, target, true); |
|
|
|
TargetedActionMsg cmm = new TargetedActionMsg(ac, ac, total, 0); |
|
DispatchMessage.sendToAllInRange(target, cmm); |
|
|
|
} |
|
} |
|
} |
|
|
|
public static float calcHitBox(AbstractWorldObject ac) { |
|
|
|
//TODO Figure out how Str Affects HitBox |
|
|
|
float hitBox = 1; |
|
|
|
switch (ac.getObjectType()) { |
|
case PlayerCharacter: |
|
PlayerCharacter pc = (PlayerCharacter) ac; |
|
if (MBServerStatics.COMBAT_TARGET_HITBOX_DEBUG) { |
|
Logger.info("Hit box radius for " + pc.getFirstName() + " is " + ((int) pc.statStrBase / 20f)); |
|
} |
|
hitBox = 1.5f + (int) ((PlayerCharacter) ac).statStrBase / 20f; |
|
break; |
|
|
|
case Mob: |
|
Mob mob = (Mob) ac; |
|
if (MBServerStatics.COMBAT_TARGET_HITBOX_DEBUG) |
|
Logger.info("Hit box radius for " + mob.getFirstName() |
|
+ " is " + ((Mob) ac).getMobBase().getHitBoxRadius()); |
|
|
|
hitBox = ((Mob) ac).getMobBase().getHitBoxRadius(); |
|
break; |
|
case Building: |
|
Building building = (Building) ac; |
|
if (building.getBlueprint() == null) |
|
return 32; |
|
hitBox = Math.max(building.getBlueprint().getBuildingGroup().getExtents().x, |
|
building.getBlueprint().getBuildingGroup().getExtents().y); |
|
if (MBServerStatics.COMBAT_TARGET_HITBOX_DEBUG) |
|
Logger.info("Hit box radius for " + building.getName() + " is " + hitBox); |
|
break; |
|
|
|
} |
|
return hitBox; |
|
} |
|
|
|
private static void testItemDamage(AbstractCharacter ac, AbstractWorldObject awo, Item weapon, ItemBase wb) { |
|
|
|
if (ac == null) |
|
return; |
|
|
|
//get chance to damage |
|
|
|
int chance = 4500; |
|
|
|
if (wb != null) |
|
if (wb.isGlass()) //glass used weighted so fast weapons don't break faster |
|
chance = 9000 / wb.getWeight(); |
|
|
|
//test damaging attackers weapon |
|
|
|
int takeDamage = ThreadLocalRandom.current().nextInt(chance); |
|
|
|
if (takeDamage == 0 && wb != null && (ac.getObjectType().equals(GameObjectType.PlayerCharacter))) |
|
ac.getCharItemManager().damageItem(weapon, 1); |
|
|
|
|
|
//test damaging targets gear |
|
|
|
takeDamage = ThreadLocalRandom.current().nextInt(chance); |
|
|
|
if (takeDamage == 0 && awo != null && (awo.getObjectType().equals(GameObjectType.PlayerCharacter))) |
|
((AbstractCharacter) awo).getCharItemManager().damageRandomArmor(1); |
|
} |
|
|
|
}
|
|
|