diff --git a/data/json/npcs/common_chat/TALK_COMMON_ALLY.json b/data/json/npcs/common_chat/TALK_COMMON_ALLY.json index dce4fbecab3a6..60f59ec78e0a9 100644 --- a/data/json/npcs/common_chat/TALK_COMMON_ALLY.json +++ b/data/json/npcs/common_chat/TALK_COMMON_ALLY.json @@ -932,6 +932,12 @@ "condition": { "and": [ { "not": "npc_has_activity" }, { "not": { "npc_has_trait": "HALLUCINATION" } } ] }, "effect": "do_eread" }, + { + "text": "Please study from books you have in order.", + "topic": "TALK_DONE", + "condition": { "and": [ { "not": "npc_has_activity" }, { "not": { "npc_has_trait": "HALLUCINATION" } } ] }, + "effect": "do_read_repeatedly" + }, { "text": "Please start deconstructing any vehicles in a deconstruction zone.", "topic": "TALK_DONE", diff --git a/data/json/player_activities.json b/data/json/player_activities.json index 21b131e7d41e2..6ab3ca975c657 100644 --- a/data/json/player_activities.json +++ b/data/json/player_activities.json @@ -66,6 +66,17 @@ "no_resume": true, "multi_activity": true }, + { + "id": "ACT_MULTIPLE_READ", + "type": "activity_type", + "activity_level": "NO_EXERCISE", + "verb": "reading", + "based_on": "neither", + "suspendable": false, + "no_resume": true, + "multi_activity": true, + "auto_needs": true + }, { "id": "ACT_MOP", "type": "activity_type", diff --git a/src/activity_handlers.cpp b/src/activity_handlers.cpp index 92d017da4c40e..ca28eeff68831 100644 --- a/src/activity_handlers.cpp +++ b/src/activity_handlers.cpp @@ -137,6 +137,7 @@ static const activity_id ACT_MULTIPLE_FARM( "ACT_MULTIPLE_FARM" ); static const activity_id ACT_MULTIPLE_FISH( "ACT_MULTIPLE_FISH" ); static const activity_id ACT_MULTIPLE_MINE( "ACT_MULTIPLE_MINE" ); static const activity_id ACT_MULTIPLE_MOP( "ACT_MULTIPLE_MOP" ); +static const activity_id ACT_MULTIPLE_READ( "ACT_MULTIPLE_READ" ); static const activity_id ACT_OPERATION( "ACT_OPERATION" ); static const activity_id ACT_PICKAXE( "ACT_PICKAXE" ); static const activity_id ACT_PLANT_SEED( "ACT_PLANT_SEED" ); @@ -279,6 +280,7 @@ activity_handlers::do_turn_functions = { { ACT_STUDY_SPELL, study_spell_do_turn }, { ACT_WAIT_STAMINA, wait_stamina_do_turn }, { ACT_MULTIPLE_DIS, multiple_dis_do_turn }, + { ACT_MULTIPLE_READ, multiple_read_do_turn }, }; const std::map< activity_id, std::function > @@ -3275,11 +3277,17 @@ void activity_handlers::multiple_butcher_do_turn( player_activity *act, Characte { generic_multi_activity_handler( *act, *you ); } + void activity_handlers::multiple_dis_do_turn( player_activity *act, Character *you ) { generic_multi_activity_handler( *act, *you ); } +void activity_handlers::multiple_read_do_turn( player_activity *act, Character *you ) +{ + generic_multi_activity_handler( *act, *you ); +} + void activity_handlers::vehicle_deconstruction_do_turn( player_activity *act, Character *you ) { generic_multi_activity_handler( *act, *you ); diff --git a/src/activity_handlers.h b/src/activity_handlers.h index 05fa9be9f8542..9e7c0253a8533 100644 --- a/src/activity_handlers.h +++ b/src/activity_handlers.h @@ -67,6 +67,7 @@ enum class do_activity_reason : int { NEEDS_PLANTING, // For farming - tile can be planted NEEDS_TILLING, // For farming - tile can be tilled BLOCKING_TILE, // Something has made it's way onto the tile, so the activity cannot proceed + NEEDS_BOOK_TO_LEARN, // There is book to learn NEEDS_CHOPPING, // There is wood there to be chopped NEEDS_TREE_CHOPPING, // There is a tree there that needs to be chopped NEEDS_BIG_BUTCHERING, // There is at least one corpse there to butcher, and it's a big one @@ -180,6 +181,7 @@ void multiple_construction_do_turn( player_activity *act, Character *you ); void multiple_dis_do_turn( player_activity *act, Character *you ); void multiple_farm_do_turn( player_activity *act, Character *you ); void multiple_fish_do_turn( player_activity *act, Character *you ); +void multiple_read_do_turn( player_activity *act, Character *you ); void multiple_mine_do_turn( player_activity *act, Character *you ); void multiple_mop_do_turn( player_activity *act, Character *you ); void operation_do_turn( player_activity *act, Character *you ); diff --git a/src/activity_item_handling.cpp b/src/activity_item_handling.cpp index c4241821e5381..3e2a684a37d89 100644 --- a/src/activity_item_handling.cpp +++ b/src/activity_item_handling.cpp @@ -85,6 +85,7 @@ static const activity_id ACT_MULTIPLE_FARM( "ACT_MULTIPLE_FARM" ); static const activity_id ACT_MULTIPLE_FISH( "ACT_MULTIPLE_FISH" ); static const activity_id ACT_MULTIPLE_MINE( "ACT_MULTIPLE_MINE" ); static const activity_id ACT_MULTIPLE_MOP( "ACT_MULTIPLE_MOP" ); +static const activity_id ACT_MULTIPLE_READ( "ACT_MULTIPLE_READ" ); static const activity_id ACT_PICKAXE( "ACT_PICKAXE" ); static const activity_id ACT_TIDY_UP( "ACT_TIDY_UP" ); static const activity_id ACT_VEHICLE( "ACT_VEHICLE" ); @@ -1219,6 +1220,19 @@ static activity_reason_info can_do_activity_there( const activity_id &act, Chara } return activity_reason_info::fail( do_activity_reason::NO_ZONE ); } + if( act == ACT_MULTIPLE_READ ) { + const item_filter filter = [ &you ]( const item & i ) { + // Check well lit after + read_condition_result condition = you.check_read_condition( i ); + return condition == read_condition_result::SUCCESS || + condition == read_condition_result::TOO_DARK; + }; + if( !you.items_with( filter ).empty() ) { + return activity_reason_info::ok( do_activity_reason::NEEDS_BOOK_TO_LEARN ); + } + // TODO: find books from zone? + return activity_reason_info::fail( do_activity_reason::ALREADY_DONE ); + } if( act == ACT_MULTIPLE_CHOP_PLANKS ) { //are there even any logs there? for( item &i : here.i_at( src_loc ) ) { @@ -2503,6 +2517,11 @@ static std::unordered_set generic_multi_activity_locations( } } } + } else if( act_id == ACT_MULTIPLE_READ ) { + // anywhere well lit + for( const tripoint_bub_ms &elem : here.points_in_radius( localpos, ACTIVITY_SEARCH_DISTANCE ) ) { + src_set.insert( here.getglobal( elem ) ); + } } else if( act_id != ACT_FETCH_REQUIRED ) { zone_type_id zone_type = get_zone_for_act( tripoint_bub_ms{}, mgr, act_id, _fac_id( you ) ); src_set = mgr.get_near( zone_type, abspos, ACTIVITY_SEARCH_DISTANCE, nullptr, _fac_id( you ) ); @@ -2876,6 +2895,20 @@ static bool generic_multi_activity_do( you.backlog.emplace_front( act_id ); return false; } + } else if( reason == do_activity_reason::NEEDS_BOOK_TO_LEARN ) { + const item_filter filter = [ &you ]( const item & i ) { + read_condition_result condition = you.check_read_condition( i ); + return condition == read_condition_result::SUCCESS; + }; + std::vector books = you.items_with( filter ); + if( !books.empty() && books[0] ) { + const time_duration time_taken = you.time_to_read( *books[0], you ); + item_location book = item_location( you, books[0] ); + item_location ereader; + you.backlog.emplace_front( act_id ); + you.assign_activity( read_activity_actor( time_taken, book, ereader, true ) ); + return false; + } } else if( reason == do_activity_reason::CAN_DO_CONSTRUCTION ) { if( here.partial_con_at( src_loc ) ) { you.backlog.emplace_front( act_id ); diff --git a/src/character.cpp b/src/character.cpp index d8b465cbccd84..6d73d8425569e 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -11043,6 +11043,49 @@ bool Character::beyond_final_warning( const faction_id &id ) return false; } +read_condition_result Character::check_read_condition( const item &book ) const +{ + read_condition_result result = read_condition_result::SUCCESS; + if( !book.is_book() ) { + result |= read_condition_result::NOT_BOOK; + } else { + const optional_vpart_position vp = get_map().veh_at( pos() ); + if( vp && vp->vehicle().player_in_control( *this ) ) { + result |= read_condition_result::DRIVING; + } + + if( !fun_to_read( book ) && !has_morale_to_read() && has_identified( book.typeId() ) ) { + result |= read_condition_result::MORALE_LOW; + } + + const book_mastery mastery = get_book_mastery( book ); + if( mastery == book_mastery::CANT_UNDERSTAND ) { + result |= read_condition_result::CANT_UNDERSTAND; + } + if( mastery == book_mastery::MASTERED ) { + result |= read_condition_result::MASTERED; + } + + const bool book_requires_intelligence = book.type->book->intel > 0; + if( book_requires_intelligence && has_trait( trait_ILLITERATE ) ) { + result |= read_condition_result::ILLITERATE; + } + if( has_flag( json_flag_HYPEROPIC ) && + !worn_with_flag( STATIC( flag_id( "FIX_FARSIGHT" ) ) ) && + !has_effect( effect_contacts ) && + !has_flag( STATIC( json_character_flag( "ENHANCED_VISION" ) ) ) ) { + result |= read_condition_result::NEED_GLASSES; + } + if( fine_detail_vision_mod() > 4 ) { + result |= read_condition_result::TOO_DARK; + } + if( is_blind() ) { + result |= read_condition_result::BLIND; + } + } + return result; +} + const Character *Character::get_book_reader( const item &book, std::vector &reasons ) const { @@ -11059,22 +11102,21 @@ const Character *Character::get_book_reader( const item &book, const cata::value_ptr &type = book.type->book; const skill_id &book_skill = type->skill; const int book_skill_requirement = type->req; - const bool book_requires_intelligence = type->intel > 0; // Check for conditions that immediately disqualify the player from reading: - const optional_vpart_position vp = get_map().veh_at( pos() ); - if( vp && vp->vehicle().player_in_control( *this ) ) { + read_condition_result condition = check_read_condition( book ); + if( condition & read_condition_result::DRIVING ) { reasons.emplace_back( _( "It's a bad idea to read while driving!" ) ); return nullptr; } - if( !fun_to_read( book ) && !has_morale_to_read() && has_identified( book.typeId() ) ) { + if( condition & read_condition_result::MORALE_LOW ) { // Low morale still permits skimming reasons.emplace_back( is_avatar() ? _( "What's the point of studying? (Your morale is too low!)" ) : string_format( _( "What's the point of studying? (%s)'s morale is too low!)" ), disp_name() ) ); return nullptr; } - if( get_book_mastery( book ) == book_mastery::CANT_UNDERSTAND ) { + if( condition & read_condition_result::CANT_UNDERSTAND ) { reasons.push_back( is_avatar() ? string_format( _( "%s %d needed to understand. You have %d" ), book_skill->name(), book_skill_requirement, get_knowledge_level( book_skill ) ) : string_format( _( "%s %d needed to understand. %s has %d" ), book_skill->name(), @@ -11083,16 +11125,13 @@ const Character *Character::get_book_reader( const item &book, } // Check for conditions that disqualify us only if no NPCs can read to us - if( book_requires_intelligence && has_trait( trait_ILLITERATE ) ) { + if( condition & read_condition_result::ILLITERATE ) { reasons.emplace_back( is_avatar() ? _( "You're illiterate!" ) : string_format( _( "%s is illiterate!" ), disp_name() ) ); - } else if( has_flag( json_flag_HYPEROPIC ) && - !worn_with_flag( STATIC( flag_id( "FIX_FARSIGHT" ) ) ) && - !has_effect( effect_contacts ) && - !has_flag( STATIC( json_character_flag( "ENHANCED_VISION" ) ) ) ) { + } else if( condition & read_condition_result::NEED_GLASSES ) { reasons.emplace_back( is_avatar() ? _( "Your eyes won't focus without reading glasses." ) : string_format( _( "%s's eyes won't focus without reading glasses." ), disp_name() ) ); - } else if( fine_detail_vision_mod() > 4 ) { + } else if( condition & read_condition_result::TOO_DARK ) { // Too dark to read only applies if the player can read to himself reasons.emplace_back( _( "It's too dark to read!" ) ); return nullptr; @@ -11117,30 +11156,28 @@ const Character *Character::get_book_reader( const item &book, for( const npc *elem : candidates ) { // Check for disqualifying factors: - if( book_requires_intelligence && elem->has_trait( trait_ILLITERATE ) ) { + condition = elem->check_read_condition( book ); + if( condition & read_condition_result::ILLITERATE ) { reasons.push_back( string_format( _( "%s is illiterate!" ), elem->disp_name() ) ); - } else if( elem->get_book_mastery( book ) == book_mastery::CANT_UNDERSTAND ) { + } else if( condition & read_condition_result::CANT_UNDERSTAND ) { reasons.push_back( string_format( _( "%s %d needed to understand. %s has %d" ), book_skill->name(), book_skill_requirement, elem->disp_name(), elem->get_knowledge_level( book_skill ) ) ); - } else if( elem->has_flag( json_flag_HYPEROPIC ) && - !elem->worn_with_flag( STATIC( flag_id( "FIX_FARSIGHT" ) ) ) && - !elem->has_effect( effect_contacts ) ) { + } else if( condition & read_condition_result::NEED_GLASSES ) { reasons.push_back( string_format( _( "%s needs reading glasses!" ), elem->disp_name() ) ); - } else if( std::min( fine_detail_vision_mod(), elem->fine_detail_vision_mod() ) > 4 ) { + } else if( condition & read_condition_result::TOO_DARK ) { reasons.push_back( string_format( _( "It's too dark for %s to read!" ), elem->disp_name() ) ); } else if( !elem->sees( *this ) ) { reasons.push_back( string_format( _( "%s could read that to you, but they can't see you." ), elem->disp_name() ) ); - } else if( !elem->fun_to_read( book ) && !elem->has_morale_to_read() && - has_identified( book.typeId() ) ) { + } else if( condition & read_condition_result::MORALE_LOW ) { // Low morale still permits skimming reasons.push_back( string_format( _( "%s morale is too low!" ), elem->disp_name( true ) ) ); - } else if( elem->is_blind() ) { + } else if( condition & read_condition_result::BLIND ) { reasons.push_back( string_format( _( "%s is blind." ), elem->disp_name() ) ); } else { time_duration proj_time = time_to_read( book, *elem ); diff --git a/src/character.h b/src/character.h index b3f80573545f9..92fa31971d2d6 100644 --- a/src/character.h +++ b/src/character.h @@ -387,6 +387,24 @@ enum class book_mastery { MASTERED // can no longer increase skill by reading }; +enum class read_condition_result { + SUCCESS = 0, + NOT_BOOK = 1 << 0, + CANT_UNDERSTAND = 1 << 1, + MASTERED = 1 << 2, + DRIVING = 1 << 3, + ILLITERATE = 1 << 4, + NEED_GLASSES = 1 << 5, + TOO_DARK = 1 << 6, + MORALE_LOW = 1 << 7, + BLIND = 1 << 8 +}; + +template<> +struct enum_traits { + static constexpr bool is_flag_enum = true; +}; + /** @relates ret_val */ template<> struct ret_val::default_success : public @@ -2324,6 +2342,13 @@ class Character : public Creature, public visitable time_duration time_to_read( const item &book, const Character &reader, const Character *learner = nullptr ) const; + /** + * Helper function for get_book_reader + * + * @param book The book being read + */ + read_condition_result check_read_condition( const item &book ) const; + /** Calls Creature::normalize() * nulls out the player's weapon * Should only be called through player::normalize(), not on it's own! diff --git a/src/npc.cpp b/src/npc.cpp index 83454254213b4..5279e81a4f96f 100644 --- a/src/npc.cpp +++ b/src/npc.cpp @@ -87,7 +87,6 @@ #include "name.h" static const efftype_id effect_bouldering( "bouldering" ); -static const efftype_id effect_contacts( "contacts" ); static const efftype_id effect_controlled( "controlled" ); static const efftype_id effect_drunk( "drunk" ); static const efftype_id effect_high( "high" ); @@ -115,8 +114,6 @@ static const item_group_id Item_spawn_data_survivor_bashing( "survivor_bashing" static const item_group_id Item_spawn_data_survivor_cutting( "survivor_cutting" ); static const item_group_id Item_spawn_data_survivor_stabbing( "survivor_stabbing" ); -static const json_character_flag json_flag_HYPEROPIC( "HYPEROPIC" ); - static const mfaction_str_id monfaction_bee( "bee" ); static const mfaction_str_id monfaction_human( "human" ); static const mfaction_str_id monfaction_player( "player" ); @@ -151,7 +148,6 @@ static const trait_id trait_BEE( "BEE" ); static const trait_id trait_CANNIBAL( "CANNIBAL" ); static const trait_id trait_DEBUG_MIND_CONTROL( "DEBUG_MIND_CONTROL" ); static const trait_id trait_HALLUCINATION( "HALLUCINATION" ); -static const trait_id trait_ILLITERATE( "ILLITERATE" ); static const trait_id trait_MUTE( "MUTE" ); static const trait_id trait_NO_BASH( "NO_BASH" ); static const trait_id trait_PROF_DICEMASTER( "PROF_DICEMASTER" ); @@ -1340,35 +1336,31 @@ void npc::starting_weapon( const npc_class_id &type ) bool npc::can_read( const item &book, std::vector &fail_reasons ) { - if( !book.is_book() ) { - fail_reasons.push_back( string_format( _( "This %s is not good reading material." ), - book.tname() ) ); - return false; - } Character *pl = dynamic_cast( this ); if( !pl ) { return false; } - const auto &type = book.type->book; - const skill_id &skill = type->skill; - const int skill_level = pl->get_knowledge_level( skill ); - if( skill && skill_level < type->req ) { + read_condition_result condition = check_read_condition( book ); + if( condition & read_condition_result::NOT_BOOK ) { + fail_reasons.push_back( string_format( _( "This %s is not good reading material." ), + book.tname() ) ); + return false; + } + if( condition & read_condition_result::CANT_UNDERSTAND ) { fail_reasons.push_back( string_format( _( "I'm not smart enough to read this book." ) ) ); return false; } - if( !skill || skill_level >= type->level ) { + if( condition & read_condition_result::MASTERED ) { fail_reasons.push_back( string_format( _( "I won't learn anything from this book." ) ) ); return false; } // Check for conditions that disqualify us - if( type->intel > 0 && has_trait( trait_ILLITERATE ) ) { + if( condition & read_condition_result::ILLITERATE ) { fail_reasons.emplace_back( _( "I can't read!" ) ); - } else if( has_flag( json_flag_HYPEROPIC ) && !worn_with_flag( flag_FIX_FARSIGHT ) && - !has_effect( effect_contacts ) && - !has_flag( STATIC( json_character_flag( "ENHANCED_VISION" ) ) ) ) { + } else if( condition & read_condition_result::NEED_GLASSES ) { fail_reasons.emplace_back( _( "I can't read without my glasses." ) ); - } else if( fine_detail_vision_mod() > 4 ) { + } else if( condition & read_condition_result::TOO_DARK ) { // Too dark to read only applies if the player can read to himself fail_reasons.emplace_back( _( "It's too dark to read!" ) ); return false; diff --git a/src/npctalk.cpp b/src/npctalk.cpp index f2da574a10bfc..a44f5323ab0b7 100644 --- a/src/npctalk.cpp +++ b/src/npctalk.cpp @@ -343,6 +343,7 @@ enum npc_chat_menu { NPC_CHAT_ACTIVITIES_FISHING, NPC_CHAT_ACTIVITIES_MINING, NPC_CHAT_ACTIVITIES_MOPPING, + NPC_CHAT_ACTIVITIES_READ_REPEATEDLY, NPC_CHAT_ACTIVITIES_VEHICLE_DECONSTRUCTION, NPC_CHAT_ACTIVITIES_VEHICLE_REPAIR, NPC_CHAT_ACTIVITIES_UNASSIGN @@ -591,6 +592,8 @@ static int npc_activities_menu() nmenu.addentry( NPC_CHAT_ACTIVITIES_FISHING, true, 'F', _( "Fishing in a zone" ) ); nmenu.addentry( NPC_CHAT_ACTIVITIES_MINING, true, 'M', _( "Mining out tiles" ) ); nmenu.addentry( NPC_CHAT_ACTIVITIES_MOPPING, true, 'm', _( "Mopping up stains" ) ); + nmenu.addentry( NPC_CHAT_ACTIVITIES_READ_REPEATEDLY, true, 'R', + _( "Study from books you have in order" ) ); nmenu.addentry( NPC_CHAT_ACTIVITIES_VEHICLE_DECONSTRUCTION, true, 'v', _( "Deconstructing vehicles" ) ); nmenu.addentry( NPC_CHAT_ACTIVITIES_VEHICLE_REPAIR, true, 'V', _( "Repairing vehicles" ) ); @@ -1038,6 +1041,10 @@ void game::chat() talk_function::do_fishing( *selected_npc ); break; } + case NPC_CHAT_ACTIVITIES_READ_REPEATEDLY: { + talk_function::do_read_repeatedly( *selected_npc ); + break; + } case NPC_CHAT_ACTIVITIES_MINING: { talk_function::do_mining( *selected_npc ); break; @@ -5407,6 +5414,7 @@ void talk_effect_t::parse_string_effect( const std::string &effect_id, const Jso WRAP( do_mopping ), WRAP( do_read ), WRAP( do_eread ), + WRAP( do_read_repeatedly ), WRAP( do_butcher ), WRAP( do_farming ), WRAP( assign_guard ), diff --git a/src/npctalk.h b/src/npctalk.h index 7a3063d808810..b73faf8e1a9a5 100644 --- a/src/npctalk.h +++ b/src/npctalk.h @@ -54,6 +54,7 @@ void do_mining( npc & ); void do_mopping( npc & ); void do_read( npc & ); void do_eread( npc & ); +void do_read_repeatedly( npc & ); void do_chop_plank( npc & ); void do_vehicle_deconstruct( npc & ); void do_vehicle_repair( npc & ); diff --git a/src/npctalk_funcs.cpp b/src/npctalk_funcs.cpp index 38f88980c0a4d..c6f63188353e8 100644 --- a/src/npctalk_funcs.cpp +++ b/src/npctalk_funcs.cpp @@ -71,6 +71,7 @@ static const activity_id ACT_MULTIPLE_FARM( "ACT_MULTIPLE_FARM" ); static const activity_id ACT_MULTIPLE_FISH( "ACT_MULTIPLE_FISH" ); static const activity_id ACT_MULTIPLE_MINE( "ACT_MULTIPLE_MINE" ); static const activity_id ACT_MULTIPLE_MOP( "ACT_MULTIPLE_MOP" ); +static const activity_id ACT_MULTIPLE_READ( "ACT_MULTIPLE_READ" ); static const activity_id ACT_SOCIALIZE( "ACT_SOCIALIZE" ); static const activity_id ACT_TRAIN( "ACT_TRAIN" ); static const activity_id ACT_TRAIN_TEACHER( "ACT_TRAIN_TEACHER" ); @@ -267,6 +268,11 @@ void talk_function::do_eread( npc &p ) p.do_npc_read( true ); } +void talk_function::do_read_repeatedly( npc &p ) +{ + p.assign_activity( ACT_MULTIPLE_READ ); +} + void talk_function::dismount( npc &p ) { p.npc_dismount();