/** * Whenever a hazard collides with an entity, this function resolves the effect * Called by HazardManager * * Returns false on miss */ bool Entity::takeHit(Hazard &h) { //check if this enemy should be affected by this hazard based on the category if(!powers->powers[h.power_index].target_categories.empty() && !stats.hero) { //the power has a target category requirement, so if it doesnt match, dont continue bool match_found = false; for (unsigned int i=0; i<stats.categories.size(); i++) { if(std::find(powers->powers[h.power_index].target_categories.begin(), powers->powers[h.power_index].target_categories.end(), stats.categories[i]) != powers->powers[h.power_index].target_categories.end()) { match_found = true; } } if(!match_found) return false; } // check if this entity allows attacks from this power id if (!stats.power_filter.empty() && std::find(stats.power_filter.begin(), stats.power_filter.end(), h.power_index) == stats.power_filter.end()) { return false; } //if the target is already dead, they cannot be hit if ((stats.cur_state == StatBlock::ENEMY_DEAD || stats.cur_state == StatBlock::ENEMY_CRITDEAD) && !stats.hero) return false; if(stats.cur_state == StatBlock::AVATAR_DEAD && stats.hero) return false; // some attacks will always miss enemies of a certain movement type if (stats.movement_type == MapCollision::MOVE_NORMAL && !h.power->target_movement_normal) return false; else if (stats.movement_type == MapCollision::MOVE_FLYING && !h.power->target_movement_flying) return false; else if (stats.movement_type == MapCollision::MOVE_INTANGIBLE && !h.power->target_movement_intangible) return false; // prevent hazard aoe from hitting targets behind walls if (h.power->walls_block_aoe && !mapr->collider.lineOfMovement(stats.pos.x, stats.pos.y, h.pos.x, h.pos.y, MapCollision::MOVE_NORMAL)) return false; // some enemies can be invicible based on campaign status if (!stats.hero && !stats.hero_ally && h.source_type != Power::SOURCE_TYPE_ENEMY) { bool invincible = false; for (size_t i = 0; i < stats.invincible_requires_status.size(); ++i) { if (!camp->checkStatus(stats.invincible_requires_status[i])) { invincible = false; break; } invincible = true; } if (invincible) return false; for (size_t i = 0; i < stats.invincible_requires_not_status.size(); ++i) { if (camp->checkStatus(stats.invincible_requires_not_status[i])) { invincible = false; break; } invincible = true; } if (invincible) return false; } //if the target is an enemy and they are not already in combat, activate a beacon to draw other enemies into battle if (!stats.in_combat && !stats.hero && !stats.hero_ally && !powers->powers[h.power_index].no_aggro) { stats.join_combat = true; } // exit if it was a beacon (to prevent stats.targeted from being set) if (powers->powers[h.power_index].beacon) return false; // prepare the combat text CombatText *combat_text = comb; if (h.power->type == Power::TYPE_MISSILE && Math::percentChance(stats.get(Stats::REFLECT))) { // reflect the missile 180 degrees h.setAngle(h.angle+static_cast<float>(M_PI)); // change hazard source to match the reflector's type // maybe we should change the source stats pointer to the reflector's StatBlock if (h.source_type == Power::SOURCE_TYPE_HERO || h.source_type == Power::SOURCE_TYPE_ALLY) h.source_type = Power::SOURCE_TYPE_ENEMY; else if (h.source_type == Power::SOURCE_TYPE_ENEMY) h.source_type = stats.hero ? Power::SOURCE_TYPE_HERO : Power::SOURCE_TYPE_ALLY; // reset the hazard ticks h.lifespan = h.power->lifespan; if (activeAnimation->getName() == "block") { playSound(Entity::SOUND_BLOCK); } return false; } // if it's a miss, do nothing int accuracy = h.accuracy; if(powers->powers[h.power_index].mod_accuracy_mode == Power::STAT_MODIFIER_MODE_MULTIPLY) accuracy = (accuracy * powers->powers[h.power_index].mod_accuracy_value) / 100; else if(powers->powers[h.power_index].mod_accuracy_mode == Power::STAT_MODIFIER_MODE_ADD) accuracy += powers->powers[h.power_index].mod_accuracy_value; else if(powers->powers[h.power_index].mod_accuracy_mode == Power::STAT_MODIFIER_MODE_ABSOLUTE) accuracy = powers->powers[h.power_index].mod_accuracy_value; int avoidance = 0; if(!powers->powers[h.power_index].trait_avoidance_ignore) { avoidance = stats.get(Stats::AVOIDANCE); } int true_avoidance = 100 - (accuracy - avoidance); bool is_overhit = (true_avoidance < 0 && !h.src_stats->perfect_accuracy) ? Math::percentChance(abs(true_avoidance)) : false; true_avoidance = std::min(std::max(true_avoidance, eset->combat.min_avoidance), eset->combat.max_avoidance); bool missed = false; if (!h.src_stats->perfect_accuracy && Math::percentChance(true_avoidance)) { missed = true; } // calculate base damage int dmg = Math::randBetween(h.dmg_min, h.dmg_max); if(powers->powers[h.power_index].mod_damage_mode == Power::STAT_MODIFIER_MODE_MULTIPLY) dmg = dmg * powers->powers[h.power_index].mod_damage_value_min / 100; else if(powers->powers[h.power_index].mod_damage_mode == Power::STAT_MODIFIER_MODE_ADD) dmg += powers->powers[h.power_index].mod_damage_value_min; else if(powers->powers[h.power_index].mod_damage_mode == Power::STAT_MODIFIER_MODE_ABSOLUTE) dmg = Math::randBetween(powers->powers[h.power_index].mod_damage_value_min, powers->powers[h.power_index].mod_damage_value_max); // apply elemental resistance if (h.power->trait_elemental >= 0 && static_cast<size_t>(h.power->trait_elemental) < stats.vulnerable.size()) { size_t i = h.power->trait_elemental; int vulnerable = std::max(stats.vulnerable[i], eset->combat.min_resist); if (stats.vulnerable[i] < 100) vulnerable = std::min(vulnerable, eset->combat.max_resist); dmg = (dmg * vulnerable) / 100; } if (!h.power->trait_armor_penetration) { // armor penetration ignores all absorption // subtract absorption from armor int absorption = Math::randBetween(stats.get(Stats::ABS_MIN), stats.get(Stats::ABS_MAX)); if (absorption > 0 && dmg > 0) { int abs = absorption; if (stats.effects.triggered_block) { if ((abs*100)/dmg < eset->combat.min_block) absorption = (dmg * eset->combat.min_block) /100; if ((abs*100)/dmg > eset->combat.max_block) absorption = (dmg * eset->combat.max_block) /100; } else { if ((abs*100)/dmg < eset->combat.min_absorb) absorption = (dmg * eset->combat.min_absorb) /100; if ((abs*100)/dmg > eset->combat.max_absorb) absorption = (dmg * eset->combat.max_absorb) /100; } // Sometimes, the absorb limits cause absorbtion to drop to 1 // This could be confusing to a player that has something with an absorb of 1 equipped // So we round absorption up in this case if (absorption == 0) absorption = 1; } dmg = dmg - absorption; if (dmg <= 0) { dmg = 0; if (!powers->powers[h.power_index].ignore_zero_damage) { if (h.power->trait_elemental < 0) { if (stats.effects.triggered_block && eset->combat.max_block < 100) dmg = 1; else if (!stats.effects.triggered_block && eset->combat.max_absorb < 100) dmg = 1; } else { if (eset->combat.max_resist < 100) dmg = 1; } if (activeAnimation->getName() == "block") { playSound(Entity::SOUND_BLOCK); resetActiveAnimation(); } } } } // check for crits int true_crit_chance = h.crit_chance; if(powers->powers[h.power_index].mod_crit_mode == Power::STAT_MODIFIER_MODE_MULTIPLY) true_crit_chance = true_crit_chance * powers->powers[h.power_index].mod_crit_value / 100; else if(powers->powers[h.power_index].mod_crit_mode == Power::STAT_MODIFIER_MODE_ADD) true_crit_chance += powers->powers[h.power_index].mod_crit_value; else if(powers->powers[h.power_index].mod_crit_mode == Power::STAT_MODIFIER_MODE_ABSOLUTE) true_crit_chance = powers->powers[h.power_index].mod_crit_value; if (stats.effects.stun || stats.effects.speed < 100) true_crit_chance += h.power->trait_crits_impaired; bool crit = Math::percentChance(true_crit_chance); if (crit) { // default is dmg * 2 dmg = (dmg * Math::randBetween(eset->combat.min_crit_damage, eset->combat.max_crit_damage)) / 100; if(!stats.hero) mapr->shaky_cam_timer.setDuration(settings->max_frames_per_sec/2); } else if (is_overhit) { dmg = (dmg * Math::randBetween(eset->combat.min_overhit_damage, eset->combat.max_overhit_damage)) / 100; // Should we use shakycam for overhits? } // misses cause reduced damage if (missed) { dmg = (dmg * Math::randBetween(eset->combat.min_miss_damage, eset->combat.max_miss_damage)) / 100; } if (!powers->powers[h.power_index].ignore_zero_damage) { if (dmg == 0) { combat_text->addString(msg->get("miss"), stats.pos, CombatText::MSG_MISS); return false; } else if(stats.hero) combat_text->addInt(dmg, stats.pos, CombatText::MSG_TAKEDMG); else { if(crit || is_overhit) combat_text->addInt(dmg, stats.pos, CombatText::MSG_CRIT); else if (missed) combat_text->addInt(dmg, stats.pos, CombatText::MSG_MISS); else combat_text->addInt(dmg, stats.pos, CombatText::MSG_GIVEDMG); } } // temporarily save the current HP for calculating HP/MP steal on final blow int prev_hp = stats.hp; // save debuff status to check for on_debuff powers later bool was_debuffed = stats.effects.isDebuffed(); // apply damage stats.takeDamage(dmg); // after effects if (dmg > 0 || powers->powers[h.power_index].ignore_zero_damage) { // damage always breaks stun stats.effects.removeEffectType(Effect::STUN); powers->effect(&stats, h.src_stats, static_cast<int>(h.power_index), h.source_type); // HP/MP steal is cumulative between stat bonus and power bonus int hp_steal = h.power->hp_steal + h.src_stats->get(Stats::HP_STEAL); if (!stats.effects.immunity_hp_steal && hp_steal != 0) { int steal_amt = (std::min(dmg, prev_hp) * hp_steal) / 100; if (steal_amt == 0) steal_amt = 1; combat_text->addString(msg->get("+%d HP",steal_amt), h.src_stats->pos, CombatText::MSG_BUFF); h.src_stats->hp = std::min(h.src_stats->hp + steal_amt, h.src_stats->get(Stats::HP_MAX)); } int mp_steal = h.power->mp_steal + h.src_stats->get(Stats::MP_STEAL); if (!stats.effects.immunity_mp_steal && mp_steal != 0) { int steal_amt = (std::min(dmg, prev_hp) * mp_steal) / 100; if (steal_amt == 0) steal_amt = 1; combat_text->addString(msg->get("+%d MP",steal_amt), h.src_stats->pos, CombatText::MSG_BUFF); h.src_stats->mp = std::min(h.src_stats->mp + steal_amt, h.src_stats->get(Stats::MP_MAX)); } // deal return damage if (!h.src_stats->effects.immunity_damage_reflect && stats.get(Stats::RETURN_DAMAGE) > 0) { int dmg_return = static_cast<int>(static_cast<float>(dmg * stats.get(Stats::RETURN_DAMAGE)) / 100.f); if (dmg_return == 0) dmg_return = 1; h.src_stats->takeDamage(dmg_return); comb->addInt(dmg_return, h.src_stats->pos, CombatText::MSG_GIVEDMG); } } if (dmg > 0 || powers->powers[h.power_index].ignore_zero_damage) { // remove effect by ID stats.effects.removeEffectID(powers->powers[h.power_index].remove_effects); // post power if (h.power->post_power > 0 && Math::percentChance(h.power->post_power_chance)) { powers->activate(h.power->post_power, h.src_stats, stats.pos); } } // interrupted to new state if (dmg > 0) { bool chance_poise = Math::percentChance(stats.get(Stats::POISE)); if(stats.hp <= 0) { stats.effects.triggered_death = true; if(stats.hero) stats.cur_state = StatBlock::AVATAR_DEAD; else { doRewards(h.source_type); if (crit) stats.cur_state = StatBlock::ENEMY_CRITDEAD; else stats.cur_state = StatBlock::ENEMY_DEAD; mapr->collider.unblock(stats.pos.x,stats.pos.y); } return true; } // play hit sound effect, but only if the hit cooldown is done if (stats.cooldown_hit.isEnd()) playSound(Entity::SOUND_HIT); // if this hit caused a debuff, activate an on_debuff power if (!was_debuffed && stats.effects.isDebuffed()) { StatBlock::AIPower* ai_power = stats.getAIPower(StatBlock::AI_POWER_DEBUFF); if (ai_power != NULL) { stats.cur_state = StatBlock::ENEMY_POWER; stats.activated_power = ai_power; stats.cooldown.reset(Timer::END); // ignore global cooldown return true; } } // roll to see if the enemy's ON_HIT power is casted StatBlock::AIPower* ai_power = stats.getAIPower(StatBlock::AI_POWER_HIT); if (ai_power != NULL) { stats.cur_state = StatBlock::ENEMY_POWER; stats.activated_power = ai_power; stats.cooldown.reset(Timer::END); // ignore global cooldown return true; } // don't go through a hit animation if stunned or successfully poised // however, critical hits ignore poise if(stats.cooldown_hit.isEnd()) { stats.cooldown_hit.reset(Timer::BEGIN); if (!stats.effects.stun && (!chance_poise || crit) && !stats.prevent_interrupt) { if(stats.hero) { stats.cur_state = StatBlock::AVATAR_HIT; } else { if (stats.cur_state == StatBlock::ENEMY_POWER) { stats.cooldown.reset(Timer::BEGIN); stats.activated_power = NULL; } stats.cur_state = StatBlock::ENEMY_HIT; } if (stats.untransform_on_hit) stats.transform_duration = 0; } } } return true; }
/** * Whenever a hazard collides with an entity, this function resolves the effect * Called by HazardManager * * Returns false on miss */ bool Entity::takeHit(Hazard &h) { //check if this enemy should be affected by this hazard based on the category if(!powers->powers[h.power_index].target_categories.empty() && !stats.hero) { //the power has a target category requirement, so if it doesnt match, dont continue bool match_found = false; for (unsigned int i=0; i<stats.categories.size(); i++) { if(std::find(powers->powers[h.power_index].target_categories.begin(), powers->powers[h.power_index].target_categories.end(), stats.categories[i]) != powers->powers[h.power_index].target_categories.end()) { match_found = true; } } if(!match_found) return false; } //if the target is already dead, they cannot be hit if ((stats.cur_state == ENEMY_DEAD || stats.cur_state == ENEMY_CRITDEAD) && !stats.hero) return false; if(stats.cur_state == AVATAR_DEAD && stats.hero) return false; //if the target is an enemy and they are not already in combat, activate a beacon to draw other enemies into battle if (!stats.in_combat && !stats.hero && !stats.hero_ally) { stats.join_combat = true; stats.in_combat = true; powers->activate(stats.power_index[BEACON], &stats, stats.pos); //emit beacon } // exit if it was a beacon (to prevent stats.targeted from being set) if (powers->powers[h.power_index].beacon) return false; // prepare the combat text CombatText *combat_text = comb; // if it's a miss, do nothing int accuracy = h.accuracy; if(powers->powers[h.power_index].mod_accuracy_mode == STAT_MODIFIER_MODE_MULTIPLY) accuracy = accuracy * powers->powers[h.power_index].mod_accuracy_value / 100; else if(powers->powers[h.power_index].mod_accuracy_mode == STAT_MODIFIER_MODE_ADD) accuracy += powers->powers[h.power_index].mod_accuracy_value; else if(powers->powers[h.power_index].mod_accuracy_mode == STAT_MODIFIER_MODE_ABSOLUTE) accuracy = powers->powers[h.power_index].mod_accuracy_value; int avoidance = 0; if(!powers->powers[h.power_index].trait_avoidance_ignore) { avoidance = stats.get(STAT_AVOIDANCE); } int true_avoidance = 100 - (accuracy + 25 - avoidance); //if we are using an absolute accuracy, offset the constant 25 added to the accuracy if(powers->powers[h.power_index].mod_accuracy_mode == STAT_MODIFIER_MODE_ABSOLUTE) true_avoidance += 25; clampFloor(true_avoidance, MIN_AVOIDANCE); clampCeil(true_avoidance, MAX_AVOIDANCE); if (h.missile && percentChance(stats.get(STAT_REFLECT))) { // reflect the missile 180 degrees h.setAngle(h.angle+M_PI); // change hazard source to match the reflector's type // maybe we should change the source stats pointer to the reflector's StatBlock if (h.source_type == SOURCE_TYPE_HERO || h.source_type == SOURCE_TYPE_ALLY) h.source_type = SOURCE_TYPE_ENEMY; else if (h.source_type == SOURCE_TYPE_ENEMY) h.source_type = stats.hero ? SOURCE_TYPE_HERO : SOURCE_TYPE_ALLY; // reset the hazard ticks h.lifespan = h.base_lifespan; if (activeAnimation->getName() == "block") { play_sfx_block = true; } return false; } if (percentChance(true_avoidance)) { combat_text->addMessage(msg->get("miss"), stats.pos, COMBAT_MESSAGE_MISS); return false; } // calculate base damage int dmg = randBetween(h.dmg_min, h.dmg_max); if(powers->powers[h.power_index].mod_damage_mode == STAT_MODIFIER_MODE_MULTIPLY) dmg = dmg * powers->powers[h.power_index].mod_damage_value_min / 100; else if(powers->powers[h.power_index].mod_damage_mode == STAT_MODIFIER_MODE_ADD) dmg += powers->powers[h.power_index].mod_damage_value_min; else if(powers->powers[h.power_index].mod_damage_mode == STAT_MODIFIER_MODE_ABSOLUTE) dmg = randBetween(powers->powers[h.power_index].mod_damage_value_min, powers->powers[h.power_index].mod_damage_value_max); // apply elemental resistance if (h.trait_elemental >= 0 && unsigned(h.trait_elemental) < stats.vulnerable.size()) { unsigned i = h.trait_elemental; int vulnerable = stats.vulnerable[i]; clampFloor(vulnerable,MIN_RESIST); if (stats.vulnerable[i] < 100) clampCeil(vulnerable,MAX_RESIST); dmg = (dmg * vulnerable) / 100; } if (!h.trait_armor_penetration) { // armor penetration ignores all absorption // substract absorption from armor int absorption = randBetween(stats.get(STAT_ABS_MIN), stats.get(STAT_ABS_MAX)); if (absorption > 0 && dmg > 0) { int abs = absorption; if ((abs*100)/dmg < MIN_BLOCK) absorption = (dmg * MIN_BLOCK) /100; if ((abs*100)/dmg > MAX_BLOCK) absorption = (dmg * MAX_BLOCK) /100; if ((abs*100)/dmg < MIN_ABSORB && !stats.effects.triggered_block) absorption = (dmg * MIN_ABSORB) /100; if ((abs*100)/dmg > MAX_ABSORB && !stats.effects.triggered_block) absorption = (dmg * MAX_ABSORB) /100; // Sometimes, the absorb limits cause absorbtion to drop to 1 // This could be confusing to a player that has something with an absorb of 1 equipped // So we round absorption up in this case if (absorption == 0) absorption = 1; } dmg = dmg - absorption; if (dmg <= 0) { dmg = 0; if (h.trait_elemental < 0) { if (stats.effects.triggered_block && MAX_BLOCK < 100) dmg = 1; else if (!stats.effects.triggered_block && MAX_ABSORB < 100) dmg = 1; } else { if (MAX_RESIST < 100) dmg = 1; } if (activeAnimation->getName() == "block") { play_sfx_block = true; resetActiveAnimation(); } } } // check for crits int true_crit_chance = h.crit_chance; if(powers->powers[h.power_index].mod_crit_mode == STAT_MODIFIER_MODE_MULTIPLY) true_crit_chance = true_crit_chance * powers->powers[h.power_index].mod_crit_value / 100; else if(powers->powers[h.power_index].mod_crit_mode == STAT_MODIFIER_MODE_ADD) true_crit_chance += powers->powers[h.power_index].mod_crit_value; else if(powers->powers[h.power_index].mod_crit_mode == STAT_MODIFIER_MODE_ABSOLUTE) true_crit_chance = powers->powers[h.power_index].mod_crit_value; if (stats.effects.stun || stats.effects.speed < 100) true_crit_chance += h.trait_crits_impaired; bool crit = percentChance(true_crit_chance); if (crit) { dmg = dmg + h.dmg_max; if(!stats.hero) mapr->shaky_cam_ticks = MAX_FRAMES_PER_SEC/2; } if(stats.hero) combat_text->addMessage(dmg, stats.pos, COMBAT_MESSAGE_TAKEDMG); else { if(crit) combat_text->addMessage(dmg, stats.pos, COMBAT_MESSAGE_CRIT); else combat_text->addMessage(dmg, stats.pos, COMBAT_MESSAGE_GIVEDMG); } // temporarily save the current HP for calculating HP/MP steal on final blow int prev_hp = stats.hp; // apply damage stats.takeDamage(dmg); // after effects if (dmg > 0) { // damage always breaks stun stats.effects.removeEffectType(EFFECT_STUN); if (stats.hp > 0) { powers->effect(&stats, h.src_stats, h.power_index,h.source_type); } if (!stats.effects.immunity) { if (h.hp_steal != 0) { int steal_amt = (std::min(dmg, prev_hp) * h.hp_steal) / 100; if (steal_amt == 0) steal_amt = 1; combat_text->addMessage(msg->get("+%d HP",steal_amt), h.src_stats->pos, COMBAT_MESSAGE_BUFF); h.src_stats->hp = std::min(h.src_stats->hp + steal_amt, h.src_stats->get(STAT_HP_MAX)); } if (h.mp_steal != 0) { int steal_amt = (std::min(dmg, prev_hp) * h.mp_steal) / 100; if (steal_amt == 0) steal_amt = 1; combat_text->addMessage(msg->get("+%d MP",steal_amt), h.src_stats->pos, COMBAT_MESSAGE_BUFF); h.src_stats->mp = std::min(h.src_stats->mp + steal_amt, h.src_stats->get(STAT_MP_MAX)); } } } // post effect power if (h.post_power > 0 && dmg > 0) { powers->activate(h.post_power, h.src_stats, stats.pos); } // loot if (dmg > 0 && !h.loot.empty()) { for (unsigned i=0; i<h.loot.size(); i++) { powers->loot.push_back(h.loot[i]); powers->loot.back().x = (int)stats.pos.x; powers->loot.back().y = (int)stats.pos.y; } } // interrupted to new state if (dmg > 0) { bool chance_poise = percentChance(stats.get(STAT_POISE)); if(stats.hp <= 0) { stats.effects.triggered_death = true; if(stats.hero) stats.cur_state = AVATAR_DEAD; else { doRewards(h.source_type); if (crit) stats.cur_state = ENEMY_CRITDEAD; else stats.cur_state = ENEMY_DEAD; mapr->collider.unblock(stats.pos.x,stats.pos.y); } } // don't go through a hit animation if stunned else if (!stats.effects.stun && !chance_poise) { play_sfx_hit = true; if(!chance_poise && stats.cooldown_hit_ticks == 0) { if(stats.hero) stats.cur_state = AVATAR_HIT; else stats.cur_state = ENEMY_HIT; stats.cooldown_hit_ticks = stats.cooldown_hit; if (stats.untransform_on_hit) stats.transform_duration = 0; } // roll to see if the enemy's ON_HIT power is casted if (percentChance(stats.power_chance[ON_HIT])) { powers->activate(stats.power_index[ON_HIT], &stats, stats.pos); } } // just play the hit sound else play_sfx_hit = true; } return true; }