/******************************************************************************* * Copyright (c) 2013 * * This File is part of NGECore2. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * Using NGEngine to work with NGECore2 is making a combined work based on NGEngine. * Therefore all terms and conditions of the GNU Lesser General Public License cover the combination. ******************************************************************************/ package services.combat; import java.util.List; import java.util.Map; import java.util.Random; import org.apache.mina.core.buffer.IoBuffer; import protocol.swg.ObjControllerMessage; import protocol.swg.objectControllerObjects.CombatAction; import protocol.swg.objectControllerObjects.CombatSpam; import protocol.swg.objectControllerObjects.CommandEnqueueRemove; import protocol.swg.objectControllerObjects.StartTask; import resources.common.FileUtilities; import resources.objects.creature.CreatureObject; import resources.objects.tangible.TangibleObject; import resources.objects.weapon.WeaponObject; import services.command.CombatCommand; import main.NGECore; import engine.resources.common.CRC; import engine.resources.objects.SWGObject; import engine.resources.service.INetworkDispatch; import engine.resources.service.INetworkRemoteEvent; public class CombatService implements INetworkDispatch { private NGECore core; public CombatService(NGECore core) { this.core = core; CombatCommands.registerCommands(core); } @Override public void insertOpcodes(Map arg0, Map arg1) { } @Override public void shutdown() { // TODO Auto-generated method stub } public void doCombat(CreatureObject attacker, TangibleObject target, WeaponObject weapon, CombatCommand command, int actionCounter) { if(!applySpecialCost(attacker, weapon, command)) return; if((command.getAttackType() == 0 || command.getAttackType() == 1 || command.getAttackType() == 3) && !attemptCombat(attacker, target)) return; // use preRun for delayed effects like officer orbital strike, grenades etc. if(FileUtilities.doesFileExist("scripts/commands/combat" + command.getCommandName() + ".py")) core.scriptService.callScript("scripts/commands/combat", command.getCommandName(), "preRun", core, attacker, target, command); if(command.getAttackType() == 1) doSingleTargetCombat(attacker, target, weapon, command, actionCounter); else if(command.getAttackType() == 0 || command.getAttackType() == 2 || command.getAttackType() == 3) doAreaCombat(attacker, target, weapon, command, actionCounter); } private void doAreaCombat(CreatureObject attacker, TangibleObject target, WeaponObject weapon, CombatCommand command, int actionCounter) { if(target instanceof CreatureObject) { doAreaCombat(attacker, (CreatureObject) target, weapon, command, actionCounter); return; } } private void doSingleTargetCombat(CreatureObject attacker, TangibleObject target, WeaponObject weapon, CombatCommand command, int actionCounter) { if(target instanceof CreatureObject) { doSingleTargetCombat(attacker, (CreatureObject) target, weapon, command, actionCounter); return; } float damage = calculateDamage(attacker, target, weapon, command); } private void doAreaCombat(CreatureObject attacker, CreatureObject target, WeaponObject weapon, CombatCommand command, int actionCounter) { float x = attacker.getWorldPosition().x; float z = attacker.getWorldPosition().z; float dirX = target.getWorldPosition().x - x; float dirZ = target.getWorldPosition().z - z; float range = command.getConeLength(); List inRangeObjects = core.simulationService.get(attacker.getPlanet(), target.getWorldPosition().x, target.getWorldPosition().z, (int) range); for(SWGObject obj : inRangeObjects) { if(!(obj instanceof TangibleObject) || obj == attacker) continue; if(obj instanceof CreatureObject && (((CreatureObject) obj).getPosture() == 13 || ((CreatureObject) obj).getPosture() == 14)) continue; if(command.getAttackType() == 0 && !isInConeAngle(attacker, obj, (int) command.getConeLength(), (int) command.getConeWidth(), dirX, dirZ)) continue; if(!core.simulationService.checkLineOfSight(target, obj)) continue; if(!attemptCombat(attacker, (TangibleObject) obj)) continue; doSingleTargetCombat(attacker, (TangibleObject) obj, weapon, command, actionCounter); } } private void doSingleTargetCombat(CreatureObject attacker, CreatureObject target, WeaponObject weapon, CombatCommand command, int actionCounter) { float damage = calculateDamage(attacker, target, weapon, command); byte hitType = getHitType(attacker, target, weapon, command); switch(hitType) { case HitType.MISS: damage = 0; break; case HitType.DODGE: damage = 0; break; case HitType.PARRY: damage = 0; break; case HitType.CRITICAL: damage *= 1.5f; break; } byte mitigationType = -1; if(hitType == HitType.CRITICAL || hitType == HitType.HIT || hitType == HitType.STRIKETHROUGH) { mitigationType = doMitigationRolls(attacker, target, weapon, command, hitType); if(mitigationType == HitType.GLANCE) { damage *= 0.4f; } else if(mitigationType == HitType.EVASION) { float evasionValue = (attacker.getSkillMod("combat_evasion_value").getBase() / 4) / 100; damage *= (1 - evasionValue); } } int damageBeforeArmor = (int) damage; damage *= (1 - getArmorReduction(attacker, target, weapon, command, hitType)); int armorAbsorbed = (int) (damageBeforeArmor - damage); if(mitigationType == HitType.BLOCK) { float blockValue = (attacker.getSkillMod("strength_modified").getBase() + attacker.getSkillMod("combat_block_value").getBase()) / 2 + 25; damage -= blockValue; } if(damage > 0) applyDamage(attacker, target, (int) damage); sendCombatPackets(attacker, target, weapon, command, actionCounter, damage, armorAbsorbed, hitType); if(FileUtilities.doesFileExist("scripts/commands/combat" + command.getCommandName() + ".py")) core.scriptService.callScript("scripts/commands/combat", command.getCommandName(), "run", core, attacker, target, null); } private void sendCombatPackets(CreatureObject attacker, CreatureObject target, WeaponObject weapon, CombatCommand command, int actionCounter, float damage, int armorAbsorbed, int hitType) { String animationStr = command.getRandomAnimation(weapon); CombatAction combatAction = new CombatAction(CRC.StringtoCRC(animationStr), attacker.getObjectID(), weapon.getObjectID(), target.getObjectID(), command.getCommandCRC()); ObjControllerMessage objController = new ObjControllerMessage(0x1B, combatAction); attacker.notifyObserversInRange(objController, true, 125); StartTask startTask = new StartTask(actionCounter, attacker.getObjectID(), command.getCommandCRC()); ObjControllerMessage objController2 = new ObjControllerMessage(0x0B, startTask); attacker.getClient().getSession().write(objController2.serialize()); CommandEnqueueRemove commandRemove = new CommandEnqueueRemove(attacker.getObjectID(), actionCounter); ObjControllerMessage objController3 = new ObjControllerMessage(0x0B, commandRemove); attacker.getClient().getSession().write(objController3.serialize()); CombatSpam combatSpam = new CombatSpam(attacker.getObjectID(), target.getObjectID(), weapon.getObjectID(), (int) damage, armorAbsorbed, hitType); ObjControllerMessage objController4 = new ObjControllerMessage(0x1B, combatSpam); IoBuffer spam = objController4.serialize(); attacker.getClient().getSession().write(spam); if(target.getClient() != null) target.getClient().getSession().write(spam); } private float getArmorReduction(CreatureObject attacker, CreatureObject target, WeaponObject weapon, CombatCommand command, byte hitType) { int elementalType = 1; if(command.getPercentFromWeapon() > 0) { // TODO: elemental mitigation and damage if(weapon.getStringAttribute("cat_wpn_damage.wpn_damage_type").equals("@obj_attr_n:armor_eff_kinetic")) elementalType = ElementalType.KINETIC; else if(weapon.getStringAttribute("cat_wpn_damage.wpn_damage_type").equals("@obj_attr_n:armor_eff_energy")) elementalType = ElementalType.ENERGY; } else { elementalType = command.getElementalType(); } int baseArmor = 0; switch(elementalType) { case ElementalType.KINETIC: baseArmor = target.getSkillMod("kinetic").getBase(); case ElementalType.ENERGY: baseArmor = target.getSkillMod("energy").getBase(); case ElementalType.HEAT: baseArmor = target.getSkillMod("heat").getBase(); case ElementalType.COLD: baseArmor = target.getSkillMod("cold").getBase(); case ElementalType.ACID: baseArmor = target.getSkillMod("acid").getBase(); case ElementalType.ELECTRICITY: baseArmor = target.getSkillMod("electricity").getBase(); } float mitigation = (float) (90 * (1 - Math.exp(-0.000125 * baseArmor)) + baseArmor / 9000); if(hitType == HitType.STRIKETHROUGH) { float stMaxValue = attacker.getSkillMod("combat_strikethrough_value").getBase() / 2 + attacker.getSkillMod("luck_modified").getBase() / 10; if(stMaxValue > 99) stMaxValue = 99; float stMinValue = stMaxValue / 2; float stValue = new Random().nextInt((int) (stMaxValue - stMinValue + 1)) + stMinValue; stValue /= 100; stValue = 1 - stValue; mitigation *= stValue; } return mitigation / 100; } private boolean attemptCombat(CreatureObject attacker, TangibleObject target) { if(target.getDefendersList().contains(attacker) && attacker.getDefendersList().contains(target)) return true; if(attacker.getStateBitmask() == 0x8000000) return false; if(!target.isAttackableBy(attacker)) return false; target.addDefender(attacker); attacker.addDefender(target); return true; } private boolean applySpecialCost(CreatureObject attacker, WeaponObject weapon, CombatCommand command) { float actionCost = command.getActionCost(); float healthCost = command.getHealthCost(); if(actionCost == 0 && healthCost == 0) return true; float newAction = attacker.getAction() - actionCost; if(newAction <= 0) return false; float newHealth = attacker.getHealth() - healthCost; if(newHealth <= 0) return false; if(newAction != attacker.getAction()) attacker.setAction((int) newAction); if(newHealth != attacker.getHealth()) attacker.setHealth((int) newHealth); return true; } private float calculateDamage(CreatureObject attacker, CreatureObject target, WeaponObject weapon, CombatCommand command) { float rawDamage = command.getAddedDamage(); if(command.getPercentFromWeapon() > 0 && weapon != attacker.getSlottedObject("default_weapon")) { float weaponMinDmg = weapon.getIntAttribute("cat_wpn_damage.wpn_damage_min"); float weaponMaxDmg = weapon.getIntAttribute("cat_wpn_damage.wpn_damage_max"); float weaponDmg = new Random().nextInt((int) (weaponMaxDmg - weaponMinDmg + 1)) + weaponMinDmg; weaponDmg *= command.getPercentFromWeapon(); rawDamage += weaponDmg; if(weapon.isMelee() && attacker.getSkillMod("strength_modified") != null) { if(attacker.getSkillMod("strength_modified").getBase() > 0) { rawDamage += ((attacker.getSkillMod("strength_modified").getBase() / 100) * 33); } } } else if(command.getPercentFromWeapon() > 0) { float weaponMinDmg = 50; float weaponMaxDmg = 100; float weaponDmg = new Random().nextInt((int) (weaponMaxDmg - weaponMinDmg + 1)) + weaponMinDmg; weaponDmg *= command.getPercentFromWeapon(); rawDamage += weaponDmg; if(weapon.isMelee() && attacker.getSkillMod("strength_modified") != null) { if(attacker.getSkillMod("strength_modified").getBase() > 0) { rawDamage += ((attacker.getSkillMod("strength_modified").getBase() / 100) * 33); } } } if(target.getSkillMod("damage_decrease_percentage") != null) { rawDamage *= (target.getSkillMod("damage_decrease_percentage").getBase() / 100); } return rawDamage; } private float calculateDamage(CreatureObject attacker, TangibleObject target, WeaponObject weapon, CombatCommand command) { float rawDamage = command.getAddedDamage(); if(command.getPercentFromWeapon() > 0 && weapon != attacker.getSlottedObject("default_weapon")) { float weaponMinDmg = weapon.getIntAttribute("cat_wpn_damage.wpn_damage_min"); float weaponMaxDmg = weapon.getIntAttribute("cat_wpn_damage.wpn_damage_max"); float weaponDmg = new Random().nextInt((int) (weaponMaxDmg - weaponMinDmg + 1)) + weaponMinDmg; weaponDmg *= command.getPercentFromWeapon(); rawDamage += weaponDmg; if(weapon.isMelee() && attacker.getSkillMod("strength_modified") != null) { if(attacker.getSkillMod("strength_modified").getBase() > 0) { rawDamage += ((attacker.getSkillMod("strength_modified").getBase() / 100) * 33); } } } else if(command.getPercentFromWeapon() > 0) { float weaponMinDmg = 50; float weaponMaxDmg = 100; float weaponDmg = new Random().nextInt((int) (weaponMaxDmg - weaponMinDmg + 1)) + weaponMinDmg; weaponDmg *= command.getPercentFromWeapon(); rawDamage += weaponDmg; if(weapon.isMelee() && attacker.getSkillMod("strength_modified") != null) { if(attacker.getSkillMod("strength_modified").getBase() > 0) { rawDamage += ((attacker.getSkillMod("strength_modified").getBase() / 100) * 33); } } } return rawDamage; } public byte getHitType(CreatureObject attacker, CreatureObject target, WeaponObject weapon, CombatCommand command) { Random rand = new Random(); float r; // negation rolls(parry, miss and dodge) can only roll on single target attacks, strikethrough also only rolls on single target attacks if(command.getAttackType() == 1) { if(weapon.isRanged()) { float missChance = 0.5f; if(attacker.getSkillMod("strength_modified").getBase() > 0) { float missNegation = (float) ((attacker.getSkillMod("strength_modified").getBase() / 100) * 0.1); if(missNegation > 0.4f) missNegation = 0.4f; missChance -= missNegation; } r = rand.nextFloat(); if(r <= missChance) return HitType.MISS; } float dodgeChance = target.getSkillMod("display_only_dodge").getBase() / 10000; r = rand.nextFloat(); if(r <= dodgeChance) return HitType.DODGE; WeaponObject weapon2 = (WeaponObject) core.objectService.getObject(((CreatureObject) target).getWeaponId()); if(weapon2 != null && weapon2.isMelee()) { float parryChance = target.getSkillMod("display_only_parry").getBase() / 10000; r = rand.nextFloat(); if(r <= parryChance) return HitType.PARRY; } float stChance = attacker.getSkillMod("display_only_strikethrough").getBase() / 10000; r = rand.nextFloat(); if(r <= stChance) return HitType.STRIKETHROUGH; } float critChance = attacker.getSkillMod("display_only_critical").getBase() / 10000; r = rand.nextFloat(); if(r <= critChance) return HitType.CRITICAL; // TODO: Punishing blow once AI is implemented return HitType.HIT; } public byte doMitigationRolls(CreatureObject attacker, CreatureObject target, WeaponObject weapon, CombatCommand command, byte hitType) { Random rand = new Random(); float r; float blockChance = target.getSkillMod("display_only_block").getBase() / 10000; r = rand.nextFloat(); if(r <= blockChance) return HitType.BLOCK; if(command.getAttackType() == 0 || command.getAttackType() == 2 || command.getAttackType() == 3) { float evasionChance = target.getSkillMod("display_only_evasion").getBase() / 10000; r = rand.nextFloat(); if(r <= evasionChance) return HitType.EVASION; } if(hitType == HitType.HIT) { float glanceChance = target.getSkillMod("display_only_glancing_blow").getBase() / 10000; r = rand.nextFloat(); if(r <= glanceChance) return HitType.GLANCE; } return -1; } public void applyDamage(CreatureObject attacker, CreatureObject target, int damage) { if(target.getHealth() - damage <= 0) { target.setHealth(1); target.setPosture((byte) 13); if(target.getSlottedObject("ghost") != null) attacker.sendSystemMessage("You incapacitate " + target.getCustomName() + ".", (byte) 0); return; } target.setHealth(target.getHealth() - damage); } private boolean isInConeAngle(CreatureObject attacker, SWGObject target, int coneLength, int coneWidth, float directionX, float directionZ) { float radius = coneWidth / 2; float angle = (float) (2 * Math.atan(coneLength / radius)); float targetX = target.getWorldPosition().x - attacker.getWorldPosition().x; float targetZ = target.getWorldPosition().z - attacker.getWorldPosition().z; float targetAngle = (float) (Math.atan2(targetZ, targetX) - Math.atan2(directionZ, directionX)); float degrees = (float) (targetAngle * 180 / Math.PI); float coneAngle = angle / 2; if(degrees > coneAngle || degrees < -coneAngle) return false; return true; } public enum HitType{; public static final byte MISS = 0; public static final byte DODGE = 1; public static final byte PARRY = 2; public static final byte STRIKETHROUGH = 3; public static final byte CRITICAL = 4; public static final byte PUNISHING = 5; public static final byte HIT = 6; public static final byte BLOCK = 7; public static final byte EVASION = 8; public static final byte GLANCE = 9; } public enum ElementalType {; public static final int KINETIC = 1; public static final int ENERGY = 2; public static final int BLAST = 4; public static final int STUN = 8; public static final int HEAT = 32; public static final int COLD = 64; public static final int ACID = 128; public static final int ELECTRICITY = 256; } }