/* Function: friendly_move The per-turn movement and action calculation of any friendly monsters. */ void monster::friendly_move() { point next; bool moved = false; //If we successfully calculated a plan in the generic monster movement function, begin executing it. if (!plans.empty() && (plans[0].x != g->u.posx || plans[0].y != g->u.posy) && (can_move_to(plans[0].x, plans[0].y) || (has_flag(MF_BASHES) && g->m.bash_rating(bash_skill(), plans[0].x, plans[0].y) > 0))) { next = plans[0]; plans.erase(plans.begin()); moved = true; } else { //Otherwise just stumble around randomly until we formulate a plan. moves -= 100; stumble(moved); } if (moved) { int& x = next.x; int& y = next.y; // Define alias for x and y bool did_something = attack_at(x, y) || bash_at(x, y) || move_to(x, y); //If all else fails in our plan (an issue with pathfinding maybe) stumble around instead. if(!did_something) { stumble(moved); moves -= 100; } } }
int monster::bash_estimate() { int estimate = bash_skill(); if( has_flag(MF_GROUP_BASH) ) { // Right now just give them a boost so they try to bash a lot of stuff. // TODO: base it on number of nearby friendlies. estimate += 20; } return estimate; }
int monster::bash_at(int x, int y) { if (has_effect("pacified")) return 0; //Hallucinations can't bash stuff. if(is_hallucination()) { return 0; } bool try_bash = !can_move_to(x, y) || one_in(3); bool can_bash = g->m.is_bashable(x, y) && has_flag(MF_BASHES); if(try_bash && can_bash) { int bashskill = bash_skill(); // pileup = more bashskill, but only help bashing mob directly infront of target const int max_helper_depth = 5; const std::vector<point> bzone = get_bashing_zone( point(x, y), pos(), max_helper_depth ); int diffx = pos().x - x; int diffy = pos().y - y; int mo_bash = 0; for( size_t i = 0; i < bzone.size(); ++i ) { if ( g->mon_at( bzone[i] ) != -1 ) { monster & helpermon = g->zombie( g->mon_at( bzone[i] ) ); // trying for the same door and can bash; put on helper hat if ( helpermon.wandx == wandx && helpermon.wandy == wandy && helpermon.has_flag(MF_BASHES) ) { // helpers lined up behind primary basher add full strength, // so do those at either shoulder, others add 50% int addbash = int(helpermon.type->melee_dice * helpermon.type->melee_sides); if (helpermon.has_flag(MF_DESTROYS)) { addbash *= 2.5; } // helpers lined up behind primary basher add full strength, others 50% addbash *= ( ( diffx == 0 && bzone[i].x == pos().x ) || ( diffy == 0 && bzone[i].y == pos().y ) ) ? 2 : 1; mo_bash += addbash; } } } // by our powers combined... bashskill += int (mo_bash / 2); g->m.bash( x, y, bashskill ); moves -= 100; return 1; } return 0; }
point monster::scent_move() { std::vector<point> smoves; int maxsmell = 10; // Squares with smell 0 are not eligible targets. int smell_threshold = 200; // Squares at or above this level are ineligible. if (has_flag(MF_KEENNOSE)) { maxsmell = 1; smell_threshold = 400; } int minsmell = 9999; point pbuff, next(-1, -1); int smell; const bool fleeing = is_fleeing(g->u); if( !fleeing && g->scent( posx(), posy() ) > smell_threshold ) { return next; } for (int x = -1; x <= 1; x++) { for (int y = -1; y <= 1; y++) { const int nx = posx() + x; const int ny = posy() + y; smell = g->scent(nx, ny); int mon = g->mon_at(nx, ny); if ((mon == -1 || g->zombie(mon).friendly != 0 || has_flag(MF_ATTACKMON)) && (can_move_to(nx, ny) || (nx == g->u.posx && ny == g->u.posy) || (has_flag(MF_BASHES) && g->m.bash_rating(bash_skill(), nx, ny) > 0))) { if ((!fleeing && smell > maxsmell) || (fleeing && smell < minsmell)) { smoves.clear(); pbuff.x = nx; pbuff.y = ny; smoves.push_back(pbuff); maxsmell = smell; minsmell = smell; } else if ((!fleeing && smell == maxsmell) || (fleeing && smell == minsmell)) { pbuff.x = nx; pbuff.y = ny; smoves.push_back(pbuff); } } } } if (!smoves.empty()) { int nextsq = rng(0, smoves.size() - 1); next = smoves[nextsq]; } return next; }
tripoint monster::scent_move() { // @todo Remove when scentmap is 3D if( abs( posz() - g->get_levz() ) > 1 ) { return { -1, -1, INT_MIN }; } std::vector<tripoint> smoves; int bestsmell = 10; // Squares with smell 0 are not eligible targets. int smell_threshold = 200; // Squares at or above this level are ineligible. if( has_flag( MF_KEENNOSE ) ) { bestsmell = 1; smell_threshold = 400; } const bool fleeing = is_fleeing( g->u ); if( fleeing ) { bestsmell = g->scent.get( pos() ); } tripoint next( -1, -1, posz() ); if( ( !fleeing && g->scent.get( pos() ) > smell_threshold ) || ( fleeing && bestsmell == 0 ) ) { return next; } const bool can_bash = bash_skill() > 0; for( const auto &dest : g->m.points_in_radius( pos(), 1, 1 ) ) { int smell = g->scent.get( dest ); if( g->m.valid_move( pos(), dest, can_bash, true ) && ( can_move_to( dest ) || ( dest == g->u.pos() ) || ( can_bash && g->m.bash_rating( bash_estimate(), dest ) > 0 ) ) ) { if( ( !fleeing && smell > bestsmell ) || ( fleeing && smell < bestsmell ) ) { smoves.clear(); smoves.push_back( dest ); bestsmell = smell; } else if( ( !fleeing && smell == bestsmell ) || ( fleeing && smell == bestsmell ) ) { smoves.push_back( dest ); } } } return random_entry( smoves, next ); }
int monster::group_bash_skill( point target ) { if( !has_flag(MF_GROUP_BASH) ) { return bash_skill(); } int bashskill = 0; // pileup = more bashskill, but only help bashing mob directly infront of target const int max_helper_depth = 5; const std::vector<point> bzone = get_bashing_zone( target, pos(), max_helper_depth ); for( point candidate : bzone ) { // Drawing this line backwards excludes the target and includes the candidate. std::vector<point> path_to_target = line_to( target.x, target.y, candidate.x, candidate.y, 0); bool connected = true; int mondex = -1; for( point in_path : path_to_target ) { // If any point in the line from zombie to target is not a cooperating zombie, // it can't contribute. mondex = g->mon_at( in_path ); if( mondex == -1 ) { connected = false; break; } monster &helpermon = g->zombie( mondex ); if( !helpermon.has_flag(MF_GROUP_BASH) ) { connected = false; break; } } if( !connected ) { continue; } // If we made it here, the last monster checked was the candidate. monster &helpermon = g->zombie( mondex ); // Contribution falls off rapidly with distance from target. bashskill += helpermon.bash_skill() / rl_dist( candidate, target ); } return bashskill; }
int monster::group_bash_skill( const tripoint &target ) { if( !has_flag( MF_GROUP_BASH ) ) { return bash_skill(); } int bashskill = 0; // pileup = more bash skill, but only help bashing mob directly in front of target const int max_helper_depth = 5; const std::vector<tripoint> bzone = get_bashing_zone( target, pos(), max_helper_depth ); for( const tripoint &candidate : bzone ) { // Drawing this line backwards excludes the target and includes the candidate. std::vector<tripoint> path_to_target = line_to( target, candidate, 0, 0 ); bool connected = true; monster *mon = nullptr; for( const tripoint &in_path : path_to_target ) { // If any point in the line from zombie to target is not a cooperating zombie, // it can't contribute. mon = g->critter_at<monster>( in_path ); if( !mon ) { connected = false; break; } monster &helpermon = *mon; if( !helpermon.has_flag( MF_GROUP_BASH ) || helpermon.is_hallucination() ) { connected = false; break; } } if( !connected || !mon ) { continue; } // If we made it here, the last monster checked was the candidate. monster &helpermon = *mon; // Contribution falls off rapidly with distance from target. bashskill += helpermon.bash_skill() / rl_dist( candidate, target ); } return bashskill; }
bool monster::bash_at( const tripoint &p ) { if( p.z != posz() ) { return false; // TODO: Remove this } //Hallucinations can't bash stuff. if( is_hallucination() ) { return false; } bool try_bash = !can_move_to( p ) || one_in( 3 ); bool can_bash = g->m.is_bashable( p ) && bash_skill() > 0; if( try_bash && can_bash ) { int bashskill = group_bash_skill( p ); g->m.bash( p, bashskill ); moves -= 100; return true; } return false; }
// General movement. // Currently, priority goes: // 1) Special Attack // 2) Sight-based tracking // 3) Scent-based tracking // 4) Sound-based tracking void monster::move() { // We decrement wandf no matter what. We'll save our wander_to plans until // after we finish out set_dest plans, UNLESS they time out first. if( wandf > 0 ) { wandf--; } //Hallucinations have a chance of disappearing each turn if( is_hallucination() && one_in( 25 ) ) { die( nullptr ); return; } //The monster can consume objects it stands on. Check if there are any. //If there are. Consume them. if( !is_hallucination() && has_flag( MF_ABSORBS ) && !g->m.has_flag( TFLAG_SEALED, pos() ) && g->m.has_items( pos() ) ) { if( g->u.sees( *this ) ) { add_msg( _( "The %s flows around the objects on the floor and they are quickly dissolved!" ), name().c_str() ); } static const auto volume_per_hp = units::from_milliliter( 250 ); for( auto &elem : g->m.i_at( pos() ) ) { hp += elem.volume() / volume_per_hp; // Yeah this means it can get more HP than normal. } g->m.i_clear( pos() ); } const bool pacified = has_effect( effect_pacified ); // First, use the special attack, if we can! // The attack may change `monster::special_attacks` (e.g. by transforming // this into another monster type). Therefor we can not iterate over it // directly and instead iterate over the map from the monster type // (properties of monster types should never change). for( const auto &sp_type : type->special_attacks ) { const std::string &special_name = sp_type.first; const auto local_iter = special_attacks.find( special_name ); if( local_iter == special_attacks.end() ) { continue; } mon_special_attack &local_attack_data = local_iter->second; if( !local_attack_data.enabled ) { continue; } if( local_attack_data.cooldown > 0 ) { local_attack_data.cooldown--; } if( local_attack_data.cooldown == 0 && !pacified && !is_hallucination() ) { if( !sp_type.second->call( *this ) ) { continue; } // `special_attacks` might have changed at this point. Sadly `reset_special` // doesn't check the attack name, so we need to do it here. if( special_attacks.count( special_name ) == 0 ) { continue; } reset_special( special_name ); } } // The monster can sometimes hang in air due to last fall being blocked const bool can_fly = has_flag( MF_FLIES ); if( !can_fly && g->m.has_flag( TFLAG_NO_FLOOR, pos() ) ) { g->m.creature_on_trap( *this, false ); } if( moves < 0 ) { return; } // TODO: Move this to attack_at/move_to/etc. functions bool attacking = false; if( !move_effects( attacking ) ) { moves = 0; return; } if( has_flag( MF_IMMOBILE ) ) { moves = 0; return; } if( has_effect( effect_stunned ) ) { stumble(); moves = 0; return; } if( friendly > 0 ) { --friendly; } // Set attitude to attitude to our current target monster_attitude current_attitude = attitude( nullptr ); if( !wander() ) { if( goal == g->u.pos() ) { current_attitude = attitude( &( g->u ) ); } else { for( auto &i : g->active_npc ) { if( goal == i->pos() ) { current_attitude = attitude( i ); } } } } if( current_attitude == MATT_IGNORE || ( current_attitude == MATT_FOLLOW && rl_dist( pos(), goal ) <= MONSTER_FOLLOW_DIST ) ) { moves -= 100; stumble(); return; } bool moved = false; tripoint destination; // If true, don't try to greedily avoid locally bad paths bool pathed = false; if( !wander() ) { while( !path.empty() && path.front() == pos() ) { path.erase( path.begin() ); } const auto &pf_settings = get_pathfinding_settings(); if( pf_settings.max_dist >= rl_dist( pos(), goal ) && ( path.empty() || rl_dist( pos(), path.front() ) >= 2 || path.back() != goal ) ) { // We need a new path path = g->m.route( pos(), goal, pf_settings, get_path_avoid() ); } // Try to respect old paths, even if we can't pathfind at the moment if( !path.empty() && path.back() == goal ) { destination = path.front(); moved = true; pathed = true; } else { // Straight line forward, probably because we can't pathfind (well enough) destination = goal; moved = true; } } if( !moved && has_flag( MF_SMELLS ) ) { // No sight... or our plans are invalid (e.g. moving through a transparent, but // solid, square of terrain). Fall back to smell if we have it. unset_dest(); tripoint tmp = scent_move(); if( tmp.x != -1 ) { destination = tmp; moved = true; } } if( wandf > 0 && !moved ) { // No LOS, no scent, so as a fall-back follow sound unset_dest(); if( wander_pos != pos() ) { destination = wander_pos; moved = true; } } if( !g->m.has_zlevels() ) { // Otherwise weird things happen destination.z = posz(); } tripoint next_step; const bool staggers = has_flag( MF_STUMBLES ); if( moved ) { // Implement both avoiding obstacles and staggering. moved = false; float switch_chance = 0.0; const bool can_bash = bash_skill() > 0; // This is a float and using trig_dist() because that Does the Right Thing(tm) // in both circular and roguelike distance modes. const float distance_to_target = trig_dist( pos(), destination ); for( const tripoint &candidate : squares_closer_to( pos(), destination ) ) { if( candidate.z != posz() ) { bool can_z_move = true; if( !g->m.valid_move( pos(), candidate, false, true ) ) { // Can't phase through floor can_z_move = false; } if( can_z_move && !can_fly && candidate.z > posz() && !g->m.has_floor_or_support( candidate ) ) { // Can't "jump" up a whole z-level can_z_move = false; } // Last chance - we can still do the z-level stair teleport bullshit that isn't removed yet // @todo Remove z-level stair bullshit teleport after aligning all stairs if( !can_z_move && posx() / ( SEEX * 2 ) == candidate.x / ( SEEX * 2 ) && posy() / ( SEEY * 2 ) == candidate.y / ( SEEY * 2 ) ) { const tripoint &upper = candidate.z > posz() ? candidate : pos(); const tripoint &lower = candidate.z > posz() ? pos() : candidate; if( g->m.has_flag( TFLAG_GOES_DOWN, upper ) && g->m.has_flag( TFLAG_GOES_UP, lower ) ) { can_z_move = true; } } if( !can_z_move ) { continue; } } // A flag to allow non-stumbling critters to stumble when the most direct choice is bad. bool bad_choice = false; const Creature *target = g->critter_at( candidate, is_hallucination() ); if( target != nullptr ) { const Creature::Attitude att = attitude_to( *target ); if( att == A_HOSTILE ) { // When attacking an adjacent enemy, we're direct. moved = true; next_step = candidate; break; } else if( att == A_FRIENDLY && ( target->is_player() || target->is_npc() ) ) { continue; // Friendly firing the player or an NPC is illegal for gameplay reasons } else if( !has_flag( MF_ATTACKMON ) && !has_flag( MF_PUSH_MON ) ) { // Bail out if there's a non-hostile monster in the way and we're not pushy. continue; } // Friendly fire and pushing are always bad choices - they take a lot of time bad_choice = true; } // Bail out if we can't move there and we can't bash. if( !pathed && !can_move_to( candidate ) ) { if( !can_bash ) { continue; } const int estimate = g->m.bash_rating( bash_estimate(), candidate ); if( estimate <= 0 ) { continue; } if( estimate < 5 ) { bad_choice = true; } } const float progress = distance_to_target - trig_dist( candidate, destination ); // The x2 makes the first (and most direct) path twice as likely, // since the chance of switching is 1/1, 1/4, 1/6, 1/8 switch_chance += progress * 2; // Randomly pick one of the viable squares to move to weighted by distance. if( moved == false || x_in_y( progress, switch_chance ) ) { moved = true; next_step = candidate; // If we stumble, pick a random square, otherwise take the first one, // which is the most direct path. // Except if the direct path is bad, then check others // Or if the path is given by pathfinder if( !staggers && ( !bad_choice || pathed ) ) { break; } } } } // Finished logic section. By this point, we should have chosen a square to // move to (moved = true). if( moved ) { // Actual effects of moving to the square we've chosen const bool did_something = ( !pacified && attack_at( next_step ) ) || ( !pacified && bash_at( next_step ) ) || ( !pacified && push_to( next_step, 0, 0 ) ) || move_to( next_step, false, get_stagger_adjust( pos(), destination, next_step ) ); if( !did_something ) { moves -= 100; // If we don't do this, we'll get infinite loops. } } else { moves -= 100; stumble(); path.clear(); } }
point monster::wander_next() { point next; bool xbest = true; if (abs(wandy - posy()) > abs(wandx - posx())) {// which is more important xbest = false; } next.x = posx(); next.y = posy(); int x = posx(), x2 = posx() - 1, x3 = posx() + 1; int y = posy(), y2 = posy() - 1, y3 = posy() + 1; if (wandx < posx()) { x--; x2++; } if (wandx > posx()) { x++; x2++; x3 -= 2; } if (wandy < posy()) { y--; y2++; } if (wandy > posy()) { y++; y2++; y3 -= 2; } int bashskill = bash_skill(); if (xbest) { if (can_move_to(x, y) || (x == g->u.posx && y == g->u.posy) || (has_flag(MF_BASHES) && g->m.bash_rating(bashskill, x, y) > 0)) { next.x = x; next.y = y; } else if (can_move_to(x, y2) || (x == g->u.posx && y == g->u.posy) || (has_flag(MF_BASHES) && g->m.bash_rating(bashskill, x, y2) > 0)) { next.x = x; next.y = y2; } else if (can_move_to(x2, y) || (x == g->u.posx && y == g->u.posy) || (has_flag(MF_BASHES) && g->m.bash_rating(bashskill, x2, y) > 0)) { next.x = x2; next.y = y; } else if (can_move_to(x, y3) || (x == g->u.posx && y == g->u.posy) || (has_flag(MF_BASHES) && g->m.bash_rating(bashskill, x, y3) > 0)) { next.x = x; next.y = y3; } else if (can_move_to(x3, y) || (x == g->u.posx && y == g->u.posy) || (has_flag(MF_BASHES) && g->m.bash_rating(bashskill, x3, y) > 0)) { next.x = x3; next.y = y; } } else { if (can_move_to(x, y) || (x == g->u.posx && y == g->u.posy) || (has_flag(MF_BASHES) && g->m.bash_rating(bashskill, x, y) > 0)) { next.x = x; next.y = y; } else if (can_move_to(x2, y) || (x == g->u.posx && y == g->u.posy) || (has_flag(MF_BASHES) && g->m.bash_rating(bashskill, x2, y) > 0)) { next.x = x2; next.y = y; } else if (can_move_to(x, y2) || (x == g->u.posx && y == g->u.posy) || (has_flag(MF_BASHES) && g->m.bash_rating(bashskill, x, y2) > 0)) { next.x = x; next.y = y2; } else if (can_move_to(x3, y) || (x == g->u.posx && y == g->u.posy) || (has_flag(MF_BASHES) && g->m.bash_rating(bashskill, x3, y) > 0)) { next.x = x3; next.y = y; } else if (can_move_to(x, y3) || (x == g->u.posx && y == g->u.posy) || (has_flag(MF_BASHES) && g->m.bash_rating(bashskill, x, y3) > 0)) { next.x = x; next.y = y3; } } return next; }
// General movement. // Currently, priority goes: // 1) Special Attack // 2) Sight-based tracking // 3) Scent-based tracking // 4) Sound-based tracking void monster::move() { // We decrement wandf no matter what. We'll save our wander_to plans until // after we finish out set_dest plans, UNLESS they time out first. if (wandf > 0) { wandf--; } //Hallucinations have a chance of disappearing each turn if (is_hallucination() && one_in(25)) { die( nullptr ); return; } // First, use the special attack, if we can! if (sp_timeout > 0) { sp_timeout--; } //If this monster has the ability to heal in combat, do it now. if (has_flag(MF_REGENERATES_50)) { if (hp < type->hp) { if (one_in(2)) { add_msg(m_warning, _("The %s is visibly regenerating!"), name().c_str()); } hp += 50; if(hp > type->hp) { hp = type->hp; } } } if (has_flag(MF_REGENERATES_10)) { if (hp < type->hp) { if (one_in(2)) { add_msg(m_warning, _("The %s seems a little healthier."), name().c_str()); } hp += 10; if(hp > type->hp) { hp = type->hp; } } } //The monster can consume objects it stands on. Check if there are any. //If there are. Consume them. if (has_flag(MF_ABSORBS)) { if(!g->m.i_at(posx(), posy()).empty()) { add_msg(_("The %s flows around the objects on the floor and they are quickly dissolved!"), name().c_str()); std::vector<item> items_absorbed = g->m.i_at(posx(), posy()); for( size_t i = 0; i < items_absorbed.size(); ++i ) { hp += items_absorbed.at(i).volume(); //Yeah this means it can get more HP than normal. } g->m.i_clear(posx(), posy()); } } //Monster will regen morale and aggression if it is on max HP //It regens more morale and aggression if is currently fleeing. if(has_flag(MF_REGENMORALE) && hp >= type->hp){ if(is_fleeing(g->u)){ morale = type->morale; anger = type->agro; } if(morale <= type->morale) morale += 1; if(anger <= type->agro) anger += 1; if(morale < 0) morale += 5; if(anger < 0) anger += 5; } // If this critter dies in sunlight, check & assess damage. if (g->is_in_sunlight(posx(), posy()) && has_flag(MF_SUNDEATH)) { add_msg(_("The %s burns horribly in the sunlight!"), name().c_str()); hp -= 100; if(hp < 0) { hp = 0 ; } } if( sp_timeout == 0 && (friendly == 0 || has_flag(MF_FRIENDLY_SPECIAL)) && !has_effect("pacified") ) { mattack ma; if(!is_hallucination()) { (ma.*type->sp_attack)(this); } } if (moves < 0) { return; } if (has_flag(MF_IMMOBILE)) { moves = 0; return; } if (has_effect("stunned")) { stumble(false); moves = 0; return; } if (has_effect("downed")) { moves = 0; return; } if (friendly != 0) { if (friendly > 0) { friendly--; } friendly_move(); return; } bool moved = false; point next; int mondex = (!plans.empty() ? g->mon_at(plans[0].x, plans[0].y) : -1); monster_attitude current_attitude = attitude(); if (friendly == 0) { current_attitude = attitude(&(g->u)); } // If our plans end in a player, set our attitude to consider that player if (!plans.empty()) { if (plans.back().x == g->u.posx && plans.back().y == g->u.posy) { current_attitude = attitude(&(g->u)); } else { for (auto &i : g->active_npc) { if (plans.back().x == i->posx && plans.back().y == i->posy) { current_attitude = attitude(i); } } } } if (current_attitude == MATT_IGNORE || (current_attitude == MATT_FOLLOW && plans.size() <= MONSTER_FOLLOW_DIST)) { moves -= 100; stumble(false); return; } if (!plans.empty() && (mondex == -1 || g->zombie(mondex).friendly != 0 || has_flag(MF_ATTACKMON)) && (can_move_to(plans[0].x, plans[0].y) || (plans[0].x == g->u.posx && plans[0].y == g->u.posy) || (has_flag(MF_BASHES) && g->m.bash_rating(bash_skill(), plans[0].x, plans[0].y) > 0))){ // CONCRETE PLANS - Most likely based on sight next = plans[0]; moved = true; } else if (has_flag(MF_SMELLS)) { // No sight... or our plans are invalid (e.g. moving through a transparent, but // solid, square of terrain). Fall back to smell if we have it. plans.clear(); point tmp = scent_move(); if (tmp.x != -1) { next = tmp; moved = true; } } if (wandf > 0 && !moved) { // No LOS, no scent, so as a fall-back follow sound plans.clear(); point tmp = wander_next(); if (tmp.x != posx() || tmp.y != posy()) { next = tmp; moved = true; } } // Finished logic section. By this point, we should have chosen a square to // move to (moved = true). if (moved) { // Actual effects of moving to the square we've chosen // Note: The below works because C++ in A() || B() won't call B() if A() is true int& x = next.x; int& y = next.y; // Define alias for x and y bool did_something = attack_at(x, y) || bash_at(x, y) || move_to(x, y); if(!did_something) { moves -= 100; // If we don't do this, we'll get infinite loops. } } else { moves -= 100; } // If we're close to our target, we get focused and don't stumble if ((has_flag(MF_STUMBLES) && (plans.size() > 3 || plans.empty())) || !moved) { stumble(moved); } }