item_location game_menus::inv::container_for( player &p, const item &liquid, int radius ) { const auto filter = [ &liquid ]( const item_location & location ) { if( location.where() == item_location::type::character ) { Character *character = dynamic_cast<Character *>( g->critter_at( location.position() ) ); if( character == nullptr ) { debugmsg( "Invalid location supplied to the liquid filter: no character found." ); return false; } return location->get_remaining_capacity_for_liquid( liquid, *character ) > 0; } const bool allow_buckets = location.where() == item_location::type::map; return location->get_remaining_capacity_for_liquid( liquid, allow_buckets ) > 0; }; return inv_internal( p, inventory_filter_preset( filter ), string_format( _( "Container for %s" ), liquid.display_name( liquid.charges ).c_str() ), radius, string_format( _( "You don't have a suitable container for carrying %s." ), liquid.tname().c_str() ) ); }
//helper function for Pickup::pick_up //return value is amount of ammo added to quiver int Pickup::handle_quiver_insertion(item &here, bool inv_on_fail, int &moves_to_decrement, bool &picked_up) { //add ammo to quiver int quivered = here.add_ammo_to_quiver(&g->u, true); if(quivered > 0) { moves_to_decrement = 0; //moves already decremented in item::add_ammo_to_quiver() picked_up = true; return quivered; } else if (inv_on_fail) { //add to inventory instead g->u.i_add(here); picked_up = true; //display output message std::map<std::string, int> map_pickup; int charges = (here.count_by_charges()) ? here.charges : 1; map_pickup.insert(std::pair<std::string, int>(here.tname(), charges)); show_pickup_message(map_pickup); } return 0; }
item_location game_menus::inv::holster( player &p, item &holster ) { const std::string holster_name = holster.tname( 1, false ); const auto actor = dynamic_cast<const holster_actor *> ( holster.type->get_use( "holster" )->get_actor_ptr() ); if( !actor ) { const std::string msg = string_format( _( "You can't put anything into your %s." ), holster_name.c_str() ); popup( msg, PF_GET_KEY ); return item_location(); } const std::string title = actor->holster_prompt.empty() ? _( "Holster item" ) : _( actor->holster_prompt.c_str() ); const std::string hint = string_format( _( "Choose a weapon to put into your %s" ), holster_name.c_str() ); return inv_internal( p, holster_inventory_preset( p, *actor ), title, 1, string_format( _( "You have no weapons you could put into your %s." ), holster_name.c_str() ), hint ); }
bool player::feed_reactor_with( item &it ) { if( !can_feed_reactor_with( it ) ) { return false; } const auto iter = plut_charges.find( it.typeId() ); const int max_amount = iter != plut_charges.end() ? iter->second : 0; const int amount = std::min( get_acquirable_energy( it, rechargeable_cbm::reactor ), max_amount ); if( amount >= PLUTONIUM_CHARGES * 10 && !query_yn( _( "Thats a LOT of plutonium. Are you sure you want that much?" ) ) ) { return false; } add_msg_player_or_npc( _( "You add your %s to your reactor's tank." ), _( "<npcname> pours %s into their reactor's tank." ), it.tname().c_str() ); tank_plut += amount; // @todo Encapsulate it.charges -= 1; mod_moves( -250 ); return true; }
void Pickup::pick_one_up( const tripoint &pickup_target, item &newit, vehicle *veh, int cargo_part, int index, int quantity, bool &got_water, bool &offered_swap, PickupMap &mapPickup, bool autopickup ) { player &u = g->u; int moves_taken = 100; bool picked_up = false; pickup_answer option = CANCEL; item leftovers = newit; if( newit.invlet != '\0' && u.invlet_to_position( newit.invlet ) != INT_MIN ) { // Existing invlet is not re-usable, remove it and let the code in player.cpp/inventory.cpp // add a new invlet, otherwise keep the (usable) invlet. newit.invlet = '\0'; } if( quantity != 0 && newit.count_by_charges() ) { // Reinserting leftovers happens after item removal to avoid stacking issues. leftovers.charges = newit.charges - quantity; if( leftovers.charges > 0 ) { newit.charges = quantity; } } else { leftovers.charges = 0; } if( newit.made_of( LIQUID ) ) { got_water = true; } else if( !u.can_pickWeight( newit, false ) ) { add_msg( m_info, _( "The %s is too heavy!" ), newit.display_name().c_str() ); } else if( newit.is_ammo() && ( newit.ammo_type() == ammotype( "arrow" ) || newit.ammo_type() == ammotype( "bolt" ) ) ) { // @todo Make quiver code generic so that ammo pouches can use it too //add ammo to quiver int quivered = handle_quiver_insertion( newit, moves_taken, picked_up ); if( quivered > 0 ) { quantity = quivered; //already picked up some for quiver so use special case handling picked_up = true; option = NUM_ANSWERS; } if( newit.charges > 0 ) { if( !u.can_pickVolume( newit ) ) { if( !autopickup ) { // Silence some messaging if we're doing autopickup. add_msg( m_info, ngettext( "There's no room in your inventory for the %s.", "There's no room in your inventory for the %s.", newit.charges ), newit.tname( newit.charges ).c_str() ); } } else { // Add to inventory instead option = STASH; } } if( option == NUM_ANSWERS ) { //not picking up the rest so //update the charges for the item that gets re-added to the game map leftovers.charges = newit.charges; } } else if( newit.is_bucket() && !newit.is_container_empty() ) { if( !autopickup ) { const std::string &explain = string_format( _( "Can't stash %s while it's not empty" ), newit.display_name().c_str() ); option = handle_problematic_pickup( newit, offered_swap, explain ); } else { option = CANCEL; } } else if( !u.can_pickVolume( newit ) ) { if( !autopickup ) { const std::string &explain = string_format( _( "Not enough capacity to stash %s" ), newit.display_name().c_str() ); option = handle_problematic_pickup( newit, offered_swap, explain ); } else { option = CANCEL; } } else { option = STASH; } switch( option ) { case NUM_ANSWERS: // Some other option break; case CANCEL: picked_up = false; break; case WEAR: picked_up = u.wear_item( newit ); break; case WIELD: picked_up = u.wield( newit ); if( !picked_up ) { break; } if( u.weapon.invlet ) { add_msg( m_info, _( "Wielding %c - %s" ), u.weapon.invlet, u.weapon.display_name().c_str() ); } else { add_msg( m_info, _( "Wielding - %s" ), u.weapon.display_name().c_str() ); } break; case SPILL: if( newit.is_container_empty() ) { debugmsg( "Tried to spill contents from an empty container" ); break; } picked_up = newit.spill_contents( u ); if( !picked_up ) { break; } // Intentional fallthrough case STASH: auto &entry = mapPickup[newit.tname()]; entry.second += newit.count_by_charges() ? newit.charges : 1; entry.first = u.i_add( newit ); picked_up = true; break; } if( picked_up ) { Pickup::remove_from_map_or_vehicle( pickup_target, veh, cargo_part, moves_taken, index ); } if( leftovers.charges > 0 ) { bool to_map = veh == nullptr; if( !to_map ) { to_map = !veh->add_item( cargo_part, leftovers ); } if( to_map ) { g->m.add_item_or_charges( pickup_target, leftovers ); } } }
void Pickup::pick_one_up( const tripoint &pickup_target, item &newit, vehicle *veh, int cargo_part, int index, int quantity, bool &got_water, bool &offered_swap, PickupMap &mapPickup, bool autopickup ) { int moves_taken = 100; bool picked_up = false; item leftovers = newit; if( newit.invlet != '\0' && g->u.invlet_to_position( newit.invlet ) != INT_MIN ) { // Existing invlet is not re-usable, remove it and let the code in player.cpp/inventory.cpp // add a new invlet, otherwise keep the (usable) invlet. newit.invlet = '\0'; } if( quantity != 0 && newit.count_by_charges() ) { // Reinserting leftovers happens after item removal to avoid stacking issues. leftovers.charges = newit.charges - quantity; if( leftovers.charges > 0 ) { newit.charges = quantity; } } else { leftovers.charges = 0; } if( newit.made_of(LIQUID) ) { got_water = true; } else if (!g->u.can_pickWeight(newit.weight(), false)) { add_msg(m_info, _("The %s is too heavy!"), newit.display_name().c_str()); } else if( newit.is_ammo() && (newit.ammo_type() == "arrow" || newit.ammo_type() == "bolt")) { //add ammo to quiver int quivered = handle_quiver_insertion( newit, moves_taken, picked_up); if( newit.charges > 0) { if(!g->u.can_pickVolume( newit.volume())) { if(quivered > 0) { //update the charges for the item that gets re-added to the game map quantity = quivered; leftovers.charges = newit.charges; } if( !autopickup ) { // Silence some messaging if we're doing autopickup. add_msg(m_info, ngettext("There's no room in your inventory for the %s.", "There's no room in your inventory for the %s.", newit.charges), newit.tname(newit.charges).c_str()); } } else { //add to inventory instead item &it = g->u.i_add(newit); picked_up = true; //display output message PickupMap map_pickup; int charges = (newit.count_by_charges()) ? newit.charges : 1; map_pickup.insert(std::pair<std::string, ItemCount>(newit.tname(), ItemCount(it, charges))); show_pickup_message(map_pickup); } } } else if (!g->u.can_pickVolume(newit.volume())) { if( !autopickup ) { // Armor can be instantly worn if (newit.is_armor() && query_yn(_("Put on the %s?"), newit.display_name().c_str())) { if (g->u.wear_item(newit)) { picked_up = true; } } else if (g->u.is_armed()) { if (!g->u.weapon.has_flag("NO_UNWIELD")) { if( !offered_swap ) { offered_swap = true; if ( g->u.weapon.type->id != newit.type->id && query_yn(_("No space for %1$s; wield instead? (drops %2$s)"), newit.display_name().c_str(), g->u.weapon.display_name().c_str()) ) { picked_up = true; g->m.add_item_or_charges( pickup_target, g->u.remove_weapon(), 1 ); g->u.inv.assign_empty_invlet( newit, true ); // force getting an invlet. g->u.wield( &( g->u.i_add(newit) ) ); if (newit.invlet) { add_msg(m_info, _("Wielding %c - %s"), newit.invlet, newit.display_name().c_str()); } else { add_msg(m_info, _("Wielding - %s"), newit.display_name().c_str()); } } } } else { add_msg(m_info, _("There's no room in your inventory for the %s " "and you can't unwield your %s."), newit.display_name().c_str(), g->u.weapon.display_name().c_str()); } } else if( !g->u.is_armed() ) { if (g->u.keep_hands_free) { add_msg(m_info, _("There's no room in your inventory for the %s " "and you have decided to keep your hands free."), newit.display_name().c_str()); } else { g->u.inv.assign_empty_invlet(newit, true); // force getting an invlet. g->u.wield(&(g->u.i_add(newit))); picked_up = true; if (newit.invlet) { add_msg(m_info, _("Wielding %c - %s"), newit.invlet, newit.display_name().c_str()); } else { add_msg(m_info, _("Wielding - %s"), newit.display_name().c_str()); } } } // end of if unarmed } // end of if !autopickup } else { auto &entry = mapPickup[newit.tname()]; entry.second += newit.count_by_charges() ? newit.charges : 1; entry.first = g->u.i_add(newit); picked_up = true; } if(picked_up) { Pickup::remove_from_map_or_vehicle(pickup_target, veh, cargo_part, moves_taken, index); } if( leftovers.charges > 0 ) { bool to_map = veh == nullptr; if( !to_map ) { to_map = !veh->add_item( cargo_part, leftovers ); } if( to_map ) { g->m.add_item_or_charges( pickup_target, leftovers ); } } }
void game::throw_item(player &p, int tarx, int tary, item &thrown, std::vector<point> &trajectory) { int deviation = 0; int trange = 1.5 * rl_dist(p.posx, p.posy, tarx, tary); // Throwing attempts below "Basic Competency" level are extra-bad int skillLevel = p.skillLevel("throw"); if (skillLevel < 3) deviation += rng(0, 8 - skillLevel); if (skillLevel < 8) deviation += rng(0, 8 - skillLevel); else deviation -= skillLevel - 6; deviation += p.throw_dex_mod(); if (p.per_cur < 6) deviation += rng(0, 8 - p.per_cur); else if (p.per_cur > 8) deviation -= p.per_cur - 8; deviation += rng(0, p.encumb(bp_hands) * 2 + p.encumb(bp_eyes) + 1); if (thrown.volume() > 5) deviation += rng(0, 1 + (thrown.volume() - 5) / 4); if (thrown.volume() == 0) deviation += rng(0, 3); deviation += rng(0, 1 + abs(p.str_cur - thrown.weight())); double missed_by = .01 * deviation * trange; bool missed = false; int tart; if (missed_by >= 1) { // We missed D: // Shoot a random nearby space? if (missed_by > 9) missed_by = 9; tarx += rng(0 - int(sqrt(double(missed_by))), int(sqrt(double(missed_by)))); tary += rng(0 - int(sqrt(double(missed_by))), int(sqrt(double(missed_by)))); if (m.sees(p.posx, p.posy, tarx, tary, -1, tart)) trajectory = line_to(p.posx, p.posy, tarx, tary, tart); else trajectory = line_to(p.posx, p.posy, tarx, tary, 0); missed = true; if (!p.is_npc()) add_msg("You miss!"); } else if (missed_by >= .6) { // Hit the space, but not necessarily the monster there missed = true; if (!p.is_npc()) add_msg("You barely miss!"); } std::string message; int dam = (thrown.weight() / 4 + thrown.type->melee_dam / 2 + p.str_cur / 2) / double(2 + double(thrown.volume() / 4)); if (dam > thrown.weight() * 3) dam = thrown.weight() * 3; int i = 0, tx = 0, ty = 0; for (i = 0; i < trajectory.size() && dam > -10; i++) { message = ""; double goodhit = missed_by; tx = trajectory[i].x; ty = trajectory[i].y; // If there's a monster in the path of our item, and either our aim was true, // OR it's not the monster we were aiming at and we were lucky enough to hit it if (mon_at(tx, ty) != -1 && (!missed || one_in(7 - int(z[mon_at(tx, ty)].type->size)))) { if (rng(0, 100) < 20 + skillLevel * 12 && thrown.type->melee_cut > 0) { if (!p.is_npc()) { message += " You cut the "; message += z[mon_at(tx, ty)].name(); message += "!"; } if (thrown.type->melee_cut > z[mon_at(tx, ty)].armor_cut()) dam += (thrown.type->melee_cut - z[mon_at(tx, ty)].armor_cut()); } if (thrown.made_of(GLASS) && !thrown.active && // active = molotov, etc. rng(0, thrown.volume() + 8) - rng(0, p.str_cur) < thrown.volume()) { if (u_see(tx, ty)) add_msg("The %s shatters!", thrown.tname().c_str()); for (int i = 0; i < thrown.contents.size(); i++) m.add_item(tx, ty, thrown.contents[i]); sound(tx, ty, 16, "glass breaking!"); int glassdam = rng(0, thrown.volume() * 2); if (glassdam > z[mon_at(tx, ty)].armor_cut()) dam += (glassdam - z[mon_at(tx, ty)].armor_cut()); } else m.add_item(tx, ty, thrown); if (i < trajectory.size() - 1) goodhit = double(double(rand() / RAND_MAX) / 2); if (goodhit < .1 && !z[mon_at(tx, ty)].has_flag(MF_NOHEAD)) { message = "Headshot!"; dam = rng(dam, dam * 3); p.practice(turn, "throw", 5); } else if (goodhit < .2) { message = "Critical!"; dam = rng(dam, dam * 2); p.practice(turn, "throw", 2); } else if (goodhit < .4) dam = rng(int(dam / 2), int(dam * 1.5)); else if (goodhit < .5) { message = "Grazing hit."; dam = rng(0, dam); } if (!p.is_npc()) add_msg("%s You hit the %s for %d damage.", message.c_str(), z[mon_at(tx, ty)].name().c_str(), dam); else if (u_see(tx, ty)) add_msg("%s hits the %s for %d damage.", message.c_str(), z[mon_at(tx, ty)].name().c_str(), dam); if (z[mon_at(tx, ty)].hurt(dam)) kill_mon(mon_at(tx, ty), !p.is_npc()); return; } else // No monster hit, but the terrain might be. m.shoot(this, tx, ty, dam, false, 0); if (m.move_cost(tx, ty) == 0) { if (i > 0) { tx = trajectory[i - 1].x; ty = trajectory[i - 1].y; } else { tx = u.posx; ty = u.posy; } i = trajectory.size(); } } if (m.move_cost(tx, ty) == 0) { if (i > 1) { tx = trajectory[i - 2].x; ty = trajectory[i - 2].y; } else { tx = u.posx; ty = u.posy; } } if (thrown.made_of(GLASS) && !thrown.active && // active means molotov, etc rng(0, thrown.volume() + 8) - rng(0, p.str_cur) < thrown.volume()) { if (u_see(tx, ty)) add_msg("The %s shatters!", thrown.tname().c_str()); for (int i = 0; i < thrown.contents.size(); i++) m.add_item(tx, ty, thrown.contents[i]); sound(tx, ty, 16, "glass breaking!"); } else { sound(tx, ty, 8, "thud."); m.add_item(tx, ty, thrown); } }
void pick_one_up( const tripoint &pickup_target, item &newit, vehicle *veh, int cargo_part, int index, int quantity, bool &got_water, bool &offered_swap, PickupMap &mapPickup, bool autopickup ) { player &u = g->u; int moves_taken = 100; bool picked_up = false; pickup_answer option = CANCEL; item leftovers = newit; if( newit.invlet != '\0' && u.invlet_to_position( newit.invlet ) != INT_MIN ) { // Existing invlet is not re-usable, remove it and let the code in player.cpp/inventory.cpp // add a new invlet, otherwise keep the (usable) invlet. newit.invlet = '\0'; } if( quantity != 0 && newit.count_by_charges() ) { // Reinserting leftovers happens after item removal to avoid stacking issues. leftovers.charges = newit.charges - quantity; if( leftovers.charges > 0 ) { newit.charges = quantity; } } else { leftovers.charges = 0; } if( newit.made_of( LIQUID ) ) { got_water = true; } else if( !u.can_pickWeight( newit, false ) ) { add_msg( m_info, _( "The %s is too heavy!" ), newit.display_name().c_str() ); } else if( newit.is_bucket() && !newit.is_container_empty() ) { if( !autopickup ) { const std::string &explain = string_format( _( "Can't stash %s while it's not empty" ), newit.display_name().c_str() ); option = handle_problematic_pickup( newit, offered_swap, explain ); } else { option = CANCEL; } } else if( !u.can_pickVolume( newit ) ) { if( !autopickup ) { const std::string &explain = string_format( _( "Not enough capacity to stash %s" ), newit.display_name().c_str() ); option = handle_problematic_pickup( newit, offered_swap, explain ); } else { option = CANCEL; } } else { option = STASH; } switch( option ) { case NUM_ANSWERS: // Some other option break; case CANCEL: picked_up = false; break; case WEAR: picked_up = u.wear_item( newit ); break; case WIELD: picked_up = u.wield( newit ); if( !picked_up ) { break; } if( u.weapon.invlet ) { add_msg( m_info, _( "Wielding %c - %s" ), u.weapon.invlet, u.weapon.display_name().c_str() ); } else { add_msg( m_info, _( "Wielding - %s" ), u.weapon.display_name().c_str() ); } break; case SPILL: if( newit.is_container_empty() ) { debugmsg( "Tried to spill contents from an empty container" ); break; } picked_up = newit.spill_contents( u ); if( !picked_up ) { break; } // Intentional fallthrough case STASH: auto &entry = mapPickup[newit.tname()]; entry.second += newit.count_by_charges() ? newit.charges : 1; entry.first = u.i_add( newit ); picked_up = true; break; } if( picked_up ) { remove_from_map_or_vehicle( pickup_target, veh, cargo_part, moves_taken, index ); } if( leftovers.charges > 0 ) { bool to_map = veh == nullptr; if( !to_map ) { to_map = !veh->add_item( cargo_part, leftovers ); } if( to_map ) { g->m.add_item_or_charges( pickup_target, leftovers ); } } }
bool player::can_disassemble( const item &obj, const inventory &inv, std::string *err ) const { const auto error = [&err]( const std::string & message ) { if( err != nullptr ) { *err = message; } return false; }; const auto &r = recipe_dictionary::get_uncraft( obj.typeId() ); if( !r ) { return error( string_format( _( "You cannot disassemble this." ) ) ); } // check sufficient light if( lighting_craft_speed_multiplier( r ) == 0.0f ) { return error( _( "You can't see to craft!" ) ); } // refuse to disassemble rotten items if( obj.goes_bad() || ( obj.is_food_container() && obj.contents.front().goes_bad() ) ) { if( obj.rotten() || ( obj.is_food_container() && obj.contents.front().rotten() ) ) { return error( _( "It's rotten, I'm not taking that apart." ) ); } } if( obj.count_by_charges() && !r.has_flag( "UNCRAFT_SINGLE_CHARGE" ) ) { // Create a new item to get the default charges int qty = r.create_result().charges; if( obj.charges < qty ) { auto msg = ngettext( "You need at least %d charge of %s.", "You need at least %d charges of %s.", qty ); return error( string_format( msg, qty, obj.tname().c_str() ) ); } } const auto &dis = r.disassembly_requirements(); for( const auto &opts : dis.get_qualities() ) { for( const auto &qual : opts ) { if( !qual.has( inv ) ) { // Here should be no dot at the end of the string as 'to_string()' provides it. return error( string_format( _( "You need %s" ), qual.to_string().c_str() ) ); } } } for( const auto &opts : dis.get_tools() ) { const bool found = std::any_of( opts.begin(), opts.end(), [&]( const tool_comp & tool ) { return ( tool.count <= 0 && inv.has_tools( tool.type, 1 ) ) || ( tool.count > 0 && inv.has_charges( tool.type, tool.count ) ); } ); if( !found ) { if( opts.front().count <= 0 ) { return error( string_format( _( "You need %s." ), item::nname( opts.front().type ).c_str() ) ); } else { return error( string_format( ngettext( "You need a %s with %d charge.", "You need a %s with %d charges.", opts.front().count ), item::nname( opts.front().type ).c_str(), opts.front().count ) ); } } } return true; }
bool player::disassemble( item &obj, int pos, bool ground, bool interactive ) { // check sufficient tools for disassembly std::string err; if( !can_disassemble( obj, crafting_inventory(), &err ) ) { if( interactive ) { add_msg_if_player( m_info, "%s", err.c_str() ); } return false; } const auto &r = recipe_dictionary::get_uncraft( obj.typeId() ); // last chance to back out if( interactive && get_option<bool>( "QUERY_DISASSEMBLE" ) ) { const auto components( r.disassembly_requirements().get_components() ); std::ostringstream list; for( const auto &elem : components ) { list << "- " << elem.front().to_string() << std::endl; } if( !query_yn( _( "Disassembling the %s may yield:\n%s\nReally disassemble?" ), obj.tname().c_str(), list.str().c_str() ) ) { return false; } } if( activity.id() != activity_id( "ACT_DISASSEMBLE" ) ) { assign_activity( activity_id( "ACT_DISASSEMBLE" ), r.time ); } else if( activity.moves_left <= 0 ) { activity.moves_left = r.time; } activity.values.push_back( pos ); activity.coords.push_back( ground ? this->pos() : tripoint_min ); activity.str_values.push_back( r.result ); return true; }
void game::throw_item(player &p, int tarx, int tary, item &thrown, std::vector<point> &trajectory) { int deviation = 0; int trange = 1.5 * rl_dist(p.posx, p.posy, tarx, tary); std::set<std::string> no_effects; // Throwing attempts below "Basic Competency" level are extra-bad int skillLevel = p.skillLevel("throw"); if (skillLevel < 3) { deviation += rng(0, 8 - skillLevel); } if (skillLevel < 8) { deviation += rng(0, 8 - skillLevel); } else { deviation -= skillLevel - 6; } deviation += p.throw_dex_mod(); if (p.per_cur < 6) { deviation += rng(0, 8 - p.per_cur); } else if (p.per_cur > 8) { deviation -= p.per_cur - 8; } deviation += rng(0, p.encumb(bp_hands) * 2 + p.encumb(bp_eyes) + 1); if (thrown.volume() > 5) { deviation += rng(0, 1 + (thrown.volume() - 5) / 4); } if (thrown.volume() == 0) { deviation += rng(0, 3); } deviation += rng(0, std::max( 0, p.str_cur - thrown.weight() / 113 ) ); double missed_by = .01 * deviation * trange; bool missed = false; int tart; if (missed_by >= 1) { // We missed D: // Shoot a random nearby space? if (missed_by > 9) { missed_by = 9; } tarx += rng(0 - int(sqrt(double(missed_by))), int(sqrt(double(missed_by)))); tary += rng(0 - int(sqrt(double(missed_by))), int(sqrt(double(missed_by)))); if (m.sees(p.posx, p.posy, tarx, tary, -1, tart)) { trajectory = line_to(p.posx, p.posy, tarx, tary, tart); } else { trajectory = line_to(p.posx, p.posy, tarx, tary, 0); } missed = true; add_msg_if_player(&p,_("You miss!")); } else if (missed_by >= .6) { // Hit the space, but not necessarily the monster there missed = true; add_msg_if_player(&p,_("You barely miss!")); } std::string message; int real_dam = (thrown.weight() / 452 + thrown.type->melee_dam / 2 + p.str_cur / 2) / double(2 + double(thrown.volume() / 4)); if (real_dam > thrown.weight() / 40) { real_dam = thrown.weight() / 40; } if (p.has_active_bionic("bio_railgun") && (thrown.made_of("iron") || thrown.made_of("steel"))) { real_dam *= 2; } int dam = real_dam; int i = 0, tx = 0, ty = 0; for (i = 0; i < trajectory.size() && dam >= 0; i++) { message = ""; double goodhit = missed_by; tx = trajectory[i].x; ty = trajectory[i].y; const int zid = mon_at(tx, ty); // If there's a monster in the path of our item, and either our aim was true, // OR it's not the monster we were aiming at and we were lucky enough to hit it if (zid != -1 && (!missed || one_in(7 - int(zombie(zid).type->size)))) { monster &z = zombie(zid); if (rng(0, 100) < 20 + skillLevel * 12 && thrown.type->melee_cut > 0) { if (!p.is_npc()) { message += string_format(_(" You cut the %s!"), z.name().c_str()); } if (thrown.type->melee_cut > z.get_armor_cut(bp_torso)) { dam += (thrown.type->melee_cut - z.get_armor_cut(bp_torso)); } } if (thrown.made_of("glass") && !thrown.active && // active = molotov, etc. rng(0, thrown.volume() + 8) - rng(0, p.str_cur) < thrown.volume()) { if (u_see(tx, ty)) { add_msg(_("The %s shatters!"), thrown.tname().c_str()); } for (int i = 0; i < thrown.contents.size(); i++) { m.add_item_or_charges(tx, ty, thrown.contents[i]); } sound(tx, ty, 16, _("glass breaking!")); int glassdam = rng(0, thrown.volume() * 2); if (glassdam > z.get_armor_cut(bp_torso)) { dam += (glassdam - z.get_armor_cut(bp_torso)); } } else { m.add_item_or_charges(tx, ty, thrown); } if (i < trajectory.size() - 1) { goodhit = double(double(rand() / RAND_MAX) / 2); } if (goodhit < .1 && !z.has_flag(MF_NOHEAD)) { message = _("Headshot!"); dam = rng(dam, dam * 3); p.practice(turn, "throw", 5); p.lifetime_stats()->headshots++; } else if (goodhit < .2) { message = _("Critical!"); dam = rng(dam, dam * 2); p.practice(turn, "throw", 2); } else if (goodhit < .4) { dam = rng(int(dam / 2), int(dam * 1.5)); } else if (goodhit < .5) { message = _("Grazing hit."); dam = rng(0, dam); } if (u_see(tx, ty)) { g->add_msg_player_or_npc(&p, _("%s You hit the %s for %d damage."), _("%s <npcname> hits the %s for %d damage."), message.c_str(), z.name().c_str(), dam); } if (z.hurt(dam, real_dam)) { z.die(&p); } return; } else { // No monster hit, but the terrain might be. m.shoot(tx, ty, dam, false, no_effects); } // Collide with impassable terrain if (m.move_cost(tx, ty) == 0) { if (i > 0) { tx = trajectory[i - 1].x; ty = trajectory[i - 1].y; } else { tx = u.posx; ty = u.posy; } i = trajectory.size(); } if (p.has_active_bionic("bio_railgun") && (thrown.made_of("iron") || thrown.made_of("steel"))) { m.add_field(tx, ty, fd_electricity, rng(2,3)); } } if (thrown.made_of("glass") && !thrown.active && // active means molotov, etc rng(0, thrown.volume() + 8) - rng(0, p.str_cur) < thrown.volume()) { if (u_see(tx, ty)) { add_msg(_("The %s shatters!"), thrown.tname().c_str()); } for (int i = 0; i < thrown.contents.size(); i++) { m.add_item_or_charges(tx, ty, thrown.contents[i]); } sound(tx, ty, 16, _("glass breaking!")); } else { if(m.has_flag("LIQUID", tx, ty)) { sound(tx, ty, 10, _("splash!")); } else { sound(tx, ty, 8, _("thud.")); } m.add_item_or_charges(tx, ty, thrown); } }
void player::consume_effects( const item &food ) { if( !food.is_comestible() ) { debugmsg( "called player::consume_effects with non-comestible" ); return; } const auto &comest = *food.type->comestible; const int capacity = stomach_capacity(); if( has_trait( trait_id( "THRESH_PLANT" ) ) && food.type->can_use( "PLANTBLECH" ) ) { // Just keep nutrition capped, to prevent vomiting cap_nutrition_thirst( *this, capacity, true, true ); return; } if( ( has_trait( trait_id( "HERBIVORE" ) ) || has_trait( trait_id( "RUMINANT" ) ) ) && food.has_any_flag( herbivore_blacklist ) ) { // No good can come of this. return; } // Rotten food causes health loss const float relative_rot = food.get_relative_rot(); if( relative_rot > 1.0f && !has_trait( trait_id( "SAPROPHAGE" ) ) && !has_trait( trait_id( "SAPROVORE" ) ) && !has_bionic( bio_digestion ) ) { const float rottedness = clamp( 2 * relative_rot - 2.0f, 0.1f, 1.0f ); // ~-1 health per 1 nutrition at halfway-rotten-away, ~0 at "just got rotten" // But always round down int h_loss = -rottedness * comest.nutr; mod_healthy_mod( h_loss, -200 ); add_msg( m_debug, "%d health from %0.2f%% rotten food", h_loss, rottedness ); } const auto nutr = nutrition_for( food ); mod_hunger( -nutr ); mod_thirst( -comest.quench ); mod_stomach_food( nutr ); mod_stomach_water( comest.quench ); if( comest.healthy != 0 ) { // Effectively no cap on health modifiers from food mod_healthy_mod( comest.healthy, ( comest.healthy >= 0 ) ? 200 : -200 ); } if( comest.stim != 0 && ( abs( stim ) < ( abs( comest.stim ) * 3 ) || sgn( stim ) != sgn( comest.stim ) ) ) { if( comest.stim < 0 ) { stim = std::max( comest.stim * 3, stim + comest.stim ); } else { stim = std::min( comest.stim * 3, stim + comest.stim ); } } add_addiction( comest.add, comest.addict ); if( addiction_craving( comest.add ) != MORALE_NULL ) { rem_morale( addiction_craving( comest.add ) ); } // Morale is in minutes int morale_time = HOURS( 2 ) / MINUTES( 1 ); if( food.has_flag( "HOT" ) && food.has_flag( "EATEN_HOT" ) ) { morale_time = HOURS( 3 ) / MINUTES( 1 ); int clamped_nutr = std::max( 5, std::min( 20, nutr / 10 ) ); add_morale( MORALE_FOOD_HOT, clamped_nutr, 20, morale_time, morale_time / 2 ); } std::pair<int, int> fun = fun_for( food ); if( fun.first < 0 ) { add_morale( MORALE_FOOD_BAD, fun.first, fun.second, morale_time, morale_time / 2, false, food.type ); } else if( fun.first > 0 ) { add_morale( MORALE_FOOD_GOOD, fun.first, fun.second, morale_time, morale_time / 2, false, food.type ); } const bool hibernate = has_active_mutation( trait_id( "HIBERNATE" ) ); if( hibernate ) { if( ( nutr > 0 && get_hunger() < -60 ) || ( comest.quench > 0 && get_thirst() < -60 ) ) { //Tell the player what's going on add_msg_if_player( _( "You gorge yourself, preparing to hibernate." ) ); if( one_in( 2 ) ) { //50% chance of the food tiring you mod_fatigue( nutr ); } } if( ( nutr > 0 && get_hunger() < -200 ) || ( comest.quench > 0 && get_thirst() < -200 ) ) { //Hibernation should cut burn to 60/day add_msg_if_player( _( "You feel stocked for a day or two. Got your bed all ready and secured?" ) ); if( one_in( 2 ) ) { //And another 50%, intended cumulative mod_fatigue( nutr ); } } if( ( nutr > 0 && get_hunger() < -400 ) || ( comest.quench > 0 && get_thirst() < -400 ) ) { add_msg_if_player( _( "Mmm. You can still fit some more in...but maybe you should get comfortable and sleep." ) ); if( !one_in( 3 ) ) { //Third check, this one at 66% mod_fatigue( nutr ); } } if( ( nutr > 0 && get_hunger() < -600 ) || ( comest.quench > 0 && get_thirst() < -600 ) ) { add_msg_if_player( _( "That filled a hole! Time for bed..." ) ); // At this point, you're done. Schlaf gut. mod_fatigue( nutr ); } } // Moved here and changed a bit - it was too complex // Incredibly minor stuff like this shouldn't require complexity if( !is_npc() && has_trait( trait_id( "SLIMESPAWNER" ) ) && ( get_hunger() < capacity + 40 || get_thirst() < capacity + 40 ) ) { add_msg_if_player( m_mixed, _( "You feel as though you're going to split open! In a good way?" ) ); mod_pain( 5 ); std::vector<tripoint> valid; for( const tripoint &dest : g->m.points_in_radius( pos(), 1 ) ) { if( g->is_empty( dest ) ) { valid.push_back( dest ); } } 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( mon_player_blob, target ) ) { slime->friendly = -1; } } mod_hunger( 40 ); mod_thirst( 40 ); //~slimespawns have *small voices* which may be the Nice equivalent //~of the Rat King's ALL CAPS invective. Probably shared-brain telepathy. add_msg_if_player( m_good, _( "hey, you look like me! let's work together!" ) ); } // Last thing that happens before capping hunger if( get_hunger() < capacity && has_trait( trait_id( "EATHEALTH" ) ) ) { int excess_food = capacity - get_hunger(); add_msg_player_or_npc( _( "You feel the %s filling you out." ), _( "<npcname> looks better after eating the %s." ), food.tname().c_str() ); // Guaranteed 1 HP healing, no matter what. You're welcome. ;-) if( excess_food <= 5 ) { healall( 1 ); } else { // Straight conversion, except it's divided amongst all your body parts. healall( excess_food /= 5 ); } // Note: We want this here to prevent "you can't finish this" messages set_hunger( capacity ); } cap_nutrition_thirst( *this, capacity, nutr > 0, comest.quench > 0 ); }
bool player::eat( item &food, bool force ) { if( !food.is_food() ) { return false; } // Check if it's rotten before eating! food.calc_rot( global_square_location() ); const auto ret = force ? can_eat( food ) : will_eat( food, is_player() ); if( !ret.success() ) { return false; } if( food.type->has_use() ) { if( food.type->invoke( *this, food, pos() ) <= 0 ) { return false; } } // Note: the block below assumes we decided to eat it // No coming back from here const bool hibernate = has_active_mutation( trait_id( "HIBERNATE" ) ); const int nutr = nutrition_for( food ); const int quench = food.type->comestible->quench; const bool spoiled = food.rotten(); // The item is solid food const bool chew = food.type->comestible->comesttype == "FOOD" || food.has_flag( "USE_EAT_VERB" ); // This item is a drink and not a solid food (and not a thick soup) const bool drinkable = !chew && food.type->comestible->comesttype == "DRINK"; // If neither of the above is true then it's a drug and shouldn't get mealtime penalty/bonus if( hibernate && ( get_hunger() > -60 && get_thirst() > -60 ) && ( get_hunger() - nutr < -60 || get_thirst() - quench < -60 ) ) { add_memorial_log( pgettext( "memorial_male", "Began preparing for hibernation." ), pgettext( "memorial_female", "Began preparing for hibernation." ) ); add_msg_if_player( _( "You've begun stockpiling calories and liquid for hibernation. You get the feeling that you should prepare for bed, just in case, but...you're hungry again, and you could eat a whole week's worth of food RIGHT NOW." ) ); } const bool will_vomit = get_hunger() < 0 && nutr >= 5 && !has_trait( trait_id( "GOURMAND" ) ) && !hibernate && !has_trait( trait_id( "SLIMESPAWNER" ) ) && !has_trait( trait_id( "EATHEALTH" ) ) && rng( -200, 0 ) > get_hunger() - nutr; const bool saprophage = has_trait( trait_id( "SAPROPHAGE" ) ); if( spoiled && !saprophage ) { add_msg_if_player( m_bad, _( "Ick, this %s doesn't taste so good..." ), food.tname().c_str() ); if( !has_trait( trait_id( "SAPROVORE" ) ) && !has_trait( trait_id( "EATDEAD" ) ) && ( !has_bionic( bio_digestion ) || one_in( 3 ) ) ) { add_effect( effect_foodpoison, rng( 60, ( nutr + 1 ) * 60 ) ); } consume_effects( food ); } else if( spoiled && saprophage ) { add_msg_if_player( m_good, _( "Mmm, this %s tastes delicious..." ), food.tname().c_str() ); consume_effects( food ); } else { consume_effects( food ); } const bool amorphous = has_trait( trait_id( "AMORPHOUS" ) ); int mealtime = 250; if( drinkable || chew ) { // Those bonuses/penalties only apply to food // Not to smoking weed or applying bandages! if( has_trait( trait_id( "MOUTH_TENTACLES" ) ) || has_trait( trait_id( "MANDIBLES" ) ) ) { mealtime /= 2; } else if( has_trait( trait_id( "GOURMAND" ) ) ) { // Don't stack those two - that would be 25 moves per item mealtime -= 100; } if( has_trait( trait_id( "BEAK_HUM" ) ) && !drinkable ) { mealtime += 200; // Much better than PROBOSCIS but still optimized for fluids } else if( has_trait( trait_id( "SABER_TEETH" ) ) ) { mealtime += 250; // They get In The Way } if( amorphous ) { mealtime *= 1.1; // Minor speed penalty for having to flow around it // rather than just grab & munch } } moves -= mealtime; // If it's poisonous... poison us. // TODO: Move this to a flag if( food.poison > 0 && !has_trait( trait_id( "EATPOISON" ) ) && !has_trait( trait_id( "EATDEAD" ) ) ) { if( food.poison >= rng( 2, 4 ) ) { add_effect( effect_poison, food.poison * 100 ); } add_effect( effect_foodpoison, food.poison * 300 ); } if( amorphous ) { add_msg_player_or_npc( _( "You assimilate your %s." ), _( "<npcname> assimilates a %s." ), food.tname().c_str() ); } else if( drinkable ) { add_msg_player_or_npc( _( "You drink your %s." ), _( "<npcname> drinks a %s." ), food.tname().c_str() ); } else if( chew ) { add_msg_player_or_npc( _( "You eat your %s." ), _( "<npcname> eats a %s." ), food.tname().c_str() ); } if( item::find_type( food.type->comestible->tool )->tool ) { // Tools like lighters get used use_charges( food.type->comestible->tool, 1 ); } if( has_bionic( bio_ethanol ) && food.type->can_use( "ALCOHOL" ) ) { charge_power( rng( 50, 200 ) ); } if( has_bionic( bio_ethanol ) && food.type->can_use( "ALCOHOL_WEAK" ) ) { charge_power( rng( 25, 100 ) ); } if( has_bionic( bio_ethanol ) && food.type->can_use( "ALCOHOL_STRONG" ) ) { charge_power( rng( 75, 300 ) ); } if( food.has_flag( "CANNIBALISM" ) ) { // Sapiovores don't recognize humans as the same species. // But let them possibly feel cool about eating sapient stuff - treat like psycho const bool cannibal = has_trait( trait_id( "CANNIBAL" ) ); const bool psycho = has_trait( trait_id( "PSYCHOPATH" ) ) || has_trait( trait_id( "SAPIOVORE" ) ); const bool spiritual = has_trait( trait_id( "SPIRITUAL" ) ); if( cannibal && psycho && spiritual ) { add_msg_if_player( m_good, _( "You feast upon the human flesh, and in doing so, devour their spirit." ) ); // You're not really consuming anything special; you just think you are. add_morale( MORALE_CANNIBAL, 25, 300 ); } else if( cannibal && psycho ) { add_msg_if_player( m_good, _( "You feast upon the human flesh." ) ); add_morale( MORALE_CANNIBAL, 15, 200 ); } else if( cannibal && spiritual ) { add_msg_if_player( m_good, _( "You consume the sacred human flesh." ) ); // Boosted because you understand the philosophical implications of your actions, and YOU LIKE THEM. add_morale( MORALE_CANNIBAL, 15, 200 ); } else if( cannibal ) { add_msg_if_player( m_good, _( "You indulge your shameful hunger." ) ); add_morale( MORALE_CANNIBAL, 10, 50 ); } else if( psycho && spiritual ) { add_msg_if_player( _( "You greedily devour the taboo meat." ) ); // Small bonus for violating a taboo. add_morale( MORALE_CANNIBAL, 5, 50 ); } else if( psycho ) { add_msg_if_player( _( "Meh. You've eaten worse." ) ); } else if( spiritual ) { add_msg_if_player( m_bad, _( "This is probably going to count against you if there's still an afterlife." ) ); add_morale( MORALE_CANNIBAL, -60, -400, 600, 300 ); } else { add_msg_if_player( m_bad, _( "You feel horrible for eating a person." ) ); add_morale( MORALE_CANNIBAL, -60, -400, 600, 300 ); } } // Allergy check const auto allergy = allergy_type( food ); if( allergy != MORALE_NULL ) { add_msg_if_player( m_bad, _( "Yuck! How can anybody eat this stuff?" ) ); add_morale( allergy, -75, -400, 300, 240 ); } // Carnivores CAN eat junk food, but they won't like it much. // Pizza-scraping happens in consume_effects. if( has_trait( trait_id( "CARNIVORE" ) ) && food.has_flag( "ALLERGEN_JUNK" ) && !food.has_flag( "CARNIVORE_OK" ) ) { add_msg_if_player( m_bad, _( "Your stomach begins gurgling and you feel bloated and ill." ) ); add_morale( MORALE_NO_DIGEST, -25, -125, 300, 240 ); } if( !spoiled && chew && has_trait( trait_id( "SAPROPHAGE" ) ) ) { // It's OK to *drink* things that haven't rotted. Alternative is to ban water. D: add_msg_if_player( m_bad, _( "Your stomach begins gurgling and you feel bloated and ill." ) ); add_morale( MORALE_NO_DIGEST, -75, -400, 300, 240 ); } if( food.has_flag( "URSINE_HONEY" ) && ( !crossed_threshold() || has_trait( trait_id( "THRESH_URSINE" ) ) ) && mutation_category_level["MUTCAT_URSINE"] > 40 ) { //Need at least 5 bear mutations for effect to show, to filter out mutations in common with other mutcats int honey_fun = has_trait( trait_id( "THRESH_URSINE" ) ) ? std::min( mutation_category_level["MUTCAT_URSINE"] / 8, 20 ) : mutation_category_level["MUTCAT_URSINE"] / 12; if( honey_fun < 10 ) { add_msg_if_player( m_good, _( "You find the sweet taste of honey surprisingly palatable." ) ); } else { add_msg_if_player( m_good, _( "You feast upon the sweet honey." ) ); } add_morale( MORALE_HONEY, honey_fun, 100 ); } if( will_vomit ) { vomit(); } // chance to become parasitised if( !( has_bionic( bio_digestion ) || has_trait( trait_id( "PARAIMMUNE" ) ) ) ) { if( food.type->comestible->parasites > 0 && one_in( food.type->comestible->parasites ) ) { switch( rng( 0, 3 ) ) { case 0: if( !has_trait( trait_id( "EATHEALTH" ) ) ) { add_effect( effect_tapeworm, 1, num_bp, true ); } break; case 1: if( !has_trait( trait_id( "ACIDBLOOD" ) ) ) { add_effect( effect_bloodworms, 1, num_bp, true ); } break; case 2: add_effect( effect_brainworms, 1, num_bp, true ); break; case 3: add_effect( effect_paincysts, 1, num_bp, true ); } } } for( const auto &v : this->vitamins_from( food ) ) { auto qty = has_effect( effect_tapeworm ) ? v.second / 2 : v.second; // can never develop hypervitaminosis from consuming food vitamin_mod( v.first, qty ); } food.mod_charges( -1 ); return true; }
ret_val<edible_rating> player::will_eat( const item &food, bool interactive ) const { const auto ret = can_eat( food ); if( !ret.success() ) { if( interactive ) { add_msg_if_player( m_info, "%s", ret.c_str() ); } return ret; } std::vector<ret_val<edible_rating>> consequences; const auto add_consequence = [&consequences]( const std::string & msg, edible_rating code ) { consequences.emplace_back( ret_val<edible_rating>::make_failure( code, msg ) ); }; const bool saprophage = has_trait( trait_id( "SAPROPHAGE" ) ); const auto &comest = food.type->comestible; if( food.rotten() ) { const bool saprovore = has_trait( trait_id( "SAPROVORE" ) ); if( !saprophage && !saprovore ) { add_consequence( _( "This is rotten and smells awful!" ), ROTTEN ); } } const bool carnivore = has_trait( trait_id( "CARNIVORE" ) ); if( food.has_flag( "CANNIBALISM" ) && !has_trait_flag( "CANNIBAL" ) ) { add_consequence( _( "The thought of eating human flesh makes you feel sick." ), CANNIBALISM ); } const bool edible = comest->comesttype == "FOOD" || food.has_flag( "USE_EAT_VERB" ); if( edible && has_effect( effect_nausea ) ) { add_consequence( _( "You still feel nauseous and will probably puke it all up again." ), NAUSEA ); } if( ( allergy_type( food ) != MORALE_NULL ) || ( carnivore && food.has_flag( "ALLERGEN_JUNK" ) && !food.has_flag( "CARNIVORE_OK" ) ) ) { add_consequence( _( "Your stomach won't be happy (allergy)." ), ALLERGY ); } if( saprophage && edible && food.rotten() && !food.has_flag( "FERTILIZER" ) ) { // Note: We're allowing all non-solid "food". This includes drugs // Hard-coding fertilizer for now - should be a separate flag later //~ No, we don't eat "rotten" food. We eat properly aged food, like a normal person. //~ Semantic difference, but greatly facilitates people being proud of their character. add_consequence( _( "Your stomach won't be happy (not rotten enough)." ), ALLERGY_WEAK ); } const int nutr = nutrition_for( food ); const int quench = comest->quench; const int temp_hunger = get_hunger() - nutr; const int temp_thirst = get_thirst() - quench; if( !has_active_mutation( trait_id( "EATHEALTH" ) ) && !has_active_mutation( trait_id( "HIBERNATE" ) ) && !has_trait( trait_id( "SLIMESPAWNER" ) ) ) { if( get_hunger() < 0 && nutr >= 5 && !has_active_mutation( trait_id( "GOURMAND" ) ) ) { add_consequence( _( "You're full already and will be forcing yourself to eat." ), TOO_FULL ); } else if( ( ( nutr > 0 && temp_hunger < stomach_capacity() ) || ( comest->quench > 0 && temp_thirst < stomach_capacity() ) ) && !food.has_infinite_charges() ) { add_consequence( _( "You will not be able to finish it all." ), TOO_FULL ); } } if( !consequences.empty() ) { if( !interactive ) { return consequences.front(); } std::ostringstream req; for( const auto &elem : consequences ) { req << elem.str() << std::endl; } const bool eat_verb = food.has_flag( "USE_EAT_VERB" ); if( eat_verb || comest->comesttype == "FOOD" ) { req << string_format( _( "Eat your %s anyway?" ), food.tname().c_str() ); } else if( !eat_verb && comest->comesttype == "DRINK" ) { req << string_format( _( "Drink your %s anyway?" ), food.tname().c_str() ); } else { req << string_format( _( "Consume your %s anyway?" ), food.tname().c_str() ); } if( !query_yn( req.str() ) ) { return consequences.front(); } } // All checks ended, it's edible (or we're pretending it is) return ret_val<edible_rating>::make_success(); }
void player::consume_effects( item &food, bool rotten ) { if( !food.is_food() ) { debugmsg( "called player::consume_effects with non-comestible" ); return; } const auto comest = food.type->comestible.get(); const int capacity = stomach_capacity(); if( has_trait( "THRESH_PLANT" ) && food.type->can_use( "PLANTBLECH" ) ) { // Just keep nutrition capped, to prevent vomiting cap_nutrition_thirst( *this, capacity, true, true ); return; } if( ( has_trait( "HERBIVORE" ) || has_trait( "RUMINANT" ) ) && food.has_any_flag( herbivore_blacklist ) ) { // No good can come of this. return; } float factor = 1.0f; float hunger_factor = 1.0f; bool unhealthy_allowed = true; if( has_trait( "GIZZARD" ) ) { factor *= 0.6f; } if( has_trait( "CARNIVORE" ) && food.has_flag( "CARNIVORE_OK" ) ) { // At least partially edible if( food.has_any_flag( carnivore_blacklist ) ) { // Other things are in it, we only get partial benefits add_msg_if_player( _( "You pick out the edible parts and throw away the rest." ) ); factor *= 0.5f; } else { // Carnivores don't get unhealthy off pure meat diets unhealthy_allowed = false; } } // Saprophages get full nutrition from rotting food if( rotten && !has_trait( "SAPROPHAGE" ) ) { // everyone else only gets a portion of the nutrition hunger_factor *= rng_float( 0, 1 ); // and takes a health penalty if they aren't adapted if( !has_trait( "SAPROVORE" ) && !has_bionic( "bio_digestion" ) ) { mod_healthy_mod( -30, -200 ); } } // Bio-digestion gives extra nutrition if( has_bionic( "bio_digestion" ) ) { hunger_factor += rng_float( 0, 1 ); } const auto nutr = nutrition_for( food.type ); mod_hunger( -nutr * factor * hunger_factor ); mod_thirst( -comest->quench * factor ); mod_stomach_food( nutr * factor * hunger_factor ); mod_stomach_water( comest->quench * factor ); if( unhealthy_allowed || comest->healthy > 0 ) { // Effectively no cap on health modifiers from food mod_healthy_mod( comest->healthy, ( comest->healthy >= 0 ) ? 200 : -200 ); } if( comest->stim != 0 && ( abs( stim ) < ( abs( comest->stim ) * 3 ) || sgn( stim ) != sgn( comest->stim ) ) ) { if( comest->stim < 0 ) { stim = std::max( comest->stim * 3, stim + comest->stim ); } else { stim = std::min( comest->stim * 3, stim + comest->stim ); } } add_addiction( comest->add, comest->addict ); if( addiction_craving( comest->add ) != MORALE_NULL ) { rem_morale( addiction_craving( comest->add ) ); } if( food.has_flag( "HOT" ) && food.has_flag( "EATEN_HOT" ) ) { add_morale( MORALE_FOOD_HOT, 5, 10 ); } auto fun = comest->fun; if( food.has_flag( "COLD" ) && food.has_flag( "EATEN_COLD" ) && fun > 0 ) { if( fun > 0 ) { add_morale( MORALE_FOOD_GOOD, fun * 3, fun * 3, 60, 30, false, food.type ); } else { fun = 1; } } const bool gourmand = has_trait( "GOURMAND" ); const bool hibernate = has_active_mutation( "HIBERNATE" ); if( gourmand ) { if( fun < -2 ) { add_morale( MORALE_FOOD_BAD, fun * 0.5, fun, 60, 30, false, food.type ); } else if( fun > 0 ) { add_morale( MORALE_FOOD_GOOD, fun * 3, fun * 6, 60, 30, false, food.type ); } } else if( fun < 0 ) { add_morale( MORALE_FOOD_BAD, fun, fun * 6, 60, 30, false, food.type ); } else if( fun > 0 ) { add_morale( MORALE_FOOD_GOOD, fun, fun * 4, 60, 30, false, food.type ); } if( hibernate ) { if( ( nutr > 0 && get_hunger() < -60 ) || ( comest->quench > 0 && get_thirst() < -60 ) ) { //Tell the player what's going on add_msg_if_player( _( "You gorge yourself, preparing to hibernate." ) ); if( one_in( 2 ) ) { //50% chance of the food tiring you mod_fatigue( nutr ); } } if( ( nutr > 0 && get_hunger() < -200 ) || ( comest->quench > 0 && get_thirst() < -200 ) ) { //Hibernation should cut burn to 60/day add_msg_if_player( _( "You feel stocked for a day or two. Got your bed all ready and secured?" ) ); if( one_in( 2 ) ) { //And another 50%, intended cumulative mod_fatigue( nutr ); } } if( ( nutr > 0 && get_hunger() < -400 ) || ( comest->quench > 0 && get_thirst() < -400 ) ) { add_msg_if_player( _( "Mmm. You can still fit some more in...but maybe you should get comfortable and sleep." ) ); if( !one_in( 3 ) ) { //Third check, this one at 66% mod_fatigue( nutr ); } } if( ( nutr > 0 && get_hunger() < -600 ) || ( comest->quench > 0 && get_thirst() < -600 ) ) { add_msg_if_player( _( "That filled a hole! Time for bed..." ) ); // At this point, you're done. Schlaf gut. mod_fatigue( nutr ); } } // Moved here and changed a bit - it was too complex // Incredibly minor stuff like this shouldn't require complexity if( !is_npc() && has_trait( "SLIMESPAWNER" ) && ( get_hunger() < capacity + 40 || get_thirst() < capacity + 40 ) ) { add_msg_if_player( m_mixed, _( "You feel as though you're going to split open! In a good way?" ) ); mod_pain( 5 ); std::vector<tripoint> valid; for( const tripoint &dest : g->m.points_in_radius( pos(), 1 ) ) { if( g->is_empty( dest ) ) { valid.push_back( dest ); } } 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; } } mod_hunger( 40 ); mod_thirst( 40 ); //~slimespawns have *small voices* which may be the Nice equivalent //~of the Rat King's ALL CAPS invective. Probably shared-brain telepathy. add_msg_if_player( m_good, _( "hey, you look like me! let's work together!" ) ); } // Last thing that happens before capping hunger if( get_hunger() < capacity && has_trait( "EATHEALTH" ) ) { int excess_food = capacity - get_hunger(); add_msg_player_or_npc( _( "You feel the %s filling you out." ), _( "<npcname> looks better after eating the %s." ), food.tname().c_str() ); // Guaranteed 1 HP healing, no matter what. You're welcome. ;-) if( excess_food <= 5 ) { healall( 1 ); } else { // Straight conversion, except it's divided amongst all your body parts. healall( excess_food /= 5 ); } // Note: We want this here to prevent "you can't finish this" messages set_hunger( capacity ); } cap_nutrition_thirst( *this, capacity, nutr > 0, comest->quench > 0 ); }
edible_rating player::can_eat( const item &food, bool interactive, bool force ) const { if( is_npc() || force ) { // Just to be sure interactive = false; } const std::string &itname = food.tname(); // Helper to avoid ton of `if( interactive )` // Prints if interactive is true, does nothing otherwise const auto maybe_print = [interactive, &itname] ( game_message_type type, const char *str ) { if( interactive ) { add_msg( type, str, itname.c_str() ); } }; // As above, but for queries // Asks if interactive and not force // Always true if force // Never true otherwise const auto maybe_query = [force, interactive, &itname, this]( const char *str ) { if( force ) { return true; } else if( !interactive ) { return false; } return query_yn( str, itname.c_str() ); }; const auto comest = food.type->comestible.get(); if( comest == nullptr ) { maybe_print( m_info, _( "That doesn't look edible." ) ); return INEDIBLE; } if( comest->tool != "null" ) { bool has = has_amount( comest->tool, 1 ); if( item::count_by_charges( comest->tool ) ) { has = has_charges( comest->tool, 1 ); } if( !has ) { if( interactive ) { add_msg_if_player( m_info, _( "You need a %s to consume that!" ), item::nname( comest->tool ).c_str() ); } return NO_TOOL; } } if( is_underwater() ) { maybe_print( m_info, _( "You can't do that while underwater." ) ); return INEDIBLE; } // For all those folks who loved eating marloss berries. D:< mwuhahaha if( has_trait( "M_DEPENDENT" ) && food.type->id != "mycus_fruit" ) { maybe_print( m_info, _( "We can't eat that. It's not right for us." ) ); return INEDIBLE_MUTATION; } const bool drinkable = comest->comesttype == "DRINK" && !food.has_flag( "USE_EAT_VERB" ); // Here's why PROBOSCIS is such a negative trait. if( has_trait( "PROBOSCIS" ) && !drinkable ) { maybe_print( m_info, _( "Ugh, you can't drink that!" ) ); return INEDIBLE_MUTATION; } int capacity = stomach_capacity(); // TODO: Move this cache to a structure and pass it around // to speed up checking entire inventory for edibles const bool gourmand = has_trait( "GOURMAND" ); const bool hibernate = has_active_mutation( "HIBERNATE" ); const bool eathealth = has_trait( "EATHEALTH" ); const bool slimespawner = has_trait( "SLIMESPAWNER" ); const int nutr = nutrition_for( food.type ); const int quench = comest->quench; bool spoiled = food.rotten(); const int temp_hunger = get_hunger() - nutr; const int temp_thirst = get_thirst() - quench; const bool overeating = get_hunger() < 0 && nutr >= 5 && !gourmand && !eathealth && !slimespawner && !hibernate; if( interactive && hibernate && ( get_hunger() >= -60 && get_thirst() >= -60 ) && ( temp_hunger < -60 || temp_thirst < -60 ) ) { if( !maybe_query( _( "You're adequately fueled. Prepare for hibernation?" ) ) ) { return TOO_FULL; } } const bool carnivore = has_trait( "CARNIVORE" ); if( carnivore && nutr > 0 && food.has_any_flag( carnivore_blacklist ) && !food.has_flag( "CARNIVORE_OK" ) ) { maybe_print( m_info, _( "Eww. Inedible plant stuff!" ) ); return INEDIBLE_MUTATION; } if( ( has_trait( "HERBIVORE" ) || has_trait( "RUMINANT" ) ) && food.has_any_flag( herbivore_blacklist ) ) { // Like non-cannibal, but more strict! maybe_print( m_info, _( "The thought of eating that makes you feel sick. You decide not to." ) ); return INEDIBLE_MUTATION; } if( food.has_flag( "CANNIBALISM" ) ) { if( !has_trait_flag( "CANNIBAL" ) && !maybe_query( _( "The thought of eating that makes you feel sick. Really do it?" ) ) ) { return CANNIBALISM; } } if( is_allergic( food ) && !maybe_query( _( "Really eat that %s? Your stomach won't be happy." ) ) ) { return ALLERGY; } if( carnivore && food.has_flag( "ALLERGEN_JUNK" ) && !food.has_flag( "CARNIVORE_OK" ) && !maybe_query( _( "Really eat that %s? Your stomach won't be happy." ) ) ) { return ALLERGY; } const bool saprophage = has_trait( "SAPROPHAGE" ); // The item is solid food const bool chew = comest->comesttype == "FOOD" || food.has_flag( "USE_EAT_VERB" ); if( spoiled ) { if( !saprophage && !has_trait( "SAPROVORE" ) && !maybe_query( _( "This %s smells awful! Eat it?" ) ) ) { return ROTTEN; } } else if( saprophage && chew && !food.has_flag( "FERTILIZER" ) && !maybe_query( _( "Really eat that %s? Your stomach won't be happy." ) ) ) { // Note: We're allowing all non-solid "food". This includes drugs // Hardcoding fertilizer for now - should be a separate flag later //~ No, we don't eat "rotten" food. We eat properly aged food, like a normal person. //~ Semantic difference, but greatly facilitates people being proud of their character. maybe_print( m_info, _( "It's too fresh, let it age a little first." ) ); return ROTTEN; } // Print at most one of those bool overfull = false; if( overeating ) { overfull = !maybe_query( _( "You're full. Force yourself to eat?" ) ); } else if( ( ( nutr > 0 && temp_hunger < capacity ) || ( comest->quench > 0 && temp_thirst < capacity ) ) && !eathealth && !slimespawner ) { overfull = !maybe_query( _( "You will not be able to finish it all. Consume it?" ) ); } if( overfull ) { return TOO_FULL; } // All checks ended, it's edible (or we're pretending it is) return EDIBLE; }