Ejemplo n.º 1
0
/**
 * Whenever a hazard collides with an enemy, this function resolves the effect
 * Called by HazardManager
 *
 * Returns false on miss
 */
bool Enemy::takeHit(Hazard h) {
	if (stats.cur_state != ENEMY_DEAD && stats.cur_state != ENEMY_CRITDEAD) 
	{

		if (!stats.in_combat) {
			stats.in_combat = true;
			stats.last_seen.x = stats.hero_pos.x;
			stats.last_seen.y = stats.hero_pos.y;
			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;

		// if it's a miss, do nothing
		if (rand() % 100 > (h.accuracy - stats.avoidance + 25)) return false; 
		
		// calculate base damage
		int dmg;
		if (h.dmg_max > h.dmg_min) dmg = rand() % (h.dmg_max - h.dmg_min + 1) + h.dmg_min;
		else dmg = h.dmg_min;

		// apply elemental resistance
		// TODO: make this generic
		if (h.trait_elemental == ELEMENT_FIRE) {
			dmg = (dmg * stats.attunement_fire) / 100;
		}
		if (h.trait_elemental == ELEMENT_WATER) {
			dmg = (dmg * stats.attunement_ice) / 100;			
		}
		
		// substract absorption from armor
		int absorption;
		if (!h.trait_armor_penetration) { // armor penetration ignores all absorption
			if (stats.absorb_min == stats.absorb_max) absorption = stats.absorb_min;
			else absorption = stats.absorb_min + (rand() % (stats.absorb_max - stats.absorb_min + 1));
			dmg = dmg - absorption;
			if (dmg < 1 && h.dmg_min >= 1) dmg = 1; // TODO: when blocking, dmg can be reduced to 0
			if (dmg < 0) dmg = 0;
		}

		// check for crits
		int true_crit_chance = h.crit_chance;
		if (stats.stun_duration > 0 || stats.immobilize_duration > 0 || stats.slow_duration > 0)
			true_crit_chance += h.trait_crits_impaired;
			
		bool crit = (rand() % 100) < true_crit_chance;
		if (crit) {
			dmg = dmg + h.dmg_max;
			map->shaky_cam_ticks = FRAMES_PER_SEC/2;
		}
		
		// apply damage
		stats.takeDamage(dmg);
		
		// damage always breaks stun
		if (dmg > 0) stats.stun_duration=0;
		
		// after effects
		if (stats.hp > 0) {
			if (h.stun_duration > stats.stun_duration) stats.stun_duration = h.stun_duration;
			if (h.slow_duration > stats.slow_duration) stats.slow_duration = h.slow_duration;
			if (h.bleed_duration > stats.bleed_duration) stats.bleed_duration = h.bleed_duration;
			if (h.immobilize_duration > stats.immobilize_duration) stats.immobilize_duration = h.immobilize_duration;
			if (h.forced_move_duration > stats.forced_move_duration) stats.forced_move_duration = h.forced_move_duration;
			if (h.forced_move_speed != 0) {
				float theta = powers->calcTheta(stats.hero_pos.x, stats.hero_pos.y, stats.pos.x, stats.pos.y);
				stats.forced_speed.x = ceil((float)h.forced_move_speed * cos(theta));
				stats.forced_speed.y = ceil((float)h.forced_move_speed * sin(theta));
			}
			if (h.hp_steal != 0) {
				h.src_stats->hp += (int)ceil((float)dmg * (float)h.hp_steal / 100.0);
				if (h.src_stats->hp > h.src_stats->maxhp) h.src_stats->hp = h.src_stats->maxhp;
			}
			if (h.mp_steal != 0) {
				h.src_stats->mp += (int)ceil((float)dmg * (float)h.mp_steal / 100.0);
				if (h.src_stats->mp > h.src_stats->maxmp) h.src_stats->mp = h.src_stats->maxmp;
			}
		}
		
		// post effect power
		if (h.post_power >= 0 && dmg > 0) {
			powers->activate(h.post_power, h.src_stats, stats.pos);
		}
		
		// interrupted to new state
		if (dmg > 0) {
			
			if (stats.hp <= 0 && crit) {
				doRewards();
				stats.cur_state = ENEMY_CRITDEAD;
			}
			else if (stats.hp <= 0) {
				doRewards();
				stats.cur_state = ENEMY_DEAD;		
			}
			// don't go through a hit animation if stunned
			else if (h.stun_duration == 0) {
				stats.cur_state = ENEMY_HIT;
			}
		}
		
		return true;
	}
	return false;
}
Ejemplo n.º 2
0
/**
 * logic()
 * Handle a single frame.  This includes:
 * - move the enemy based on AI % chances
 * - calculate the next frame of animation
 */
void Enemy::logic() {

	stats.logic();
	if (stats.forced_move_duration > 0) {
		move(); return;
	}
	if (stats.stun_duration > 0) return;
	// check for bleeding to death
	if (stats.hp <= 0 && !(stats.cur_state == ENEMY_DEAD || stats.cur_state == ENEMY_CRITDEAD)) {
		doRewards();
		stats.cur_state = ENEMY_DEAD;
	}
	// check for bleeding spurt
	if (stats.bleed_duration % 30 == 1) {
		powers->activate(POWER_SPARK_BLOOD, &stats, stats.pos);
	}
	
	// check for teleport powers
	if (stats.teleportation) {
		
		stats.pos.x = stats.teleport_destination.x;
		stats.pos.y = stats.teleport_destination.y;	
		
		stats.teleportation = false;	
	}
	
	int dist;
	int prev_direction;
	bool los = false;
	Point pursue_pos;
	
	// set a default pursue_pos, all else failing (used in targeting)
	pursue_pos.x = stats.hero_pos.x;
	pursue_pos.y = stats.hero_pos.y;
	
	
	// SECTION 1: Steering and Vision
	// ------------------------------
	
	// check distance and line of sight between enemy and hero
	if (stats.hero_alive)
		dist = getDistance(stats.hero_pos);
	else
		dist = 0;
	
	// if the hero is too far away or dead, abandon combat and do nothing
	if (dist > stats.threat_range+stats.threat_range || !stats.hero_alive) {
		stats.in_combat = false;
		stats.patrol_ticks = 0;
		stats.last_seen.x = -1;
		stats.last_seen.y = -1;
		
		// heal rapidly if the hero has left range
		if (stats.alive && stats.hero_alive) {
			stats.hp++;
			if (stats.hp > stats.maxhp) stats.hp = stats.maxhp;
		}
	}

	if (dist < stats.threat_range && stats.hero_alive)
		los = map->collider.line_of_sight(stats.pos.x, stats.pos.y, stats.hero_pos.x, stats.hero_pos.y);
	else
		los = false;

	// if the enemy can see the hero, it pursues.
	// otherwise, it will head towards where it last saw the hero
	if (los && dist < stats.threat_range) {
		stats.in_combat = true;
		stats.last_seen.x = stats.hero_pos.x;
		stats.last_seen.y = stats.hero_pos.y;
		powers->activate(stats.power_index[BEACON], &stats, stats.pos); //emit beacon
	}
	else if (stats.last_seen.x >= 0 && stats.last_seen.y >= 0) {
		if (getDistance(stats.last_seen) <= (stats.speed+stats.speed) && stats.patrol_ticks == 0) {
			stats.last_seen.x = -1;
			stats.last_seen.y = -1;
			stats.patrol_ticks = 8; // start patrol; see note on "patrolling" below
		}		
	}


	// where is the creature heading?
	// TODO: add fleeing for X ticks
	if (los) {
		pursue_pos.x = stats.last_seen.x = stats.hero_pos.x;
		pursue_pos.y = stats.last_seen.y = stats.hero_pos.y;
		stats.patrol_ticks = 0;
	}
	else if (stats.in_combat) {
	
		// "patrolling" is a simple way to help steering.
		// When the enemy arrives at where he last saw the hero, it continues
		// walking a few steps.  This gives a better chance of re-establishing
		// line of sight around corners.
		
		if (stats.patrol_ticks > 0) {
			stats.patrol_ticks--;
			if (stats.patrol_ticks == 0) {
				stats.in_combat = false;
			}			
		}
		pursue_pos.x = stats.last_seen.x;
		pursue_pos.y = stats.last_seen.y;
	}

	
	// SECTION 2: States
	// -----------------
	
	activeAnimation->advanceFrame();

	switch(stats.cur_state) {
	
		case ENEMY_STANCE:
		
			setAnimation("stance");
			
			if (stats.in_combat) {

				// update direction to face the target
				if (++stats.dir_ticks > stats.dir_favor && stats.patrol_ticks == 0) {
					stats.direction = face(pursue_pos.x, pursue_pos.y);				
					stats.dir_ticks = 0;
				}
		
				// performed ranged actions
				if (dist > stats.melee_range && stats.cooldown_ticks == 0) {

					// CHECK: ranged physical!
					//if (!powers->powers[stats.power_index[RANGED_PHYS]].requires_los || los) {
					if (los) {
						if ((rand() % 100) < stats.power_chance[RANGED_PHYS] && stats.power_ticks[RANGED_PHYS] == 0) {
							
							newState(ENEMY_RANGED_PHYS);
							break;
						}
					}
					// CHECK: ranged spell!
					//if (!powers->powers[stats.power_index[RANGED_MENT]].requires_los || los) {
					if (los) {			
						if ((rand() % 100) < stats.power_index[RANGED_MENT] && stats.power_ticks[RANGED_MENT] == 0) {
							
							newState(ENEMY_RANGED_MENT);
							break;
						}
					}
				
					// CHECK: flee!
					
					// CHECK: pursue!
					if ((rand() % 100) < stats.chance_pursue) {
						if (move()) { // no collision
							newState(ENEMY_MOVE);
						}
						else {
							// hit an obstacle, try the next best angle
							prev_direction = stats.direction;
							stats.direction = faceNextBest(pursue_pos.x, pursue_pos.y);
							if (move()) {
								newState(ENEMY_MOVE);
								break;
							}
							else stats.direction = prev_direction;
						}
					}
					
				}
				// perform melee actions
				else if (dist <= stats.melee_range && stats.cooldown_ticks == 0) {
				
					// CHECK: melee attack!
					//if (!powers->powers[stats.power_index[MELEE_PHYS]].requires_los || los) {
					if (los) {
						if ((rand() % 100) < stats.power_chance[MELEE_PHYS] && stats.power_ticks[MELEE_PHYS] == 0) {
							
							newState(ENEMY_MELEE_PHYS);
							break;
						}
					}
					// CHECK: melee ment!
					//if (!powers->powers[stats.power_index[MELEE_MENT]].requires_los || los) {
					if (los) {
						if ((rand() % 100) < stats.power_chance[MELEE_MENT] && stats.power_ticks[MELEE_MENT] == 0) {
													
							newState(ENEMY_MELEE_MENT);
							break;
						}
					}
				}
			}
			
			break;
		
		case ENEMY_MOVE:
		
			setAnimation("run");
	
			if (stats.in_combat) {

				if (++stats.dir_ticks > stats.dir_favor && stats.patrol_ticks == 0) {
					// if no line of movement to target, use pathfinder
					if ( !map->collider.line_of_movement(stats.pos.x, stats.pos.y, pursue_pos.x, pursue_pos.y)) {
						vector<Point> path;
						// if a path is returned, target first waypoint
						if ( map->collider.compute_path(stats.pos,pursue_pos,path) ) {
							pursue_pos = path.back();
						}
					}
					stats.direction = face(pursue_pos.x, pursue_pos.y);
					stats.dir_ticks = 0;
				}
				
				if (dist > stats.melee_range && stats.cooldown_ticks == 0) {
				
					// check ranged physical!
					//if (!powers->powers[stats.power_index[RANGED_PHYS]].requires_los || los) {
					if (los) {
						if ((rand() % 100) < stats.power_chance[RANGED_PHYS] && stats.power_ticks[RANGED_PHYS] == 0) {
							
							newState(ENEMY_RANGED_PHYS);
							break;
						}
					}
					// check ranged spell!
					// if (!powers->powers[stats.power_index[RANGED_MENT]].requires_los || los) {
					if (los) {
						if ((rand() % 100) < stats.power_chance[RANGED_MENT] && stats.power_ticks[RANGED_MENT] == 0) {
							
							newState(ENEMY_RANGED_MENT);
							break;
						}
					}
				
					if (!move()) {
						// hit an obstacle.  Try the next best angle
						prev_direction = stats.direction;
						stats.direction = faceNextBest(pursue_pos.x, pursue_pos.y);
						if (!move()) {
							newState(ENEMY_STANCE);
							stats.direction = prev_direction;
						}
					}
				}
				else {
					newState(ENEMY_STANCE);
				}
			}
			else {
				newState(ENEMY_STANCE);
			}
			break;
			
		case ENEMY_MELEE_PHYS:
			
			setAnimation("melee");

			if (activeAnimation->getCurFrame() == 1) {
				sfx_phys = true;
			}

			// the attack hazard is alive for a single frame
			if (activeAnimation->getCurFrame() == activeAnimation->getMaxFrame()/2 && haz == NULL) {
				powers->activate(stats.power_index[MELEE_PHYS], &stats, pursue_pos);
				stats.power_ticks[MELEE_PHYS] = stats.power_cooldown[MELEE_PHYS];
			}

			if (activeAnimation->getCurFrame() == activeAnimation->getMaxFrame()-1) {
				newState(ENEMY_STANCE);
				stats.cooldown_ticks = stats.cooldown;
			}
			break;

		case ENEMY_RANGED_PHYS:

			setAnimation("ranged");
	
			// monsters turn to keep aim at the hero
			stats.direction = face(pursue_pos.x, pursue_pos.y);
			
			if (activeAnimation->getCurFrame() == 1) {
				sfx_phys = true;
			}
			
			// the attack hazard is alive for a single frame
			if (activeAnimation->getCurFrame() == activeAnimation->getMaxFrame()/2 && haz == NULL) {
				powers->activate(stats.power_index[RANGED_PHYS], &stats, pursue_pos);
				stats.power_ticks[RANGED_PHYS] = stats.power_cooldown[RANGED_PHYS];
			}
			
			if (activeAnimation->getCurFrame() == activeAnimation->getMaxFrame()-1) {
				newState(ENEMY_STANCE);
				stats.cooldown_ticks = stats.cooldown;
			}
			break;

		
		case ENEMY_MELEE_MENT:
	
			setAnimation("ment");

			if (activeAnimation->getCurFrame() == 1) {
				sfx_ment = true;
			}
			
			// the attack hazard is alive for a single frame
			if (activeAnimation->getCurFrame() == activeAnimation->getMaxFrame()/2 && haz == NULL) {
				powers->activate(stats.power_index[MELEE_MENT], &stats, pursue_pos);
				stats.power_ticks[MELEE_MENT] = stats.power_cooldown[MELEE_MENT];
			}
			
			if (activeAnimation->getCurFrame() == activeAnimation->getMaxFrame()-1) {
				newState(ENEMY_STANCE);
				stats.cooldown_ticks = stats.cooldown;
			}
			break;

		case ENEMY_RANGED_MENT:

			setAnimation("ment");
		
			// monsters turn to keep aim at the hero
			stats.direction = face(pursue_pos.x, pursue_pos.y);
	
			if (activeAnimation->getCurFrame() == 1) {
				sfx_ment = true;
			}
			
			// the attack hazard is alive for a single frame
			if (activeAnimation->getCurFrame() == activeAnimation->getMaxFrame()/2 && haz == NULL) {
			
				powers->activate(stats.power_index[RANGED_MENT], &stats, pursue_pos);
				stats.power_ticks[RANGED_MENT] = stats.power_cooldown[RANGED_MENT];
			}
			
			if (activeAnimation->getCurFrame() == activeAnimation->getMaxFrame()-1) {
				newState(ENEMY_STANCE);
				stats.cooldown_ticks = stats.cooldown;
			}
			break;
			
		case ENEMY_SPAWN:
			// enemy is appearing out of nowhere

			setAnimation("spawn");
			if (activeAnimation->getCurFrame() == 1) {
				sfx_ment = true;
			}

			if (activeAnimation->getCurFrame() == activeAnimation->getMaxFrame()-1) {
				newState(ENEMY_STANCE);
			}
			
			break;

	
		case ENEMY_HIT:
			// enemy has taken damage (but isn't dead)

			setAnimation("hit");
			if (activeAnimation->getCurFrame() == 1) {
				sfx_hit = true;
			}

			if (activeAnimation->getCurFrame() == activeAnimation->getMaxFrame()-1) {
				newState(ENEMY_STANCE);
			}
			
			break;
			
		case ENEMY_DEAD:

			// corpse means the creature is dead and done animating		
			if (!stats.corpse) {
				setAnimation("die");
				
				if (activeAnimation->getCurFrame() == 1) {
					sfx_die = true;
				}
			}

			break;
		
		case ENEMY_CRITDEAD:
			// critdead is an optional, more gruesome death animation
		
			// corpse means the creature is dead and done animating
			if (!stats.corpse) {
				setAnimation("critdie");
				
				if (activeAnimation->getCurFrame() == 1) {
					sfx_critdie = true;
				}
			}
			
			break;
	}

}
Ejemplo n.º 3
0
/**
 * Whenever a hazard collides with an enemy, this function resolves the effect
 * Called by HazardManager
 *
 * Returns false on miss
 */
bool Enemy::takeHit(Hazard h) {
	if (stats.cur_state != ENEMY_DEAD && stats.cur_state != ENEMY_CRITDEAD) {
	
		if (!stats.in_combat) {
			stats.in_combat = true;
			stats.last_seen.x = stats.hero_pos.x;
			stats.last_seen.y = stats.hero_pos.y;
		}
	
		// auto-miss if recently attacked
		// this is mainly to prevent slow, wide missiles from getting multiple attack attempts
		if (stats.targeted > 0) return false;
		stats.targeted = 5;
		
		// if it's a miss, do nothing
	    if (rand() % 100 > (h.accuracy - stats.avoidance + 25)) return false; 
		
		// calculate base damage
		int dmg;
		if (h.dmg_max > h.dmg_min) dmg = rand() % (h.dmg_max - h.dmg_min + 1) + h.dmg_min;
		else dmg = h.dmg_min;

		// apply elemental resistance
		// TODO: make this generic
		if (h.trait_elemental == ELEMENT_FIRE) {
			dmg = (dmg * stats.attunement_fire) / 100;
		}
		if (h.trait_elemental == ELEMENT_WATER) {
			dmg = (dmg * stats.attunement_ice) / 100;			
		}
		
		// substract absorption from armor
		int absorption;
		if (!h.trait_armor_penetration) { // armor penetration ignores all absorption
			if (stats.absorb_min == stats.absorb_max) absorption = stats.absorb_min;
			else absorption = stats.absorb_min + (rand() % (stats.absorb_max - stats.absorb_min + 1));
			dmg = dmg - absorption;
			if (dmg < 1 && h.dmg_min >= 1) dmg = 1; // TODO: when blocking, dmg can be reduced to 0
			if (dmg < 0) dmg = 0;
		}

		// check for crits
		int true_crit_chance = h.crit_chance;
		if (stats.stun_duration > 0 || stats.immobilize_duration > 0 || stats.slow_duration > 0)
			true_crit_chance += h.trait_crits_impaired;
			
		bool crit = (rand() % 100) < true_crit_chance;
		if (crit) {
			dmg = dmg + h.dmg_max;
			map->shaky_cam_ticks = FRAMES_PER_SEC/2;
		}
		
		// apply damage
		stats.takeDamage(dmg);
		
		// damage always breaks stun
		if (dmg > 0) stats.stun_duration=0;
		
		// after effects
		if (stats.hp > 0) {
			if (h.stun_duration > stats.stun_duration) stats.stun_duration = h.stun_duration;
			if (h.slow_duration > stats.slow_duration) stats.slow_duration = h.slow_duration;
			if (h.bleed_duration > stats.bleed_duration) stats.bleed_duration = h.bleed_duration;
			if (h.immobilize_duration > stats.immobilize_duration) stats.immobilize_duration = h.immobilize_duration;
			if (h.hp_steal != 0) {
				h.src_stats->hp += ceil((float)dmg * (float)h.hp_steal / 100.0);
				if (h.src_stats->hp > h.src_stats->maxhp) h.src_stats->hp = h.src_stats->maxhp;
			}
			if (h.mp_steal != 0) {
				h.src_stats->mp += ceil((float)dmg * (float)h.mp_steal / 100.0);
				if (h.src_stats->mp > h.src_stats->maxmp) h.src_stats->mp = h.src_stats->maxmp;
			}
		}
		
		// post effect power
		Point pt;
		pt.x = pt.y = 0;
		if (h.post_power >= 0 && dmg > 0) {
			powers->activate(h.post_power, &stats, pt);
		}
		
		// interrupted to new state
		if (dmg > 0) {
			
			if (stats.hp <= 0 && crit) {
				doRewards();
				stats.cur_state = ENEMY_CRITDEAD;
			}
			else if (stats.hp <= 0) {
				doRewards();
				stats.cur_state = ENEMY_DEAD;		
			}
			// don't go through a hit animation if stunned
			else if (h.stun_duration == 0) {
				stats.cur_state = ENEMY_HIT;
			}
		}
		
		return true;
	}
	return false;
}
Ejemplo n.º 4
0
/**
 * 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;
}
Ejemplo n.º 5
0
/**
 * 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;
}