void BehaviorStandard::checkMoveStateStance() { // If the enemy is capable of fleeing and is at a safe distance, have it hold its position instead of moving if (hero_dist >= e->stats.threat_range/2 && e->stats.chance_flee > 0 && e->stats.waypoints.empty()) return; if ((target_dist > e->stats.melee_range && percentChance(e->stats.chance_pursue)) || fleeing) { if (e->move()) { e->stats.cur_state = ENEMY_MOVE; } else { collided = true; if (fleeing || move_to_safe_dist) { fleeing = false; move_to_safe_dist = false; } else { unsigned char prev_direction = e->stats.direction; // hit an obstacle, try the next best angle e->stats.direction = e->faceNextBest(pursue_pos.x, pursue_pos.y); if (e->move()) { e->stats.cur_state = ENEMY_MOVE; } else e->stats.direction = prev_direction; } } } }
void BehaviorStandard::checkMoveStateStance() { // If the enemy is capable of fleeing and is at a safe distance, have it hold its position instead of moving if (hero_dist >= e->stats.threat_range/2 && e->stats.chance_flee > 0) return; if ((hero_dist > e->stats.melee_range && percentChance(e->stats.chance_pursue)) || fleeing) { if (e->move()) { e->newState(ENEMY_MOVE); } else { int prev_direction = e->stats.direction; // hit an obstacle, try the next best angle e->stats.direction = e->faceNextBest(pursue_pos.x, pursue_pos.y); if (e->move()) { e->newState(ENEMY_MOVE); } else e->stats.direction = prev_direction; } } }
/** * Perform miscellaneous state-based actions. * 1) Set animations and sound effects * 2) Return to the default state (Stance) when actions are complete */ void BehaviorStandard::updateState() { // stunned enemies can't act if (e->stats.effects.stun) return; int power_id; int power_state; // continue current animations e->activeAnimation->advanceFrame(); switch (e->stats.cur_state) { case ENEMY_STANCE: e->setAnimation("stance"); break; case ENEMY_MOVE: e->setAnimation("run"); break; case ENEMY_POWER: power_id = e->stats.power_index[e->stats.activated_powerslot]; power_state = powers->powers[power_id].new_state; // animation based on power type if (power_state == POWSTATE_INSTANT) e->instant_power = true; else if (power_state == POWSTATE_ATTACK) e->setAnimation(powers->powers[power_id].attack_anim); // sound effect based on power type if (e->activeAnimation->isFirstFrame()) { if (powers->powers[power_id].attack_anim == "swing" || powers->powers[power_id].attack_anim == "shoot") e->play_sfx_phys = true; else if (powers->powers[power_id].attack_anim == "cast") e->play_sfx_ment = true; } if (e->activeAnimation->isLastFrame() || (power_state == POWSTATE_ATTACK && e->activeAnimation->getName() != powers->powers[power_id].attack_anim)) e->newState(ENEMY_STANCE); break; case ENEMY_SPAWN: e->setAnimation("spawn"); //the second check is needed in case the entity does not have a spawn animation if (e->activeAnimation->isLastFrame() || e->activeAnimation->getName() != "spawn") { e->newState(ENEMY_STANCE); } break; case ENEMY_BLOCK: e->setAnimation("block"); break; case ENEMY_HIT: e->setAnimation("hit"); if (e->activeAnimation->isFirstFrame()) { e->stats.effects.triggered_hit = true; } if (e->activeAnimation->isLastFrame() || e->activeAnimation->getName() != "hit") e->newState(ENEMY_STANCE); break; case ENEMY_DEAD: if (e->stats.effects.triggered_death) break; e->setAnimation("die"); if (e->activeAnimation->isFirstFrame()) { e->play_sfx_die = true; e->stats.corpse_ticks = CORPSE_TIMEOUT; e->stats.effects.clearEffects(); } if (e->activeAnimation->isSecondLastFrame()) { if (percentChance(e->stats.power_chance[ON_DEATH])) powers->activate(e->stats.power_index[ON_DEATH], &e->stats, e->stats.pos); } if (e->activeAnimation->isLastFrame() || e->activeAnimation->getName() != "die") { // puts renderable under object layer e->stats.corpse = true; //allow free movement over the corpse mapr->collider.unblock(e->stats.pos.x, e->stats.pos.y); // remove corpses that land on blocked tiles, such as water or pits if (!mapr->collider.is_valid_position(e->stats.pos.x, e->stats.pos.y, MOVEMENT_NORMAL, false)) { e->stats.corpse_ticks = 0; } // prevent "jumping" when rendering alignFPoint(&e->stats.pos); } break; case ENEMY_CRITDEAD: e->setAnimation("critdie"); if (e->activeAnimation->isFirstFrame()) { e->play_sfx_critdie = true; e->stats.corpse_ticks = CORPSE_TIMEOUT; e->stats.effects.clearEffects(); } if (e->activeAnimation->isSecondLastFrame()) { if (percentChance(e->stats.power_chance[ON_DEATH])) powers->activate(e->stats.power_index[ON_DEATH], &e->stats, e->stats.pos); } if (e->activeAnimation->isLastFrame() || e->activeAnimation->getName() != "critdie") { // puts renderable under object layer e->stats.corpse = true; //allow free movement over the corpse mapr->collider.unblock(e->stats.pos.x, e->stats.pos.y); // prevent "jumping" when rendering alignFPoint(&e->stats.pos); } break; default: break; } }
/** * Check state changes related to movement */ void BehaviorStandard::checkMove() { // dying enemies can't move if (e->stats.cur_state == ENEMY_DEAD || e->stats.cur_state == ENEMY_CRITDEAD) return; // stunned enemies can't act if (e->stats.effects.stun) return; // handle not being in combat and (not patrolling waypoints or waiting at waypoint) if (!e->stats.hero_ally && !e->stats.in_combat && (e->stats.waypoints.empty() || e->stats.waypoint_pause_ticks > 0)) { if (e->stats.cur_state == ENEMY_MOVE) { e->newState(ENEMY_STANCE); } // currently enemies only move while in combat or patrolling return; } // clear current space to allow correct movement mapr->collider.unblock(e->stats.pos.x, e->stats.pos.y); // update direction if (e->stats.facing) { if (++e->stats.turn_ticks > e->stats.turn_delay) { // if blocked, face in pathfinder direction instead if (!mapr->collider.line_of_movement(e->stats.pos.x, e->stats.pos.y, pursue_pos.x, pursue_pos.y, e->stats.movement_type)) { // if a path is returned, target first waypoint bool recalculate_path = false; //if theres no path, it needs to be calculated if(path.empty()) recalculate_path = true; //if the target moved more than 1 tile away, recalculate if(calcDist(map_to_collision(prev_target), map_to_collision(pursue_pos)) > 1.f) recalculate_path = true; //if a collision ocurred then recalculate if(collided) recalculate_path = true; //add a 5% chance to recalculate on every frame. This prevents reclaulating lots of entities in the same frame chance_calc_path += 5; if(percentChance(chance_calc_path)) recalculate_path = true; //dont recalculate if we were blocked and no path was found last time //this makes sure that pathfinding calculation is not spammed when the target is unreachable and the entity is as close as its going to get if(!path_found && collided && !percentChance(chance_calc_path)) recalculate_path = false; else//reset the collision flag only if we dont want the cooldown in place collided = false; prev_target = pursue_pos; // target first waypoint if(recalculate_path) { chance_calc_path = -100; path.clear(); path_found = mapr->collider.compute_path(e->stats.pos, pursue_pos, path, e->stats.movement_type); } if(!path.empty()) { pursue_pos = path.back(); //if distance to node is lower than a tile size, the node is going to be passed and can be removed if(calcDist(e->stats.pos, pursue_pos) <= 1.f) path.pop_back(); } } else { path.clear(); } if(fleeing) e->stats.direction = calcDirection(pursue_pos, e->stats.pos); else e->stats.direction = calcDirection(e->stats.pos, pursue_pos); e->stats.turn_ticks = 0; } } // try to start moving if (e->stats.cur_state == ENEMY_STANCE) { checkMoveStateStance(); } // already moving else if (e->stats.cur_state == ENEMY_MOVE) { checkMoveStateMove(); } // if patrolling waypoints and has reached a waypoint, cycle to the next one if (!e->stats.waypoints.empty()) { FPoint waypoint = e->stats.waypoints.front(); FPoint pos = e->stats.pos; // if the patroller is close to the waypoint if (fabs(waypoint.x - pos.x) <= 0.5f && fabs(waypoint.y - pos.y) <= 0.5f) { e->stats.waypoints.pop(); // pick a new random point if we're wandering if (e->stats.wander) { waypoint = getWanderPoint(); } e->stats.waypoints.push(waypoint); e->stats.waypoint_pause_ticks = e->stats.waypoint_pause; } } // re-block current space to allow correct movement mapr->collider.block(e->stats.pos.x, e->stats.pos.y, e->stats.hero_ally); }
/** * Begin using a power if idle, based on behavior % chances. * Activate a ready power, if the attack animation has followed through */ void BehaviorStandard::checkPower() { // stunned enemies can't act if (e->stats.effects.stun || fleeing) return; // currently all enemy power use happens during combat if (!e->stats.in_combat) return; // if the enemy is on global cooldown it cannot act if (e->stats.cooldown_ticks > 0) return; // Note there are two stages to activating a power. // First is the enemy choosing to use a power based on behavioral chance // Second is the power actually firing off once the related animation reaches the active frame. // (these are separate so that interruptions can take place) // Begin Power Animation: // standard enemies can begin a power-use animation if they're standing around or moving voluntarily. if (los && (e->stats.cur_state == ENEMY_STANCE || e->stats.cur_state == ENEMY_MOVE)) { // check half dead power use if (!e->stats.on_half_dead_casted && e->stats.hp <= e->stats.get(STAT_HP_MAX)/2) { if (percentChance(e->stats.power_chance[ON_HALF_DEAD])) { e->newState(ENEMY_POWER); e->stats.activated_powerslot = ON_HALF_DEAD; return; } } // check ranged power use if (target_dist > e->stats.melee_range) { if (percentChance(e->stats.power_chance[RANGED_PHYS]) && e->stats.power_ticks[RANGED_PHYS] == 0) { bool can_use = true; if(powers->powers[e->stats.power_index[RANGED_PHYS]].type == POWTYPE_SPAWN) if(e->stats.summonLimitReached(e->stats.power_index[RANGED_PHYS])) can_use = false; if(can_use) { e->newState(ENEMY_POWER); e->stats.activated_powerslot = RANGED_PHYS; return; } } if (percentChance(e->stats.power_chance[RANGED_MENT]) && e->stats.power_ticks[RANGED_MENT] == 0) { bool can_use = true; if(powers->powers[e->stats.power_index[RANGED_MENT]].type == POWTYPE_SPAWN) if(e->stats.summonLimitReached(e->stats.power_index[RANGED_MENT])) can_use = false; if(can_use) { e->newState(ENEMY_POWER); e->stats.activated_powerslot = RANGED_MENT; return; } } } else { // check melee power use if (percentChance(e->stats.power_chance[MELEE_PHYS]) && e->stats.power_ticks[MELEE_PHYS] == 0) { bool can_use = true; if(powers->powers[e->stats.power_index[MELEE_PHYS]].type == POWTYPE_SPAWN) if(e->stats.summonLimitReached(e->stats.power_index[MELEE_PHYS])) can_use = false; if(can_use) { e->newState(ENEMY_POWER); e->stats.activated_powerslot = MELEE_PHYS; return; } } if (percentChance(e->stats.power_chance[MELEE_MENT]) && e->stats.power_ticks[MELEE_MENT] == 0) { bool can_use = true; if(powers->powers[e->stats.power_index[MELEE_MENT]].type == POWTYPE_SPAWN) if(e->stats.summonLimitReached(e->stats.power_index[MELEE_MENT])) can_use = false; if(can_use) { e->newState(ENEMY_POWER); e->stats.activated_powerslot = MELEE_MENT; return; } } } } // Activate Power: // enemy has started the animation to use a power. Activate the power on the Active animation frame if (e->stats.cur_state == ENEMY_POWER) { // if we're at the active frame of a power animation, // activate the power and set the local and global cooldowns if (e->activeAnimation->isActiveFrame() || e->instant_power) { e->instant_power = false; int power_slot = e->stats.activated_powerslot; int power_id = e->stats.power_index[e->stats.activated_powerslot]; powers->activate(power_id, &e->stats, pursue_pos); e->stats.power_ticks[power_slot] = powers->powers[power_id].cooldown; e->stats.cooldown_ticks = e->stats.cooldown; if (e->stats.activated_powerslot == ON_HALF_DEAD) { e->stats.on_half_dead_casted = true; } } } }
/** * Locate the player and set various targeting info */ void BehaviorStandard::findTarget() { float stealth_threat_range = (e->stats.threat_range * (100 - static_cast<float>(e->stats.hero_stealth))) / 100; // stunned enemies can't act if (e->stats.effects.stun) return; // check distance and line of sight between enemy and hero if (pc->stats.alive) hero_dist = calcDist(e->stats.pos, pc->stats.pos); else hero_dist = 0; // aggressive enemies are always in combat if (!e->stats.in_combat && e->stats.combat_style == COMBAT_AGGRESSIVE) { e->stats.in_combat = true; powers->activate(e->stats.power_index[BEACON], &e->stats, e->stats.pos); //emit beacon } // check entering combat (because the player hit the enemy) if (e->stats.join_combat) { if (hero_dist <= (stealth_threat_range *2)) { e->stats.join_combat = false; } else { e->stats.in_combat = true; powers->activate(e->stats.power_index[BEACON], &e->stats, e->stats.pos); //emit beacon } } // check entering combat (because the player got too close) if (!e->stats.in_combat && los && hero_dist < stealth_threat_range && e->stats.combat_style != COMBAT_PASSIVE) { e->stats.in_combat = true; powers->activate(e->stats.power_index[BEACON], &e->stats, e->stats.pos); //emit beacon } // check exiting combat (player died or got too far away) if (e->stats.in_combat && hero_dist > (e->stats.threat_range *2) && !e->stats.join_combat && e->stats.combat_style != COMBAT_AGGRESSIVE) { e->stats.in_combat = false; } // check exiting combat (player or enemy died) if ((!e->stats.alive || !pc->stats.alive) && e->stats.combat_style != COMBAT_AGGRESSIVE) { e->stats.in_combat = false; } // by default, the enemy pursues the hero directly pursue_pos.x = pc->stats.pos.x; pursue_pos.y = pc->stats.pos.y; target_dist = hero_dist; //if there are player allies closer than the hero, target an ally instead if(e->stats.in_combat) { for (unsigned int i=0; i < enemies->enemies.size(); i++) { if(!enemies->enemies[i]->stats.corpse && enemies->enemies[i]->stats.hero_ally) { //now work out the distance to the minion and compare it to the distance to the current targer (we want to target the closest ally) float ally_dist = calcDist(e->stats.pos, enemies->enemies[i]->stats.pos); if (ally_dist < target_dist) { pursue_pos.x = enemies->enemies[i]->stats.pos.x; pursue_pos.y = enemies->enemies[i]->stats.pos.y; target_dist = ally_dist; } } } } // if we just started wandering, set the first waypoint if (e->stats.wander && e->stats.waypoints.empty()) { FPoint waypoint = getWanderPoint(); e->stats.waypoints.push(waypoint); e->stats.waypoint_pause_ticks = e->stats.waypoint_pause; } // if we're not in combat, pursue the next waypoint if (!(e->stats.in_combat || e->stats.waypoints.empty())) { FPoint waypoint = e->stats.waypoints.front(); pursue_pos.x = waypoint.x; pursue_pos.y = waypoint.y; } // check line-of-sight if (target_dist < e->stats.threat_range && pc->stats.alive) los = mapr->collider.line_of_sight(e->stats.pos.x, e->stats.pos.y, pc->stats.pos.x, pc->stats.pos.y); else los = false; if(e->stats.effects.fear) fleeing = true; // If we have a successful chance_flee roll, try to move to a safe distance if (e->stats.cur_state == ENEMY_STANCE && !move_to_safe_dist && hero_dist < e->stats.threat_range/2 && hero_dist >= e->stats.melee_range && percentChance(e->stats.chance_flee)) move_to_safe_dist = true; if (move_to_safe_dist) fleeing = true; }
/** * Perform miscellaneous state-based actions. * 1) Set animations and sound effects * 2) Return to the default state (Stance) when actions are complete */ void BehaviorStandard::updateState() { // stunned enemies can't act if (e->stats.effects.stun) return; int power_id; int power_state; // continue current animations e->activeAnimation->advanceFrame(); switch (e->stats.cur_state) { case ENEMY_STANCE: e->setAnimation("stance"); break; case ENEMY_MOVE: e->setAnimation("run"); break; case ENEMY_POWER: power_id = e->stats.power_index[e->stats.activated_powerslot]; power_state = e->powers->powers[power_id].new_state; // animation based on power type if (power_state == POWSTATE_SWING) e->setAnimation("melee"); else if (power_state == POWSTATE_SHOOT) e->setAnimation("ranged"); else if (power_state == POWSTATE_CAST) e->setAnimation("ment"); else if (power_state == POWSTATE_INSTANT) e->instant_power = true; // sound effect based on power type if (e->activeAnimation->isFirstFrame()) { if (power_state == POWSTATE_SWING) e->sfx_phys = true; else if (power_state == POWSTATE_SHOOT) e->sfx_phys = true; else if (power_state == POWSTATE_CAST) e->sfx_ment = true; } if (e->activeAnimation->isLastFrame()) e->newState(ENEMY_STANCE); break; case ENEMY_SPAWN: e->setAnimation("spawn"); //the second check is needed in case the entity does not have a spawn animation if (e->activeAnimation->isLastFrame() || e->activeAnimation->getName() != "spawn") { e->newState(ENEMY_STANCE); e->CheckSummonSustained(); } break; case ENEMY_BLOCK: e->setAnimation("block"); break; case ENEMY_HIT: e->setAnimation("hit"); if (e->activeAnimation->isFirstFrame()) { e->stats.effects.triggered_hit = true; } if (e->activeAnimation->isLastFrame()) e->newState(ENEMY_STANCE); break; case ENEMY_DEAD: if (e->stats.effects.triggered_death) break; e->setAnimation("die"); if (e->activeAnimation->isFirstFrame()) { e->sfx_die = true; e->stats.corpse_ticks = CORPSE_TIMEOUT; e->stats.effects.clearEffects(); } if (e->activeAnimation->isSecondLastFrame()) { if (percentChance(e->stats.power_chance[ON_DEATH])) e->powers->activate(e->stats.power_index[ON_DEATH], &e->stats, e->stats.pos); } if (e->activeAnimation->isLastFrame()) { e->stats.corpse = true; // puts renderable under object layer //allow free movement over the corpse e->map->collider.unblock(e->stats.pos.x, e->stats.pos.y); } break; case ENEMY_CRITDEAD: e->setAnimation("critdie"); if (e->activeAnimation->isFirstFrame()) { e->sfx_critdie = true; e->stats.corpse_ticks = CORPSE_TIMEOUT; e->stats.effects.clearEffects(); } if (e->activeAnimation->isSecondLastFrame()) { if (percentChance(e->stats.power_chance[ON_DEATH])) e->powers->activate(e->stats.power_index[ON_DEATH], &e->stats, e->stats.pos); } if (e->activeAnimation->isLastFrame()) e->stats.corpse = true; // puts renderable under object layer break; default: break; } }
/** * Locate the player and set various targeting info */ void BehaviorStandard::findTarget() { int stealth_threat_range = (e->stats.threat_range * (100 - e->stats.hero_stealth)) / 100; // stunned enemies can't act if (e->stats.effects.stun) return; // check distance and line of sight between enemy and hero if (e->stats.hero_alive) hero_dist = calcDist(e->stats.pos, e->stats.hero_pos); else hero_dist = 0; // check entering combat (because the player hit the enemy) if (e->stats.join_combat) { if (hero_dist <= (stealth_threat_range *2)) { e->stats.join_combat = false; } else { e->stats.in_combat = true; e->powers->activate(e->stats.power_index[BEACON], &e->stats, e->stats.pos); //emit beacon } } // check entering combat (because the player got too close) if (!e->stats.in_combat && los && hero_dist < stealth_threat_range && !e->stats.passive_attacker) { if (e->stats.in_combat) e->stats.join_combat = true; e->stats.in_combat = true; e->powers->activate(e->stats.power_index[BEACON], &e->stats, e->stats.pos); //emit beacon } // check exiting combat (player died or got too far away) if (e->stats.in_combat && hero_dist > (e->stats.threat_range *2) && !e->stats.join_combat) { e->stats.in_combat = false; } // check exiting combat (player or enemy died) if (!e->stats.alive || !e->stats.hero_alive) { e->stats.in_combat = false; } // if the creature is a wanderer, pick a random point within the wander area to travel to if (e->stats.wander && !e->stats.in_combat && e->stats.wander_area.w > 0 && e->stats.wander_area.h > 0) { if (e->stats.wander_ticks == 0) { pursue_pos.x = e->stats.wander_area.x + (rand() % (e->stats.wander_area.w)); pursue_pos.y = e->stats.wander_area.y + (rand() % (e->stats.wander_area.h)); e->stats.wander_ticks = (rand() % 150) + 150; } } else { // by default, the enemy pursues the hero directly pursue_pos.x = e->stats.hero_pos.x; pursue_pos.y = e->stats.hero_pos.y; target_dist = hero_dist; //if there are player allies closer than the hero, target an ally instead if(e->stats.in_combat) { for (unsigned int i=0; i < enemies->enemies.size(); i++) { if(!enemies->enemies[i]->stats.corpse && enemies->enemies[i]->stats.hero_ally) { //now work out the distance to the minion and compare it to the distance to the current targer (we want to target the closest ally) int ally_dist = calcDist(e->stats.pos, enemies->enemies[i]->stats.pos); if (ally_dist < target_dist) { pursue_pos.x = enemies->enemies[i]->stats.pos.x; pursue_pos.y = enemies->enemies[i]->stats.pos.y; target_dist = ally_dist; } } } } if (!(e->stats.in_combat || e->stats.waypoints.empty())) { Point waypoint = e->stats.waypoints.front(); pursue_pos.x = waypoint.x; pursue_pos.y = waypoint.y; } } // check line-of-sight if (target_dist < e->stats.threat_range && e->stats.hero_alive) los = e->map->collider.line_of_sight(e->stats.pos.x, e->stats.pos.y, pursue_pos.x, pursue_pos.y); else los = false; if(e->stats.effects.fear) fleeing = true; // If we have a successful chance_flee roll, try to move to a safe distance if (e->stats.cur_state == ENEMY_STANCE && !move_to_safe_dist && hero_dist < e->stats.threat_range/2 && hero_dist >= e->stats.melee_range && percentChance(e->stats.chance_flee)) move_to_safe_dist = true; if (move_to_safe_dist) fleeing = true; }
void HazardManager::logic() { // remove all hazards with lifespan 0. Most hazards still display their last frame. for (size_t i=h.size(); i>0; i--) { if (h[i-1]->lifespan == 0) { delete h[i-1]; h.erase(h.begin()+(i-1)); } } checkNewHazards(); // handle single-frame transforms for (size_t i=h.size(); i>0; i--) { h[i-1]->logic(); // remove all hazards that need to die immediately (e.g. exit the map) if (h[i-1]->remove_now) { delete h[i-1]; h.erase(h.begin()+(i-1)); continue; } // if a moving hazard hits a wall, check for an after-effect if (h[i-1]->hit_wall) { if (h[i-1]->script_trigger == SCRIPT_TRIGGER_WALL) { EventManager::executeScript(h[i-1]->script, h[i-1]->pos.x, h[i-1]->pos.y); } if (h[i-1]->wall_power > 0 && percentChance(h[i-1]->wall_power_chance)) { powers->activate(h[i-1]->wall_power, h[i-1]->src_stats, h[i-1]->pos); if (powers->powers[h[i-1]->wall_power].directional) { powers->hazards.back()->animationKind = h[i-1]->animationKind; } } // clear wall hit h[i-1]->hit_wall = false; } } bool hit; // handle collisions for (size_t i=0; i<h.size(); i++) { if (h[i]->isDangerousNow()) { // process hazards that can hurt enemies if (h[i]->source_type != SOURCE_TYPE_ENEMY) { //hero or neutral sources for (unsigned int eindex = 0; eindex < enemym->enemies.size(); eindex++) { // only check living enemies if (enemym->enemies[eindex]->stats.hp > 0 && h[i]->active && (enemym->enemies[eindex]->stats.hero_ally == h[i]->target_party)) { if (isWithinRadius(h[i]->pos, h[i]->radius, enemym->enemies[eindex]->stats.pos)) { if (!h[i]->hasEntity(enemym->enemies[eindex])) { h[i]->addEntity(enemym->enemies[eindex]); if (!h[i]->beacon) last_enemy = enemym->enemies[eindex]; // hit! hit = enemym->enemies[eindex]->takeHit(*h[i]); hitEntity(i, hit); } } } } } // process hazards that can hurt the hero if (h[i]->source_type != SOURCE_TYPE_HERO && h[i]->source_type != SOURCE_TYPE_ALLY) { //enemy or neutral sources if (pc->stats.hp > 0 && h[i]->active) { if (isWithinRadius(h[i]->pos, h[i]->radius, pc->stats.pos)) { if (!h[i]->hasEntity(pc)) { h[i]->addEntity(pc); // hit! hit = pc->takeHit(*h[i]); hitEntity(i, hit); } } } //now process allies for (unsigned int eindex = 0; eindex < enemym->enemies.size(); eindex++) { // only check living allies if (enemym->enemies[eindex]->stats.hp > 0 && h[i]->active && enemym->enemies[eindex]->stats.hero_ally) { if (isWithinRadius(h[i]->pos, h[i]->radius, enemym->enemies[eindex]->stats.pos)) { if (!h[i]->hasEntity(enemym->enemies[eindex])) { h[i]->addEntity(enemym->enemies[eindex]); // hit! hit = enemym->enemies[eindex]->takeHit(*h[i]); hitEntity(i, hit); } } } } } } } }
/** * 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; }
/** * A particular event has been triggered. * Process all of this events components. * * @param The triggered event * @return Returns true if the event shall not be run again. */ bool EventManager::executeEvent(Event &ev) { // skip executing events that are on cooldown if (ev.cooldown_ticks > 0) return false; // need to know this for early returns Event_Component *ec_repeat = ev.getComponent(EC_REPEAT); if (ec_repeat) { ev.keep_after_trigger = ec_repeat->x == 0 ? false : true; } // delay event execution if (ev.delay > 0) { ev.delay_ticks = ev.delay; mapr->delayed_events.push_back(ev); mapr->delayed_events.back().delay_ticks = ev.delay; mapr->delayed_events.back().delay = 0; ev.cooldown_ticks = ev.cooldown + ev.delay; return !ev.keep_after_trigger; } // set cooldown ev.cooldown_ticks = ev.cooldown; // if chance_exec roll fails, don't execute the event // we respect the value of "repeat", even if the event doesn't execute Event_Component *ec_chance_exec = ev.getComponent(EC_CHANCE_EXEC); if (ec_chance_exec && !percentChance(ec_chance_exec->x)) { return !ev.keep_after_trigger; } Event_Component *ec; for (unsigned i = 0; i < ev.components.size(); ++i) { ec = &ev.components[i]; if (ec->type == EC_SET_STATUS) { camp->setStatus(ec->s); } else if (ec->type == EC_UNSET_STATUS) { camp->unsetStatus(ec->s); } else if (ec->type == EC_INTERMAP) { if (ec->z == 1) { // this is intermap_random std::string map_list = ec->s; Event_Component random_ec = getRandomMapFromFile(map_list); ec->s = random_ec.s; ec->x = random_ec.x; ec->y = random_ec.y; } if (fileExists(mods->locate(ec->s))) { mapr->teleportation = true; mapr->teleport_mapname = ec->s; if (ec->x == -1 && ec->y == -1) { // the teleport destination will be set to the map's hero_pos once the map is loaded mapr->teleport_destination.x = -1; mapr->teleport_destination.y = -1; } else { mapr->teleport_destination.x = static_cast<float>(ec->x) + 0.5f; mapr->teleport_destination.y = static_cast<float>(ec->y) + 0.5f; } } else { ev.keep_after_trigger = false; pc->logMsg(msg->get("Unknown destination"), false); } } else if (ec->type == EC_INTRAMAP) { mapr->teleportation = true; mapr->teleport_mapname = ""; mapr->teleport_destination.x = static_cast<float>(ec->x) + 0.5f; mapr->teleport_destination.y = static_cast<float>(ec->y) + 0.5f; } else if (ec->type == EC_MAPMOD) { if (ec->s == "collision") { if (ec->x >= 0 && ec->x < mapr->w && ec->y >= 0 && ec->y < mapr->h) { mapr->collider.colmap[ec->x][ec->y] = static_cast<unsigned short>(ec->z); mapr->map_change = true; } else logError("EventManager: Mapmod at position (%d, %d) is out of bounds 0-255.", ec->x, ec->y); } else { size_t index = static_cast<size_t>(distance(mapr->layernames.begin(), find(mapr->layernames.begin(), mapr->layernames.end(), ec->s))); if (!mapr->isValidTile(ec->z)) logError("EventManager: Mapmod at position (%d, %d) contains invalid tile id (%d).", ec->x, ec->y, ec->z); else if (index >= mapr->layers.size()) logError("EventManager: Mapmod at position (%d, %d) is on an invalid layer.", ec->x, ec->y); else if (ec->x >= 0 && ec->x < mapr->w && ec->y >= 0 && ec->y < mapr->h) mapr->layers[index][ec->x][ec->y] = static_cast<unsigned short>(ec->z); else logError("EventManager: Mapmod at position (%d, %d) is out of bounds 0-255.", ec->x, ec->y); } } else if (ec->type == EC_SOUNDFX) { FPoint pos(0,0); bool loop = false; if (ec->x != -1 && ec->y != -1) { if (ec->x != 0 && ec->y != 0) { pos.x = static_cast<float>(ec->x) + 0.5f; pos.y = static_cast<float>(ec->y) + 0.5f; } } else if (ev.location.x != 0 && ev.location.y != 0) { pos.x = static_cast<float>(ev.location.x) + 0.5f; pos.y = static_cast<float>(ev.location.y) + 0.5f; } if (ev.activate_type == EVENT_ON_LOAD || ec->z != 0) loop = true; SoundManager::SoundID sid = snd->load(ec->s, "MapRenderer background soundfx"); snd->play(sid, GLOBAL_VIRTUAL_CHANNEL, pos, loop); mapr->sids.push_back(sid); } else if (ec->type == EC_LOOT) { Event_Component *ec_lootcount = ev.getComponent(EC_LOOT_COUNT); if (ec_lootcount) { mapr->loot_count.x = ec_lootcount->x; mapr->loot_count.y = ec_lootcount->y; } else { mapr->loot_count.x = 0; mapr->loot_count.y = 0; } ec->x = ev.hotspot.x; ec->y = ev.hotspot.y; mapr->loot.push_back(*ec); } else if (ec->type == EC_MSG) { pc->logMsg(ec->s, false); } else if (ec->type == EC_SHAKYCAM) { mapr->shaky_cam_ticks = ec->x; } else if (ec->type == EC_REMOVE_CURRENCY) { camp->removeCurrency(ec->x); } else if (ec->type == EC_REMOVE_ITEM) { camp->removeItem(ec->x); } else if (ec->type == EC_REWARD_XP) { camp->rewardXP(ec->x, true); } else if (ec->type == EC_REWARD_CURRENCY) { camp->rewardCurrency(ec->x); } else if (ec->type == EC_REWARD_ITEM) { ItemStack istack; istack.item = ec->x; istack.quantity = ec->y; camp->rewardItem(istack); } else if (ec->type == EC_RESTORE) { camp->restoreHPMP(ec->s); } else if (ec->type == EC_SPAWN) { Point spawn_pos; spawn_pos.x = ec->x; spawn_pos.y = ec->y; enemym->spawn(ec->s, spawn_pos); } else if (ec->type == EC_POWER) { Event_Component *ec_path = ev.getComponent(EC_POWER_PATH); FPoint target; if (ec_path) { // targets hero option if (ec_path->s == "hero") { target.x = pc->stats.pos.x; target.y = pc->stats.pos.y; } // targets fixed path option else { target.x = static_cast<float>(ec_path->a) + 0.5f; target.y = static_cast<float>(ec_path->b) + 0.5f; } } // no path specified, targets self location else { target.x = static_cast<float>(ev.location.x) + 0.5f; target.y = static_cast<float>(ev.location.y) + 0.5f; } // ec->x is power id // ec->y is statblock index mapr->activatePower(ec->x, ec->y, target); } else if (ec->type == EC_STASH) { mapr->stash = ec->x == 0 ? false : true; if (mapr->stash) { mapr->stash_pos.x = static_cast<float>(ev.location.x) + 0.5f; mapr->stash_pos.y = static_cast<float>(ev.location.y) + 0.5f; } } else if (ec->type == EC_NPC) { mapr->event_npc = ec->s; } else if (ec->type == EC_MUSIC) { mapr->music_filename = ec->s; mapr->loadMusic(); } else if (ec->type == EC_CUTSCENE) { mapr->cutscene = true; mapr->cutscene_file = ec->s; } else if (ec->type == EC_REPEAT) { ev.keep_after_trigger = ec->x == 0 ? false : true; } else if (ec->type == EC_SAVE_GAME) { mapr->save_game = ec->x == 0 ? false : true; } else if (ec->type == EC_NPC_ID) { mapr->npc_id = ec->x; } else if (ec->type == EC_BOOK) { mapr->show_book = ec->s; } else if (ec->type == EC_SCRIPT) { if (ev.center.x != -1 && ev.center.y != -1) executeScript(ec->s, ev.center.x, ev.center.y); else executeScript(ec->s, pc->stats.pos.x, pc->stats.pos.y); } } return !ev.keep_after_trigger; }