Skip to content
Permalink
Tree: 3765602255
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
3124 lines (2775 sloc) 117 KB
#include "activity_handlers.h"
#include "action.h"
#include "catalua.h"
#include "clzones.h"
#include "construction.h"
#include "craft_command.h"
#include "debug.h"
#include "fault.h"
#include "field.h"
#include "game.h"
#include "gates.h"
#include "harvest.h"
#include "iexamine.h"
#include "itype.h"
#include "iuse_actor.h"
#include "map.h"
#include "map_iterator.h"
#include "mapdata.h"
#include "martialarts.h"
#include "messages.h"
#include "mongroup.h"
#include "morale_types.h"
#include "mtype.h"
#include "output.h"
#include "player.h"
#include "requirements.h"
#include "rng.h"
#include "skill.h"
#include "sounds.h"
#include "string_formatter.h"
#include "translations.h"
#include "ui.h"
#include "veh_interact.h"
#include "vehicle.h"
#include "vpart_position.h"
#include "map_selector.h"
#include <algorithm>
#include <cmath>
#define dbg(x) DebugLog((DebugLevel)(x),D_GAME) << __FILE__ << ":" << __LINE__ << ": "
const skill_id skill_survival( "survival" );
const skill_id skill_firstaid( "firstaid" );
const skill_id skill_electronics( "electronics" );
const species_id HUMAN( "HUMAN" );
const species_id ZOMBIE( "ZOMBIE" );
const efftype_id effect_milked( "milked" );
const efftype_id effect_sleep( "sleep" );
using namespace activity_handlers;
const std::map< activity_id, std::function<void( player_activity *, player * )> >
activity_handlers::do_turn_functions = {
{ activity_id( "ACT_BURROW" ), burrow_do_turn },
{ activity_id( "ACT_CRAFT" ), craft_do_turn },
{ activity_id( "ACT_LONGCRAFT" ), craft_do_turn },
{ activity_id( "ACT_FILL_LIQUID" ), fill_liquid_do_turn },
{ activity_id( "ACT_PICKAXE" ), pickaxe_do_turn },
{ activity_id( "ACT_DROP" ), drop_do_turn },
{ activity_id( "ACT_STASH" ), stash_do_turn },
{ activity_id( "ACT_PULP" ), pulp_do_turn },
{ activity_id( "ACT_GAME" ), game_do_turn },
{ activity_id( "ACT_START_FIRE" ), start_fire_do_turn },
{ activity_id( "ACT_VIBE" ), vibe_do_turn },
{ activity_id( "ACT_OXYTORCH" ), oxytorch_do_turn },
{ activity_id( "ACT_AIM" ), aim_do_turn },
{ activity_id( "ACT_PICKUP" ), pickup_do_turn },
{ activity_id( "ACT_MOVE_ITEMS" ), move_items_do_turn },
{ activity_id( "ACT_MOVE_LOOT" ), move_loot_do_turn },
{ activity_id( "ACT_ADV_INVENTORY" ), adv_inventory_do_turn },
{ activity_id( "ACT_ARMOR_LAYERS" ), armor_layers_do_turn },
{ activity_id( "ACT_ATM" ), atm_do_turn },
{ activity_id( "ACT_CRACKING" ), cracking_do_turn },
{ activity_id( "ACT_REPAIR_ITEM" ), repair_item_do_turn },
{ activity_id( "ACT_BUTCHER" ), butcher_do_turn },
{ activity_id( "ACT_BUTCHER_FULL" ), butcher_do_turn },
{ activity_id( "ACT_FIELD_DRESS" ), butcher_do_turn },
{ activity_id( "ACT_QUARTER" ), butcher_do_turn },
{ activity_id( "ACT_DISSECT" ), butcher_do_turn },
{ activity_id( "ACT_HACKSAW" ), hacksaw_do_turn },
{ activity_id( "ACT_CHOP_TREE" ), chop_tree_do_turn },
{ activity_id( "ACT_CHOP_LOGS" ), chop_tree_do_turn },
{ activity_id( "ACT_JACKHAMMER" ), jackhammer_do_turn },
{ activity_id( "ACT_DIG" ), dig_do_turn },
{ activity_id( "ACT_FILL_PIT" ), fill_pit_do_turn },
{ activity_id( "ACT_TILL_PLOT" ), till_plot_do_turn },
{ activity_id( "ACT_PLANT_PLOT" ), plant_plot_do_turn },
{ activity_id( "ACT_TRY_SLEEP" ), try_sleep_do_turn }
};
const std::map< activity_id, std::function<void( player_activity *, player * )> >
activity_handlers::finish_functions = {
{ activity_id( "ACT_BURROW" ), burrow_finish },
{ activity_id( "ACT_BUTCHER" ), butcher_finish },
{ activity_id( "ACT_BUTCHER_FULL" ), butcher_finish },
{ activity_id( "ACT_FIELD_DRESS" ), butcher_finish },
{ activity_id( "ACT_QUARTER" ), butcher_finish },
{ activity_id( "ACT_DISSECT" ), butcher_finish },
{ activity_id( "ACT_FIRSTAID" ), firstaid_finish },
{ activity_id( "ACT_FISH" ), fish_finish },
{ activity_id( "ACT_FORAGE" ), forage_finish },
{ activity_id( "ACT_HOTWIRE_CAR" ), hotwire_finish },
{ activity_id( "ACT_LONGSALVAGE" ), longsalvage_finish },
{ activity_id( "ACT_MAKE_ZLAVE" ), make_zlave_finish },
{ activity_id( "ACT_PICKAXE" ), pickaxe_finish },
{ activity_id( "ACT_PICKUP" ), pickup_finish },
{ activity_id( "ACT_RELOAD" ), reload_finish },
{ activity_id( "ACT_START_FIRE" ), start_fire_finish },
{ activity_id( "ACT_TRAIN" ), train_finish },
{ activity_id( "ACT_VEHICLE" ), vehicle_finish },
{ activity_id( "ACT_START_ENGINES" ), start_engines_finish },
{ activity_id( "ACT_OXYTORCH" ), oxytorch_finish },
{ activity_id( "ACT_CRACKING" ), cracking_finish },
{ activity_id( "ACT_OPEN_GATE" ), open_gate_finish },
{ activity_id( "ACT_REPAIR_ITEM" ), repair_item_finish },
{ activity_id( "ACT_MEND_ITEM" ), mend_item_finish },
{ activity_id( "ACT_GUNMOD_ADD" ), gunmod_add_finish },
{ activity_id( "ACT_TOOLMOD_ADD" ), toolmod_add_finish },
{ activity_id( "ACT_CLEAR_RUBBLE" ), clear_rubble_finish },
{ activity_id( "ACT_MEDITATE" ), meditate_finish },
{ activity_id( "ACT_READ" ), read_finish },
{ activity_id( "ACT_WAIT" ), wait_finish },
{ activity_id( "ACT_WAIT_WEATHER" ), wait_weather_finish },
{ activity_id( "ACT_WAIT_NPC" ), wait_npc_finish },
{ activity_id( "ACT_TRY_SLEEP" ), try_sleep_finish },
{ activity_id( "ACT_CRAFT" ), craft_finish },
{ activity_id( "ACT_LONGCRAFT" ), longcraft_finish },
{ activity_id( "ACT_DISASSEMBLE" ), disassemble_finish },
{ activity_id( "ACT_BUILD" ), build_finish },
{ activity_id( "ACT_VIBE" ), vibe_finish },
{ activity_id( "ACT_MOVE_ITEMS" ), move_items_finish },
{ activity_id( "ACT_MOVE_LOOT" ), move_loot_finish },
{ activity_id( "ACT_ATM" ), atm_finish },
{ activity_id( "ACT_AIM" ), aim_finish },
{ activity_id( "ACT_WASH" ), washing_finish },
{ activity_id( "ACT_HACKSAW" ), hacksaw_finish },
{ activity_id( "ACT_CHOP_TREE" ), chop_tree_finish },
{ activity_id( "ACT_CHOP_LOGS" ), chop_logs_finish },
{ activity_id( "ACT_JACKHAMMER" ), jackhammer_finish },
{ activity_id( "ACT_DIG" ), dig_finish },
{ activity_id( "ACT_FILL_PIT" ), fill_pit_finish },
{ activity_id( "ACT_SHAVE" ), shaving_finish },
{ activity_id( "ACT_HAIRCUT" ), haircut_finish }
};
void messages_in_process( const player_activity &act, const player &p )
{
if( act.moves_left <= 91000 && act.moves_left > 89000 ) {
p.add_msg_if_player( m_info, _( "You figure it'll take about an hour and a half at this rate." ) );
return;
}
if( act.moves_left <= 61000 && act.moves_left > 59000 ) {
p.add_msg_if_player( m_info, _( "About an hour left to go." ) );
return;
}
if( act.moves_left <= 31000 && act.moves_left > 29000 ) {
p.add_msg_if_player( m_info, _( "Shouldn't be more than half an hour or so now!" ) );
return;
}
if( act.moves_left <= 11000 && act.moves_left > 9000 ) {
p.add_msg_if_player( m_info, _( "Almost there! Ten more minutes of work and you'll be through." ) );
return;
}
}
void activity_handlers::burrow_do_turn( player_activity *act, player *p )
{
if( calendar::once_every( 1_minutes ) ) {
//~ Sound of a Rat mutant burrowing!
sounds::sound( act->placement, 10, _( "ScratchCrunchScrabbleScurry." ) );
messages_in_process( *act, *p );
}
}
void activity_handlers::burrow_finish( player_activity *act, player *p )
{
const tripoint &pos = act->placement;
if( g->m.is_bashable( pos ) && g->m.has_flag( "SUPPORTS_ROOF", pos ) &&
g->m.ter( pos ) != t_tree ) {
// Tunneling through solid rock is hungry, sweaty, tiring, backbreaking work
// Not quite as bad as the pickaxe, though
p->mod_hunger( 10 );
p->mod_thirst( 10 );
p->mod_fatigue( 15 );
p->mod_pain( 3 * rng( 1, 3 ) );
} else if( g->m.move_cost( pos ) == 2 && g->get_levz() == 0 &&
g->m.ter( pos ) != t_dirt && g->m.ter( pos ) != t_grass ) {
//Breaking up concrete on the surface? not nearly as bad
p->mod_hunger( 5 );
p->mod_thirst( 5 );
p->mod_fatigue( 10 );
}
p->add_msg_if_player( m_good, _( "You finish burrowing." ) );
g->m.destroy( pos, true );
act->set_to_null();
}
bool check_butcher_cbm( const int roll )
{
// 2/3 chance of failure with a roll of 0, 2/6 with a roll of 1, 2/9 etc.
// The roll is usually b/t 0 and first_aid-3, so first_aid 4 will succeed
// 50%, first_aid 5 will succeed 61%, first_aid 6 will succeed 67%, etc.
bool failed = x_in_y( 2, 3 + roll * 3 );
return !failed;
}
void butcher_cbm_item( const std::string &what, const tripoint &pos,
const time_point &age, const int roll )
{
if( roll < 0 ) {
return;
}
item cbm( check_butcher_cbm( roll ) ? what : "burnt_out_bionic", age );
add_msg( m_good, _( "You discover a %s!" ), cbm.tname().c_str() );
g->m.add_item( pos, cbm );
}
void butcher_cbm_group( const std::string &group, const tripoint &pos,
const time_point &age, const int roll )
{
if( roll < 0 ) {
return;
}
//To see if it spawns a random additional CBM
if( check_butcher_cbm( roll ) ) {
//The CBM works
const auto spawned = g->m.put_items_from_loc( group, pos, age );
for( const auto &it : spawned ) {
add_msg( m_good, _( "You discover a %s!" ), it->tname().c_str() );
}
} else {
//There is a burnt out CBM
item cbm( "burnt_out_bionic", age );
add_msg( m_good, _( "You discover a %s!" ), cbm.tname().c_str() );
g->m.add_item( pos, cbm );
}
}
void set_up_butchery( player_activity &act, player &u, butcher_type action )
{
if( !act.values.empty() ) {
act.index = act.values.back();
act.values.pop_back();
} else {
debugmsg( "Invalid butchery item index %d", act.index );
act.set_to_null();
return;
}
int factor = u.max_quality( action == DISSECT ? quality_id( "CUT_FINE" ) :
quality_id( "BUTCHER" ) );
auto items = g->m.i_at( u.pos() );
if( static_cast<size_t>( act.index ) >= items.size() ) {
// Let it print a msg for lack of corpses
act.index = INT_MAX;
return;
}
item corpse_item = items[act.index];
const mtype &corpse = *( corpse_item.get_mtype() );
if( action != DISSECT ) {
if( factor == INT_MIN ) {
u.add_msg_if_player( m_info,
_( "None of your cutting tools are suitable for butchering." ) );
act.set_to_null();
return;
} else if( factor < 0 && one_in( 3 ) ) {
u.add_msg_if_player( m_bad,
_( "You don't trust the quality of your tools, but carry on anyway." ) );
}
}
if( action == DISSECT ) {
switch( factor ) {
case INT_MIN:
u.add_msg_if_player( m_info, _( "None of your tools are sharp and precise enough to do that." ) );
act.set_to_null();
return;
case 1:
u.add_msg_if_player( m_info, _( "You could use a better tool, but this will do." ) );
break;
case 2:
u.add_msg_if_player( m_info, _( "This tool is great, but you still would like a scalpel." ) );
break;
case 3:
u.add_msg_if_player( m_info, _( "You dissect the corpse with a trusty scalpel." ) );
break;
}
}
bool has_table_nearby = false;
for( const tripoint &pt : g->m.points_in_radius( u.pos(), 2 ) ) {
if( g->m.has_flag_furn( "FLAT_SURF", pt ) || g->m.has_flag( "FLAT_SURF", pt ) ||
( g->m.veh_at( pt ) && g->m.veh_at( pt )->vehicle().has_part( "KITCHEN" ) ) ) {
has_table_nearby = true;
}
}
bool has_tree_nearby = false;
for( const tripoint &pt : g->m.points_in_radius( u.pos(), 2 ) ) {
if( g->m.has_flag( "TREE", pt ) ) {
has_tree_nearby = true;
}
}
// workshop butchery (full) prequisites
if( action == BUTCHER_FULL ) {
bool has_rope = u.has_amount( "rope_30", 1 ) || u.has_amount( "rope_makeshift_30", 1 ) ||
u.has_amount( "vine_30", 1 ) ;
bool b_rack_present = g->m.has_flag_furn( "BUTCHER_EQ", u.pos() );
bool big_corpse = corpse.size >= MS_MEDIUM;
if( big_corpse && has_rope && !has_tree_nearby && !b_rack_present ) {
u.add_msg_if_player( m_info,
_( "You need to suspend this corpse to butcher it, you have a rope to lift the corpse but there is no tree nearby." ) );
act.set_to_null();
return;
} else if( big_corpse && !has_rope && !b_rack_present ) {
u.add_msg_if_player( m_info,
_( "For a corpse this big you need a rope and a nearby tree or a butchering rack to perform a full butchery." ) );
act.set_to_null();
return;
}
if( big_corpse && !has_table_nearby ) {
u.add_msg_if_player( m_info,
_( "For a corpse this big you need a table nearby or something else with a flat surface to perform a full butchery." ) );
act.set_to_null();
return;
}
if( !u.has_quality( quality_id( "CUT" ) ) ) {
u.add_msg_if_player( m_info, _( "You need a cutting tool to perform a full butchery." ) );
act.set_to_null();
return;
}
if( big_corpse && !( u.has_quality( quality_id( "SAW_W" ) ) ||
u.has_quality( quality_id( "SAW_M" ) ) ) ) {
u.add_msg_if_player( m_info,
_( "For a corpse this big you need a saw to perform a full butchery." ) );
act.set_to_null();
return;
}
}
if( action == DISSECT && ( corpse_item.has_flag( "QUARTERED" ) ||
corpse_item.has_flag( "FIELD_DRESS_FAILED" ) ) ) {
u.add_msg_if_player( m_info,
_( "It would be futile to search for implants inside this badly damaged corpse." ) );
act.set_to_null();
return;
}
if( action == F_DRESS && ( corpse_item.has_flag( "FIELD_DRESS" ) ||
corpse_item.has_flag( "FIELD_DRESS_FAILED" ) ) ) {
u.add_msg_if_player( m_info, _( "This corpse is already field dressed." ) );
act.set_to_null();
return;
}
if( action == QUARTER ) {
if( corpse.size == MS_TINY ) {
u.add_msg_if_player( m_bad, _( "This corpse is too small to quarter without damaging." ),
corpse.nname().c_str() );
act.set_to_null();
return;
}
if( corpse_item.has_flag( "QUARTERED" ) ) {
u.add_msg_if_player( m_bad, _( "This is already quartered." ), corpse.nname().c_str() );
act.set_to_null();
return;
}
if( !( corpse_item.has_flag( "FIELD_DRESS" ) || corpse_item.has_flag( "FIELD_DRESS_FAILED" ) ) ) {
u.add_msg_if_player( m_bad, _( "You need to perform field dressing before quartering." ),
corpse.nname().c_str() );
act.set_to_null();
return;
}
}
// applies to all butchery actions
bool is_human = corpse.id == mtype_id::NULL_ID() || ( corpse.in_species( HUMAN ) &&
!corpse.in_species( ZOMBIE ) );
if( is_human && !( u.has_trait_flag( "CANNIBAL" ) || u.has_trait_flag( "PSYCHOPATH" ) ||
u.has_trait_flag( "SAPIOVORE" ) ) ) {
if( query_yn( _( "Would you dare desecrate the mortal remains of a fellow human being?" ) ) ) {
g->u.add_morale( MORALE_BUTCHER, -50, 0, 2_days, 3_hours );
switch( rng( 1, 3 ) ) {
case 1:
u.add_msg_if_player( m_bad, _( "You clench your teeth at the prospect of this gruesome job." ) );
break;
case 2:
u.add_msg_if_player( m_bad, _( "This will haunt you in your dreams." ) );
break;
case 3:
u.add_msg_if_player( m_bad,
_( "You try to look away, but this gruesome image will stay on your mind for some time." ) );
break;
}
} else {
u.add_msg_if_player( m_good, _( "It needs a coffin, not a knife." ) );
act.set_to_null();
return;
}
}
act.moves_left = butcher_time_to_cut( u, corpse_item, action );
}
int butcher_time_to_cut( const player &u, const item &corpse_item, const butcher_type action )
{
const mtype &corpse = *corpse_item.get_mtype();
const int factor = u.max_quality( action == DISSECT ? quality_id( "CUT_FINE" ) :
quality_id( "BUTCHER" ) );
int time_to_cut = 0;
switch( corpse.size ) {
// Time (roughly) in turns to cut up the corpse
case MS_TINY:
time_to_cut = 25;
break;
case MS_SMALL:
time_to_cut = 50;
break;
case MS_MEDIUM:
time_to_cut = 75;
break;
case MS_LARGE:
time_to_cut = 100;
break;
case MS_HUGE:
time_to_cut = 300;
break;
}
// At factor 0, 10 time_to_cut is 10 turns. At factor 50, it's 5 turns, at 75 it's 2.5
time_to_cut *= std::max( 25, 100 - factor );
if( time_to_cut < 500 ) {
time_to_cut = 500;
}
switch( action ) {
case BUTCHER:
break;
case BUTCHER_FULL:
if( !corpse_item.has_flag( "FIELD_DRESS" ) || corpse_item.has_flag( "FIELD_DRESS_FAILED" ) ) {
time_to_cut *= 6;
} else {
time_to_cut *= 4;
}
break;
case F_DRESS:
time_to_cut *= 2;
break;
case QUARTER:
time_to_cut /= 4;
if( time_to_cut < 200 ) {
time_to_cut = 200;
}
break;
case DISSECT:
time_to_cut *= 6;
break;
}
if( corpse_item.has_flag( "QUARTERED" ) ) {
time_to_cut /= 4;
}
return time_to_cut;
}
void butchery_drops_hardcoded( item *corpse_item, const mtype *corpse, player *p,
const time_point &age, const std::function<int()> &roll_butchery, butcher_type action )
{
itype_id meat = corpse->get_meat_itype();
int pieces = corpse->get_meat_chunks_count();
int skins = 0;
int bones = 0;
int fats = pieces / 5;
int sinews = 0;
int feathers = 0;
int wool = 0;
bool stomach = false;
int max_practice = 4;
switch( corpse->size ) {
case MS_TINY:
skins = 1;
bones = 1;
fats = 1;
sinews = 1;
feathers = 4;
wool = 1;
break;
case MS_SMALL:
skins = 2;
bones = 8;
sinews = 4;
feathers = 12;
wool = 4;
break;
case MS_MEDIUM:
skins = 5;
bones = 18;
sinews = 9;
feathers = 22;
wool = 8;
break;
case MS_LARGE:
skins = 10;
bones = 28;
sinews = 14;
feathers = 36;
wool = 16;
max_practice = 5;
break;
case MS_HUGE:
skins = 20;
bones = 42;
sinews = 21;
feathers = 48;
wool = 32;
max_practice = 6;
break;
}
if( corpse->made_of( material_id( "bone" ) ) ) {
//For butchering yield purposes, we treat it as bones, not meat
meat = "null";
bones += pieces / 4;
pieces = 0;
}
// Lose some meat, skins, etc if the rolls are low
pieces += std::min( 0, roll_butchery() );
skins += std::min( 0, roll_butchery() - 4 );
bones += std::min( 0, roll_butchery() - 2 );
fats += std::min( 0, roll_butchery() - 4 );
sinews += std::min( 0, roll_butchery() - 8 );
feathers += std::min( 0, roll_butchery() - 1 );
wool += std::min( 0, roll_butchery() );
stomach = roll_butchery() >= 0;
// (QUICK) BUTCHERY
if( action == BUTCHER && ( !corpse_item->has_flag( "FIELD_DRESS" ) ||
!corpse_item->has_flag( "FIELD_DRESS_FAILED" ) ) ) {
pieces /= 4;
if( corpse->size >= MS_MEDIUM ) {
skins /= 2;
}
bones /= 2;
fats /= 4;
sinews /= 4;
// feathers unchanged
wool /= 4;
stomach = false;
} else if( action == BUTCHER ) {
stomach = roll_butchery() >= 0;
}
//FIELD DRESSING
if( action == F_DRESS ) {
// "pieces" left unchanged because they are 'converted' to offal and don't yield meat
skins = 0;
bones = rng( 0, bones / 2 );
fats = 0;
sinews = 0;
feathers = 0;
wool = 0;
stomach = roll_butchery() >= 0;
}
// field dressing removed innards and bones from meatless limbs
if( action == BUTCHER_FULL && corpse_item->has_flag( "FIELD_DRESS" ) ) {
stomach = false;
bones = ( bones / 2 ) + rng( bones / 2, bones );
}
// unskillfull field dressing damaged the skin, meat, and other parts
if( action == BUTCHER_FULL && corpse_item->has_flag( "FIELD_DRESS_FAILED" ) ) {
pieces = rng( 0, pieces );
skins = rng( 0, skins );
bones = ( bones / 2 ) + rng( bones / 2, bones );
fats = rng( 0, fats );
feathers = rng( 0, feathers );
wool = rng( 0, wool );
stomach = false;
}
if( corpse_item->has_flag( "QUARTERED" ) ) {
pieces /= 4;
skins = 0; //quartering ruins skin
bones /= 4;
fats /= 4;
sinews /= 4;
feathers /= 4;
wool /= 4;
}
if( action == DISSECT ) {
pieces = 0;
skins = 0;
bones = 0;
fats = 0;
sinews = 0;
feathers = 0;
wool = 0;
stomach = false;
}
int practice = std::max( 0, 4 + pieces + roll_butchery() );
if( action == DISSECT ) {
p->practice( skill_firstaid, practice, max_practice );
} else {
p->practice( skill_survival, practice, max_practice );
}
if( bones > 0 ) {
if( corpse->made_of( material_id( "veggy" ) ) ) {
g->m.spawn_item( p->pos(), "plant_sac", bones, 0, age );
p->add_msg_if_player( m_good, _( "You harvest some fluid bladders!" ) );
} else if( corpse->has_flag( MF_BONES ) && corpse->has_flag( MF_POISON ) ) {
g->m.spawn_item( p->pos(), "bone_tainted", bones / 2, 0, age );
p->add_msg_if_player( m_good, _( "You harvest some salvageable bones!" ) );
} else if( corpse->has_flag( MF_BONES ) && corpse->has_flag( MF_HUMAN ) ) {
g->m.spawn_item( p->pos(), "bone_human", bones, 0, age );
p->add_msg_if_player( m_good, _( "You harvest some salvageable bones!" ) );
} else if( corpse->has_flag( MF_BONES ) ) {
g->m.spawn_item( p->pos(), "bone", bones, 0, age );
p->add_msg_if_player( m_good, _( "You harvest some usable bones!" ) );
}
} else if( meat == "null" && corpse->has_flag( MF_BONES ) ) {
//print a failure message only if the corpse doesn't have meat and has bones
p->add_msg_if_player( m_bad, _( "Your clumsy butchering destroys the bones!" ) );
}
if( sinews > 0 ) {
if( corpse->has_flag( MF_BONES ) && !corpse->has_flag( MF_POISON ) ) {
g->m.spawn_item( p->pos(), "sinew", sinews, 0, age );
p->add_msg_if_player( m_good, _( "You harvest some usable sinews!" ) );
} else if( corpse->made_of( material_id( "veggy" ) ) ) {
g->m.spawn_item( p->pos(), "plant_fibre", sinews, 0, age );
p->add_msg_if_player( m_good, _( "You harvest some plant fibers!" ) );
}
}
if( stomach ) {
const itype_id meat = corpse->get_meat_itype();
if( meat == "meat" ) {
if( corpse->size == MS_SMALL || corpse->size == MS_MEDIUM ) {
g->m.spawn_item( p->pos(), "stomach", 1, 0, age );
p->add_msg_if_player( m_good, _( "You harvest the stomach!" ) );
} else if( corpse->size == MS_LARGE || corpse->size == MS_HUGE ) {
g->m.spawn_item( p->pos(), "stomach_large", 1, 0, age );
p->add_msg_if_player( m_good, _( "You harvest the stomach!" ) );
}
} else if( meat == "human_flesh" ) {
if( corpse->size == MS_SMALL || corpse->size == MS_MEDIUM ) {
g->m.spawn_item( p->pos(), "hstomach", 1, 0, age );
p->add_msg_if_player( m_good, _( "You harvest the stomach!" ) );
} else if( corpse->size == MS_LARGE || corpse->size == MS_HUGE ) {
g->m.spawn_item( p->pos(), "hstomach_large", 1, 0, age );
p->add_msg_if_player( m_good, _( "You harvest the stomach!" ) );
}
}
}
if( ( corpse->has_flag( MF_FUR ) || corpse->has_flag( MF_LEATHER ) ||
corpse->has_flag( MF_CHITIN ) ) && skins > 0 ) {
p->add_msg_if_player( m_good, _( "You manage to skin the %s!" ), corpse->nname().c_str() );
int fur = 0;
int tainted_fur = 0;
int leather = 0;
int tainted_leather = 0;
int human_leather = 0;
int chitin = 0;
while( skins > 0 ) {
if( corpse->has_flag( MF_CHITIN ) ) {
chitin = rng( 0, skins );
skins -= chitin;
skins = std::max( skins, 0 );
}
if( corpse->has_flag( MF_FUR ) ) {
if( corpse->has_flag( MF_POISON ) ) {
tainted_fur = rng( 0, skins );
skins -= tainted_fur;
} else {
fur = rng( 0, skins );
skins -= fur;
}
skins = std::max( skins, 0 );
}
if( corpse->has_flag( MF_LEATHER ) ) {
if( corpse->has_flag( MF_POISON ) ) {
tainted_leather = rng( 0, skins );
skins -= tainted_leather;
} else if( corpse->has_flag( MF_HUMAN ) ) {
human_leather = rng( 0, skins );
skins -= human_leather;
} else {
leather = rng( 0, skins );
skins -= leather;
}
skins = std::max( skins, 0 );
}
}
if( chitin > 0 ) {
g->m.spawn_item( p->pos(), "chitin_piece", chitin, 0, age );
}
if( fur > 0 ) {
g->m.spawn_item( p->pos(), "raw_fur", fur, 0, age );
}
if( tainted_fur > 0 ) {
g->m.spawn_item( p->pos(), "raw_tainted_fur", fur, 0, age );
}
if( leather > 0 ) {
g->m.spawn_item( p->pos(), "raw_leather", leather, 0, age );
}
if( human_leather > 0 ) {
g->m.spawn_item( p->pos(), "raw_hleather", leather, 0, age );
}
if( tainted_leather > 0 ) {
g->m.spawn_item( p->pos(), "raw_tainted_leather", leather, 0, age );
}
}
if( feathers > 0 ) {
if( corpse->has_flag( MF_FEATHER ) ) {
g->m.spawn_item( p->pos(), "feather", feathers, 0, age );
p->add_msg_if_player( m_good, _( "You harvest some feathers!" ) );
}
}
if( wool > 0 ) {
if( corpse->has_flag( MF_WOOL ) ) {
g->m.spawn_item( p->pos(), "wool_staple", wool, 0, age );
p->add_msg_if_player( m_good, _( "You harvest some wool staples!" ) );
}
}
if( fats > 0 ) {
if( corpse->has_flag( MF_FAT ) && corpse->has_flag( MF_POISON ) ) {
g->m.spawn_item( p->pos(), "fat_tainted", fats, 0, age );
p->add_msg_if_player( m_good, _( "You harvest some gooey fat!" ) );
} else if( corpse->has_flag( MF_FAT ) ) {
g->m.spawn_item( p->pos(), "fat", fats, 0, age );
p->add_msg_if_player( m_good, _( "You harvest some fat!" ) );
}
}
//Add a chance of CBM recovery.
//As long as the factor is above -4 (the sinew cutoff), you will be able to extract CBMs
if( action == DISSECT ) {
if( corpse->has_flag( MF_CBM_CIV ) ) {
butcher_cbm_item( "bio_power_storage", p->pos(), age, roll_butchery() );
butcher_cbm_group( "bionics_common", p->pos(), age, roll_butchery() );
}
// Zombie scientist bionics
if( corpse->has_flag( MF_CBM_SCI ) ) {
butcher_cbm_item( "bio_power_storage", p->pos(), age, roll_butchery() );
butcher_cbm_group( "bionics_sci", p->pos(), age, roll_butchery() );
}
// Zombie technician bionics
if( corpse->has_flag( MF_CBM_TECH ) ) {
butcher_cbm_item( "bio_power_storage", p->pos(), age, roll_butchery() );
butcher_cbm_group( "bionics_tech", p->pos(), age, roll_butchery() );
}
// Substation mini-boss bionics
if( corpse->has_flag( MF_CBM_SUBS ) ) {
butcher_cbm_item( "bio_power_storage", p->pos(), age, roll_butchery() );
butcher_cbm_group( "bionics_subs", p->pos(), age, roll_butchery() );
butcher_cbm_group( "bionics_subs", p->pos(), age, roll_butchery() );
}
// Payoff for butchering the zombie bio-op
if( corpse->has_flag( MF_CBM_OP ) ) {
butcher_cbm_item( "bio_power_storage_mkII", p->pos(), age, roll_butchery() );
butcher_cbm_group( "bionics_op", p->pos(), age, roll_butchery() );
}
//Add a chance of CBM power storage recovery.
if( corpse->has_flag( MF_CBM_POWER ) ) {
butcher_cbm_item( "bio_power_storage", p->pos(), age, roll_butchery() );
}
}
// feedback that this type of corpse has implants that can be potentialy removed
if( corpse->has_flag( MF_CBM_CIV ) || corpse->has_flag( MF_CBM_SCI ) ||
corpse->has_flag( MF_CBM_TECH ) ||
corpse->has_flag( MF_CBM_SUBS ) || corpse->has_flag( MF_CBM_OP ) ||
corpse->has_flag( MF_CBM_POWER ) ) {
if( action == F_DRESS ) {
p->add_msg_if_player( m_bad,
_( "You suspect there might be bionics implanted in this corpse, that careful dissection might reveal." ) );
}
if( action == BUTCHER || action == BUTCHER_FULL ) {
switch( rng( 1, 3 ) ) {
case 1:
p->add_msg_if_player( m_bad,
_( "Your butchering tool encounters something implanted in this corpse, but your rough cuts destroy it." ) );
break;
case 2:
p->add_msg_if_player( m_bad,
_( "You find traces of implants in the body, but you care only for the flesh." ) );
break;
case 3:
p->add_msg_if_player( m_bad,
_( "You found some bionics in the body, but harvesting them would require more surgical approach." ) );
break;
}
}
}
//now handle the meat, if there is any
if( meat != "null" ) {
if( pieces <= 0 ) {
if( action == BUTCHER || action == BUTCHER_FULL ) {
p->add_msg_if_player( m_bad, _( "Your clumsy butchering destroys the flesh!" ) );
}
} else {
if( action == BUTCHER || action == BUTCHER_FULL ) {
p->add_msg_if_player( m_good, _( "You harvest some flesh." ) );
}
item chunk( meat, age, 1 );
chunk.set_mtype( corpse );
item parts( "offal", age, 0 );
parts.set_mtype( corpse );
// for now don't drop tainted or cannibal. parts overhaul of taint system to not require excessive item duplication
// also field dressing removed innards so no offal
bool make_offal = !chunk.is_tainted() && !chunk.has_flag( "CANNIBALISM" ) &&
!( corpse_item->has_flag( "FIELD_DRESS" ) || corpse_item->has_flag( "FIELD_DRESS_FAILED" ) ) &&
!chunk.made_of( material_id( "veggy" ) );
if( action == F_DRESS ) {
for( int i = 1; i < pieces; ++i ) {
if( make_offal && one_in( 3 ) ) {
parts.charges++;
}
}
chunk.charges = 0;
} else {
for( int i = 1; i < pieces; ++i ) {
if( make_offal && one_in( 6 ) ) {
parts.charges++;
} else {
chunk.charges++;
}
}
}
if( chunk.charges > 0 ) {
g->m.add_item_or_charges( p->pos(), chunk );
}
if( parts.charges > 0 ) {
g->m.add_item_or_charges( p->pos(), parts );
}
}
}
}
void butchery_drops_harvest( item *corpse_item, const mtype &mt, player &p, const time_point &age,
const std::function<int()> &roll_butchery, butcher_type action )
{
p.add_msg_if_player( m_neutral, _( mt.harvest->message().c_str() ) );
int practice = 4 + roll_butchery();
for( const auto &entry : *mt.harvest ) {
int butchery = roll_butchery();
float min_num = entry.base_num.first + butchery * entry.scale_num.first;
float max_num = entry.base_num.second + butchery * entry.scale_num.second;
int roll = std::min<int>( entry.max, round( rng_float( min_num, max_num ) ) );
const itype *drop = item::find_type( entry.drop );
// BIONIC handling - no code for DISSECT to let the bionic drop fall through
if( drop->bionic.has_value() ) {
if( action == F_DRESS ) {
p.add_msg_if_player( m_bad,
_( "You suspect there might be bionics implanted in this corpse, that careful dissection might reveal." ) );
continue;
}
if( action == BUTCHER || action == BUTCHER_FULL ) {
switch( rng( 1, 3 ) ) {
case 1:
p.add_msg_if_player( m_bad,
_( "Your butchering tool encounters something implanted in this corpse, but your rough cuts destroy it." ) );
break;
case 2:
p.add_msg_if_player( m_bad,
_( "You find traces of implants in the body, but you care only for the flesh." ) );
break;
case 3:
p.add_msg_if_player( m_bad,
_( "You found some bionics in the body, but harvesting them would require more surgical approach." ) );
break;
}
continue;
}
} else if( action == DISSECT ) {
continue;
}
// QUICK BUTCHERY
if( action == BUTCHER ) {
if( entry.drop == "meat" || entry.drop == "meat_tainted" || entry.drop == "fish" ||
entry.drop == "veggy" || entry.drop == "veggy_tainted" || entry.drop == "scrap" ||
entry.drop == "wool_staple" || entry.drop == "fat" || entry.drop == "fat_tainted" ) {
roll = roll / 4;
} else if( entry.drop != "bone" ) {
roll = roll / 2;
} else if( corpse_item->get_mtype()->size >= MS_MEDIUM && ( entry.drop == "raw_fur" ||
entry.drop == "raw_leather" ||
entry.drop == "raw_tainted_fur" || entry.drop == "raw_tainted_leather" ||
entry.drop == "raw_hleather" || entry.drop == "chitin_piece" ||
entry.drop == "acidchitin_piece" ) ) {
roll /= 2 ;
} else {
continue;
}
}
// field dressing ignores everything outside below list
if( action == F_DRESS ) {
if( entry.drop != "bone" ) {
roll = rng( 0, roll / 2 );
}
if( entry.drop == "fat" || entry.drop == "fat_tainted" || entry.drop == "meat" ||
entry.drop == "meat_tainted" || entry.drop == "fish" ||
entry.drop == "feathers" || entry.drop == "raw_fur" || entry.drop == "raw_leather" ||
entry.drop == "raw_tainted_fur" || entry.drop == "raw_tainted_leather" ||
entry.drop == "raw_hleather" || entry.drop == "wool_staple" || entry.drop == "chitin_piece" ||
entry.drop == "acidchitin_piece" || entry.drop == "veggy" || entry.drop == "veggy tainted" ||
entry.drop == "brain" ) {
continue;
}
}
// field dressing removed innards and bones from meatless limbs
if( ( action == BUTCHER_FULL ) && corpse_item->has_flag( "FIELD_DRESS" ) ) {
if( entry.drop == "stomach" || entry.drop == "stomach_large" ||
entry.drop == "hstomach" || entry.drop == "hstomach_large" ||
entry.drop == "offal" || entry.drop == "plant_sac" ||
entry.drop == "liver" || entry.drop == "kidney" ||
entry.drop == "lung" || entry.drop == "sweetbread" ) {
continue;
}
if( entry.drop == "bone" ) {
roll = ( roll / 2 ) + rng( roll / 2, roll );
}
}
// unskillfull field dressing may damage the skin, meat, and other parts
if( ( action == BUTCHER_FULL ) && corpse_item->has_flag( "FIELD_DRESS_FAILED" ) ) {
if( entry.drop == "stomach" || entry.drop == "stomach_large" ||
entry.drop == "hstomach" || entry.drop == "hstomach_large" ||
entry.drop == "offal" ) {
continue;
}
if( entry.drop == "bone" || entry.drop == "bone_human" ) {
roll = ( roll / 2 ) + rng( roll / 2, roll );
}
if( entry.drop == "fat" || entry.drop == "fat_tainted" || entry.drop == "meat" ||
entry.drop == "meat_tainted" || entry.drop == "fish" ||
entry.drop == "feathers" || entry.drop == "raw_fur" || entry.drop == "raw_leather" ||
entry.drop == "raw_tainted_fur" || entry.drop == "raw_tainted_leather" ||
entry.drop == "raw_hleather" || entry.drop == "wool_staple" || entry.drop == "chitin_piece" ||
entry.drop == "acidchitin_piece" || entry.drop == "veggy" || entry.drop == "veggy tainted" ) {
roll = rng( 0, roll );
}
}
// quartering ruins skin
if( corpse_item->has_flag( "QUARTERED" ) ) {
if( entry.drop == "feathers" || entry.drop == "raw_fur" || entry.drop == "raw_leather" ||
entry.drop == "raw_hleather" || entry.drop == "wool_staple" ||
entry.drop == "raw_tainted_leather" || entry.drop == "chitin_piece" ||
entry.drop == "acidchitin_piece" ) {
roll = 0; //not continue to show fail effect
} else {
roll /= 4;
}
}
if( roll <= 0 ) {
p.add_msg_if_player( m_bad, _( "You fail to harvest: %s" ), drop->nname( 1 ).c_str() );
continue;
}
if( drop->phase == LIQUID ) {
g->handle_all_liquid( item( drop, age, roll ), 1 );
} else if( drop->stackable ) {
g->m.add_item_or_charges( p.pos(), item( drop, age, roll ) );
} else {
item obj( drop, age );
obj.set_mtype( &mt );
for( int i = 0; i != roll; ++i ) {
g->m.add_item_or_charges( p.pos(), obj );
}
}
p.add_msg_if_player( m_good, _( "You harvest: %s" ), drop->nname( roll ).c_str() );
practice++;
}
if( action == DISSECT ) {
p.practice( skill_firstaid, std::max( 0, practice ), std::max( mt.size - MS_MEDIUM, 0 ) + 4 );
} else {
p.practice( skill_survival, std::max( 0, practice ), std::max( mt.size - MS_MEDIUM, 0 ) + 4 );
}
}
void butchery_quarter( item *corpse_item, player &p )
{
corpse_item->set_flag( "QUARTERED" );
p.add_msg_if_player( m_good,
_( "You roughly slice the corpse of %s into four parts and set them aside." ),
corpse_item->get_mtype()->nname().c_str() );
for( int i = 1; i <= 3; i++ ) { // 4 quarters (one exists, add 3, flag does the rest)
g->m.add_item_or_charges( p.pos(), *corpse_item, true );
}
}
void activity_handlers::butcher_finish( player_activity *act, player *p )
{
butcher_type action = BUTCHER;
if( act->id() == activity_id( "ACT_BUTCHER" ) ) {
action = BUTCHER;
} else if( act->id() == activity_id( "ACT_BUTCHER_FULL" ) ) {
action = BUTCHER_FULL;
} else if( act->id() == activity_id( "ACT_FIELD_DRESS" ) ) {
action = F_DRESS;
} else if( act->id() == activity_id( "ACT_QUARTER" ) ) {
action = QUARTER;
} else if( act->id() == activity_id( "ACT_DISSECT" ) ) {
action = DISSECT;
}
if( act->index < 0 ) {
set_up_butchery( *act, *p, action );
return;
}
// Corpses can disappear (rezzing!), so check for that
auto items_here = g->m.i_at( p->pos() );
if( static_cast<int>( items_here.size() ) <= act->index ||
!( items_here[act->index].is_corpse() ) ) {
p->add_msg_if_player( m_info, _( "There's no corpse to butcher!" ) );
act->set_to_null();
return;
}
item &corpse_item = items_here[act->index];
auto contents = corpse_item.contents;
const mtype *corpse = corpse_item.get_mtype();
time_point bday = corpse_item.birthday();
const field_id type_blood = corpse->bloodType();
const field_id type_gib = corpse->gibType();
// corpse decays at 75% factor, but meat shares birthday and not relative_rot so this takes care of it
// no FIELD_DRESS_FAILED here as it gets no benefit
if( corpse_item.has_flag( "FIELD_DRESS" ) && !corpse_item.is_going_bad() ) {
bday += corpse_item.age() * 3 / 4;
}
if( action == QUARTER ) {
butchery_quarter( &corpse_item, *p );
act->set_to_null();
return;
}
int skill_level = p->get_skill_level( skill_survival );
int factor = p->max_quality( action == DISSECT ? quality_id( "CUT_FINE" ) :
quality_id( "BUTCHER" ) );
// DISSECT has special case factor calculation and results.
if( action == DISSECT ) {
skill_level = p->get_skill_level( skill_firstaid );
skill_level += p->max_quality( quality_id( "CUT_FINE" ) );
skill_level += p->get_skill_level( skill_electronics ) / 2;
}
auto roll_butchery = [&]() {
double skill_shift = 0.0;
///\EFFECT_SURVIVAL randomly increases butcher rolls
skill_shift += rng_float( 0, skill_level - 3 );
///\EFFECT_DEX >8 randomly increases butcher rolls, slightly, <8 decreases
skill_shift += rng_float( 0, p->dex_cur - 8 ) / 4.0;
if( factor < 0 ) {
skill_shift -= rng_float( 0, -factor / 5.0 );
}
return static_cast<int>( round( skill_shift ) );
};
//all BUTCHERY types - FATAL FAILURE
if( action != DISSECT && roll_butchery() <= ( -15 ) && one_in( 2 ) ) {
switch( rng( 1, 3 ) ) {
case 1:
p->add_msg_if_player( m_warning,
_( "You hack up the corpse so unskillfully, that there is nothing left to salvage from this bloody mess." ) );
break;
case 2:
p->add_msg_if_player( m_warning,
_( "You wanted to cut the corpse, but instead you hacked the meat, spilled the guts all over it, and made a bloody mess." ) );
break;
case 3:
p->add_msg_if_player( m_warning,
_( "You made so many mistakes during the process that you doubt even vultures will be interested in what's left of it." ) );
break;
}
g->m.i_rem( p->pos(), act->index );
g->m.add_splatter( type_gib, p->pos(), rng( corpse->size + 2, ( corpse->size + 1 ) * 2 ) );
g->m.add_splatter( type_blood, p->pos(), rng( corpse->size + 2, ( corpse->size + 1 ) * 2 ) );
for( int i = 1; i <= corpse->size; i++ ) {
g->m.add_splatter_trail( type_gib, p->pos(), random_entry( g->m.points_in_radius( p->pos(),
corpse->size + 1 ) ) );
g->m.add_splatter_trail( type_blood, p->pos(), random_entry( g->m.points_in_radius( p->pos(),
corpse->size + 1 ) ) );
}
act->set_to_null();
return;
}
// all action types - yields
if( corpse->harvest.is_null() ) {
butchery_drops_hardcoded( &corpse_item, corpse, p, bday, roll_butchery, action );
} else {
butchery_drops_harvest( &corpse_item, *corpse, *p, bday, roll_butchery, action );
}
// reveal hidden items / hidden content
if( action != F_DRESS ) {
for( auto &content : contents ) {
if( ( roll_butchery() + 10 ) * 5 > rng( 0, 100 ) ) {
//~ %1$s - item name, %2$s - monster name
p->add_msg_if_player( m_good, _( "You discover a %1$s in the %2$s!" ), content.tname().c_str(),
corpse->nname().c_str() );
g->m.add_item_or_charges( p->pos(), content );
} else if( content.is_bionic() ) {
g->m.spawn_item( p->pos(), "burnt_out_bionic", 1, 0, bday );
}
}
}
//end messages and effects
switch( action ) {
case QUARTER:
break;
case BUTCHER:
p->add_msg_if_player( m_good,
_( "You apply few quick cuts to the %s and leave what's left of it for scavengers." ),
corpse_item.tname().c_str() );
g->m.i_rem( p->pos(), act->index );
break; //no set_to_null here, for multibutchering
case BUTCHER_FULL:
p->add_msg_if_player( m_good, _( "You finish butchering the %s." ), corpse_item.tname().c_str() );
g->m.i_rem( p->pos(), act->index );
break;
case F_DRESS:
if( roll_butchery() < 0 ) { // partial failure
switch( rng( 1, 3 ) ) {
case 1:
p->add_msg_if_player( m_warning,
_( "You unskillfully hack up the corpse and chop off some excess body parts. You're left wondering how you did so poorly." ) );
break;
case 2:
p->add_msg_if_player( m_warning,
_( "Your unskilled hands slip and damage the corpse. You still hope it's not a total waste though." ) );
break;
case 3:
p->add_msg_if_player( m_warning,
_( "You did something wrong and hacked the corpse badly. Maybe it's still recoverable." ) );
break;
}
corpse_item.set_flag( "FIELD_DRESS_FAILED" );
g->m.add_splatter( type_gib, p->pos(), rng( corpse->size + 2, ( corpse->size + 1 ) * 2 ) );
g->m.add_splatter( type_blood, p->pos(), rng( corpse->size + 2, ( corpse->size + 1 ) * 2 ) );
for( int i = 1; i <= corpse->size; i++ ) {
g->m.add_splatter_trail( type_gib, p->pos(), random_entry( g->m.points_in_radius( p->pos(),
corpse->size + 1 ) ) );
g->m.add_splatter_trail( type_blood, p->pos(), random_entry( g->m.points_in_radius( p->pos(),
corpse->size + 1 ) ) );
}
} else { // success
switch( rng( 1, 3 ) ) {
case 1:
p->add_msg_if_player( m_good, _( "You field dress the %s." ), corpse->nname().c_str() );
break;
case 2:
p->add_msg_if_player( m_good,
_( "You slice the corpse's belly and remove intestines and organs, until you're confident that it will not rot from inside." ) );
break;
case 3:
p->add_msg_if_player( m_good,
_( "You remove guts and excess parts, preparing the corpse for later use." ) );
break;
}
corpse_item.set_flag( "FIELD_DRESS" );
g->m.add_splatter( type_gib, p->pos(), rng( corpse->size + 2, ( corpse->size + 1 ) * 2 ) );
g->m.add_splatter( type_blood, p->pos(), rng( corpse->size + 2, ( corpse->size + 1 ) * 2 ) );
for( int i = 1; i <= corpse->size; i++ ) {
g->m.add_splatter_trail( type_gib, p->pos(), random_entry( g->m.points_in_radius( p->pos(),
corpse->size + 1 ) ) );
g->m.add_splatter_trail( type_blood, p->pos(), random_entry( g->m.points_in_radius( p->pos(),
corpse->size + 1 ) ) );
}
}
act->set_to_null();
return;
case DISSECT:
p->add_msg_if_player( m_good, _( "You finish dissecting the %s." ), corpse_item.tname().c_str() );
g->m.i_rem( p->pos(), act->index );
break;
}
// multibutchering
if( act->values.empty() ) {
act->set_to_null();
} else {
set_up_butchery( *act, *p, action );
}
}
enum liquid_source_type { LST_INFINITE_MAP = 1, LST_MAP_ITEM = 2, LST_VEHICLE = 3, LST_MONSTER = 4};
// All serialize_liquid_source functions should add the same number of elements to the vectors of
// the activity. This makes it easier to distinguish the values of the source and the values of the target.
void serialize_liquid_source( player_activity &act, const vehicle &veh, const int part_num,
const item &liquid )
{
act.values.push_back( LST_VEHICLE );
act.values.push_back( part_num );
act.coords.push_back( veh.global_pos3() );
act.str_values.push_back( serialize( liquid ) );
}
void serialize_liquid_source( player_activity &act, const monster &mon, const item &liquid )
{
act.values.push_back( LST_MONSTER );
act.values.push_back( 0 ); // dummy
act.coords.push_back( mon.pos() );
act.str_values.push_back( serialize( liquid ) );
}
void serialize_liquid_source( player_activity &act, const tripoint &pos, const item &liquid )
{
const auto stack = g->m.i_at( pos );
// Need to store the *index* of the item on the ground, but it may be a virtual item from
// an infinite liquid source.
const auto iter = std::find_if( stack.begin(), stack.end(), [&]( const item & i ) {
return &i == &liquid;
} );
if( iter == stack.end() ) {
act.values.push_back( LST_INFINITE_MAP );
act.values.push_back( 0 ); // dummy
} else {
act.values.push_back( LST_MAP_ITEM );
act.values.push_back( std::distance( stack.begin(), iter ) );
}
act.coords.push_back( pos );
act.str_values.push_back( serialize( liquid ) );
}
enum liquid_target_type { LTT_CONTAINER = 1, LTT_VEHICLE = 2, LTT_MAP = 3, LTT_MONSTER = 4 };
void serialize_liquid_target( player_activity &act, const vehicle &veh )
{
act.values.push_back( LTT_VEHICLE );
act.values.push_back( 0 ); // dummy
act.coords.push_back( veh.global_pos3() );
}
void serialize_liquid_target( player_activity &act, int container_item_pos )
{
act.values.push_back( LTT_CONTAINER );
act.values.push_back( container_item_pos );
act.coords.push_back( tripoint() ); // dummy
}
void serialize_liquid_target( player_activity &act, const tripoint &pos )
{
act.values.push_back( LTT_MAP );
act.values.push_back( 0 ); // dummy
act.coords.push_back( pos );
}
void serialize_liquid_target( player_activity &act, const monster &mon )
{
act.values.push_back( LTT_MAP );
act.values.push_back( 0 ); // dummy
act.coords.push_back( mon.pos() );
}
void activity_handlers::fill_liquid_do_turn( player_activity *act_, player *p )
{
player_activity &act = *act_;
try {
// 1. Gather the source item.
vehicle *source_veh = nullptr;
const tripoint source_pos = act.coords.at( 0 );
map_stack source_stack = g->m.i_at( source_pos );
std::list<item>::iterator on_ground;
monster *source_mon = nullptr;
item liquid;
const auto source_type = static_cast<liquid_source_type>( act.values.at( 0 ) );
int part_num = -1;
long veh_charges = 0;
switch( source_type ) {
case LST_VEHICLE:
source_veh = veh_pointer_or_null( g->m.veh_at( source_pos ) );
if( source_veh == nullptr ) {
throw std::runtime_error( "could not find source vehicle for liquid transfer" );
}
deserialize( liquid, act.str_values.at( 0 ) );
part_num = static_cast<int>( act.values.at( 1 ) );
veh_charges = liquid.charges;
break;
case LST_INFINITE_MAP:
deserialize( liquid, act.str_values.at( 0 ) );
liquid.charges = item::INFINITE_CHARGES;
break;
case LST_MAP_ITEM:
if( static_cast<size_t>( act.values.at( 1 ) ) >= source_stack.size() ) {
throw std::runtime_error( "could not find source item on ground for liquid transfer" );
}
on_ground = source_stack.begin();
std::advance( on_ground, act.values.at( 1 ) );
liquid = *on_ground;
break;
case LST_MONSTER:
Creature *c = g->critter_at( source_pos );
source_mon = dynamic_cast<monster *>( c );
if( source_mon == nullptr ) {
debugmsg( "could not find source creature for liquid transfer" );
act.set_to_null();
}
deserialize( liquid, act.str_values.at( 0 ) );
liquid.charges = 1;
break;
}
static const auto volume_per_turn = units::from_liter( 4 );
const long charges_per_turn = std::max( 1l, liquid.charges_per_volume( volume_per_turn ) );
liquid.charges = std::min( charges_per_turn, liquid.charges );
const long original_charges = liquid.charges;
// 2. Transfer charges.
switch( static_cast<liquid_target_type>( act.values.at( 2 ) ) ) {
case LTT_VEHICLE:
if( const optional_vpart_position vp = g->m.veh_at( act.coords.at( 1 ) ) ) {
p->pour_into( vp->vehicle(), liquid );
} else {
throw std::runtime_error( "could not find target vehicle for liquid transfer" );
}
break;
case LTT_CONTAINER:
p->pour_into( p->i_at( act.values.at( 3 ) ), liquid );
break;
case LTT_MAP:
if( iexamine::has_keg( act.coords.at( 1 ) ) ) {
iexamine::pour_into_keg( act.coords.at( 1 ), liquid );
} else {
g->m.add_item_or_charges( act.coords.at( 1 ), liquid );
p->add_msg_if_player( _( "You pour %1$s onto the ground." ), liquid.tname().c_str() );
liquid.charges = 0;
}
break;
case LTT_MONSTER:
liquid.charges = 0;
break;
}
const long removed_charges = original_charges - liquid.charges;
if( removed_charges == 0 ) {
// Nothing has been transferred, target must be full.
act.set_to_null();
return;
}
// 3. Remove charges from source.
switch( source_type ) {
case LST_VEHICLE:
if( part_num != -1 ) {
source_veh->drain( part_num, removed_charges );
liquid.charges = veh_charges - removed_charges;
// If there's no liquid left in this tank we're done, otherwise
// we need to update our liquid serialization to reflect how
// many charges are actually left for the next time we come
// around this loop.
if( !liquid.charges ) {
act.set_to_null();
} else {
act.str_values.at( 0 ) = serialize( liquid );
}
} else {
source_veh->drain( liquid.typeId(), removed_charges );
}
if( source_veh->fuel_left( liquid.typeId() ) <= 0 ) {
act.set_to_null();
}
break;
case LST_MAP_ITEM:
on_ground->charges -= removed_charges;
if( on_ground->charges <= 0 ) {
source_stack.erase( on_ground );
if( g->m.ter( source_pos ).obj().examine == &iexamine::gaspump ) {
add_msg( _( "With a clang and a shudder, the %s pump goes silent." ),
liquid.type_name( 1 ).c_str() );
} else if( g->m.furn( source_pos ).obj().examine == &iexamine::fvat_full ) {
g->m.furn_set( source_pos, f_fvat_empty );
add_msg( _( "You squeeze the last drops of %s from the vat." ),
liquid.type_name( 1 ).c_str() );
}
act.set_to_null();
}
break;
case LST_INFINITE_MAP:
// nothing, the liquid source is infinite
break;
case LST_MONSTER:
// liquid source charges handled in monexamine::milk_source
if( liquid.charges == 0 ) {
act.set_to_null();
}
break;
}
if( removed_charges < original_charges ) {
// Transferred less than the available charges -> target must be full
act.set_to_null();
}
} catch( const std::runtime_error &err ) {
debugmsg( "error in activity data: \"%s\"", err.what() );
act.set_to_null();
return;
}
}
// handles equipping an item on ACT_PICKUP, if requested
void activity_handlers::pickup_finish( player_activity *act, player *p )
{
// loop through all the str_values, and if we find equip, do so.
// if no str_values present, carry on
for( auto &elem : act->str_values ) {
if( elem == "equip" ) {
item &it = p->i_at( act->position );
p->wear_item( it );
}
}
}
void activity_handlers::move_items_finish( player_activity *act, player *p )
{
pickup_finish( act, p );
}
void activity_handlers::move_loot_finish( player_activity *act, player *p )
{
pickup_finish( act, p );
}
void activity_handlers::firstaid_finish( player_activity *act, player *p )
{
static const std::string iuse_name_string( "heal" );
item &it = p->i_at( act->position );
item *used_tool = it.get_usable_item( iuse_name_string );
if( used_tool == nullptr ) {
debugmsg( "Lost tool used for healing" );
act->set_to_null();
return;
}
const auto use_fun = used_tool->get_use( iuse_name_string );
const auto *actor = dynamic_cast<const heal_actor *>( use_fun->get_actor_ptr() );
if( actor == nullptr ) {
debugmsg( "iuse_actor type descriptor and actual type mismatch" );
act->set_to_null();
return;
}
// TODO: Store the patient somehow, retrieve here
player &patient = *p;
hp_part healed = static_cast<hp_part>( act->values[0] );
long charges_consumed = actor->finish_using( *p, patient, *used_tool, healed );
p->consume_charges( it, charges_consumed );
// Erase activity and values.
act->set_to_null();
act->values.clear();
}
// fish-with-rod fish catching function.
static void rod_fish( player *p, int sSkillLevel, int fishChance, const tripoint &fish_point )
{
if( sSkillLevel > fishChance ) {
std::vector<monster *> fishables = g->get_fishable( 60, fish_point ); //get the nearby fish list.
//if the vector is empty (no fish around) the player is still given a small chance to get a (let us say it was hidden) fish
if( fishables.empty() ) {
if( one_in( 20 ) ) {
item fish;
const std::vector<mtype_id> fish_group = MonsterGroupManager::GetMonstersFromGroup(
mongroup_id( "GROUP_FISH" ) );
const mtype_id &fish_mon = random_entry_ref( fish_group );
g->m.add_item_or_charges( p->pos(), item::make_corpse( fish_mon ) );
p->add_msg_if_player( m_good, _( "You caught a %s." ), fish_mon.obj().nname().c_str() );
} else {
p->add_msg_if_player( _( "You didn't catch anything." ) );
}
} else {
g->catch_a_monster( fishables, p->pos(), p, 50_hours );
}
} else {
p->add_msg_if_player( _( "You didn't catch anything." ) );
}
}
void activity_handlers::fish_finish( player_activity *act, player *p )
{
item &it = p->i_at( act->position );
int sSkillLevel = 0;
int fishChance = 20;
if( it.has_flag( "FISH_POOR" ) ) {
sSkillLevel = p->get_skill_level( skill_survival ) + dice( 1, 6 );
fishChance = dice( 1, 20 );
} else if( it.has_flag( "FISH_GOOD" ) ) {
// Much better chances with a good fishing implement.
sSkillLevel = p->get_skill_level( skill_survival ) * 1.5 + dice( 1, 6 ) + 3;
fishChance = dice( 1, 20 );
}
const tripoint fish_pos = act->placement;
///\EFFECT_SURVIVAL increases chance of fishing success
rod_fish( p, sSkillLevel, fishChance, fish_pos );
p->practice( skill_survival, rng( 5, 15 ) );
act->set_to_null();
}
void activity_handlers::forage_finish( player_activity *act, player *p )
{
int veggy_chance = rng( 1, 100 );
bool found_something = false;
items_location loc;
ter_str_id next_ter;
switch( season_of_year( calendar::turn ) ) {
case SPRING:
loc = "forage_spring";
next_ter = ter_str_id( "t_underbrush_harvested_spring" );
break;
case SUMMER:
loc = "forage_summer";
next_ter = ter_str_id( "t_underbrush_harvested_summer" );
break;
case AUTUMN:
loc = "forage_autumn";
next_ter = ter_str_id( "t_underbrush_harvested_autumn" );
break;
case WINTER:
loc = "forage_winter";
next_ter = ter_str_id( "t_underbrush_harvested_winter" );
break;
}
g->m.ter_set( act->placement, next_ter );
// Survival gives a bigger boost, and Perception is leveled a bit.
// Both survival and perception affect time to forage
///\EFFECT_SURVIVAL increases forage success chance
///\EFFECT_PER slightly increases forage success chance
if( veggy_chance < p->get_skill_level( skill_survival ) * 3 + p->per_cur - 2 ) {
const auto dropped = g->m.put_items_from_loc( loc, p->pos(), calendar::turn );
for( const auto &it : dropped ) {
add_msg( m_good, _( "You found: %s!" ), it->tname().c_str() );
found_something = true;
}
}
// 10% to drop a item/items from this group.
if( one_in( 10 ) ) {
const auto dropped = g->m.put_items_from_loc( "trash_forest", p->pos(), calendar::turn );
for( const auto &it : dropped ) {
add_msg( m_good, _( "You found: %s!" ), it->tname().c_str() );
found_something = true;
if( it->typeId() == "mushroom" ) {
if( one_in( 10 ) ) {
it->item_tags.insert( "HIDDEN_POISON" );
it->poison = rng( 2, 7 );
break;
} else if( one_in( 10 ) ) {
it->item_tags.insert( "HIDDEN_HALLU" );
break;
}
}
}
}
if( !found_something ) {
add_msg( _( "You didn't find anything." ) );
}
///\EFFECT_INT Intelligence caps survival skill gains from foraging
const int max_forage_skill = p->int_cur / 3 + 1;
///\EFFECT_SURVIVAL decreases survival skill gain from foraging (NEGATIVE)
const int max_exp = 2 * ( max_forage_skill - p->get_skill_level( skill_survival ) );
// Award experience for foraging attempt regardless of success
p->practice( skill_survival, rng( 1, max_exp ), max_forage_skill );
act->set_to_null();
}
void activity_handlers::game_do_turn( player_activity *act, player *p )
{
//Gaming takes time, not speed
act->moves_left -= 100;
item &game_item = p->i_at( act->position );
//Deduct 1 battery charge for every minute spent playing
if( calendar::once_every( 1_minutes ) ) {
game_item.ammo_consume( 1, p->pos() );
p->add_morale( MORALE_GAME, 1, 100 ); //1 points/min, almost 2 hours to fill
}
if( game_item.ammo_remaining() == 0 ) {
act->moves_left = 0;
add_msg( m_info, _( "The %s runs out of batteries." ), game_item.tname().c_str() );
}
}
void activity_handlers::hotwire_finish( player_activity *act, player *pl )
{
//Grab this now, in case the vehicle gets shifted
if( const optional_vpart_position vp = g->m.veh_at( tripoint( act->values[0], act->values[1],
pl->posz() ) ) ) {
vehicle *const veh = &vp->vehicle();
int mech_skill = act->values[2];
if( mech_skill > static_cast<int>( rng( 1, 6 ) ) ) {
//success
veh->is_locked = false;
add_msg( _( "This wire will start the engine." ) );
} else if( mech_skill > static_cast<int>( rng( 0, 4 ) ) ) {
//soft fail
veh->is_locked = false;
veh->is_alarm_on = veh->has_security_working();
add_msg( _( "This wire will probably start the engine." ) );
} else if( veh->is_alarm_on ) {
veh->is_locked = false;
add_msg( _( "By process of elimination, this wire will start the engine." ) );
} else {
//hard fail
veh->is_alarm_on = veh->has_security_working();
add_msg( _( "The red wire always starts the engine, doesn't it?" ) );
}
} else {
dbg( D_ERROR ) << "game:process_activity: ACT_HOTWIRE_CAR: vehicle not found";
debugmsg( "process_activity ACT_HOTWIRE_CAR: vehicle not found" );
}
act->set_to_null();
}
void activity_handlers::longsalvage_finish( player_activity *act, player *p )
{
const static std::string salvage_string = "salvage";
item &main_tool = p->i_at( act->index );
auto items = g->m.i_at( p->pos() );
item *salvage_tool = main_tool.get_usable_item( salvage_string );
if( salvage_tool == nullptr ) {
debugmsg( "Lost tool used for long salvage" );
act->set_to_null();
return;
}
const auto use_fun = salvage_tool->get_use( salvage_string );
const auto actor = dynamic_cast<const salvage_actor *>( use_fun->get_actor_ptr() );
if( actor == nullptr ) {
debugmsg( "iuse_actor type descriptor and actual type mismatch" );
act->set_to_null();
return;
}
for( auto it = items.begin(); it != items.end(); ++it ) {
if( actor->valid_to_cut_up( *it ) ) {
item_location item_loc( map_cursor( p->pos() ), &*it );
actor->cut_up( *p, *salvage_tool, item_loc );
return;
}
}
add_msg( _( "You finish salvaging." ) );
act->set_to_null();
}
void activity_handlers::make_zlave_finish( player_activity *act, player *p )
{
act->set_to_null();
auto items = g->m.i_at( p->pos() );
std::string corpse_name = act->str_values[0];
item *body = nullptr;
for( auto it = items.begin(); it != items.end(); ++it ) {
if( it->display_name() == corpse_name ) {
body = &*it;
}
}
if( body == nullptr ) {
add_msg( m_info, _( "There's no corpse to make into a zombie slave!" ) );
return;
}
int success = act->values[0];
if( success > 0 ) {
p->practice( skill_firstaid, rng( 2, 5 ) );
p->practice( skill_survival, rng( 2, 5 ) );
p->add_msg_if_player( m_good,
_( "You slice muscles and tendons, and remove body parts until you're confident the zombie won't be able to attack you when it reanimates." ) );
body->set_var( "zlave", "zlave" );
//take into account the chance that the body yet can regenerate not as we need.
if( one_in( 10 ) ) {
body->set_var( "zlave", "mutilated" );
}
} else {
if( success > -20 ) {
p->practice( skill_firstaid, rng( 3, 6 ) );
p->practice( skill_survival, rng( 3, 6 ) );
p->add_msg_if_player( m_warning,
_( "You hack into the corpse and chop off some body parts. You think the zombie won't be able to attack when it reanimates." ) );
success += rng( 1, 20 );
if( success > 0 && !one_in( 5 ) ) {
body->set_var( "zlave", "zlave" );
} else {
body->set_var( "zlave", "mutilated" );
}
} else {
p->practice( skill_firstaid, rng( 1, 8 ) );
p->practice( skill_survival, rng( 1, 8 ) );
body->mod_damage( rng( 0, body->max_damage() - body->damage() ), DT_STAB );
if( body->damage() == body->max_damage() ) {
body->active = false;
p->add_msg_if_player( m_warning, _( "You cut up the corpse too much, it is thoroughly pulped." ) );
} else {
p->add_msg_if_player( m_warning,
_( "You cut into the corpse trying to make it unable to attack, but you don't think you have it right." ) );
}
}
}
}
void activity_handlers::pickaxe_do_turn( player_activity *act, player *p )
{
const tripoint &pos = act->placement;
if( calendar::once_every( 1_minutes ) ) { // each turn is too much
//~ Sound of a Pickaxe at work!
sounds::sound( pos, 30, _( "CHNK! CHNK! CHNK!" ) );
messages_in_process( *act, *p );
}
}
void activity_handlers::pickaxe_finish( player_activity *act, player *p )
{
const tripoint pos( act->placement );
item &it = p->i_at( act->position );
act->set_to_null(); // Invalidate the activity early to prevent a query from mod_pain()
if( g->m.is_bashable( pos ) && g->m.has_flag( "SUPPORTS_ROOF", pos ) &&
g->m.ter( pos ) != t_tree ) {
// Tunneling through solid rock is hungry, sweaty, tiring, backbreaking work
// Betcha wish you'd opted for the J-Hammer ;P
p->mod_hunger( 15 );
p->mod_thirst( 15 );
if( p->has_trait( trait_id( "STOCKY_TROGLO" ) ) ) {
p->mod_fatigue( 20 ); // Yep, dwarves can dig longer before tiring
} else {
p->mod_fatigue( 30 );
}
p->mod_pain( 2 * rng( 1, 3 ) );
} else if( g->m.move_cost( pos ) == 2 && g->get_levz() == 0 &&
g->m.ter( pos ) != t_dirt && g->m.ter( pos ) != t_grass ) {
//Breaking up concrete on the surface? not nearly as bad
p->mod_hunger( 5 );
p->mod_thirst( 5 );
p->mod_fatigue( 10 );
}
p->add_msg_if_player( m_good, _( "You finish digging." ) );
g->m.destroy( pos, true );
it.charges = std::max( long( 0 ), it.charges - it.type->charges_to_use() );
if( it.charges == 0 && it.destroyed_at_zero_charges() ) {
p->i_rem( &it );
}
}
void activity_handlers::pulp_do_turn( player_activity *act, player *p )
{
const tripoint &pos = act->placement;
// Stabbing weapons are a lot less effective at pulping
int cut_power = std::max( p->weapon.damage_melee( DT_CUT ), p->weapon.damage_melee( DT_STAB ) / 2 );
///\EFFECT_STR increases pulping power, with diminishing returns
float pulp_power = sqrt( ( p->str_cur + p->weapon.damage_melee( DT_BASH ) ) *
( cut_power + 1.0f ) );
// Multiplier to get the chance right + some bonus for survival skill
pulp_power *= 40 + p->get_skill_level( skill_survival ) * 5;
const int mess_radius = p->weapon.has_flag( "MESSY" ) ? 2 : 1;
int moves = 0;
int &num_corpses = act->index; // use this to collect how many corpse are pulped
auto corpse_pile = g->m.i_at( pos );
for( auto &corpse : corpse_pile ) {
const mtype *corpse_mtype = corpse.get_mtype();
if( !corpse.is_corpse() || !corpse_mtype->has_flag( MF_REVIVES ) ||
( std::find( act->str_values.begin(), act->str_values.end(), "auto_pulp_no_acid" ) !=
act->str_values.end() && corpse_mtype->bloodType() == fd_acid ) ) {
// Don't smash non-rezing corpses //don't smash acid zombies when auto pulping
continue;
}
if( corpse.damage() >= corpse.max_damage() ) {
// Deactivate already-pulped corpses that weren't properly deactivated
corpse.active = false;
continue;
}
while( corpse.damage() < corpse.max_damage() ) {
// Increase damage as we keep smashing ensuring we eventually smash the target.
if( x_in_y( pulp_power, corpse.volume() / units::legacy_volume_factor ) ) {
corpse.inc_damage( DT_BASH );
if( corpse.damage() == corpse.max_damage() ) {
corpse.active = false;
num_corpses++;
}
}
if( x_in_y( pulp_power, corpse.volume() /
units::legacy_volume_factor ) ) { // Splatter some blood around
// Splatter a bit more randomly, so that it looks cooler
const int radius = mess_radius + x_in_y( pulp_power, 500 ) + x_in_y( pulp_power, 1000 );
const tripoint dest( pos.x + rng( -radius, radius ), pos.y + rng( -radius, radius ), pos.z );
const field_id type_blood = ( mess_radius > 1 && x_in_y( pulp_power, 10000 ) ) ?
corpse.get_mtype()->gibType() :
corpse.get_mtype()->bloodType();
g->m.add_splatter_trail( type_blood, pos, dest );
}
float stamina_ratio = static_cast<float>( p->stamina ) / p->get_stamina_max();
p->mod_stat( "stamina", stamina_ratio * -40 );
moves += 100 / std::max( 0.25f, stamina_ratio );
if( one_in( 4 ) ) {
// Smashing may not be butchery, but it involves some zombie anatomy
p->practice( skill_survival, 2, 2 );
}
if( moves >= p->moves ) {
// Enough for this turn;
p->moves -= moves;
return;
}
}
}
// If we reach this, all corpses have been pulped, finish the activity
act->moves_left = 0;
if( num_corpses == 0 ) {
p->add_msg_if_player( m_bad, _( "The corpse moved before you could finish smashing it!" ) );
return;
}
// TODO: Factor in how long it took to do the smashing.
p->add_msg_player_or_npc( ngettext( "The corpse is thoroughly pulped.",
"The corpses are thoroughly pulped.", num_corpses ),
ngettext( "<npcname> finished pulping the corpse.",
"<npcname> finished pulping the corpses.", num_corpses ) );
}
void activity_handlers::reload_finish( player_activity *act, player *p )
{
act->set_to_null();
if( act->targets.size() != 2 || act->index <= 0 ) {
debugmsg( "invalid arguments to ACT_RELOAD" );
return;
}
if( !act->targets[0] ) {
debugmsg( "reload target is null, failed to reload" );
return;
}
if( !act->targets[1] ) {
debugmsg( "ammo target is null, failed to reload" );
return;
}
item &reloadable = *act->targets[ 0 ];
int qty = act->index;
bool is_speedloader = act->targets[ 1 ]->has_flag( "SPEEDLOADER" );
if( !reloadable.reload( *p, std::move( act->targets[ 1 ] ), qty ) ) {
add_msg( m_info, _( "Can't reload the %s." ), reloadable.tname().c_str() );
return;
}
std::string msg = _( "You reload the %s." );
if( reloadable.is_gun() ) {
p->recoil = MAX_RECOIL;
if( reloadable.has_flag( "RELOAD_ONE" ) && !is_speedloader ) {
for( int i = 0; i != qty; ++i ) {
if( reloadable.ammo_type() == ammotype( "bolt" ) ) {
msg = _( "You insert a bolt into the %s." );
} else {
msg = _( "You insert a cartridge into the %s." );
}
}
}
if( reloadable.type->gun->reload_noise_volume > 0 ) {
sfx::play_variant_sound( "reload", reloadable.typeId(), sfx::get_heard_volume( p->pos() ) );
sounds::ambient_sound( p->pos(), reloadable.type->gun->reload_noise_volume,
reloadable.type->gun->reload_noise );
}
} else if( reloadable.is_watertight_container() ) {
msg = _( "You refill the %s." );
}
add_msg( msg.c_str(), reloadable.tname().c_str() );
}
void activity_handlers::start_fire_finish( player_activity *act, player *p )
{
firestarter_actor::resolve_firestarter_use( *p, act->placement );
act->set_to_null();
}
void activity_handlers::start_fire_do_turn( player_activity *act, player *p )
{
item &lens_item = p->i_at( act->position );
const auto usef = lens_item.type->get_use( "firestarter" );
if( usef == nullptr || usef->get_actor_ptr() == nullptr ) {
add_msg( m_bad, _( "You have lost the item you were using to start the fire." ) );
p->cancel_activity();
return;
}
p->mod_moves( -p->moves );
const auto actor = dynamic_cast<const firestarter_actor *>( usef->get_actor_ptr() );
float light = actor->light_mod( p->pos() );
act->moves_left -= light * 100;
if( light < 0.1 ) {
add_msg( m_bad, _( "There is not enough sunlight to start a fire now. You stop trying." ) );
p->cancel_activity();
}
}
void activity_handlers::train_finish( player_activity *act, player *p )
{
const skill_id sk( act->name );
if( sk.is_valid() ) {
const Skill &skill = sk.obj();
std::string skill_name = skill.name();
int new_skill_level = p->get_skill_level( sk ) + 1;
p->set_skill_level( sk, new_skill_level );
add_msg( m_good, _( "You finish training %s to level %d." ), skill_name, new_skill_level );
if( new_skill_level % 4 == 0 ) {
//~ %d is skill level %s is skill name
p->add_memorial_log( pgettext( "memorial_male", "Reached skill level %1$d in %2$s." ),
pgettext( "memorial_female", "Reached skill level %1$d in %2$s." ),
new_skill_level, skill_name );
}
const std::string skill_increase_source = "training";
CallbackArgumentContainer lua_callback_args_info;
lua_callback_args_info.emplace_back( p->getID() );
lua_callback_args_info.emplace_back( skill_increase_source );
lua_callback_args_info.emplace_back( sk.str() );
lua_callback_args_info.emplace_back( new_skill_level );
lua_callback( "on_player_skill_increased", lua_callback_args_info );
lua_callback( "on_skill_increased" ); // Legacy callback
act->set_to_null();
return;
}
const auto &ma_id = matype_id( act->name );
if( ma_id.is_valid() ) {
const auto &mastyle = ma_id.obj();
// Trained martial arts,
add_msg( m_good, _( "You learn %s." ), mastyle.name.c_str() );
//~ %s is martial art
p->add_memorial_log( pgettext( "memorial_male", "Learned %s." ),
pgettext( "memorial_female", "Learned %s." ),
mastyle.name.c_str() );
p->add_martialart( mastyle.id );
} else {
debugmsg( "train_finish without a valid skill or style name" );
}
act->set_to_null();
return;
}
void activity_handlers::vehicle_finish( player_activity *act, player *pl )
{
//Grab this now, in case the vehicle gets shifted
const optional_vpart_position vp = g->m.veh_at( tripoint( act->values[0], act->values[1],
pl->posz() ) );
veh_interact::complete_vehicle();
// complete_vehicle set activity type to NULL if the vehicle
// was completely dismantled, otherwise the vehicle still exist and
// is to be examined again.
if( act->is_null() ) {
return;
}
act->set_to_null();
if( act->values.size() < 7 ) {
dbg( D_ERROR ) << "game:process_activity: invalid ACT_VEHICLE values: "
<< act->values.size();
debugmsg( "process_activity invalid ACT_VEHICLE values:%d",
act->values.size() );
} else {
if( vp ) {
g->refresh_all();
// TODO: Z (and also where the activity is queued)
// Or not, because the vehicle coordinates are dropped anyway
g->exam_vehicle( vp->vehicle(), act->values[ 2 ], act->values[ 3 ] );
return;
} else {
dbg( D_ERROR ) << "game:process_activity: ACT_VEHICLE: vehicle not found";
debugmsg( "process_activity ACT_VEHICLE: vehicle not found" );
}
}
}
void activity_handlers::vibe_do_turn( player_activity *act, player *p )
{
//Using a vibrator takes time (10 minutes), not speed
//Linear increase in morale during action with a small boost at end
//Deduct 1 battery charge for every minute in use, or vibrator is much less effective
act->moves_left -= 100;
item &vibrator_item = p->i_at( act->position );
if( ( p->is_wearing( "rebreather" ) ) || ( p->is_wearing( "rebreather_xl" ) ) ||
( p->is_wearing( "mask_h20survivor" ) ) ) {
act->moves_left = 0;
add_msg( m_bad, _( "You have trouble breathing, and stop." ) );
}
if( calendar::once_every( 1_minutes ) ) {
p->mod_fatigue( 1 );
if( vibrator_item.ammo_remaining() > 0 ) {
vibrator_item.ammo_consume( 1, p->pos() );
p->add_morale( MORALE_FEELING_GOOD, 3, 40 );
if( vibrator_item.ammo_remaining() == 0 ) {
add_msg( m_info, _( "The %s runs out of batteries." ), vibrator_item.tname().c_str() );
}
} else {
p->add_morale( MORALE_FEELING_GOOD, 1, 40 ); //twenty minutes to fill
}
}
if( p->get_fatigue() >= DEAD_TIRED ) { // Dead Tired: different kind of relaxation needed
act->moves_left = 0;
add_msg( m_info, _( "You're too tired to continue." ) );
}
// Vibrator requires that you be able to move around, stretch, etc, so doesn't play
// well with roots. Sorry. :-(
p->pause();
}
void activity_handlers::start_engines_finish( player_activity *act, player *p )
{
act->set_to_null();
// Find the vehicle by looking for a remote vehicle first, then by player relative coordinates
vehicle *veh = g->remoteveh();
if( !veh ) {
const tripoint pos = act->placement + g->u.pos();
veh = veh_pointer_or_null( g->m.veh_at( pos ) );
if( !veh ) {
return;
}
}
int attempted = 0;
int non_muscle_attempted = 0;
int started = 0;
int non_muscle_started = 0;
const bool take_control = act->values[0];
for( size_t e = 0; e < veh->engines.size(); ++e ) {
if( veh->is_engine_on( e ) ) {
attempted++;
if( !veh->is_engine_type( e, "muscle" ) ) {
non_muscle_attempted++;
}
if( veh->start_engine( e ) ) {
started++;
if( !veh->is_engine_type( e, "muscle" ) ) {
non_muscle_started++;
}
}
}
}
//Did any engines start?
veh->engine_on = started;
if( attempted == 0 ) {
add_msg( m_info, _( "The %s doesn't have an engine!" ), veh->name.c_str() );
} else if( non_muscle_attempted > 0 ) {
//Some non-muscle engines tried to start
if( non_muscle_attempted == non_muscle_started ) {
//All of the non-muscle engines started
add_msg( ngettext( "The %s's engine starts up.",
"The %s's engines start up.", non_muscle_started ), veh->name.c_str() );
} else if( non_muscle_started > 0 ) {
//Only some of the non-muscle engines started
add_msg( ngettext( "One of the %s's engines start up.",
"Some of the %s's engines start up.", non_muscle_started ), veh->name.c_str() );
} else {
//All of the non-muscle engines failed
add_msg( m_bad, ngettext( "The %s's engine fails to start.",
"The %s's engines fail to start.", non_muscle_attempted ), veh->name.c_str() );
}
}
if( take_control && !veh->engine_on && !veh->velocity ) {
p->controlling_vehicle = false;
add_msg( _( "You let go of the controls." ) );
}
}
void activity_handlers::oxytorch_do_turn( player_activity *act, player *p )
{
if( act->values[0] <= 0 ) {
return;
}
item &it = p->i_at( act->position );
// act->values[0] is the number of charges yet to be consumed
const long charges_used = std::min( long( act->values[0] ), it.ammo_required() );
it.ammo_consume( charges_used, p->pos() );
act->values[0] -= int( charges_used );
if( calendar::once_every( 2_turns ) ) {
sounds::sound( act->placement, 10, _( "hissssssssss!" ) );
}
}
void activity_handlers::oxytorch_finish( player_activity *act, player *p )
{
act->set_to_null();
const tripoint &pos = act->placement;
const ter_id ter = g->m.ter( pos );
// fast players might still have some charges left to be consumed
p->i_at( act->position ).ammo_consume( act->values[0], p->pos() );
if( g->m.furn( pos ) == f_rack ) {
g->m.furn_set( pos, f_null );
g->m.spawn_item( p->pos(), "steel_chunk", rng( 2, 6 ) );
} else if( ter == t_chainfence || ter == t_chaingate_c || ter == t_chaingate_l ) {
g->m.ter_set( pos, t_dirt );
g->m.spawn_item( pos, "pipe", rng( 1, 4 ) );
g->m.spawn_item( pos, "wire", rng( 4, 16 ) );
} else if( ter == t_chainfence_posts ) {
g->m.ter_set( pos, t_dirt );
g->m.spawn_item( pos, "pipe", rng( 1, 4 ) );
} else if( ter == t_door_metal_locked || ter == t_door_metal_c || ter == t_door_bar_c ||
ter == t_door_bar_locked || ter == t_door_metal_pickable ) {
g->m.ter_set( pos, t_mdoor_frame );
g->m.spawn_item( pos, "steel_plate", rng( 0, 1 ) );
g->m.spawn_item( pos, "steel_chunk", rng( 3, 8 ) );
} else if( ter == t_window_enhanced || ter == t_window_enhanced_noglass ) {
g->m.ter_set( pos, t_window_empty );
g->m.spawn_item( pos, "steel_plate", rng( 0, 1 ) );
g->m.spawn_item( pos, "sheet_metal", rng( 1, 3 ) );
} else if( ter == t_reb_cage ) {
g->m.ter_set( pos, t_pit );
g->m.spawn_item( pos, "spike", rng( 1, 19 ) );
g->m.spawn_item( pos, "scrap", rng( 1, 8 ) );
} else if( ter == t_bars ) {
if( g->m.ter( {pos.x + 1, pos.y, pos.z} ) == t_sewage || g->m.ter( {pos.x, pos.y + 1, pos.z} ) ==
t_sewage ||
g->m.ter( {pos.x - 1, pos.y, pos.z} ) == t_sewage || g->m.ter( {pos.x, pos.y - 1, pos.z} ) ==
t_sewage ) {
g->m.ter_set( pos, t_sewage );
g->m.spawn_item( p->pos(), "pipe", rng( 1, 2 ) );
} else {
g->m.ter_set( pos, t_floor );
g->m.spawn_item( p->pos(), "pipe", rng( 1, 2 ) );
}
} else if( ter == t_window_bars_alarm ) {
g->m.ter_set( pos, t_window_alarm );
g->m.spawn_item( p->pos(), "pipe", rng( 1, 2 ) );
} else if( ter == t_window_bars ) {
g->m.ter_set( pos, t_window_empty );
g->m.spawn_item( p->pos(), "pipe", rng( 1, 2 ) );
}
}
void activity_handlers::cracking_finish( player_activity *act, player *p )
{
p->add_msg_if_player( m_good, _( "The safe opens!" ) );
g->m.furn_set( act->placement, f_safe_o );
act->set_to_null();
}
void activity_handlers::open_gate_finish( player_activity *act, player * )
{
const tripoint pos = act->placement; // Don't use reference and don't inline, because act can change
gates::open_gate( pos );
act->set_to_null();
}
enum repeat_type : int {
REPEAT_ONCE = 0, // Repeat just once
REPEAT_FOREVER, // Repeat for as long as possible
REPEAT_FULL, // Repeat until damage==0
REPEAT_EVENT, // Repeat until something interesting happens
REPEAT_CANCEL, // Stop repeating
REPEAT_INIT // Haven't found repeat value yet.
};
repeat_type repeat_menu( const std::string &title, repeat_type last_selection )
{
uilist rmenu;
rmenu.text = title;
rmenu.addentry( REPEAT_ONCE, true, '1', _( "Repeat once" ) );
rmenu.addentry( REPEAT_FOREVER, true, '2', _( "Repeat as long as you can" ) );
rmenu.addentry( REPEAT_FULL, true, '3', _( "Repeat until fully repaired, but don't reinforce" ) );
rmenu.addentry( REPEAT_EVENT, true, '4', _( "Repeat until success/failure/level up" ) );
rmenu.selected = last_selection;
rmenu.query();
if( rmenu.ret >= REPEAT_ONCE && rmenu.ret <= REPEAT_EVENT ) {
return static_cast<repeat_type>( rmenu.ret );
}
return REPEAT_CANCEL;
}
// This is a part of a hack to provide pseudo items for long repair activity
// Note: similar hack could be used to implement all sorts of vehicle pseudo-items
// and possibly CBM pseudo-items too.
struct weldrig_hack {
vehicle *veh;
int part;
item pseudo;
weldrig_hack()
: veh( nullptr )
, part( -1 )
, pseudo( "welder", calendar::turn )
{ }
bool init( const player_activity &act ) {
if( act.coords.empty() || act.values.size() < 2 ) {
return false;
}
part = act.values[1];
veh = veh_pointer_or_null( g->m.veh_at( act.coords[0] ) );
if( veh == nullptr || veh->parts.size() <= static_cast<size_t>( part ) ) {
part = -1;
return false;
}
part = veh->part_with_feature( part, "WELDRIG", true );
return part >= 0;
}
item &get_item() {
if( veh != nullptr && part >= 0 ) {
pseudo.charges = veh->drain( "battery", 1000 - pseudo.charges );
return pseudo;
}
// null item should be handled just fine
return null_item_reference();
}
void clean_up() {
// Return unused charges
if( veh == nullptr || part < 0 ) {
return;
}
veh->charge_battery( pseudo.charges );
pseudo.charges = 0;
}
};
void activity_handlers::repair_item_finish( player_activity *act, player *p )
{
const std::string iuse_name_string = act->get_str_value( 0, "repair_item" );
repeat_type repeat = static_cast<repeat_type>( act->get_value( 0, REPEAT_INIT ) );
weldrig_hack w_hack;
item_location *ploc = nullptr;
if( !act->targets.empty() ) {
ploc = &act->targets[0];
}
item &main_tool = !w_hack.init( *act ) ?
ploc ?
**ploc : p->i_at( act->index ) : w_hack.get_item();
item *used_tool = main_tool.get_usable_item( iuse_name_string );
if( used_tool == nullptr ) {
debugmsg( "Lost tool used for long repair" );
act->set_to_null();
return;
}
bool event_happened = false;
const auto use_fun = used_tool->get_use( iuse_name_string );
// TODO: De-uglify this block. Something like get_use<iuse_actor_type>() maybe?
const auto *actor = dynamic_cast<const repair_item_actor *>( use_fun->get_actor_ptr() );
if( actor == nullptr ) {
debugmsg( "iuse_actor type descriptor and actual type mismatch" );
act->set_to_null();
return;
}
// TODO: Allow setting this in the actor
// TODO: Don't use charges_to_use: welder has 50 charges per use, soldering iron has 1
if( !used_tool->ammo_sufficient() ) {
p->add_msg_if_player( _( "Your %s ran out of charges" ), used_tool->tname().c_str() );
act->set_to_null();
return;
}
item &fix = p->i_at( act->position );
// The first time through we just find out how many times the player wants to repeat the action.
if( repeat != REPEAT_INIT ) {
// Remember our level: we want to stop retrying on level up
const int old_level = p->get_skill_level( actor->used_skill );
const auto attempt = actor->repair( *p, *used_tool, fix );
if( attempt != repair_item_actor::AS_CANT ) {
if( ploc && ploc->where() == item_location::type::map ) {
used_tool->ammo_consume( used_tool->ammo_required(), ploc->position() );
} else {
p->consume_charges( *used_tool, used_tool->ammo_required() );
}
}
// Print message explaining why we stopped
// But only if we didn't destroy the item (because then it's obvious)
const bool destroyed = attempt == repair_item_actor::AS_DESTROYED;
if( attempt == repair_item_actor::AS_CANT ||
destroyed ||
!actor->can_repair( *p, *used_tool, fix, !destroyed ) ) {
// Can't repeat any more
act->set_to_null();
w_hack.clean_up();
return;
}
event_happened =
attempt == repair_item_actor::AS_FAILURE ||
attempt == repair_item_actor::AS_SUCCESS ||
old_level != p->get_skill_level( actor->used_skill );
} else {
repeat = REPEAT_ONCE;
}
w_hack.clean_up();
const bool need_input =
repeat == REPEAT_ONCE ||
( repeat == REPEAT_EVENT && event_happened ) ||
( repeat == REPEAT_FULL && fix.damage() <= 0 );
if( need_input ) {
g->draw();
const int level = p->get_skill_level( actor->used_skill );
auto action_type = actor->default_action( fix, level );
if( action_type == repair_item_actor::RT_NOTHING ) {
p->add_msg_if_player( _( "You won't learn anything more by doing that." ) );
}
const auto chance = actor->repair_chance( *p, fix, action_type );
if( chance.first <= 0.0f ) {
action_type = repair_item_actor::RT_PRACTICE;
}
const std::string title = string_format(
_( "%s\nSuccess chance: %.1f%%\nDamage chance: %.1f%%" ),
repair_item_actor::action_description( action_type ).c_str(),
100.0f * chance.first, 100.0f * chance.second );
repeat_type answer = repeat_menu( title, repeat );
if( answer == REPEAT_CANCEL ) {
act->set_to_null();
return;
}
if( act->values.empty() ) {
act->values.resize( 1 );
}
act->values[0] = static_cast<int>( answer );
}
// Otherwise keep retrying
act->moves_left = actor->move_cost;
}
void activity_handlers::mend_item_finish( player_activity *act, player *p )
{
act->set_to_null();
if( act->targets.size() != 1 ) {
debugmsg( "invalid arguments to ACT_MEND_ITEM" );
return;
}
item_location &target = act->targets[ 0 ];
auto f = target->faults.find( fault_id( act->name ) );
if( f == target->faults.end() ) {
debugmsg( "item %s does not have fault %s", target->tname().c_str(), act->name.c_str() );
return;
}
auto inv = p->crafting_inventory();
const auto &reqs = f->obj().requirements();
if( !reqs.can_make_with_inventory( inv ) ) {
add_msg( m_info, _( "You are currently unable to mend the %s." ), target->tname().c_str() );
}
for( const auto &e : reqs.get_components() ) {
p->consume_items( e );
}
for( const auto &e : reqs.get_tools() ) {
p->consume_tools( e );
}
p->invalidate_crafting_inventory();
target->faults.erase( *f );
add_msg( m_good, _( "You successfully mended the %s." ), target->tname().c_str() );
}
void activity_handlers::gunmod_add_finish( player_activity *act, player *p )
{
act->set_to_null();
// first unpack all of our arguments
if( act->values.size() != 4 ) {
debugmsg( "Insufficient arguments to ACT_GUNMOD_ADD" );
return;
}
item &gun = p->i_at( act->position );
item &mod = p->i_at( act->values[0] );
int roll = act->values[1]; // chance of success (%)
int risk = act->values[2]; // chance of damage (%)
// any tool charges used during installation
std::string tool = act->name;
int qty = act->values[3];
if( !gun.is_gunmod_compatible( mod ).success() ) {
debugmsg( "Invalid arguments in ACT_GUNMOD_ADD" );
return;
}
if( !tool.empty() && qty > 0 ) {
p->use_charges( tool, qty );
}
if( rng( 0, 100 ) <= roll ) {
add_msg( m_good, _( "You successfully attached the %1$s to your %2$s." ), mod.tname().c_str(),
gun.tname().c_str() );
gun.contents.push_back( p->i_rem( &mod ) );
} else if( rng( 0, 100 ) <= risk ) {
if( gun.inc_damage() ) {
// Remove irremovable mods prior to destroying the gun
for( auto mod : gun.gunmods() ) {
if( mod->is_irremovable() ) {
p->remove_item( *mod );
}
}
add_msg( m_bad, _( "You failed at installing the %s and destroyed your %s!" ), mod.tname().c_str(),
gun.tname().c_str() );
p->i_rem( &gun );
} else {
add_msg( m_bad, _( "You failed at installing the %s and damaged your %s!" ), mod.tname().c_str(),
gun.tname().c_str() );
}
} else {
add_msg( m_info, _( "You failed at installing the %s." ), mod.tname().c_str() );
}
}
void activity_handlers::toolmod_add_finish( player_activity *act, player *p )
{
act->set_to_null();
if( act->targets.size() != 2 || !act->targets[0] || !act->targets[1] ) {
debugmsg( "Incompatible arguments to ACT_TOOLMOD_ADD" );
return;
}
item &tool = *act->targets[0];
item &mod = *act->targets[1];
p->add_msg_if_player( m_good, _( "You successfully attached the %1$s to your %2$s." ),
mod.tname().c_str(),
tool.tname().c_str() );
mod.item_tags.insert( "IRREMOVABLE" );
tool.contents.push_back( mod );
act->targets[1].remove_item();
}
void activity_handlers::clear_rubble_finish( player_activity *act, player *p )
{
const tripoint &pos = act->placement;
p->add_msg_if_player( m_info, _( "You clear up the %s." ),
g->m.furnname( pos ).c_str() );
g->m.furn_set( pos, f_null );
act->set_to_null();
}
void activity_handlers::meditate_finish( player_activity *act, player *p )
{
p->add_msg_if_player( m_good, _( "You pause to engage in spiritual contemplation." ) );
p->add_morale( MORALE_FEELING_GOOD, 5, 10 );
act->set_to_null();
}
void activity_handlers::aim_do_turn( player_activity *act, player * )
{
if( act->index == 0 ) {
g->m.build_map_cache( g->get_levz() );
g->plfire();
}
}
void activity_handlers::pickup_do_turn( player_activity *, player * )
{
activity_on_turn_pickup();
}
void activity_handlers::move_items_do_turn( player_activity *, player * )
{
activity_on_turn_move_items();
}
void activity_handlers::move_loot_do_turn( player_activity *act, player *p )
{
activity_on_turn_move_loot( *act, *p );
}
void activity_handlers::adv_inventory_do_turn( player_activity *, player *p )
{
p->cancel_activity();
advanced_inv();
}
void activity_handlers::armor_layers_do_turn( player_activity *, player *p )
{
p->cancel_activity();
p->sort_armor();
}
void activity_handlers::atm_do_turn( player_activity *, player *p )
{
iexamine::atm( *p, p->pos() );
}
void activity_handlers::cracking_do_turn( player_activity *act, player *p )
{
if( !( p->has_amount( "stethoscope", 1 ) || p->has_bionic( bionic_id( "bio_ears" ) ) ) ) {
// We lost our cracking tool somehow, bail out.
act->set_to_null();
return;
}
}
void activity_handlers::repair_item_do_turn( player_activity *act, player *p )
{
// Moves are decremented based on a combination of speed and good vision (not in the dark, farsighted, etc)
const int effective_moves = p->moves / p->fine_detail_vision_mod();
if( effective_moves <= act->moves_left ) {
act->moves_left -= effective_moves;
p->moves = 0;
} else {
p->moves -= act->moves_left * p->fine_detail_vision_mod();
act->moves_left = 0;
}
}
void activity_handlers::butcher_do_turn( player_activity *, player *p )
{
p->mod_stat( "stamina", -20.0f * p->stamina / p->get_stamina_max() );
}
void activity_handlers::read_finish( player_activity *act, player *p )
{
p->do_read( *act->targets.front().get_item() );
if( !act ) {
p->add_msg_if_player( m_info, _( "You finish reading." ) );
}
}
void activity_handlers::wait_finish( player_activity *act, player *p )
{
p->add_msg_if_player( _( "You finish waiting." ) );
act->set_to_null();
}
void activity_handlers::wait_weather_finish( player_activity *act, player *p )
{
p->add_msg_if_player( _( "You finish waiting." ) );
act->set_to_null();
}
void activity_handlers::wait_npc_finish( player_activity *act, player *p )
{
p->add_msg_if_player( _( "%s finishes with you..." ), act->str_values[0].c_str() );
act->set_to_null();
}
void activity_handlers::try_sleep_do_turn( player_activity *act, player *p )
{
if( !p->has_effect( effect_sleep ) ) {
if( p->can_sleep() ) {
act->set_to_null();
p->fall_asleep();
} else if( one_in( 1000 ) ) {
p->add_msg_if_player( _( "You toss and turn..." ) );
}
}
}
void activity_handlers::try_sleep_finish( player_activity *act, player *p )
{
if( !p->has_effect( effect_sleep ) ) {
p->add_msg_if_player( _( "You try to sleep, but can't..." ) );
}
act->set_to_null();
}
void activity_handlers::craft_do_turn( player_activity *act, player *p )
{
const recipe &rec = recipe_id( act->name ).obj();
float crafting_speed = p->crafting_speed_multiplier( rec, true );
if( crafting_speed <= 0.0f ) {
if( p->lighting_craft_speed_multiplier( rec ) <= 0.0f ) {
p->add_msg_if_player( m_bad, _( "You can no longer see well enough to keep crafting." ) );
} else {
p->add_msg_if_player( m_bad, _( "You are too frustrated to continue and just give up." ) );
}
p->cancel_activity();
return;
}
act->moves_left -= crafting_speed * p->get_moves();
p->set_moves( 0 );
if( calendar::once_every( 1_hours ) && crafting_speed < 0.75f ) {
// @todo Describe the causes of slowdown
p->add_msg_if_player( m_bad, _( "You can't focus and are working slowly." ) );
}
}
void activity_handlers::craft_finish( player_activity *act, player *p )
{
p->complete_craft();
act->set_to_null();
}
void activity_handlers::longcraft_finish( player_activity *act, player *p )
{
int batch_size = act->values.front();
p->complete_craft();
act->set_to_null();
// Workaround for a bug where longcraft can be unset in complete_craft().
if( p->making_would_work( p->lastrecipe, batch_size ) ) {
p->last_craft->execute();
}
}
void activity_handlers::disassemble_finish( player_activity *, player *p )
{
p->complete_disassemble();
}
void activity_handlers::build_finish( player_activity *, player * )
{
complete_construction();
}
void activity_handlers::vibe_finish( player_activity *act, player *p )
{
p->add_msg_if_player( m_good, _( "You feel much better." ) );
p->add_morale( MORALE_FEELING_GOOD, 10, 40 );
act->set_to_null();
}
void activity_handlers::atm_finish( player_activity *act, player * )
{
// ATM sets index to 0 to indicate it's finished.
if( !act->index ) {
act->set_to_null();
}
}
void activity_handlers::aim_finish( player_activity *, player * )
{
// Aim bails itself by resetting itself every turn,
// you only re-enter if it gets set again.
return;
}
void activity_handlers::hacksaw_do_turn( player_activity *act, player *p )
{
if( calendar::once_every( 1_minutes ) ) {
//~ Sound of a metal sawing tool at work!
sounds::sound( act->placement, 15, _( "grnd grnd grnd" ) );
messages_in_process( *act, *p );
}
}
void activity_handlers::hacksaw_finish( player_activity *act, player *p )
{
const tripoint &pos = act->placement;
const ter_id ter = g->m.ter( pos );
if( g->m.furn( pos ) == f_rack ) {
g->m.furn_set( pos, f_null );
g->m.spawn_item( pos, "pipe", rng( 1, 3 ) );
g->m.spawn_item( pos, "steel_chunk" );
} else if( ter == t_chainfence || ter == t_chaingate_c || ter == t_chaingate_l ) {
g->m.ter_set( pos, t_dirt );
g->m.spawn_item( pos, "pipe", 6 );
g->m.spawn_item( pos, "wire", 20 );
} else if( ter == t_chainfence_posts ) {
g->m.ter_set( pos, t_dirt );
g->m.spawn_item( pos, "pipe", 6 );
} else if( ter == t_window_bars_alarm ) {
g->m.ter_set( pos, t_window_alarm );
g->m.spawn_item( pos, "pipe", 6 );
} else if( ter == t_window_bars ) {
g->m.ter_set( pos, t_window_empty );
g->m.spawn_item( pos, "pipe", 6 );
} else if( ter == t_window_enhanced ) {
g->m.ter_set( pos, t_window_reinforced );
g->m.spawn_item( pos, "spike", rng( 1, 4 ) );
} else if( ter == t_window_enhanced_noglass ) {
g->m.ter_set( pos, t_window_reinforced_noglass );
g->m.spawn_item( pos, "spike", rng( 1, 4 ) );
} else if( ter == t_reb_cage ) {
g->m.ter_set( pos, t_pit );
g->m.spawn_item( pos, "spike", 19 );
g->m.spawn_item( pos, "scrap", 8 );
} else if( ter == t_bars ) {
if( g->m.ter( { pos.x + 1, pos.y, pos.z } ) == t_sewage || g->m.ter( { pos.x, pos.y + 1, pos.z } )
== t_sewage ||
g->m.ter( { pos.x - 1, pos.y, pos.z } ) == t_sewage || g->m.ter( { pos.x, pos.y - 1, pos.z } ) ==
t_sewage ) {
g->m.ter_set( pos, t_sewage );
g->m.spawn_item( pos, "pipe", 3 );
} else {
g->m.ter_set( pos, t_floor );
g->m.spawn_item( pos, "pipe", 3 );
}
} else if( ter == t_door_bar_c || ter == t_door_bar_locked ) {
g->m.ter_set( pos, t_mdoor_frame );
g->m.spawn_item( pos, "pipe", 12 );
}
p->mod_hunger( 5 );
p->mod_thirst( 5 );
p->mod_fatigue( 10 );
p->add_msg_if_player( m_good, _( "You finish cutting the metal." ) );
act->set_to_null();
}
void activity_handlers::chop_tree_do_turn( player_activity *act, player *p )
{
if( calendar::once_every( 1_minutes ) ) {
//~ Sound of a wood chopping tool at work!
sounds::sound( act->placement, 15, _( "CHK!" ) );
messages_in_process( *act, *p );
}
}
void activity_handlers::chop_tree_finish( player_activity *act, player *p )
{
const tripoint &pos = act->placement;
tripoint direction;
while( true ) {
if( const cata::optional<tripoint> dir = choose_direction(
_( "Select a direction for the tree to fall in." ) ) ) {
direction = *dir;
break;
}
// try again
}
tripoint to = pos + point( 3 * direction.x + rng( -1, 1 ), 3 * direction.y + rng( -1, 1 ) );
std::vector<tripoint> tree = line_to( pos, to, rng( 1, 8 ) );
for( auto &elem : tree ) {
g->m.destroy( elem );
g->m.ter_set( elem, t_trunk );
}
g->m.ter_set( pos, t_stump );
p->mod_hunger( 5 );
p->mod_thirst( 5 );
p->mod_fatigue( 10 );
p->add_msg_if_player( m_good, _( "You finish chopping down a tree." ) );
act->set_to_null();
}
void activity_handlers::chop_logs_finish( player_activity *act, player *p )
{
const tripoint &pos = act->placement;
if( g->m.ter( pos ) == t_trunk ) {
g->m.spawn_item( pos.x, pos.y, "log", rng( 2, 3 ), 0, calendar::turn );
g->m.spawn_item( pos.x, pos.y, "stick_long", rng( 0, 1 ), 0, calendar::turn );
} else if( g->m.ter( pos ) == t_stump ) {
g->m.spawn_item( pos.x, pos.y, "log", rng( 0, 2 ), 0, calendar::turn );
g->m.spawn_item( pos.x, pos.y, "splinter", rng( 5, 15 ), 0, calendar::turn );
}
g->m.ter_set( pos, t_dirt );
p->mod_hunger( 5 );
p->mod_thirst( 5 );
p->mod_fatigue( 10 );
p->add_msg_if_player( m_good, _( "You finish chopping wood." ) );
act->set_to_null();
}
void activity_handlers::jackhammer_do_turn( player_activity *act, player *p )
{
if( calendar::once_every( 1_minutes ) ) {
//~ Sound of a jackhammer at work!
sounds::sound( act->placement, 15, _( "TATATATATATATAT!" ) );
messages_in_process( *act, *p );
}
}
void activity_handlers::jackhammer_finish( player_activity *act, player *p )
{
const tripoint &pos = act->placement;
g->m.destroy( pos, true );
p->mod_hunger( 5 );
p->mod_thirst( 5 );
p->mod_fatigue( 10 );
p->add_msg_if_player( m_good, _( "You finish drilling." ) );
act->set_to_null();
}
void activity_handlers::dig_do_turn( player_activity *act, player *p )
{
if( calendar::once_every( 1_minutes ) ) {
//~ Sound of a shovel digging a pit at work!
sounds::sound( act->placement, 10, _( "hsh!" ) );
messages_in_process( *act, *p );
}
}
void activity_handlers::dig_finish( player_activity *act, player *p )
{
const tripoint &pos = act->placement;
if( g->m.ter( pos ) == t_pit_shallow ) {
g->m.ter_set( pos, t_pit );
} else {
g->m.ter_set( pos, t_pit_shallow );
}
p->mod_hunger( 5 );
p->mod_thirst( 5 );
p->mod_fatigue( 10 );
p->add_msg_if_player( m_good, _( "You finish digging up %s." ), g->m.ter( pos ).obj().name() );
act->set_to_null();
}
void activity_handlers::fill_pit_do_turn( player_activity *act, player *p )
{
if( calendar::once_every( 1_minutes ) ) {
//~ Sound of a shovel filling a pit or mound at work!
sounds::sound( act->placement, 10, _( "hsh!" ) );
messages_in_process( *act, *p );
}
}
void activity_handlers::fill_pit_finish( player_activity *act, player *p )
{
const tripoint &pos = act->placement;
const ter_id ter = g->m.ter( pos );
const ter_id old_ter = ter;
if( ter == t_pit || ter == t_pit_spiked || ter == t_pit_glass ||
ter == t_pit_corpsed ) {
g->m.ter_set( pos, t_pit_shallow );
} else {
g->m.ter_set( pos, t_dirt );
}
p->mod_hunger( 5 );
p->mod_thirst( 5 );
p->mod_fatigue( 10 );
p->add_msg_if_player( m_good, _( "You finish filling up %s." ), old_ter.obj().name() );
act->set_to_null();
}
void activity_handlers::shaving_finish( player_activity *act, player *p )
{
p->add_msg_if_player( _( "You open up your kit and shave." ) );
p->add_morale( MORALE_SHAVE, 8, 8, 240_minutes, 3_minutes );
act->set_to_null();
}
void activity_handlers::haircut_finish( player_activity *act, player *p )
{
p->add_msg_if_player( _( "You give your hair a trim." ) );
p->add_morale( MORALE_HAIRCUT, 3, 3, 480_minutes, 3_minutes );
act->set_to_null();
}
std::vector<tripoint> get_sorted_tiles_by_distance( const tripoint abspos,
const std::unordered_set<tripoint> &tiles )
{
auto cmp = [abspos]( tripoint a, tripoint b ) {
int da = rl_dist( abspos, a );
int db = rl_dist( abspos, b );
return da < db;
};
std::vector<tripoint> sorted( tiles.begin(), tiles.end() );
std::sort( sorted.begin(), sorted.end(), cmp );
return sorted;
}
template<typename fn>
static void cleanup_tiles( std::unordered_set<tripoint> &tiles, fn &cleanup )
{
auto it = tiles.begin();
while( it != tiles.end() ) {
auto current = it++;
const auto &tile_loc = g->m.getlocal( *current );
if( cleanup( tile_loc ) ) {
tiles.erase( current );
}
}
}
void activity_handlers::till_plot_do_turn( player_activity *, player *p )
{
const auto &mgr = zone_manager::get_manager();
const auto abspos = g->m.getabs( p->pos() );
auto unsorted_tiles = mgr.get_near( zone_type_id( "FARM_PLOT" ), abspos );
// Nuke the current activity, leaving the backlog alone.
p->activity = player_activity();
// cleanup unwanted tiles
auto cleanup = [p]( const tripoint & tile ) {
return !p->sees( tile ) || !g->m.has_flag( "DIGGABLE", tile ) || g->m.has_flag( "PLANT", tile ) ||
g->m.ter( tile ) == t_dirtmound;
};
cleanup_tiles( unsorted_tiles, cleanup );
// sort remaining tiles by distance
const auto &tiles = get_sorted_tiles_by_distance( abspos, unsorted_tiles );
for( auto &tile : tiles ) {
const auto &tile_loc = g->m.getlocal( tile );
auto route = g->m.route( p->pos(), tile_loc, p->get_pathfinding_settings(), p->get_path_avoid() );
if( route.size() > 1 ) {
route.pop_back();
// check for safe mode, we don't want to trigger moving if it is activated
if( g->check_safe_mode_allowed() ) {
p->set_destination( route, player_activity( activity_id( "ACT_TILL_PLOT" ) ) );
}
return;
} else { // we are at destination already
p->add_msg_if_player( _( "You churn up the earth here." ) );
p->mod_moves( -300 );
g->m.ter_set( tile_loc, t_dirtmound );
if( p->moves <= 0 ) {
// Restart activity and break from cycle.
p->assign_activity( activity_id( "ACT_TILL_PLOT" ) );
return;
}
}
}
// If we got here without restarting the activity, it means we're done
add_msg( m_info, _( "You tilled every tile you could." ) );
}
void activity_handlers::plant_plot_do_turn( player_activity *, player *p )
{
const auto &mgr = zone_manager::get_manager();
const auto abspos = g->m.getabs( p->pos() );
auto unsorted_tiles = mgr.get_near( zone_type_id( "FARM_PLOT" ), abspos );
// Nuke the current activity, leaving the backlog alone.
p->activity = player_activity();
std::vector<item *> seed_inv = p->items_with( []( const item & itm ) {
return itm.is_seed();
} );
// get seeds requested by zones on the tile (local coords)
auto get_seeds = [&]( const tripoint & tile ) {
auto seeds = std::vector<std::string>();
const auto &zones = mgr.get_zones( zone_type_id( "FARM_PLOT" ), g->m.getabs( tile ) );
for( const auto &zone : zones ) {
const auto options = dynamic_cast<const plot_options &>( zone.get_options() );
const auto seed = options.get_seed();
if( !seed.empty() && std::find( seeds.begin(), seeds.end(), seed ) == seeds.end() ) {
seeds.emplace_back( seed );
}
}
return seeds;
};
// cleanup unwanted tiles (local coords)
auto cleanup = [&]( const tripoint & tile ) {
if( !p->sees( tile ) || g->m.ter( tile ) != t_dirtmound ) {
return true;
}
const auto seeds = get_seeds( tile );
return std::all_of( seeds.begin(), seeds.end(), [&]( std::string seed ) {
return std::all_of( seed_inv.begin(), seed_inv.end(), [seed]( item * it ) {
return it->typeId() != itype_id( seed );
} );
} );
};
cleanup_tiles( unsorted_tiles, cleanup );
// sort remaining tiles by distance
const auto &tiles = get_sorted_tiles_by_distance( abspos, unsorted_tiles );
for( auto &tile : tiles ) {
const auto &tile_loc = g->m.getlocal( tile );
auto route = g->m.route( p->pos(), tile_loc, p->get_pathfinding_settings(), p->get_path_avoid() );
if( route.size() > 1 ) {
route.pop_back();
// check for safe mode, we don't want to trigger moving if it is activated
if( g->check_safe_mode_allowed() ) {
p->set_destination( route, player_activity( activity_id( "ACT_PLANT_PLOT" ) ) );
}
return;
} else { // we are at destination already
const auto seeds = get_seeds( tile_loc );
std::vector<item *> seed_inv = p->items_with( [seeds]( const item & itm ) {
return itm.is_seed() && std::any_of( seeds.begin(), seeds.end(), [itm]( std::string seed ) {
return itm.typeId() == itype_id( seed );
} );
} );
if( !seed_inv.empty() ) {
auto it = seed_inv.front();
iexamine::plant_seed( *p, tile_loc, it->typeId() );
}
if( p->moves <= 0 ) {
// Restart activity and break from cycle.
p->assign_activity( activity_id( "ACT_PLANT_PLOT" ) );
return;
}
}
}
// If we got here without restarting the activity, it means we're done
add_msg( m_info, _( "You planted all seeds you could." ) );
}
You can’t perform that action at this time.