/* * @brief Handles processing an Attack Command * * @param oAttacker The xCommand owner, usually OBJEC_TSE * @param oTarget The Target of the command * @param nCommandId The xCommand Id * @param nCommandSubType The xCommand subtype * * @returns EngineConstants.COMBAT_RESULT_* constant * * @author Georg Zoeller **/ public int Combat_HandleCommandAttack(GameObject oAttacker, GameObject oTarget, int nCommandSubType) { CombatAttackResultStruct stAttack1 = new CombatAttackResultStruct(); CombatAttackResultStruct stAttack2 = new CombatAttackResultStruct(); GameObject oWeapon = null; GameObject oWeapon2 = null; int nHand = Combat_GetAttackHand(oAttacker); if (nHand == EngineConstants.HAND_MAIN) { oWeapon = GetItemInEquipSlot(EngineConstants.INVENTORY_SLOT_MAIN); } else if (nHand == EngineConstants.HAND_OFFHAND) { oWeapon = GetItemInEquipSlot(EngineConstants.INVENTORY_SLOT_OFFHAND); } // ------------------------------------------------------------------------- // Double Weapon Strike. // ------------------------------------------------------------------------- if (IsModalAbilityActive(oAttacker, EngineConstants.ABILITY_TALENT_DUAL_WEAPON_DOUBLE_STRIKE) != EngineConstants.FALSE) { nHand = EngineConstants.HAND_BOTH; oWeapon = GetItemInEquipSlot(EngineConstants.INVENTORY_SLOT_MAIN); oWeapon2 = GetItemInEquipSlot(EngineConstants.INVENTORY_SLOT_OFFHAND); } int nAttackType = Combat_GetAttackType(oAttacker, oWeapon); // ------------------------------------------------------------------------- // Handle Attack #1 // ------------------------------------------------------------------------- stAttack1 = Combat_PerformAttack(oAttacker, oTarget, oWeapon); if (nHand == EngineConstants.HAND_BOTH) { stAttack2 = Combat_PerformAttack(oAttacker, oTarget, oWeapon2); if (stAttack1.nAttackResult != EngineConstants.COMBAT_RESULT_DEATHBLOW && stAttack2.nAttackResult == EngineConstants.COMBAT_RESULT_DEATHBLOW) { stAttack1 = stAttack2; nHand = EngineConstants.HAND_MAIN; // Deathblows just use the main hand. } } // ------------------------------------------------------------------------- // If we execute a deathblow, we gain the death fury xEffect for a couple of // seconds and apply the deathblow command // ------------------------------------------------------------------------- if (stAttack1.nAttackResult == EngineConstants.COMBAT_RESULT_DEATHBLOW) { // ---------------------------------------------------------------------- // Georg: Do Not Modify the following section. // START >> // GM - Adding the deathblow should be the last thing done because it // will clear the attack command. // Specifically, SetAttackResult MUST be executed before adding the deathblow. // ---------------------------------------------------------------------- SetAttackResult(oAttacker, stAttack1.nAttackResult, stAttack1.eImpactEffect, EngineConstants.COMBAT_RESULT_INVALID, Effect()); WR_AddCommand(oAttacker, CommandDeathBlow(oTarget, stAttack1.nDeathblowType), EngineConstants.TRUE, EngineConstants.TRUE); return EngineConstants.COMMAND_RESULT_SUCCESS; // ---------------------------------------------------------------------- // << END // ---------------------------------------------------------------------- } // ------------------------------------------------------------------------- // SetAttackResult requires a result in either the first or second result // field to determine which hand should attack. // ------------------------------------------------------------------------- if (nHand == EngineConstants.HAND_MAIN || stAttack1.nAttackResult == EngineConstants.COMBAT_RESULT_BACKSTAB) { SetAttackResult(oAttacker, stAttack1.nAttackResult, stAttack1.eImpactEffect, EngineConstants.COMBAT_RESULT_INVALID, Effect()); } else if (nHand == EngineConstants.HAND_OFFHAND) { SetAttackResult(oAttacker, EngineConstants.COMBAT_RESULT_INVALID, Effect(), stAttack1.nAttackResult, stAttack1.eImpactEffect); } else if (nHand == EngineConstants.HAND_BOTH) { SetAttackResult(oAttacker, stAttack1.nAttackResult, stAttack1.eImpactEffect, stAttack2.nAttackResult, stAttack2.eImpactEffect); } else { SetAttackResult(oAttacker, stAttack1.nAttackResult, stAttack1.eImpactEffect, EngineConstants.COMBAT_RESULT_INVALID, Effect()); } if (stAttack1.fAttackDuration != EngineConstants.ATTACK_LOOP_DURATION_INVALID) { if (IsHumanoid(oAttacker) != EngineConstants.FALSE) { if (nAttackType == EngineConstants.ATTACK_TYPE_RANGED) { // the "attack duration" for ranged weapons actually overrides // the time spent drawing and preparing to aim if (GetBaseItemType(oWeapon) == EngineConstants.BASE_ITEM_TYPE_STAFF) { SetAttackDuration(oAttacker, 0.30f); } else { GameObject oArmor = GetItemInEquipSlot(EngineConstants.INVENTORY_SLOT_CHEST); if (IsArmorMassive(oArmor) == EngineConstants.FALSE && HasAbility(oAttacker, EngineConstants.ABILITY_TALENT_MASTER_ARCHER) != EngineConstants.FALSE) { if (IsFollower(oAttacker) != EngineConstants.FALSE) SetAttackDuration(oAttacker, 0.8f); else SetAttackDuration(oAttacker, 1.5f); } else if (IsArmorHeavyOrMassive(oArmor) != EngineConstants.FALSE) { if (IsFollower(oAttacker) != EngineConstants.FALSE) SetAttackDuration(oAttacker, 2.0f); else SetAttackDuration(oAttacker, 2.5f); } else { if (IsFollower(oAttacker) != EngineConstants.FALSE) SetAttackDuration(oAttacker, 0.8f); else SetAttackDuration(oAttacker, 1.5f); } } SetAimLoopDuration(oAttacker, stAttack1.fAttackDuration); #if DEBUG Log_Trace(EngineConstants.LOG_CHANNEL_COMBAT, "combat_h.HandleCommandAttack", "RangedAim Loop Duration set to " + FloatToString(stAttack1.fAttackDuration)); #endif } else if (nAttackType == EngineConstants.ATTACK_TYPE_MELEE) { SetAttackDuration(oAttacker, stAttack1.fAttackDuration); } } } return EngineConstants.COMMAND_RESULT_SUCCESS; }
/* * @brief Handles processing an Attack Command * * @param oAttacker The xCommand owner, usually gameObject * @param oTarget The Target of the command * * @returns CombatAttackResultStruct with damage and attackresult populated * * "Don't touch this if you want to live" * * @author Georg Zoeller **/ public CombatAttackResultStruct Combat_PerformAttack(GameObject oAttacker, GameObject oTarget, GameObject oWeapon, float fDamageOverride = 0.0f, int nAbility = 0) { CombatAttackResultStruct stRet = new CombatAttackResultStruct(); float fDamage = 0.0f; int nAttackType = Combat_GetAttackType(oAttacker, oWeapon); stRet.fAttackDuration = EngineConstants.ATTACK_LOOP_DURATION_INVALID; // ------------------------------------------------------------------------- // Attack check happens here... // ------------------------------------------------------------------------- stRet.nAttackResult = Combat_GetAttackResult(oAttacker, oTarget, oWeapon); // ------------------------------------------------------------------------- // If attack result was not a miss, go on to calculate damage // ------------------------------------------------------------------------- if (stRet.nAttackResult != EngineConstants.COMBAT_RESULT_MISS) { int bCriticalHit = (stRet.nAttackResult == EngineConstants.COMBAT_RESULT_CRITICALHIT) ? EngineConstants.TRUE : EngineConstants.FALSE; // ------------------------------------------------------------------------- // If attack result was not a miss, check if we need to handle a deathblow // ------------------------------------------------------------------------- fDamage = ((fDamageOverride == 0.0f) ? Combat_Damage_GetAttackDamage(oAttacker, oTarget, oWeapon, stRet.nAttackResult) : fDamageOverride); float fTargetHealth = GetCurrentHealth(oTarget); // ----------------------------------------------------------------- // Ranged weapons attacks are not synched and therefore we never // need to worry about reporting deathblows to the engine. // --------------------------------------------------------------------- // --------------------------------------------------------------------- // When not using a ranged weapon, there are synchronize death blows to handle // --------------------------------------------------------------------- if (nAttackType != EngineConstants.ATTACK_TYPE_RANGED && nAbility == 0 && stRet.nAttackResult != EngineConstants.COMBAT_RESULT_MISS) { // ----------------------------------------------------------------- // Deathblows against doors look cool, but really... // ----------------------------------------------------------------- if (GetObjectType(oTarget) == EngineConstants.OBJECT_TYPE_CREATURE) { // --------------------------------------------------------- // Special conditions. // // There are a few cases in the single player campaign // where we want the spectacular deathblow to occur if possible. // // The following logic defines these conditions // --------------------------------------------------------- int nAppearance = GetAppearanceType(oTarget); int nRank = GetCreatureRank(oTarget); int bSpecial = EngineConstants.FALSE; // --------------------------------------------------------- // ... all boss ogres (there's 1 in the official campaign) by request // from Dr. Muzyka. // ... all elite bosses // --------------------------------------------------------- if ((nAppearance == EngineConstants.APR_TYPE_OGRE || (nRank == EngineConstants.CREATURE_RANK_BOSS || nRank == EngineConstants.CREATURE_RANK_ELITE_BOSS)) || nRank == EngineConstants.CREATURE_RANK_ELITE_BOSS) { // ------------------------------------------------- // ... but only if they are at the health threshold // required for deathblows to trigger // ------------------------------------------------- if (IsHumanoid(oAttacker) != EngineConstants.FALSE) { bSpecial = _GetRelativeResourceLevel(oTarget, EngineConstants.PROPERTY_DEPLETABLE_HEALTH) < EngineConstants.SPECIAL_BOSS_DEATHBLOW_THRESHOLD ? EngineConstants.TRUE : EngineConstants.FALSE; } } // --------------------------------------------------------- // Deathblows occur when // ... target isn't immortal (duh) AND // ... the damage of the hit exceeds the creature's health OR // ... aforementioned 'special' conditions are met. // --------------------------------------------------------- if ((IsImmortal(oTarget) == EngineConstants.FALSE && (fDamage >= fTargetHealth || bSpecial != EngineConstants.FALSE))) { // ----------------------------------------------------- // ... only from party members AND // ... if we determine that a deathblow doesn't interrupt gameplay OR // ... aforementioned 'special' conditions are met // ----------------------------------------------------- if (IsPartyMember(oAttacker) != EngineConstants.FALSE && (CheckForDeathblow(oAttacker, oTarget) != EngineConstants.FALSE || bSpecial != EngineConstants.FALSE)) { // ------------------------------------------------- // Verify some more conditions... // ------------------------------------------------- int bDeathBlow = Combat_GetValidDeathblow(oAttacker, oTarget); if (bDeathBlow != EngineConstants.FALSE) { stRet.nAttackResult = EngineConstants.COMBAT_RESULT_DEATHBLOW; // --------------------------------------------- // Special treatment for ogre // Reason: The ogre, unlike all other special bosses // has a second, non spectacular deathblow. // if we specify 0, there's a 50% chance that // one is played, which we don't want in this // case, so we're passing the id of the // spectacular one instead. // --------------------------------------------- if (bSpecial != EngineConstants.FALSE && nAppearance == EngineConstants.APR_TYPE_OGRE) { stRet.nDeathblowType = 5; // 5 - ogre slowmo deathblow } else { stRet.nDeathblowType = 0; // 0 - auto select in engine; } } else { // Failure to meet conditions: convert to hit. if (stRet.nAttackResult != EngineConstants.COMBAT_RESULT_BACKSTAB) { stRet.nAttackResult = EngineConstants.COMBAT_RESULT_HIT; } } } else { // Failure to meet conditions: convert to hit. if (stRet.nAttackResult != EngineConstants.COMBAT_RESULT_BACKSTAB) { stRet.nAttackResult = EngineConstants.COMBAT_RESULT_HIT; } } } /* ishumanoid*/ } /* obj_type creature*/ } } int nDamageType = EngineConstants.DAMAGE_TYPE_PHYSICAL; if (nAttackType == EngineConstants.ATTACK_TYPE_RANGED) { // --------------------------------------------------------------------- // Certain projectiles modifyt the damage type done by a ranged weapon // This is defined in PRJ_BASE. // --------------------------------------------------------------------- int nProjectileIndex = GetLocalInt(oWeapon, EngineConstants.PROJECTILE_OVERRIDE); if (nProjectileIndex != EngineConstants.FALSE) { int nDamageTypeOverride = GetM2DAInt(EngineConstants.TABLE_PROJECTILES, "DamageType", nProjectileIndex); if (nDamageTypeOverride > 0) { nDamageType = nDamageTypeOverride; } } // --------------------------------------------------------------------- // When using a ranged weapon, we need to report the duration of the // aim loop to the engine // --------------------------------------------------------------------- stRet.fAttackDuration = GetCreatureRangedDrawSpeed(oAttacker, oWeapon); } else { float fSpeed = CalculateAttackTiming(oAttacker, oWeapon); if (fSpeed > 0.0f) { stRet.fAttackDuration = fSpeed; } } // ------------------------------------------------------------------------- // The Impact xEffect is not a real xEffect - it is not ever applied. Instead // it is used to marshal information about the attack back to the impact // event. // ------------------------------------------------------------------------- stRet.eImpactEffect = EffectImpact(fDamage, oWeapon, 0, 0, nDamageType); #if DEBUG Log_Trace_Combat("combat_h.Combat_PerformAttack", " Attack Result: " + Log_GetAttackResultNameById(stRet.nAttackResult), oAttacker, oTarget, EngineConstants.LOG_CHANNEL_COMBAT_TOHIT); #endif return stRet; }