bool Creature::sees( const Creature &critter ) const { if( critter.is_hallucination() ) { // hallucinations are imaginations of the player character, npcs or monsters don't hallucinate. // Invisible hallucinations would be pretty useless (nobody would see them at all), therefor // the player will see them always. return is_player(); } const auto p = dynamic_cast< const player* >( &critter ); if( p != nullptr && p->is_invisible() ) { // Let invisible players see themselves (simplifies drawing) return p == this; } if( !fov_3d && !debug_mode && posz() != critter.posz() ) { return false; } const int wanted_range = rl_dist( pos(), critter.pos() ); if( wanted_range <= 1 && ( posz() == critter.posz() || g->m.valid_move( pos(), critter.pos(), false, true ) ) ) { return true; } else if( ( wanted_range > 1 && critter.digging() ) || (critter.has_flag(MF_NIGHT_INVISIBILITY) && g->m.light_at(critter.pos()) <= LL_LOW ) || ( critter.is_underwater() && !is_underwater() && g->m.is_divable( critter.pos() ) ) ) { return false; } return sees( critter.pos(), critter.is_player() ); }
/* Random walking even when we've moved * To simulate zombie stumbling and ineffective movement * Note that this is sub-optimal; stumbling may INCREASE a zombie's speed. * Most of the time (out in the open) this effect is insignificant compared to * the negative effects, but in a hallway it's perfectly even */ void monster::stumble( bool moved ) { // don't stumble every turn. every 3rd turn, or 8th when walking. if( ( moved && !one_in( 8 ) ) || !one_in( 3 ) ) { return; } std::vector<tripoint> valid_stumbles; const bool avoid_water = has_flag( MF_NO_BREATHE ) && !has_flag( MF_SWIMS ) && !has_flag( MF_AQUATIC ); for( int i = -1; i <= 1; i++ ) { for( int j = -1; j <= 1; j++ ) { tripoint dest( posx() + i, posy() + j, posz() ); if( ( i || j ) && can_move_to( dest ) && //Stop zombies and other non-breathing monsters wandering INTO water //(Unless they can swim/are aquatic) //But let them wander OUT of water if they are there. !( avoid_water && g->m.has_flag( "SWIMMABLE", dest ) && !g->m.has_flag( "SWIMMABLE", pos3() ) ) && g->critter_at( dest ) == nullptr ) { valid_stumbles.push_back( dest ); } } } if( g->m.has_zlevels() ) { tripoint below( posx(), posy(), posz() - 1 ); tripoint above( posx(), posy(), posz() + 1 ); if( g->m.valid_move( pos(), below, false, true ) && can_move_to( below ) ) { valid_stumbles.push_back( below ); } // More restrictions for moving up // It should happen during "shambling around", but not as actual stumbling if( !moved && one_in( 5 ) && has_flag( MF_FLIES ) && g->m.valid_move( pos(), above, false, true ) && can_move_to( above ) ) { valid_stumbles.push_back( above ); } } if( valid_stumbles.empty() ) { //nowhere to stumble? return; } move_to( random_entry( valid_stumbles ), false ); // Here we have to fix our plans[] list, // acquiring a new path to the previous target. // target == either end of current plan, or the player. int bresenham_slope, junk; if( !plans.empty() ) { if( g->m.sees( pos3(), plans.back(), -1, bresenham_slope, junk ) ) { set_dest( plans.back(), bresenham_slope ); } else if( sees( g->u, bresenham_slope ) ) { set_dest( g->u.pos(), bresenham_slope ); } else { //durr, i'm suddenly calm. what was i doing? plans.clear(); } } }
bool Creature::sees( const tripoint &t, bool is_player ) const { if( !fov_3d && posz() != t.z ) { return false; } const int range_cur = sight_range( g->m.ambient_light_at( t ) ); const int range_day = sight_range( DAYLIGHT_LEVEL ); const int range_night = sight_range( 0 ); const int range_max = std::max( range_day, range_night ); const int range_min = std::min( range_cur, range_max ); const int wanted_range = rl_dist( pos(), t ); if( wanted_range <= range_min || ( wanted_range <= range_max && g->m.ambient_light_at( t ) > g->natural_light_level( t.z ) ) ) { int range = 0; if( g->m.ambient_light_at( t ) > g->natural_light_level( t.z ) ) { range = wanted_range; } else { range = range_min; } if( is_player ) { // Special case monster -> player visibility, forcing it to be symmetric with player vision. return range >= wanted_range && g->m.get_cache_ref(pos().z).seen_cache[pos().x][pos().y] > LIGHT_TRANSPARENCY_SOLID; } else { return g->m.sees( pos(), t, range ); } } else { return false; } }
bool Creature::sees( const Creature &critter, int &bresen1, int &bresen2 ) const { if( critter.is_hallucination() ) { // hallucinations are imaginations of the player character, npcs or monsters don't hallucinate. // Invisible hallucinations would be pretty useless (nobody would see them at all), therefor // the player will see them always. return is_player(); } const auto p = dynamic_cast< const player* >( &critter ); if( p != nullptr && p->is_invisible() ) { // Let invisible players see themselves (simplifies drawing) return p == this; } if( posz() != critter.posz() && !debug_mode ) { return false; // TODO: Remove this } const int wanted_range = rl_dist( pos3(), critter.pos3() ); if( wanted_range <= 1 ) { return true; } else if( ( wanted_range > 1 && critter.digging() ) || ( g->m.is_divable( critter.pos3() ) && critter.is_underwater() && !is_underwater() ) ) { return false; } return sees( critter.pos3(), bresen1, bresen2 ); }
bool monster::attack_at( const tripoint &p ) { if( p.z != posz() ) { return false; // TODO: Remove this } if( has_effect( "pacified" ) ) { return false; } if( p == g->u.pos3() ) { melee_attack( g->u, true ); return true; } const int mondex = g->mon_at( p ); if( mondex != -1 ) { monster &mon = g->zombie( mondex ); // Don't attack yourself. if( &mon == this ) { return false; } // Special case: Target is hallucination if( mon.is_hallucination() ) { mon.die( nullptr ); // We haven't actually attacked anything, i.e. we can still do things. // Hallucinations(obviously) shouldn't affect the way real monsters act. return false; } // With no melee dice, we can't attack, but we had to process until here // because hallucinations require no melee dice to destroy. if( type->melee_dice <= 0 ) { return false; } auto attitude = attitude_to( mon ); // MF_ATTACKMON == hulk behavior, whack everything in your way if( attitude == A_HOSTILE || has_flag( MF_ATTACKMON ) ) { hit_monster( mon ); return true; } return false; } const int npcdex = g->npc_at( p ); if( npcdex != -1 && type->melee_dice > 0 ) { // For now we're always attacking NPCs that are getting into our // way. This is consistent with how it worked previously, but // later on not hitting allied NPCs would be cool. melee_attack( *g->active_npc[npcdex], true ); return true; } // Nothing to attack. return false; }
bool monster::bash_at( const tripoint &p ) { if( p.z != posz() ) { return false; // TODO: Remove this } if( has_effect( "pacified" ) ) { return false; } //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 ) && ( has_flag( MF_BASHES ) || has_flag( MF_BORES ) ); if( try_bash && can_bash ) { int bashskill = group_bash_skill( p ); g->m.bash( p, bashskill ); moves -= 100; return true; } return false; }
/** * Stumble in a random direction, but with some caveats. */ void monster::stumble( ) { // Only move every 3rd turn. if( !one_in( 3 ) ) { return; } std::vector<tripoint> valid_stumbles; const bool avoid_water = has_flag( MF_NO_BREATHE ) && !has_flag( MF_SWIMS ) && !has_flag( MF_AQUATIC ); for( int i = -1; i <= 1; i++ ) { for( int j = -1; j <= 1; j++ ) { tripoint dest( posx() + i, posy() + j, posz() ); if( ( i || j ) && can_move_to( dest ) && //Stop zombies and other non-breathing monsters wandering INTO water //(Unless they can swim/are aquatic) //But let them wander OUT of water if they are there. !( avoid_water && g->m.has_flag( TFLAG_SWIMMABLE, dest ) && !g->m.has_flag( TFLAG_SWIMMABLE, pos3() ) ) && ( g->critter_at( dest, is_hallucination() ) == nullptr ) ) { valid_stumbles.push_back( dest ); } } } if( g->m.has_zlevels() ) { tripoint below( posx(), posy(), posz() - 1 ); tripoint above( posx(), posy(), posz() + 1 ); if( g->m.valid_move( pos(), below, false, true ) && can_move_to( below ) ) { valid_stumbles.push_back( below ); } // More restrictions for moving up if( one_in( 5 ) && has_flag( MF_FLIES ) && g->m.valid_move( pos(), above, false, true ) && can_move_to( above ) ) { valid_stumbles.push_back( above ); } } if( valid_stumbles.empty() ) { //nowhere to stumble? return; } move_to( random_entry( valid_stumbles ), false ); }
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 ); }
/* will_reach() is used for determining whether we'll get to stairs (and * potentially other locations of interest). It is generally permissive. * TODO: Pathfinding; Make sure that non-smashing monsters won't "teleport" through windows Injure monsters if they're gonna be walking through pits or whatevs */ bool monster::will_reach( int x, int y ) { monster_attitude att = attitude( &( g->u ) ); if( att != MATT_FOLLOW && att != MATT_ATTACK && att != MATT_FRIEND && att != MATT_ZLAVE ) { return false; } if( has_flag( MF_DIGS ) || has_flag( MF_AQUATIC ) ) { return false; } if( has_flag( MF_IMMOBILE ) && ( posx() != x || posy() != y ) ) { return false; } std::vector<tripoint> path = g->m.route( pos(), tripoint(x, y, posz()), 0, 100 ); if( path.empty() ) { return false; } if( has_flag( MF_SMELLS ) && g->scent( pos3() ) > 0 && g->scent( { x, y, posz() } ) > g->scent( pos3() ) ) { return true; } if( can_hear() && wandf > 0 && rl_dist( wander_pos.x, wander_pos.y, x, y ) <= 2 && rl_dist( posx(), posy(), wander_pos.x, wander_pos.y ) <= wandf ) { return true; } int t; if( can_see() && g->m.sees( posx(), posy(), x, y, g->light_level(), t ) ) { return true; } return false; }
/* will_reach() is used for determining whether we'll get to stairs (and * potentially other locations of interest). It is generally permissive. * TODO: Pathfinding; Make sure that non-smashing monsters won't "teleport" through windows Injure monsters if they're gonna be walking through pits or whatevs */ bool monster::will_reach( int x, int y ) { monster_attitude att = attitude( &( g->u ) ); if( att != MATT_FOLLOW && att != MATT_ATTACK && att != MATT_FRIEND && att != MATT_ZLAVE ) { return false; } if( has_flag( MF_DIGS ) || has_flag( MF_AQUATIC ) ) { return false; } if( has_flag( MF_IMMOBILE ) && ( posx() != x || posy() != y ) ) { return false; } auto path = g->m.route( pos(), tripoint( x, y, posz() ), get_pathfinding_settings() ); if( path.empty() ) { return false; } if( has_flag( MF_SMELLS ) && g->scent.get( pos() ) > 0 && g->scent.get( { x, y, posz() } ) > g->scent.get( pos() ) ) { return true; } if( can_hear() && wandf > 0 && rl_dist( wander_pos.x, wander_pos.y, x, y ) <= 2 && rl_dist( posx(), posy(), wander_pos.x, wander_pos.y ) <= wandf ) { return true; } if( can_see() && sees( tripoint( x, y, posz() ) ) ) { return true; } return false; }
bool monster::attack_at( const tripoint &p ) { if( p.z != posz() ) { return false; // TODO: Remove this } if( p == g->u.pos() ) { melee_attack( g->u, true ); return true; } if( const auto mon_ = g->critter_at<monster>( p, is_hallucination() ) ) { monster &mon = *mon_; // Don't attack yourself. if( &mon == this ) { return false; } // With no melee dice, we can't attack, but we had to process until here // because hallucinations require no melee dice to destroy. if( type->melee_dice <= 0 ) { return false; } auto attitude = attitude_to( mon ); // MF_ATTACKMON == hulk behavior, whack everything in your way if( attitude == A_HOSTILE || has_flag( MF_ATTACKMON ) ) { melee_attack( mon, true ); return true; } return false; } npc *const guy = g->critter_at<npc>( p ); if( guy && type->melee_dice > 0 ) { // For now we're always attacking NPCs that are getting into our // way. This is consistent with how it worked previously, but // later on not hitting allied NPCs would be cool. melee_attack( *guy, true ); return true; } // Nothing to attack. return false; }
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; }
tripoint monster::scent_move() { 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( pos() ); } tripoint next( -1, -1, posz() ); if( ( !fleeing && g->scent( pos() ) > smell_threshold ) || ( fleeing && bestsmell == 0 ) ) { return next; } const bool can_bash = has_flag( MF_BASHES ) || has_flag( MF_BORES ); for( const auto &dest : g->m.points_in_radius( pos(), 1 ) ) { int smell = g->scent( dest ); int mon = g->mon_at( dest ); if( ( mon == -1 || g->zombie( mon ).friendly != 0 || has_flag( MF_ATTACKMON ) ) && ( can_move_to( dest ) || ( dest == g->u.pos3() ) || ( 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 ); }
/** Create or adjust "pos" parameter for a component * Assumed that name either equals "x", "y" or "z" otherwise this * method will not add or modify "pos" parameter * @param comp :: Component * @param name :: name of the parameter * @param value :: value * @param pDescription :: a pointer (may be NULL) to a string, containing * parameter's * description. If provided, the contents of the string is copied to the * parameters * memory */ void ParameterMap::addPositionCoordinate( const IComponent *comp, const std::string &name, const double value, const std::string *const pDescription) { Parameter_sptr param = get(comp, pos()); V3D position; if (param) { // so "pos" already defined position = param->value<V3D>(); } else { // so "pos" is not defined - therefore get position from component position = comp->getPos(); } // adjust position if (name.compare(posx()) == 0) position.setX(value); else if (name.compare(posy()) == 0) position.setY(value); else if (name.compare(posz()) == 0) position.setZ(value); else { g_log.warning() << "addPositionCoordinate() called with unrecognized " "coordinate symbol: " << name; // set description if one is provided if (pDescription) { param->setDescription(*pDescription); } return; } // clear the position cache clearPositionSensitiveCaches(); // finally add or update "pos" parameter addV3D(comp, pos(), position, pDescription); }
int monster::turns_to_reach( int x, int y ) { // This function is a(n old) temporary hack that should soon be removed auto path = g->m.route( pos(), tripoint( x, y, posz() ), get_pathfinding_settings() ); if( path.empty() ) { return 999; } double turns = 0.; for( size_t i = 0; i < path.size(); i++ ) { const tripoint &next = path[i]; if( g->m.impassable( next ) ) { // No bashing through, it looks stupid when you go back and find // the doors intact. return 999; } else if( i == 0 ) { turns += double( calc_movecost( pos(), next ) ) / get_speed(); } else { turns += double( calc_movecost( path[i - 1], next ) ) / get_speed(); } } return int( turns + .9 ); // Halve (to get turns) and round up }
bool Creature::sees( const tripoint &t, int &bresen1, int &bresen2 ) const { // TODO: FoV update bresen2 = 0; if( posz() != t.z ) { return false; } const int range_cur = sight_range( g->m.ambient_light_at(t) ); const int range_day = sight_range( DAYLIGHT_LEVEL ); const int range_min = std::min( range_cur, range_day ); const int wanted_range = rl_dist( pos3(), t ); if( wanted_range <= range_min || ( wanted_range <= range_day && g->m.ambient_light_at( t ) > g->natural_light_level() ) ) { if( g->m.ambient_light_at( t ) > g->natural_light_level() ) { return g->m.sees( pos3(), t, wanted_range, bresen1, bresen2 ); } else { return g->m.sees( pos3(), t, range_min, bresen1, bresen2 ); } } else { return false; } }
/* * Drawing-related functions */ void Creature::draw(WINDOW *w, int player_x, int player_y, bool inverted) const { draw( w, tripoint( player_x, player_y, posz() ), inverted ); }
bool Creature::sees( const point t ) const { int bresen1, bresen2; return sees( tripoint( t, posz() ), bresen1, bresen2 ); }
bool Creature::sees( const int tx, const int ty, int &bresenham_slope ) const { int junk; return sees( tripoint( tx, ty, posz() ), bresenham_slope, junk ); }
tripoint monster::wander_next() { tripoint next = pos(); bool xbest = true; if( abs( wander_pos.y - posy() ) > abs( wander_pos.x - posx() ) ) { // Which is more important xbest = false; } int x = posx(), x2 = posx() - 1, x3 = posx() + 1; int y = posy(), y2 = posy() - 1, y3 = posy() + 1; int z = posz(); // Used to avoid checking same points 3 times when moving in a straight line // *_move is true if pos*() != wander_pos.* bool x_move = true; bool y_move = true; bool z_move = true; if( wander_pos.x < posx() ) { x--; x2++; } else if( wander_pos.x > posx() ) { x++; x2++; x3 -= 2; } else { x_move = false; } if( wander_pos.y < posy() ) { y--; y2++; } else if( wander_pos.y > posy() ) { y++; y2++; y3 -= 2; } else { y_move = false; } if( wander_pos.z < posz() ) { z--; } else if( wander_pos.z > posz() ) { z++; } else { z_move = false; } if( !x_move && !y_move && !z_move ) { return next; } // Any creature can "fly" downwards const bool flies = z < posz() || has_flag( MF_FLIES ); const bool climbs = has_flag( MF_CLIMBS ); const bool canbash = has_flag( MF_BASHES ) || has_flag( MF_BORES ); const int bash_est = bash_estimate(); // Check if we can move into position, attack player on position or bash position // If yes, set next to this position and return true, otherwise return false const auto try_pos = [&]( const int x, const int y, const int z ) { tripoint dest( x, y, z ); if( ( canbash && g->m.bash_rating( bash_est, dest ) > 0 ) || ( ( flies || g->m.has_floor_or_support( dest ) ) && can_move_to( dest ) ) ) { next = dest; return true; } return false; }; bool found = false; const bool can_climb = z_move && (flies || climbs || g->m.has_flag( TFLAG_RAMP, pos() )); if( z_move && g->m.valid_move( pos(), tripoint( posx(), posy(), z ), false, can_climb ) ) { found = true; if( ( x_move || y_move ) && try_pos( x, y, z ) ) { } else if( y_move && try_pos( x, y2, z ) ) { } else if( x_move && try_pos( x2, y, z ) ) { } else if( y_move && try_pos( x, y3, z ) ) { } else if( x_move && try_pos( x3, y, z ) ) { } else if( try_pos( posx(), posy(), z ) ) { } else { found = false; } } if( found ) { return next; } if( xbest ) { if( ( x_move || y_move ) && try_pos( x, y, posz() ) ) { // Do nothing in each of those ifs, the if-else is just for convenience } else if( y_move && try_pos( x, y2, posz() ) ) { } else if( x_move && try_pos( x2, y, posz() ) ) { } else if( y_move && try_pos( x, y3, posz() ) ) { } else if( x_move && try_pos( x3, y, posz() ) ) { } } else { if( ( x_move || y_move ) && try_pos( x, y, posz() ) ) { } else if( x_move && try_pos( x2, y, posz() ) ) { } else if( y_move && try_pos( x, y2, posz() ) ) { } else if( x_move && try_pos( x3, y, posz() ) ) { } else if( y_move && try_pos( x, y3, posz() ) ) { } } return next; }
bool Creature::sees( const int tx, const int ty ) const { int bresen1, bresen2; return sees( tripoint( tx, ty, posz() ), bresen1, bresen2 ); }
bool monster::move_to( const tripoint &p, bool force, float slope ) { const bool digs = digging(); const bool flies = has_flag( MF_FLIES ); const bool on_ground = !digs && !flies; const bool climbs = has_flag( MF_CLIMBS ) && g->m.has_flag( TFLAG_NO_FLOOR, p ); // Allows climbing monsters to move on terrain with movecost <= 0 Creature *critter = g->critter_at( p, is_hallucination() ); if( g->m.has_flag( "CLIMBABLE", p ) ) { if( g->m.move_cost( p ) == 0 && critter == nullptr ) { if( flies ) { moves -= 100; force = true; if (g->u.sees( *this )){ add_msg(_("The %1$s flies over the %2$s."), name().c_str(), g->m.has_flag_furn("CLIMBABLE", p) ? g->m.furnname(p).c_str() : g->m.tername(p).c_str()); } } else if (has_flag(MF_CLIMBS)) { moves -= 150; force = true; if (g->u.sees( *this )){ add_msg(_("The %1$s climbs over the %2$s."), name().c_str(), g->m.has_flag_furn("CLIMBABLE", p) ? g->m.furnname(p).c_str() : g->m.tername(p).c_str()); } } } } if( critter != nullptr && !force ) { return false; } // Make sure that we can move there, unless force is true. if( !force && !can_move_to( p ) ) { return false; } if( !force ) { // This adjustment is to make it so that monster movement speed relative to the player // is consistent even if the monster stumbles, // and the same regardless of the distance measurement mode. const float stumble_multiplier = has_flag(MF_STUMBLES) ? (trigdist ? 0.83 : 1.0 - (0.25 * slope)) : 1.0; const int cost = stumble_multiplier * (float)(climbs ? calc_climb_cost( pos(), p ) : calc_movecost( pos(), p )); if( cost > 0 ) { moves -= cost; } else { return false; } } //Check for moving into/out of water bool was_water = g->m.is_divable( pos3() ); bool will_be_water = on_ground && can_submerge() && g->m.is_divable( p ); if( was_water && !will_be_water && g->u.sees( p ) ) { //Use more dramatic messages for swimming monsters add_msg( m_warning, _( "A %1$s %2$s from the %3$s!" ), name().c_str(), has_flag( MF_SWIMS ) || has_flag( MF_AQUATIC ) ? _( "leaps" ) : _( "emerges" ), g->m.tername( pos() ).c_str() ); } else if( !was_water && will_be_water && g->u.sees( p ) ) { add_msg( m_warning, _( "A %1$s %2$s into the %3$s!" ), name().c_str(), has_flag( MF_SWIMS ) || has_flag( MF_AQUATIC ) ? _( "dives" ) : _( "sinks" ), g->m.tername( p ).c_str() ); } setpos( p ); footsteps( p ); underwater = will_be_water; if( is_hallucination() ) { //Hallucinations don't do any of the stuff after this point return true; } // TODO: Make tanks stop taking damage from rubble, because it's just silly if( type->size != MS_TINY && on_ground ) { if( g->m.has_flag( "SHARP", pos() ) && !one_in( 4 ) ) { apply_damage( nullptr, bp_torso, rng( 1, 10 ) ); } if( g->m.has_flag( "ROUGH", pos() ) && one_in( 6 ) ) { apply_damage( nullptr, bp_torso, rng( 1, 2 ) ); } } if( g->m.has_flag( "UNSTABLE", p ) && on_ground ) { add_effect( "bouldering", 1, num_bp, true ); } else if( has_effect( "bouldering" ) ) { remove_effect( "bouldering" ); } g->m.creature_on_trap( *this ); if( !will_be_water && ( has_flag( MF_DIGS ) || has_flag( MF_CAN_DIG ) ) ) { underwater = g->m.has_flag( "DIGGABLE", pos() ); } // Diggers turn the dirt into dirtmound if( digging() ) { int factor = 0; switch( type->size ) { case MS_TINY: factor = 100; break; case MS_SMALL: factor = 30; break; case MS_MEDIUM: factor = 6; break; case MS_LARGE: factor = 3; break; case MS_HUGE: factor = 1; break; } if( has_flag( MF_VERMIN ) ) { factor *= 100; } if( one_in( factor ) ) { g->m.ter_set( pos(), t_dirtmound ); } } // Acid trail monsters leave... a trail of acid if( has_flag( MF_ACIDTRAIL ) ) { g->m.add_field( pos(), fd_acid, 3, 0 ); } if( has_flag( MF_SLUDGETRAIL ) ) { for( const tripoint &sludge_p : g->m.points_in_radius( pos(), 1 ) ) { const int fstr = 3 - ( abs( sludge_p.x - posx() ) + abs( sludge_p.y - posy() ) ); if( fstr >= 2 ) { g->m.add_field( sludge_p, fd_sludge, fstr, 0 ); } } } if( has_flag( MF_LEAKSGAS ) ) { if( one_in( 6 ) ) { tripoint dest( posx() + rng( -1, 1 ), posy() + rng( -1, 1 ), posz() ); g->m.add_field( dest, fd_toxic_gas, 3, 0 ); } } return true; }
void player::activate_mutation( const trait_id &mut ) { const mutation_branch &mdata = mut.obj(); auto &tdata = my_mutations[mut]; int cost = mdata.cost; // You can take yourself halfway to Near Death levels of hunger/thirst. // Fatigue can go to Exhausted. if ((mdata.hunger && get_hunger() >= 700) || (mdata.thirst && get_thirst() >= 260) || (mdata.fatigue && get_fatigue() >= EXHAUSTED)) { // Insufficient Foo to *maintain* operation is handled in player::suffer add_msg_if_player(m_warning, _("You feel like using your %s would kill you!"), mdata.name.c_str()); return; } if (tdata.powered && tdata.charge > 0) { // Already-on units just lose a bit of charge tdata.charge--; } else { // Not-on units, or those with zero charge, have to pay the power cost if (mdata.cooldown > 0) { tdata.charge = mdata.cooldown - 1; } if (mdata.hunger){ mod_hunger(cost); } if (mdata.thirst){ mod_thirst(cost); } if (mdata.fatigue){ mod_fatigue(cost); } tdata.powered = true; // Handle stat changes from activation apply_mods(mut, true); recalc_sight_limits(); } if( mut == trait_WEB_WEAVER ) { g->m.add_field(pos(), fd_web, 1, 0); add_msg_if_player(_("You start spinning web with your spinnerets!")); } else if (mut == "BURROW"){ if( is_underwater() ) { add_msg_if_player(m_info, _("You can't do that while underwater.")); tdata.powered = false; return; } tripoint dirp; if (!choose_adjacent(_("Burrow where?"), dirp)) { tdata.powered = false; return; } if( dirp == pos() ) { add_msg_if_player(_("You've got places to go and critters to beat.")); add_msg_if_player(_("Let the lesser folks eat their hearts out.")); tdata.powered = false; return; } int turns; if (g->m.is_bashable(dirp) && g->m.has_flag("SUPPORTS_ROOF", dirp) && g->m.ter(dirp) != t_tree) { // Takes 30 minutes // Being better-adapted to the task means that skillful Survivors can do it almost twice as fast. turns = MINUTES( 30 ); } else if (g->m.move_cost(dirp) == 2 && g->get_levz() == 0 && g->m.ter(dirp) != t_dirt && g->m.ter(dirp) != t_grass) { turns = MINUTES( 10 ); } else { add_msg_if_player(m_info, _("You can't burrow there.")); tdata.powered = false; return; } assign_activity( activity_id( "ACT_BURROW" ), turns * 100, -1, 0 ); activity.placement = dirp; add_msg_if_player(_("You tear into the %s with your teeth and claws."), g->m.tername(dirp).c_str()); tdata.powered = false; return; // handled when the activity finishes } else if( mut == trait_SLIMESPAWNER ) { std::vector<tripoint> valid; for (int x = posx() - 1; x <= posx() + 1; x++) { for (int y = posy() - 1; y <= posy() + 1; y++) { tripoint dest(x, y, posz()); if (g->is_empty(dest)) { valid.push_back( dest ); } } } // Oops, no room to divide! if (valid.size() == 0) { add_msg_if_player(m_bad, _("You focus, but are too hemmed in to birth a new slimespring!")); tdata.powered = false; return; } add_msg_if_player(m_good, _("You focus, and with a pleasant splitting feeling, birth a new slimespring!")); int numslime = 1; for (int i = 0; i < numslime && !valid.empty(); i++) { const tripoint target = random_entry_removed( valid ); if( monster * const slime = g->summon_mon( mtype_id( "mon_player_blob" ), target ) ) { slime->friendly = -1; } } if (one_in(3)) { //~ Usual enthusiastic slimespring small voices! :D add_msg_if_player(m_good, _("wow! you look just like me! we should look out for each other!")); } else if (one_in(2)) { //~ Usual enthusiastic slimespring small voices! :D add_msg_if_player(m_good, _("come on, big me, let's go!")); } else { //~ Usual enthusiastic slimespring small voices! :D add_msg_if_player(m_good, _("we're a team, we've got this!")); } tdata.powered = false; return; } else if( mut == trait_NAUSEA || mut == trait_VOMITOUS ) { vomit(); tdata.powered = false; return; } else if( mut == trait_M_FERTILE ) { spores(); tdata.powered = false; return; } else if( mut == trait_M_BLOOM ) { blossoms(); tdata.powered = false; return; } else if( mut == trait_SELFAWARE ) { print_health(); tdata.powered = false; return; } else if( !mdata.spawn_item.empty() ) { item tmpitem( mdata.spawn_item ); i_add_or_drop( tmpitem ); add_msg_if_player( _( mdata.spawn_item_message.c_str() ) ); tdata.powered = false; return; } }
void player::activate_mutation( const std::string &mut ) { const auto &mdata = mutation_branch::get( mut ); auto &tdata = my_mutations[mut]; int cost = mdata.cost; // You can take yourself halfway to Near Death levels of hunger/thirst. // Fatigue can go to Exhausted. if ((mdata.hunger && hunger >= 700) || (mdata.thirst && thirst >= 260) || (mdata.fatigue && fatigue >= 575)) { // Insufficient Foo to *maintain* operation is handled in player::suffer add_msg(m_warning, _("You feel like using your %s would kill you!"), mdata.name.c_str()); return; } if (tdata.powered && tdata.charge > 0) { // Already-on units just lose a bit of charge tdata.charge--; } else { // Not-on units, or those with zero charge, have to pay the power cost if (mdata.cooldown > 0) { tdata.charge = mdata.cooldown - 1; } if (mdata.hunger){ hunger += cost; } if (mdata.thirst){ thirst += cost; } if (mdata.fatigue){ fatigue += cost; } tdata.powered = true; // Handle stat changes from activation apply_mods(mut, true); recalc_sight_limits(); } if( mut == "WEB_WEAVER" ) { g->m.add_field(pos(), fd_web, 1, 0); add_msg(_("You start spinning web with your spinnerets!")); } else if (mut == "BURROW"){ if (g->u.is_underwater()) { add_msg_if_player(m_info, _("You can't do that while underwater.")); tdata.powered = false; return; } int dirx, diry; if (!choose_adjacent(_("Burrow where?"), dirx, diry)) { tdata.powered = false; return; } if (dirx == g->u.posx() && diry == g->u.posy()) { add_msg_if_player(_("You've got places to go and critters to beat.")); add_msg_if_player(_("Let the lesser folks eat their hearts out.")); tdata.powered = false; return; } int turns; if (g->m.is_bashable(dirx, diry) && g->m.has_flag("SUPPORTS_ROOF", dirx, diry) && g->m.ter(dirx, diry) != t_tree) { // Takes about 100 minutes (not quite two hours) base time. // Being better-adapted to the task means that skillful Survivors can do it almost twice as fast. turns = (100000 - 5000 * g->u.skillLevel("carpentry")); } else if (g->m.move_cost(dirx, diry) == 2 && g->get_levz() == 0 && g->m.ter(dirx, diry) != t_dirt && g->m.ter(dirx, diry) != t_grass) { turns = 18000; } else { add_msg_if_player(m_info, _("You can't burrow there.")); tdata.powered = false; return; } g->u.assign_activity(ACT_BURROW, turns, -1, 0); g->u.activity.placement = tripoint(dirx, diry,0); add_msg_if_player(_("You tear into the %s with your teeth and claws."), g->m.tername(dirx, diry).c_str()); tdata.powered = false; return; // handled when the activity finishes } else if (mut == "SLIMESPAWNER") { std::vector<tripoint> valid; for (int x = posx() - 1; x <= posx() + 1; x++) { for (int y = posy() - 1; y <= posy() + 1; y++) { tripoint dest(x, y, posz()); if (g->is_empty(dest)) { valid.push_back( dest ); } } } // Oops, no room to divide! if (valid.size() == 0) { add_msg(m_bad, _("You focus, but are too hemmed in to birth a new slimespring!")); tdata.powered = false; return; } add_msg(m_good, _("You focus, and with a pleasant splitting feeling, birth a new slimespring!")); int numslime = 1; for (int i = 0; i < numslime && !valid.empty(); i++) { const tripoint target = random_entry_removed( valid ); if (g->summon_mon("mon_player_blob", target)) { monster *slime = g->monster_at( target ); slime->friendly = -1; } } //~ Usual enthusiastic slimespring small voices! :D if (one_in(3)) { add_msg(m_good, _("wow! you look just like me! we should look out for each other!")); } else if (one_in(2)) { add_msg(m_good, _("come on, big me, let's go!")); } else { add_msg(m_good, _("we're a team, we've got this!")); } tdata.powered = false; return; } else if (mut == "SHOUT1") { sounds::sound(pos(), 10 + 2 * str_cur, _("You shout loudly!")); tdata.powered = false; return; } else if (mut == "SHOUT2"){ sounds::sound(pos(), 15 + 3 * str_cur, _("You scream loudly!")); tdata.powered = false; return; } else if (mut == "SHOUT3"){ sounds::sound(pos(), 20 + 4 * str_cur, _("You let out a piercing howl!")); tdata.powered = false; return; } else if ((mut == "NAUSEA") || (mut == "VOMITOUS") ){ vomit(); tdata.powered = false; return; } else if (mut == "M_FERTILE"){ spores(); tdata.powered = false; return; } else if (mut == "M_BLOOM"){ blossoms(); tdata.powered = false; return; } else if (mut == "VINES3"){ item newit("vine_30", calendar::turn, false); if (!can_pickVolume(newit.volume())) { //Accounts for result_mult add_msg(_("You detach a vine but don't have room to carry it, so you drop it.")); g->m.add_item_or_charges(posx(), posy(), newit); } else if (!can_pickWeight(newit.weight(), !OPTIONS["DANGEROUS_PICKUPS"])) { add_msg(_("Your freshly-detached vine is too heavy to carry, so you drop it.")); g->m.add_item_or_charges(posx(), posy(), newit); } else { inv.assign_empty_invlet(newit); newit = i_add(newit); add_msg(m_info, "%c - %s", newit.invlet == 0 ? ' ' : newit.invlet, newit.tname().c_str()); } tdata.powered = false; return; } }
bool Creature::sees( const int tx, const int ty ) const { return sees( tripoint( tx, ty, posz() ) ); }
/* * Drawing-related functions */ void Creature::draw( const catacurses::window &w, int player_x, int player_y, bool inverted ) const { draw( w, tripoint( player_x, player_y, posz() ), inverted ); }
bool Creature::sees( const point t ) const { return sees( tripoint( t, posz() ) ); }
void monster::plan( const mfactions &factions ) { // Bots are more intelligent than most living stuff bool electronic = has_flag( MF_ELECTRONIC ); Creature *target = nullptr; // 8.6f is rating for tank drone 60 tiles away, moose 16 or boomer 33 float dist = !electronic ? 1000 : 8.6f; int bresenham_slope = 0; int bresen2 = 0; // Unused until FoV update int selected_slope = 0; bool fleeing = false; bool docile = has_flag( MF_VERMIN ) || ( friendly != 0 && has_effect( "docile" ) ); bool angers_hostile_weak = type->anger.find( MTRIG_HOSTILE_WEAK ) != type->anger.end(); int angers_hostile_near = ( type->anger.find( MTRIG_HOSTILE_CLOSE ) != type->anger.end() ) ? 5 : 0; int fears_hostile_near = ( type->fear.find( MTRIG_HOSTILE_CLOSE ) != type->fear.end() ) ? 5 : 0; bool group_morale = has_flag( MF_GROUP_MORALE ) && morale < type->morale; bool swarms = has_flag( MF_SWARMS ); auto mood = attitude(); // If we can see the player, move toward them or flee. if( friendly == 0 && sees( g->u, bresenham_slope ) ) { dist = rate_target( g->u, bresenham_slope, bresen2, dist, electronic ); fleeing = fleeing || is_fleeing( g->u ); target = &g->u; selected_slope = bresenham_slope; if( dist <= 5 ) { anger += angers_hostile_near; morale -= fears_hostile_near; } } else if( friendly != 0 && !docile ) { // Target unfriendly monsters, only if we aren't interacting with the player. for( int i = 0, numz = g->num_zombies(); i < numz; i++ ) { monster &tmp = g->zombie( i ); if( tmp.friendly == 0 ) { float rating = rate_target( tmp, bresenham_slope, bresen2, dist, electronic ); if( rating < dist ) { target = &tmp; dist = rating; selected_slope = bresenham_slope; } } } } if( docile ) { if( friendly != 0 && target != nullptr ) { int slope = rng( 0, 1 ); set_dest( target->pos(), slope ); } return; } for( size_t i = 0; i < g->active_npc.size(); i++ ) { npc *me = g->active_npc[i]; float rating = rate_target( *me, bresenham_slope, bresen2, dist, electronic ); bool fleeing_from = is_fleeing( *me ); // Switch targets if closer and hostile or scarier than current target if( ( rating < dist && fleeing ) || ( rating < dist && attitude( me ) == MATT_ATTACK ) || ( !fleeing && fleeing_from ) ) { target = me; dist = rating; selected_slope = bresenham_slope; } fleeing = fleeing || fleeing_from; if( rating <= 5 ) { anger += angers_hostile_near; morale -= fears_hostile_near; } } fleeing = fleeing || ( mood == MATT_FLEE ); if( friendly == 0 ) { for( const auto &fac : factions ) { auto faction_att = faction.obj().attitude( fac.first ); if( faction_att == MFA_NEUTRAL || faction_att == MFA_FRIENDLY ) { continue; } for( int i : fac.second ) { // mon indices monster &mon = g->zombie( i ); float rating = rate_target( mon, bresenham_slope, bresen2, dist, electronic ); if( rating < dist ) { target = &mon; dist = rating; selected_slope = bresenham_slope; } if( rating <= 5 ) { anger += angers_hostile_near; morale -= fears_hostile_near; } } } } // Friendly monsters here // Avoid for hordes of same-faction stuff or it could get expensive const auto actual_faction = friendly == 0 ? faction : mfaction_str_id( "player" ); auto const &myfaction_iter = factions.find( actual_faction ); if( myfaction_iter == factions.end() ) { DebugLog( D_ERROR, D_GAME ) << disp_name() << " tried to find faction " << actual_faction.id().str() << " which wasn't loaded in game::monmove"; swarms = false; group_morale = false; } swarms = swarms && target == nullptr; // Only swarm if we have no target if( group_morale || swarms ) { for( const int i : myfaction_iter->second ) { monster &mon = g->zombie( i ); float rating = rate_target( mon, bresenham_slope, bresen2, dist, electronic ); if( group_morale && rating <= 10 ) { morale += 10 - rating; } if( swarms ) { if( rating < 5 ) { // Too crowded here wander_pos.x = posx() * rng( 1, 3 ) - mon.posx(); wander_pos.y = posy() * rng( 1, 3 ) - mon.posy(); wandf = 2; target = nullptr; // Swarm to the furthest ally you can see } else if( rating < INT_MAX && rating > dist && wandf <= 0 ) { target = &mon; dist = rating; selected_slope = bresenham_slope; } } } } if( target != nullptr ) { if( one_in( 2 ) ) { // Random for the diversity of the trajectory ++selected_slope; } else { --selected_slope; } tripoint dest = target->pos(); auto att_to_target = attitude_to( *target ); if( att_to_target == Attitude::A_HOSTILE && !fleeing ) { set_dest( dest, selected_slope ); } else if( fleeing ) { set_dest( tripoint( posx() * 2 - dest.x, posy() * 2 - dest.y, posz() ), selected_slope ); } if( angers_hostile_weak && att_to_target != Attitude::A_FRIENDLY ) { int hp_per = target->hp_percentage(); if( hp_per <= 70 ) { anger += 10 - int( hp_per / 10 ); } } } else if( friendly > 0 && one_in( 3 ) ) { // Grow restless with no targets friendly--; } else if( friendly < 0 && sees( g->u, bresenham_slope ) ) { if( rl_dist( pos3(), g->u.pos3() ) > 2 ) { set_dest( g->u.pos3(), bresenham_slope ); } else { plans.clear(); } } // If we're not adjacent to the start of our plan path, don't act on it. // This is to catch when we had pre-existing invalid plans and // made it through the function without changing them. if( !plans.empty() && square_dist( pos3(), plans.front() ) > 1 ) { plans.clear(); } }
void monster::plan( const mfactions &factions ) { // Bots are more intelligent than most living stuff bool smart_planning = has_flag( MF_PRIORITIZE_TARGETS ); Creature *target = nullptr; // 8.6f is rating for tank drone 60 tiles away, moose 16 or boomer 33 float dist = !smart_planning ? 1000 : 8.6f; bool fleeing = false; bool docile = friendly != 0 && has_effect( effect_docile ); bool angers_hostile_weak = type->anger.find( MTRIG_HOSTILE_WEAK ) != type->anger.end(); int angers_hostile_near = ( type->anger.find( MTRIG_HOSTILE_CLOSE ) != type->anger.end() ) ? 5 : 0; int fears_hostile_near = ( type->fear.find( MTRIG_HOSTILE_CLOSE ) != type->fear.end() ) ? 5 : 0; bool group_morale = has_flag( MF_GROUP_MORALE ) && morale < type->morale; bool swarms = has_flag( MF_SWARMS ); auto mood = attitude(); // If we can see the player, move toward them or flee. if( friendly == 0 && sees( g->u ) ) { dist = rate_target( g->u, dist, smart_planning ); fleeing = fleeing || is_fleeing( g->u ); target = &g->u; if( dist <= 5 ) { anger += angers_hostile_near; morale -= fears_hostile_near; } } else if( friendly != 0 && !docile ) { // Target unfriendly monsters, only if we aren't interacting with the player. for( int i = 0, numz = g->num_zombies(); i < numz; i++ ) { monster &tmp = g->zombie( i ); if( tmp.friendly == 0 ) { float rating = rate_target( tmp, dist, smart_planning ); if( rating < dist ) { target = &tmp; dist = rating; } } } } if( docile ) { if( friendly != 0 && target != nullptr ) { set_dest( target->pos() ); } return; } for( size_t i = 0; i < g->active_npc.size(); i++ ) { npc &who = *g->active_npc[i]; auto faction_att = faction.obj().attitude( who.get_monster_faction() ); if( faction_att == MFA_NEUTRAL || faction_att == MFA_FRIENDLY ) { continue; } float rating = rate_target( who, dist, smart_planning ); bool fleeing_from = is_fleeing( who ); // Switch targets if closer and hostile or scarier than current target if( ( rating < dist && fleeing ) || ( rating < dist && attitude( &who ) == MATT_ATTACK ) || ( !fleeing && fleeing_from ) ) { target = &who; dist = rating; } fleeing = fleeing || fleeing_from; if( rating <= 5 ) { anger += angers_hostile_near; morale -= fears_hostile_near; } } fleeing = fleeing || ( mood == MATT_FLEE ); if( friendly == 0 ) { for( const auto &fac : factions ) { auto faction_att = faction.obj().attitude( fac.first ); if( faction_att == MFA_NEUTRAL || faction_att == MFA_FRIENDLY ) { continue; } for( int i : fac.second ) { // mon indices monster &mon = g->zombie( i ); float rating = rate_target( mon, dist, smart_planning ); if( rating < dist ) { target = &mon; dist = rating; } if( rating <= 5 ) { anger += angers_hostile_near; morale -= fears_hostile_near; } } } } // Friendly monsters here // Avoid for hordes of same-faction stuff or it could get expensive const auto actual_faction = friendly == 0 ? faction : mfaction_str_id( "player" ); auto const &myfaction_iter = factions.find( actual_faction ); if( myfaction_iter == factions.end() ) { DebugLog( D_ERROR, D_GAME ) << disp_name() << " tried to find faction " << actual_faction.id().str() << " which wasn't loaded in game::monmove"; swarms = false; group_morale = false; } swarms = swarms && target == nullptr; // Only swarm if we have no target if( group_morale || swarms ) { for( const int i : myfaction_iter->second ) { monster &mon = g->zombie( i ); float rating = rate_target( mon, dist, smart_planning ); if( group_morale && rating <= 10 ) { morale += 10 - rating; } if( swarms ) { if( rating < 5 ) { // Too crowded here wander_pos.x = posx() * rng( 1, 3 ) - mon.posx(); wander_pos.y = posy() * rng( 1, 3 ) - mon.posy(); wandf = 2; target = nullptr; // Swarm to the furthest ally you can see } else if( rating < INT_MAX && rating > dist && wandf <= 0 ) { target = &mon; dist = rating; } } } } if( target != nullptr ) { tripoint dest = target->pos(); auto att_to_target = attitude_to( *target ); if( att_to_target == Attitude::A_HOSTILE && !fleeing ) { set_dest( dest ); } else if( fleeing ) { set_dest( tripoint( posx() * 2 - dest.x, posy() * 2 - dest.y, posz() ) ); } if( angers_hostile_weak && att_to_target != Attitude::A_FRIENDLY ) { int hp_per = target->hp_percentage(); if( hp_per <= 70 ) { anger += 10 - int( hp_per / 10 ); } } } else if( friendly > 0 && one_in( 3 ) ) { // Grow restless with no targets friendly--; } else if( friendly < 0 && sees( g->u ) ) { if( rl_dist( pos(), g->u.pos() ) > 2 ) { set_dest( g->u.pos() ); } else { unset_dest(); } } }
// 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(); } }