1 #include "mission_companion.h"
2
3 #include <algorithm>
4 #include <cstdlib>
5 #include <functional>
6 #include <list>
7 #include <map>
8 #include <memory>
9 #include <set>
10 #include <tuple>
11 #include <unordered_map>
12 #include <utility>
13 #include <vector>
14
15 #include "basecamp.h"
16 #include "bodypart.h"
17 #include "calendar.h"
18 #include "cata_assert.h"
19 #include "catacharset.h"
20 #include "character.h"
21 #include "colony.h"
22 #include "color.h"
23 #include "coordinates.h"
24 #include "creature.h"
25 #include "cursesdef.h"
26 #include "debug.h"
27 #include "enums.h"
28 #include "faction.h"
29 #include "faction_camp.h"
30 #include "game.h"
31 #include "game_constants.h"
32 #include "input.h"
33 #include "inventory.h"
34 #include "item.h"
35 #include "item_group.h"
36 #include "item_stack.h"
37 #include "itype.h"
38 #include "line.h"
39 #include "map.h"
40 #include "map_iterator.h"
41 #include "mapdata.h"
42 #include "memory_fast.h"
43 #include "messages.h"
44 #include "monster.h"
45 #include "mtype.h"
46 #include "npc.h"
47 #include "optional.h"
48 #include "output.h"
49 #include "overmap.h"
50 #include "overmapbuffer.h"
51 #include "point.h"
52 #include "rng.h"
53 #include "skill.h"
54 #include "string_formatter.h"
55 #include "translations.h"
56 #include "ui.h"
57 #include "ui_manager.h"
58 #include "value_ptr.h"
59 #include "weather.h"
60 #include "weighted_list.h"
61
62 class character_id;
63
64 static const efftype_id effect_riding( "riding" );
65
66 static const itype_id itype_fungal_seeds( "fungal_seeds" );
67 static const itype_id itype_marloss_seed( "marloss_seed" );
68
69 static const skill_id skill_bashing( "bashing" );
70 static const skill_id skill_cutting( "cutting" );
71 static const skill_id skill_dodge( "dodge" );
72 static const skill_id skill_fabrication( "fabrication" );
73 static const skill_id skill_gun( "gun" );
74 static const skill_id skill_melee( "melee" );
75 static const skill_id skill_stabbing( "stabbing" );
76 static const skill_id skill_survival( "survival" );
77 static const skill_id skill_unarmed( "unarmed" );
78
79 static const trait_id trait_DEBUG_HS( "DEBUG_HS" );
80 static const trait_id trait_NPC_MISSION_LEV_1( "NPC_MISSION_LEV_1" );
81 static const trait_id trait_NPC_CONSTRUCTION_LEV_1( "NPC_CONSTRUCTION_LEV_1" );
82 static const trait_id trait_NPC_CONSTRUCTION_LEV_2( "NPC_CONSTRUCTION_LEV_2" );
83
84 struct comp_rank {
85 int industry;
86 int combat;
87 int survival;
88 };
89
mission_data()90 mission_data::mission_data()
91 {
92 for( int tab_num = base_camps::TAB_MAIN; tab_num != base_camps::TAB_NW + 3; tab_num++ ) {
93 std::vector<mission_entry> k;
94 entries.push_back( k );
95 }
96 }
97
98 namespace talk_function
99 {
100 void scavenger_patrol( mission_data &mission_key, npc &p );
101 void scavenger_raid( mission_data &mission_key, npc &p );
102 void commune_menial( mission_data &mission_key, npc &p );
103 void commune_carpentry( mission_data &mission_key, npc &p );
104 void commune_farmfield( mission_data &mission_key, npc &p );
105 void commune_forage( mission_data &mission_key, npc &p );
106 void commune_refuge_caravan( mission_data &mission_key, npc &p );
107 bool handle_outpost_mission( const mission_entry &cur_key, npc &p );
108 } // namespace talk_function
109
companion_mission(npc & p)110 void talk_function::companion_mission( npc &p )
111 {
112 mission_data mission_key;
113
114 std::string role_id = p.companion_mission_role_id;
115 const tripoint_abs_omt omt_pos = p.global_omt_location();
116 std::string title = _( "Outpost Missions" );
117 if( role_id == "SCAVENGER" ) {
118 title = _( "Junk Shop Missions" );
119 scavenger_patrol( mission_key, p );
120 if( p.has_trait( trait_NPC_MISSION_LEV_1 ) ) {
121 scavenger_raid( mission_key, p );
122 }
123 } else if( role_id == "COMMUNE CROPS" ) {
124 title = _( "Agricultural Missions" );
125 commune_farmfield( mission_key, p );
126 commune_forage( mission_key, p );
127 commune_refuge_caravan( mission_key, p );
128 } else if( role_id == "FOREMAN" ) {
129 title = _( "Construction Missions" );
130 commune_menial( mission_key, p );
131 if( p.has_trait( trait_NPC_MISSION_LEV_1 ) ) {
132 commune_carpentry( mission_key, p );
133 }
134 } else if( role_id == "REFUGEE MERCHANT" ) {
135 title = _( "Free Merchant Missions" );
136 commune_refuge_caravan( mission_key, p );
137 } else {
138 return;
139 }
140 if( display_and_choose_opts( mission_key, omt_pos, role_id, title ) ) {
141 handle_outpost_mission( mission_key.cur_key, p );
142 }
143 }
144
scavenger_patrol(mission_data & mission_key,npc & p)145 void talk_function::scavenger_patrol( mission_data &mission_key, npc &p )
146 {
147 std::string entry = _( "Profit: $25-$500\nDanger: Low\nTime: 10 hour missions\n\n"
148 "Assigning one of your allies to patrol the surrounding wilderness "
149 "and isolated buildings presents the opportunity to build survival "
150 "skills while engaging in relatively safe combat against isolated "
151 "creatures." );
152 mission_key.add( "Assign Scavenging Patrol", _( "Assign Scavenging Patrol" ), entry );
153 std::vector<npc_ptr> npc_list = companion_list( p, "_scavenging_patrol" );
154 if( !npc_list.empty() ) {
155 entry = _( "Profit: $25-$500\nDanger: Low\nTime: 10 hour missions\n\nPatrol Roster:\n" );
156 for( auto &elem : npc_list ) {
157 entry = entry + " " + elem->name + " [" + std::to_string( to_hours<int>( calendar::turn -
158 elem->companion_mission_time ) ) + _( " hours]\n" );
159 }
160 entry = entry + _( "\n\nDo you wish to bring your allies back into your party?" );
161 mission_key.add( "Retrieve Scavenging Patrol", _( "Retrieve Scavenging Patrol" ), entry );
162 }
163 }
164
scavenger_raid(mission_data & mission_key,npc & p)165 void talk_function::scavenger_raid( mission_data &mission_key, npc &p )
166 {
167 std::string entry = _( "Profit: $200-$1000\nDanger: Medium\nTime: 10 hour missions\n\n"
168 "Scavenging raids target formerly populated areas to loot as many "
169 "valuable items as possible before being surrounded by the undead. "
170 "Combat is to be expected and assistance from the rest of the party "
171 "can't be guaranteed. The rewards are greater and there is a chance "
172 "of the companion bringing back items." );
173 mission_key.add( "Assign Scavenging Raid", _( "Assign Scavenging Raid" ), entry );
174 std::vector<npc_ptr> npc_list = companion_list( p, "_scavenging_raid" );
175 if( !npc_list.empty() ) {
176 entry = _( "Profit: $200-$1000\nDanger: Medium\nTime: 10 hour missions\n\n"
177 "Raid Roster:\n" );
178 for( auto &elem : npc_list ) {
179 entry = entry + " " + elem->name + " [" + std::to_string( to_hours<int>( calendar::turn -
180 elem->companion_mission_time ) ) + _( " hours]\n" );
181 }
182 entry = entry + _( "\n\nDo you wish to bring your allies back into your party?" );
183 mission_key.add( "Retrieve Scavenging Raid", _( "Retrieve Scavenging Raid" ), entry );
184 }
185 }
186
commune_menial(mission_data & mission_key,npc & p)187 void talk_function::commune_menial( mission_data &mission_key, npc &p )
188 {
189 mission_key.add( "Assign Ally to Menial Labor", _( "Assign Ally to Menial Labor" ) );
190 std::vector<npc_ptr> npc_list = companion_list( p, "_labor" );
191 if( !npc_list.empty() ) {
192 std::string entry = _( "Profit: $8/hour\nDanger: Minimal\nTime: 1 hour minimum\n\n"
193 "Assigning one of your allies to menial labor is a safe way to teach "
194 "them basic skills and build reputation with the outpost. Don't expect "
195 "much of a reward though.\n\nLabor Roster:\n" );
196 for( auto &elem : npc_list ) {
197 entry = entry + " " + elem->name + " [" + std::to_string( to_hours<int>( calendar::turn -
198 elem->companion_mission_time ) ) + _( " hours]\n" );
199 }
200 entry = entry + _( "\n\nDo you wish to bring your allies back into your party?" );
201 mission_key.add( "Recover Ally from Menial Labor", _( "Recover Ally from Menial Labor" ),
202 entry );
203 }
204 }
205
commune_carpentry(mission_data & mission_key,npc & p)206 void talk_function::commune_carpentry( mission_data &mission_key, npc &p )
207 {
208 std::string entry = _( "Profit: $12/hour\nDanger: Minimal\nTime: 1 hour minimum\n\n"
209 "Carpentry work requires more skill than menial labor while offering "
210 "modestly improved pay. It is unlikely that your companions will face "
211 "combat but there are hazards working on makeshift buildings." );
212 mission_key.add( "Assign Ally to Carpentry Work", _( "Assign Ally to Carpentry Work" ), entry );
213 std::vector<npc_ptr> npc_list = companion_list( p, "_carpenter" );
214 if( !npc_list.empty() ) {
215 entry = _( "Profit: $12/hour\nDanger: Minimal\nTime: 1 hour minimum\n\nLabor Roster:\n" );
216 for( auto &elem : npc_list ) {
217 entry = entry + " " + elem->name + " [" + std::to_string( to_hours<int>( calendar::turn -
218 elem->companion_mission_time ) ) + _( " hours]\n" );
219 }
220 entry = entry + _( "\n\nDo you wish to bring your allies back into your party?" );
221 mission_key.add( "Recover Ally from Carpentry Work",
222 _( "Recover Ally from Carpentry Work" ), entry );
223 }
224 }
225
commune_farmfield(mission_data & mission_key,npc & p)226 void talk_function::commune_farmfield( mission_data &mission_key, npc &p )
227 {
228 if( !p.has_trait( trait_NPC_CONSTRUCTION_LEV_1 ) ) {
229 std::string entry = _( "Cost: $1000\n\n\n"
230 " .........\n" // NOLINT(cata-text-style)
231 " .........\n" // NOLINT(cata-text-style)
232 " .........\n" // NOLINT(cata-text-style)
233 " .........\n" // NOLINT(cata-text-style)
234 " .........\n" // NOLINT(cata-text-style)
235 " .........\n" // NOLINT(cata-text-style)
236 " ..#....**\n" // NOLINT(cata-text-style)
237 " ..#Ov..**\n" // NOLINT(cata-text-style)
238 " ...O|....\n\n" // NOLINT(cata-text-style)
239 "We're willing to let you purchase a field at a substantial "
240 "discount to use for your own agricultural enterprises. We'll "
241 "plow it for you so you know exactly what is yours… after you "
242 "have a field you can hire workers to plant or harvest crops for "
243 "you. If the crop is something we have a demand for, we'll be "
244 "willing to liquidate it." );
245 mission_key.add( "Purchase East Field", _( "Purchase East Field" ), entry );
246 }
247 if( p.has_trait( trait_NPC_CONSTRUCTION_LEV_1 ) && !p.has_trait( trait_NPC_CONSTRUCTION_LEV_2 ) ) {
248 std::string entry = _( "Cost: $5500\n\n"
249 "\n ........." // NOLINT(cata-text-style)
250 "\n ........." // NOLINT(cata-text-style)
251 "\n ........." // NOLINT(cata-text-style)
252 "\n ........." // NOLINT(cata-text-style)
253 "\n ........." // NOLINT(cata-text-style)
254 "\n ........." // NOLINT(cata-text-style)
255 "\n ..#....**" // NOLINT(cata-text-style)
256 "\n ..#Ov..**" // NOLINT(cata-text-style)
257 "\n ...O|....\n\n" // NOLINT(cata-text-style)
258 "Protecting your field with a sturdy picket fence will keep most "
259 "wildlife from nibbling your crops apart. You can expect yields to "
260 "increase." );
261 mission_key.add( "Upgrade East Field I", _( "Upgrade East Field I" ), entry );
262 }
263
264 if( p.has_trait( trait_NPC_CONSTRUCTION_LEV_1 ) ) {
265 std::string entry = _( "Cost: $3.00/plot\n\n"
266 "\n ........." // NOLINT(cata-text-style)
267 "\n ........." // NOLINT(cata-text-style)
268 "\n ........." // NOLINT(cata-text-style)
269 "\n ........." // NOLINT(cata-text-style)
270 "\n ........." // NOLINT(cata-text-style)
271 "\n ........." // NOLINT(cata-text-style)
272 "\n ..#....**" // NOLINT(cata-text-style)
273 "\n ..#Ov..**" // NOLINT(cata-text-style)
274 "\n ...O|....\n\n" // NOLINT(cata-text-style)
275 "We'll plant the field with your choice of crop if you are willing "
276 "to finance it. When the crop is ready to harvest you can have us "
277 "liquidate it or harvest it for you." );
278 mission_key.add( "Plant East Field", _( "Plant East Field" ), entry );
279 entry = _( "Cost: $2.00/plot\n\n"
280 "\n ........." // NOLINT(cata-text-style)
281 "\n ........." // NOLINT(cata-text-style)
282 "\n ........." // NOLINT(cata-text-style)
283 "\n ........." // NOLINT(cata-text-style)
284 "\n ........." // NOLINT(cata-text-style)
285 "\n ........." // NOLINT(cata-text-style)
286 "\n ..#....**" // NOLINT(cata-text-style)
287 "\n ..#Ov..**" // NOLINT(cata-text-style)
288 "\n ...O|....\n\n" // NOLINT(cata-text-style)
289 "You can either have us liquidate the crop and give you the cash or pay us to "
290 "harvest it for you." );
291 mission_key.add( "Harvest East Field", _( "Harvest East Field" ), entry );
292 }
293 }
294
commune_forage(mission_data & mission_key,npc & p)295 void talk_function::commune_forage( mission_data &mission_key, npc &p )
296 {
297 std::string entry = _( "Profit: $10/hour\nDanger: Low\nTime: 4 hour minimum\n\n"
298 "Foraging for food involves dispatching a companion to search the "
299 "surrounding wilderness for wild edibles. Combat will be avoided but "
300 "encounters with wild animals are to be expected. The low pay is "
301 "supplemented with the odd item as a reward for particularly large "
302 "hauls." );
303 mission_key.add( "Assign Ally to Forage for Food", _( "Assign Ally to Forage for Food" ),
304 entry );
305 std::vector<npc_ptr> npc_list = companion_list( p, "_forage" );
306 if( !npc_list.empty() ) {
307 entry = _( "Profit: $10/hour\nDanger: Low\nTime: 4 hour minimum\n\nLabor Roster:\n" );
308 for( auto &elem : npc_list ) {
309 entry = entry + " " + elem->name + " [" + std::to_string( to_hours<int>( calendar::turn -
310 elem->companion_mission_time ) ) + _( " hours]\n" );
311 }
312 entry = entry + _( "\n\nDo you wish to bring your allies back into your party?" );
313 mission_key.add( "Recover Ally from Foraging", _( "Recover Ally from Foraging" ), entry );
314 }
315 }
316
commune_refuge_caravan(mission_data & mission_key,npc & p)317 void talk_function::commune_refuge_caravan( mission_data &mission_key, npc &p )
318 {
319 std::string entry = _( "Profit: $18/hour\nDanger: High\nTime: UNKNOWN\n\n"
320 "Adding companions to the caravan team increases the likelihood of "
321 "success. By nature, caravans are extremely tempting targets for "
322 "raiders or hostile groups so only a strong party is recommended. The "
323 "rewards are significant for those participating but are even more "
324 "important for the factions that profit.\n\n"
325 "The commune is sending food to the Free Merchants in the Refugee "
326 "Center as part of a tax and in exchange for skilled labor." );
327 mission_key.add( "Caravan Commune-Refugee Center", _( "Caravan Commune-Refugee Center" ),
328 entry );
329 std::vector<npc_ptr> npc_list = companion_list( p, "_commune_refugee_caravan" );
330 std::vector<npc_ptr> npc_list_aux;
331 if( !npc_list.empty() ) {
332 entry = _( "Profit: $18/hour\nDanger: High\nTime: UNKNOWN\n\n"
333 "\nRoster:\n" );
334 for( auto &elem : npc_list ) {
335 if( elem->companion_mission_time == calendar::before_time_starts ) {
336 entry = entry + " " + elem->name + _( " [READY]\n" );
337 npc_list_aux.push_back( elem );
338 } else if( calendar::turn >= elem->companion_mission_time ) {
339 entry = entry + " " + elem->name + _( " [COMPLETE]\n" );
340 } else {
341 entry = entry + " " + elem->name + " [" + std::to_string( std::abs( to_hours<int>
342 ( calendar::turn - elem->companion_mission_time ) ) ) + _( " Hours]\n" );
343 }
344 }
345 if( !npc_list_aux.empty() ) {
346 std::string entry_aux = _( "Profit: $18/hour\nDanger: High\nTime: UNKNOWN\n\n"
347 "\nRoster:\n" );
348 const std::string entry_suffix = _( " [READY]\n" );
349 for( auto &elem : npc_list_aux ) {
350 if( elem->companion_mission_time == calendar::before_time_starts ) {
351 entry_aux = entry_aux + " " + elem->name + entry_suffix;
352 }
353 }
354 entry_aux = entry_aux + _( "\n\n"
355 "The caravan will contain two or three additional members "
356 "from the commune, are you ready to depart?" );
357 mission_key.add( "Begin Commune-Refugee Center Run",
358 _( "Begin Commune-Refugee Center Run" ), entry );
359 }
360 entry = entry + _( "\n\nDo you wish to bring your allies back into your party?" );
361 mission_key.add( "Recover Commune-Refugee Center", _( "Recover Commune-Refugee Center" ),
362 entry );
363 }
364 }
365
display_and_choose_opts(mission_data & mission_key,const tripoint_abs_omt & omt_pos,const std::string & role_id,const std::string & title)366 bool talk_function::display_and_choose_opts(
367 mission_data &mission_key, const tripoint_abs_omt &omt_pos, const std::string &role_id,
368 const std::string &title )
369 {
370 if( mission_key.entries.empty() ) {
371 popup( _( "There are no missions at this colony. Press Spacebar…" ) );
372 return false;
373 }
374
375 int TITLE_TAB_HEIGHT = 0;
376 if( role_id == "FACTION_CAMP" ) {
377 TITLE_TAB_HEIGHT = 1;
378 }
379
380 base_camps::tab_mode tab_mode = base_camps::TAB_MAIN;
381
382 size_t sel = 0;
383
384 // The following are for managing the right pane scrollbar.
385 size_t info_offset = 0;
386 nc_color col = c_white;
387 std::vector<std::string> name_text;
388 std::vector<std::string> mission_text;
389
390 input_context ctxt( "FACTIONS" );
391 ctxt.register_action( "UP", to_translation( "Move cursor up" ) );
392 ctxt.register_action( "DOWN", to_translation( "Move cursor down" ) );
393 ctxt.register_action( "NEXT_TAB" );
394 ctxt.register_action( "PREV_TAB" );
395 ctxt.register_action( "PAGE_UP" );
396 ctxt.register_action( "PAGE_DOWN" );
397 ctxt.register_action( "CONFIRM" );
398 ctxt.register_action( "QUIT" );
399 ctxt.register_action( "HELP_KEYBINDINGS" );
400 std::vector<mission_entry> cur_key_list;
401
402 auto reset_cur_key_list = [&]() {
403 cur_key_list = mission_key.entries[0];
404 for( const auto &k : mission_key.entries[1] ) {
405 bool has = false;
406 for( const auto &keys : cur_key_list ) {
407 if( k.id == keys.id ) {
408 has = true;
409 break;
410 }
411 }
412 if( !has ) {
413 cur_key_list.push_back( k );
414 }
415 }
416 };
417
418 reset_cur_key_list();
419
420 if( cur_key_list.empty() ) {
421 popup( _( "There are no missions at this colony. Press Spacebar…" ) );
422 return false;
423 }
424
425 size_t part_y = 0;
426 size_t part_x = 0;
427 size_t maxy = 0;
428 size_t maxx = 0;
429 size_t info_height = 0;
430 size_t info_width = 0;
431
432 catacurses::window w_list;
433 catacurses::window w_tabs;
434 catacurses::window w_info;
435
436 ui_adaptor ui;
437 ui.on_screen_resize( [&]( ui_adaptor & ui ) {
438 part_y = TERMY > FULL_SCREEN_HEIGHT ? ( TERMY - FULL_SCREEN_HEIGHT ) / 4 : 0;
439 part_x = TERMX > FULL_SCREEN_WIDTH ? ( TERMX - FULL_SCREEN_WIDTH ) / 4 : 0;
440 maxy = part_y ? TERMY - 2 * part_y : FULL_SCREEN_HEIGHT;
441 maxx = part_x ? TERMX - 2 * part_x : FULL_SCREEN_WIDTH;
442 info_height = maxy - 3;
443 info_width = maxx - 1 - MAX_FAC_NAME_SIZE;
444
445 w_list = catacurses::newwin( maxy, maxx,
446 point( part_x, part_y + TITLE_TAB_HEIGHT ) );
447 w_info = catacurses::newwin( info_height, info_width,
448 point( part_x + MAX_FAC_NAME_SIZE, part_y + TITLE_TAB_HEIGHT + 1 ) );
449
450 if( role_id == "FACTION_CAMP" ) {
451 w_tabs = catacurses::newwin( TITLE_TAB_HEIGHT, maxx, point( part_x, part_y ) );
452 ui.position( point( part_x, part_y ), point( maxx, maxy + TITLE_TAB_HEIGHT ) );
453 } else {
454 ui.position( point( part_x, part_y + TITLE_TAB_HEIGHT ), point( maxx, maxy ) );
455 }
456 } );
457 ui.mark_resize();
458
459 ui.on_redraw( [&]( const ui_adaptor & ) {
460 werase( w_list );
461 draw_border( w_list );
462 // NOLINTNEXTLINE(cata-use-named-point-constants)
463 mvwprintz( w_list, point( 1, 1 ), c_white, name_mission_tabs( omt_pos, role_id, title,
464 tab_mode ) );
465
466 std::vector<std::vector<std::string>> folded_names;
467 size_t folded_names_lines = 0;
468 for( const auto &cur_key_entry : cur_key_list ) {
469 std::vector<std::string> f_name = foldstring( cur_key_entry.name_display, MAX_FAC_NAME_SIZE - 5,
470 ' ' );
471 folded_names_lines += f_name.size();
472 folded_names.emplace_back( f_name );
473 }
474
475 int name_offset = 0;
476 size_t sel_pos = 0;
477 // Translate from entry index to line index
478 for( size_t i = 0; i < sel; i++ ) {
479 sel_pos += folded_names[i].size();
480 }
481
482 calcStartPos( name_offset, sel_pos, info_height, folded_names_lines );
483
484 int name_index = 0;
485 // Are we so far down the list that we bump into the end?
486 bool last_section = folded_names_lines < info_height ||
487 folded_names_lines - info_height <= static_cast<size_t>( name_offset );
488
489 // Translate back from desired line index to the corresponding entry, making sure to round up
490 // near the end to ensure the last line gets included.
491 if( name_offset > 0 ) {
492 for( size_t i = 0; i < cur_key_list.size(); i++ ) {
493 name_offset -= folded_names[i].size();
494 if( name_offset <= 0 ) {
495 if( name_offset == 0 || last_section ) {
496 name_index++;
497 }
498 break;
499 }
500 name_index++;
501 }
502 }
503
504 size_t list_line = 2;
505 for( size_t current = name_index; list_line < info_height + 2 &&
506 current < cur_key_list.size(); current++ ) {
507 nc_color col = ( current == sel ? h_white : c_white );
508 //highlight important missions
509 for( const auto &k : mission_key.entries[0] ) {
510 if( cur_key_list[current].id == k.id ) {
511 col = ( current == sel ? h_white : c_yellow );
512 break;
513 }
514 }
515 //dull uncraftable items
516 for( const auto &k : mission_key.entries[10] ) {
517 if( cur_key_list[current].id == k.id ) {
518 col = ( current == sel ? h_white : c_dark_gray );
519 break;
520 }
521 }
522 std::vector<std::string> &name_text = folded_names[current];
523 if( list_line + name_text.size() > info_height + 2 ) {
524 break; // Skip last entry that would spill over into the bar.
525 }
526 for( size_t name_line = 0; name_line < name_text.size(); name_line++ ) {
527 print_colored_text( w_list, point( name_line ? 5 : 1, list_line ),
528 col, col, name_text[name_line] );
529 list_line += 1;
530 }
531 }
532
533 if( folded_names_lines > info_height ) {
534 scrollbar()
535 .offset_x( 0 )
536 .offset_y( 1 )
537 .content_size( folded_names_lines )
538 .viewport_pos( sel_pos )
539 .viewport_size( info_height )
540 .apply( w_list );
541 }
542 wnoutrefresh( w_list );
543 werase( w_info );
544
545 // Fold mission text, store it for scrolling
546 mission_text = foldstring( mission_key.cur_key.text, info_width - 2, ' ' );
547 if( info_height >= mission_text.size() ) {
548 info_offset = 0;
549 } else if( info_offset + info_height > mission_text.size() ) {
550 info_offset = mission_text.size() - info_height;
551 }
552 if( mission_text.size() > info_height ) {
553 scrollbar()
554 .offset_x( info_width - 1 )
555 .offset_y( 0 )
556 .content_size( mission_text.size() )
557 .viewport_pos( info_offset )
558 .viewport_size( info_height )
559 .apply( w_info );
560 }
561 const size_t end_line = std::min( info_height, mission_text.size() - info_offset );
562
563 // Display the current subset of the mission text.
564 for( size_t start_line = 0; start_line < end_line; start_line++ ) {
565 print_colored_text( w_info, point( 0, start_line ), col, col,
566 mission_text[start_line + info_offset] );
567 }
568
569 wnoutrefresh( w_info );
570
571 if( role_id == "FACTION_CAMP" ) {
572 werase( w_tabs );
573 draw_camp_tabs( w_tabs, tab_mode, mission_key.entries );
574 wnoutrefresh( w_tabs );
575 }
576 } );
577
578 while( true ) {
579 mission_key.cur_key = cur_key_list[sel];
580 ui_manager::redraw();
581 const std::string action = ctxt.handle_input();
582 if( action == "DOWN" ) {
583 if( sel == cur_key_list.size() - 1 ) {
584 sel = 0; // Wrap around
585 } else {
586 sel++;
587 }
588 info_offset = 0;
589 } else if( action == "UP" ) {
590 if( sel == 0 ) {
591 sel = cur_key_list.size() - 1; // Wrap around
592 } else {
593 sel--;
594 }
595 info_offset = 0;
596 } else if( action == "PAGE_UP" ) {
597 if( info_offset > 0 ) {
598 info_offset--;
599 }
600 } else if( action == "PAGE_DOWN" ) {
601 info_offset++;
602 } else if( action == "NEXT_TAB" && role_id == "FACTION_CAMP" ) {
603 sel = 0;
604 info_offset = 0;
605
606 do {
607 if( tab_mode == base_camps::TAB_NW ) {
608 tab_mode = base_camps::TAB_MAIN;
609 reset_cur_key_list();
610 } else {
611 tab_mode = static_cast<base_camps::tab_mode>( tab_mode + 1 );
612 cur_key_list = mission_key.entries[tab_mode + 1];
613 }
614 } while( cur_key_list.empty() );
615 } else if( action == "PREV_TAB" && role_id == "FACTION_CAMP" ) {
616 sel = 0;
617 info_offset = 0;
618
619 do {
620 if( tab_mode == base_camps::TAB_MAIN ) {
621 tab_mode = base_camps::TAB_NW;
622 } else {
623 tab_mode = static_cast<base_camps::tab_mode>( tab_mode - 1 );
624 }
625
626 if( tab_mode == base_camps::TAB_MAIN ) {
627 reset_cur_key_list();
628 } else {
629 cur_key_list = mission_key.entries[tab_mode + 1];
630 }
631 } while( cur_key_list.empty() );
632 } else if( action == "QUIT" ) {
633 mission_entry dud;
634 dud.id = "NONE";
635 dud.name_display = "NONE";
636 mission_key.cur_key = dud;
637 break;
638 } else if( action == "CONFIRM" ) {
639 if( mission_key.cur_key.possible ) {
640 break;
641 } else {
642 continue;
643 }
644 }
645 }
646 return true;
647 }
648
handle_outpost_mission(const mission_entry & cur_key,npc & p)649 bool talk_function::handle_outpost_mission( const mission_entry &cur_key, npc &p )
650 {
651 if( cur_key.id == "Caravan Commune-Refugee Center" ) {
652 individual_mission( p, _( "joins the caravan team…" ), "_commune_refugee_caravan", true );
653 }
654 if( cur_key.id == "Begin Commune-Refugee Center Run" ) {
655 caravan_depart( p, "evac_center_18", "_commune_refugee_caravan" );
656 }
657 if( cur_key.id == "Recover Commune-Refugee Center" ) {
658 caravan_return( p, "evac_center_18", "_commune_refugee_caravan" );
659 }
660 if( cur_key.id == "Purchase East Field" ) {
661 field_build_1( p );
662 }
663 if( cur_key.id == "Upgrade East Field I" ) {
664 field_build_2( p );
665 }
666 if( cur_key.id == "Plant East Field" ) {
667 field_plant( p, "ranch_camp_63" );
668 }
669 if( cur_key.id == "Harvest East Field" ) {
670 field_harvest( p, "ranch_camp_63" );
671 }
672 if( cur_key.id == "Assign Scavenging Patrol" ) {
673 individual_mission( p, _( "departs on the scavenging patrol…" ), "_scavenging_patrol" );
674 }
675 if( cur_key.id == "Retrieve Scavenging Patrol" ) {
676 scavenging_patrol_return( p );
677 }
678 if( cur_key.id == "Assign Scavenging Raid" ) {
679 individual_mission( p, _( "departs on the scavenging raid…" ), "_scavenging_raid" );
680 }
681 if( cur_key.id == "Retrieve Scavenging Raid" ) {
682 scavenging_raid_return( p );
683 }
684 if( cur_key.id == "Assign Ally to Menial Labor" ) {
685 individual_mission( p, _( "departs to work as a laborer…" ), "_labor" );
686 }
687 if( cur_key.id == "Recover Ally from Menial Labor" ) {
688 labor_return( p );
689 }
690
691 if( cur_key.id == "Assign Ally to Carpentry Work" ) {
692 individual_mission( p, _( "departs to work as a carpenter…" ), "_carpenter" );
693 }
694 if( cur_key.id == "Recover Ally from Carpentry Work" ) {
695 carpenter_return( p );
696 }
697 if( cur_key.id == "Assign Ally to Forage for Food" ) {
698 individual_mission( p, _( "departs to forage for food…" ), "_forage" );
699 }
700 if( cur_key.id == "Recover Ally from Foraging" ) {
701 forage_return( p );
702 }
703
704 return true;
705 }
706
individual_mission(npc & p,const std::string & desc,const std::string & miss_id,bool group,const std::vector<item * > & equipment,const std::map<skill_id,int> & required_skills)707 npc_ptr talk_function::individual_mission( npc &p, const std::string &desc,
708 const std::string &miss_id, bool group, const std::vector<item *> &equipment,
709 const std::map<skill_id, int> &required_skills )
710 {
711 const tripoint_abs_omt omt_pos = p.global_omt_location();
712 return individual_mission( omt_pos, p.companion_mission_role_id, desc, miss_id, group,
713 equipment, required_skills );
714 }
individual_mission(const tripoint_abs_omt & omt_pos,const std::string & role_id,const std::string & desc,const std::string & miss_id,bool group,const std::vector<item * > & equipment,const std::map<skill_id,int> & required_skills)715 npc_ptr talk_function::individual_mission( const tripoint_abs_omt &omt_pos,
716 const std::string &role_id, const std::string &desc,
717 const std::string &miss_id, bool group, const std::vector<item *> &equipment,
718 const std::map<skill_id, int> &required_skills )
719 {
720 npc_ptr comp = companion_choose( required_skills );
721 if( comp == nullptr ) {
722 return comp;
723 }
724 // make sure, for now, that NPCs dismount their horse before going on a mission.
725 if( comp->has_effect( effect_riding ) ) {
726 comp->npc_dismount();
727 }
728 Character &player_character = get_player_character();
729 //Ensure we have someone to give equipment to before we lose it
730 for( item *i : equipment ) {
731 comp->companion_mission_inv.add_item( *i );
732 //comp->i_add(*i);
733 if( item::count_by_charges( i->typeId() ) ) {
734 player_character.use_charges( i->typeId(), i->charges );
735 } else {
736 player_character.use_amount( i->typeId(), 1 );
737 }
738 }
739 if( comp->in_vehicle ) {
740 get_map().unboard_vehicle( comp->pos() );
741 }
742 popup( "%s %s", comp->name, desc );
743 comp->set_companion_mission( omt_pos, role_id, miss_id );
744 if( group ) {
745 comp->companion_mission_time = calendar::before_time_starts;
746 } else {
747 comp->companion_mission_time = calendar::turn;
748 }
749 g->reload_npcs();
750 g->validate_npc_followers();
751 cata_assert( !comp->is_active() );
752 return comp;
753 }
754
caravan_depart(npc & p,const std::string & dest,const std::string & id)755 void talk_function::caravan_depart( npc &p, const std::string &dest, const std::string &id )
756 {
757 std::vector<npc_ptr> npc_list = companion_list( p, id );
758 int distance = caravan_dist( dest );
759 time_duration time = 20_minutes + distance * 10_minutes;
760 popup( _( "The caravan departs with an estimated total travel time of %d hours…" ),
761 to_hours<int>( time ) );
762
763 for( auto &elem : npc_list ) {
764 if( elem->companion_mission_time == calendar::before_time_starts ) {
765 //Adds a 10% error in estimated travel time
766 elem->companion_mission_time = calendar::turn + time * rng_float( -1.1, 1.1 );
767 }
768 }
769 }
770
771 //Could be expanded to actually path to the site, just returns the distance
caravan_dist(const std::string & dest)772 int talk_function::caravan_dist( const std::string &dest )
773 {
774 Character &player_character = get_player_character();
775 const tripoint_abs_omt site =
776 overmap_buffer.find_closest( player_character.global_omt_location(), dest, 0, false );
777 int distance = rl_dist( player_character.global_omt_location(), site );
778 return distance;
779 }
780
caravan_return(npc & p,const std::string & dest,const std::string & id)781 void talk_function::caravan_return( npc &p, const std::string &dest, const std::string &id )
782 {
783 npc_ptr comp = companion_choose_return( p, id, calendar::turn );
784 if( comp == nullptr ) {
785 return;
786 }
787 if( comp->companion_mission_time == calendar::before_time_starts ) {
788 popup( _( "%s returns to your party." ), comp->name );
789 companion_return( *comp );
790 return;
791 }
792 //So we have chosen to return an individual or party who went on the mission
793 //Everyone who was on the mission will have the same companion_mission_time
794 //and will simulate the mission and return together
795 std::vector<npc_ptr> caravan_party;
796 std::vector<npc_ptr> bandit_party;
797 std::vector<npc_ptr> npc_list = companion_list( p, id );
798 const int rand_caravan_size = rng( 1, 3 );
799 caravan_party.reserve( npc_list.size() + rand_caravan_size );
800 for( int i = 0; i < rand_caravan_size; i++ ) {
801 caravan_party.push_back( temp_npc( string_id<npc_template>( "commune_guard" ) ) );
802 }
803 for( auto &elem : npc_list ) {
804 if( elem->companion_mission_time == comp->companion_mission_time ) {
805 caravan_party.push_back( elem );
806 }
807 }
808
809 int distance = caravan_dist( dest );
810 int time = 200 + distance * 100;
811 int experience = rng( 10, time / 300 );
812
813 const int rand_bandit_size = rng( 1, 3 );
814 bandit_party.reserve( rand_bandit_size * 2 );
815 for( int i = 0; i < rand_bandit_size * 2; i++ ) {
816 bandit_party.push_back( temp_npc( string_id<npc_template>( "bandit" ) ) );
817 bandit_party.push_back( temp_npc( string_id<npc_template>( "thug" ) ) );
818 }
819
820 if( one_in( 3 ) ) {
821 if( one_in( 2 ) ) {
822 popup( _( "A bandit party approaches the caravan in the open!" ) );
823 force_on_force( caravan_party, "caravan", bandit_party, "band", 1 );
824 } else if( one_in( 3 ) ) {
825 popup( _( "A bandit party attacks the caravan while it it's camped!" ) );
826 force_on_force( caravan_party, "caravan", bandit_party, "band", 2 );
827 } else {
828 popup( _( "The caravan walks into a bandit ambush!" ) );
829 force_on_force( caravan_party, "caravan", bandit_party, "band", -1 );
830 }
831 }
832
833 int money = 0;
834 for( const auto &elem : caravan_party ) {
835 //Scrub temporary party members and the dead
836 if( elem->get_part_hp_cur( bodypart_id( "torso" ) ) == 0 && elem->has_companion_mission() ) {
837 overmap_buffer.remove_npc( comp->getID() );
838 money += ( time / 600 ) * 9;
839 } else if( elem->has_companion_mission() ) {
840 money += ( time / 600 ) * 18;
841 companion_skill_trainer( *elem, "combat", experience * 10_minutes, 10 );
842 companion_return( *elem );
843 }
844 }
845
846 if( money != 0 ) {
847 get_player_character().cash += ( 100 * money );
848 popup( _( "The caravan party has returned. Your share of the profits are $%d!" ), money );
849 } else {
850 popup( _( "The caravan was a disaster and your companions never made it home…" ) );
851 }
852 }
853
854 //A random NPC on one team attacks a random monster on the opposite
attack_random(const std::vector<npc_ptr> & attacker,const std::vector<monster * > & group)855 void talk_function::attack_random( const std::vector<npc_ptr> &attacker,
856 const std::vector< monster * > &group )
857 {
858 if( attacker.empty() || group.empty() ) {
859 return;
860 }
861 const auto att = random_entry( attacker );
862 monster *def = random_entry( group );
863 att->melee_attack_abstract( *def, false, matec_id( "" ) );
864 if( def->get_hp() <= 0 ) {
865 popup( _( "%s is wasted by %s!" ), def->type->nname(), att->name );
866 }
867 }
868
869 //A random monster on one side attacks a random NPC on the other
attack_random(const std::vector<monster * > & group,const std::vector<npc_ptr> & defender)870 void talk_function::attack_random( const std::vector< monster * > &group,
871 const std::vector<npc_ptr> &defender )
872 {
873 if( defender.empty() || group.empty() ) {
874 return;
875 }
876 monster *att = random_entry( group );
877 const auto def = random_entry( defender );
878 att->melee_attack( *def );
879 //monster mon;
880 if( def->get_part_hp_cur( bodypart_id( "torso" ) ) <= 0 || def->is_dead() ) {
881 popup( _( "%s is wasted by %s!" ), def->name, att->type->nname() );
882 }
883 }
884
885 //A random NPC on one team attacks a random NPC on the opposite
attack_random(const std::vector<npc_ptr> & attacker,const std::vector<npc_ptr> & defender)886 void talk_function::attack_random( const std::vector<npc_ptr> &attacker,
887 const std::vector<npc_ptr> &defender )
888 {
889 if( attacker.empty() || defender.empty() ) {
890 return;
891 }
892 const auto att = random_entry( attacker );
893 const auto def = random_entry( defender );
894 const skill_id best = att->best_skill();
895 int best_score = 1;
896 if( best ) {
897 best_score = att->get_skill_level( best );
898 }
899 ///\EFFECT_DODGE_NPC increases avoidance of random attacks
900 if( rng( -1, best_score ) >= rng( 0, def->get_skill_level( skill_dodge ) ) ) {
901 def->set_part_hp_cur( bodypart_id( "torso" ), 0 );
902 popup( _( "%s is wasted by %s!" ), def->name, att->name );
903 } else {
904 popup( _( "%s dodges %s's attack!" ), def->name, att->name );
905 }
906 }
907
908 //Used to determine when to retreat, might want to add in a random factor so that engagements aren't
909 //drawn out wars of attrition
combat_score(const std::vector<npc_ptr> & group)910 int talk_function::combat_score( const std::vector<npc_ptr> &group )
911 {
912 int score = 0;
913 for( const auto &elem : group ) {
914 if( elem->get_part_hp_cur( bodypart_id( "torso" ) ) != 0 ) {
915 const skill_id best = elem->best_skill();
916 if( best ) {
917 score += elem->get_skill_level( best );
918 } else {
919 score += 1;
920 }
921 }
922 }
923 return score;
924 }
925
combat_score(const std::vector<monster * > & group)926 int talk_function::combat_score( const std::vector< monster * > &group )
927 {
928 int score = 0;
929 for( const auto &elem : group ) {
930 if( elem->get_hp() > 0 ) {
931 score += elem->type->difficulty;
932 }
933 }
934 return score;
935 }
936
temp_npc(const string_id<npc_template> & type)937 npc_ptr talk_function::temp_npc( const string_id<npc_template> &type )
938 {
939 npc_ptr temp = make_shared_fast<npc>();
940 temp->normalize();
941 temp->load_npc_template( type );
942 return temp;
943 }
944
945 //The field is designed as more of a convenience than a profit opportunity.
field_build_1(npc & p)946 void talk_function::field_build_1( npc &p )
947 {
948 Character &player_character = get_player_character();
949 if( player_character.cash < 100000 ) {
950 popup( _( "I'm sorry, you don't have enough money." ) );
951 return;
952 }
953 p.set_mutation( trait_NPC_CONSTRUCTION_LEV_1 );
954 player_character.cash += -100000;
955 const tripoint_abs_omt site =
956 overmap_buffer.find_closest( player_character.global_omt_location(), "ranch_camp_63", 20,
957 false );
958 tinymap bay;
959 bay.load( project_to<coords::sm>( site ), false );
960 bay.draw_square_ter( t_dirt, point( 5, 4 ), point( 15, 14 ) );
961 bay.draw_square_ter( t_dirtmound, point( 6, 5 ), point( 6, 13 ) );
962 bay.draw_square_ter( t_dirtmound, point( 8, 5 ), point( 8, 13 ) );
963 bay.draw_square_ter( t_dirtmound, point( 10, 5 ), point( 10, 13 ) );
964 bay.draw_square_ter( t_dirtmound, point( 12, 5 ), point( 12, 13 ) );
965 bay.draw_square_ter( t_dirtmound, point( 14, 5 ), point( 14, 13 ) );
966 bay.save();
967 popup( _( "%s jots your name down on a ledger and yells out to nearby laborers to begin "
968 "plowing your new field." ), p.name );
969 }
970
971 //Really expensive, but that is so you can't tear down the fence and sell the wood for a profit!
field_build_2(npc & p)972 void talk_function::field_build_2( npc &p )
973 {
974 Character &player_character = get_player_character();
975 if( player_character.cash < 550000 ) {
976 popup( _( "I'm sorry, you don't have enough money." ) );
977 return;
978 }
979 p.set_mutation( trait_NPC_CONSTRUCTION_LEV_2 );
980 player_character.cash += -550000;
981 const tripoint_abs_omt site =
982 overmap_buffer.find_closest( player_character.global_omt_location(), "ranch_camp_63", 20,
983 false );
984 tinymap bay;
985 bay.load( project_to<coords::sm>( site ), false );
986 bay.draw_square_ter( t_fence, point( 4, 3 ), point( 16, 3 ) );
987 bay.draw_square_ter( t_fence, point( 4, 15 ), point( 16, 15 ) );
988 bay.draw_square_ter( t_fence, point( 4, 3 ), point( 4, 15 ) );
989 bay.draw_square_ter( t_fence, point( 16, 3 ), point( 16, 15 ) );
990 bay.draw_square_ter( t_fencegate_c, point( 10, 3 ), point( 10, 3 ) );
991 bay.draw_square_ter( t_fencegate_c, point( 10, 15 ), point( 10, 15 ) );
992 bay.draw_square_ter( t_fencegate_c, point( 4, 9 ), point( 4, 9 ) );
993 bay.save();
994 popup( _( "After counting your money %s directs a nearby laborer to begin constructing a "
995 "fence around your plot…" ), p.name );
996 }
997
field_plant(npc & p,const std::string & place)998 void talk_function::field_plant( npc &p, const std::string &place )
999 {
1000 Character &player_character = get_player_character();
1001 if( !warm_enough_to_plant( player_character.pos() ) ) {
1002 popup( _( "It is too cold to plant anything now." ) );
1003 return;
1004 }
1005 std::vector<item *> seed_inv = player_character.items_with( []( const item & itm ) {
1006 return itm.is_seed() && itm.typeId() != itype_marloss_seed &&
1007 itm.typeId() != itype_fungal_seeds;
1008 } );
1009 if( seed_inv.empty() ) {
1010 popup( _( "You have no seeds to plant!" ) );
1011 return;
1012 }
1013
1014 int empty_plots = 0;
1015 int free_seeds = 0;
1016
1017 std::vector<itype_id> seed_types;
1018 std::vector<std::string> seed_names;
1019 for( auto &seed : seed_inv ) {
1020 if( std::find( seed_types.begin(), seed_types.end(), seed->typeId() ) == seed_types.end() ) {
1021 seed_types.push_back( seed->typeId() );
1022 seed_names.push_back( seed->tname() );
1023 }
1024 }
1025 // Choose seed if applicable
1026 const int seed_index = uilist( _( "Which seeds do you wish to have planted?" ), seed_names );
1027 // Did we cancel?
1028 if( seed_index < 0 || static_cast<size_t>( seed_index ) >= seed_types.size() ) {
1029 popup( _( "You saved your seeds for later." ) );
1030 return;
1031 }
1032
1033 const auto &seed_id = seed_types[seed_index];
1034 if( item::count_by_charges( seed_id ) ) {
1035 free_seeds = player_character.charges_of( seed_id );
1036 } else {
1037 free_seeds = player_character.amount_of( seed_id );
1038 }
1039
1040 //Now we need to find how many free plots we have to plant in...
1041 const tripoint_abs_omt site = overmap_buffer.find_closest(
1042 player_character.global_omt_location(), place, 20, false );
1043 tinymap bay;
1044 bay.load( project_to<coords::sm>( site ), false );
1045 for( const tripoint &plot : bay.points_on_zlevel() ) {
1046 if( bay.ter( plot ) == t_dirtmound ) {
1047 empty_plots++;
1048 }
1049 }
1050
1051 if( empty_plots == 0 ) {
1052 popup( _( "You have no room to plant seeds…" ) );
1053 return;
1054 }
1055
1056 int limiting_number = free_seeds;
1057 if( free_seeds > empty_plots ) {
1058 limiting_number = empty_plots;
1059 }
1060
1061 signed int a = limiting_number * 300;
1062 if( a > player_character.cash ) {
1063 popup( _( "I'm sorry, you don't have enough money to plant those seeds…" ) );
1064 return;
1065 }
1066 if( !query_yn( _( "Do you wish to have %d %s planted here for $%d?" ), limiting_number,
1067 seed_names[seed_index], limiting_number * 3 ) ) {
1068 return;
1069 }
1070
1071 //Plant the actual seeds
1072 for( const tripoint &plot : bay.points_on_zlevel() ) {
1073 if( bay.ter( plot ) == t_dirtmound && limiting_number > 0 ) {
1074 std::list<item> used_seed;
1075 if( item::count_by_charges( seed_id ) ) {
1076 used_seed = player_character.use_charges( seed_id, 1 );
1077 } else {
1078 used_seed = player_character.use_amount( seed_id, 1 );
1079 }
1080 used_seed.front().set_age( 0_turns );
1081 bay.add_item_or_charges( plot, used_seed.front() );
1082 bay.set( plot, t_dirt, f_plant_seed );
1083 limiting_number--;
1084 }
1085 }
1086 bay.draw_square_ter( t_fence, point( 4, 3 ), point( 16, 3 ) );
1087 bay.save();
1088 popup( _( "After counting your money and collecting your seeds, %s calls forth a labor party "
1089 "to plant your field." ), p.name );
1090 }
1091
field_harvest(npc & p,const std::string & place)1092 void talk_function::field_harvest( npc &p, const std::string &place )
1093 {
1094 Character &player_character = get_player_character();
1095 //First we need a list of plants that can be harvested...
1096 const tripoint_abs_omt site = overmap_buffer.find_closest(
1097 player_character.global_omt_location(), place, 20, false );
1098 tinymap bay;
1099 item tmp;
1100 std::vector<itype_id> seed_types;
1101 std::vector<itype_id> plant_types;
1102 std::vector<std::string> plant_names;
1103 bay.load( project_to<coords::sm>( site ), false );
1104 for( const tripoint &plot : bay.points_on_zlevel() ) {
1105 map_stack items = bay.i_at( plot );
1106 if( bay.furn( plot ) == furn_str_id( "f_plant_harvest" ) && !items.empty() ) {
1107 // Can't use item_stack::only_item() since there might be fertilizer
1108 map_stack::iterator seed = std::find_if( items.begin(), items.end(), []( const item & it ) {
1109 return it.is_seed();
1110 } );
1111
1112 if( seed != items.end() ) {
1113 const islot_seed &seed_data = *seed->type->seed;
1114 tmp = item( seed_data.fruit_id, calendar::turn );
1115 bool check = false;
1116 for( const std::string &elem : plant_names ) {
1117 if( elem == tmp.type_name( 3 ) ) {
1118 check = true;
1119 }
1120 }
1121 if( !check ) {
1122 plant_types.push_back( tmp.typeId() );
1123 plant_names.push_back( tmp.type_name( 3 ) );
1124 seed_types.push_back( seed->typeId() );
1125 }
1126 }
1127 }
1128 }
1129 if( plant_names.empty() ) {
1130 popup( _( "There aren't any plants that are ready to harvest…" ) );
1131 return;
1132 }
1133 // Choose the crop to harvest
1134 const int plant_index = uilist( _( "Which plants do you want to have harvested?" ),
1135 plant_names );
1136 // Did we cancel?
1137 if( plant_index < 0 || static_cast<size_t>( plant_index ) >= plant_types.size() ) {
1138 popup( _( "You decided to hold off for now…" ) );
1139 return;
1140 }
1141
1142 int number_plots = 0;
1143 int number_plants = 0;
1144 int number_seeds = 0;
1145 int skillLevel = 2;
1146 if( p.has_trait( trait_NPC_CONSTRUCTION_LEV_2 ) ) {
1147 skillLevel += 2;
1148 }
1149
1150 for( const tripoint &plot : bay.points_on_zlevel() ) {
1151 if( bay.furn( plot ) == furn_str_id( "f_plant_harvest" ) ) {
1152 // Can't use item_stack::only_item() since there might be fertilizer
1153 map_stack items = bay.i_at( plot );
1154 map_stack::iterator seed = std::find_if( items.begin(), items.end(), []( const item & it ) {
1155 return it.is_seed();
1156 } );
1157
1158 if( seed != items.end() ) {
1159 const islot_seed &seed_data = *seed->type->seed;
1160 tmp = item( seed_data.fruit_id, calendar::turn );
1161 if( tmp.typeId() == plant_types[plant_index] ) {
1162 number_plots++;
1163 bay.i_clear( plot );
1164 bay.furn_set( plot, f_null );
1165 bay.ter_set( plot, t_dirtmound );
1166 int plantCount = rng( skillLevel / 2, skillLevel );
1167 if( plantCount >= 9 ) {
1168 plantCount = 9;
1169 } else if( plantCount <= 0 ) {
1170 plantCount = 1;
1171 }
1172 number_plants += plantCount;
1173 number_seeds += std::max( 1, rng( plantCount / 4, plantCount / 2 ) );
1174 }
1175 }
1176 }
1177 }
1178 bay.save();
1179 tmp = item( plant_types[plant_index], calendar::turn );
1180 int money = ( number_plants * tmp.price( true ) - number_plots * 2 ) / 100;
1181 bool liquidate = false;
1182
1183 signed int a = number_plots * 2;
1184 if( a > player_character.cash ) {
1185 liquidate = true;
1186 popup( _( "You don't have enough to pay the workers to harvest the crop so you are forced "
1187 "to sell…" ) );
1188 } else {
1189 liquidate = query_yn( _( "Do you wish to sell the crop of %d %s for a profit of $%d?" ),
1190 number_plants, plant_names[plant_index], money );
1191 }
1192
1193 //Add fruit
1194 if( liquidate ) {
1195 add_msg( _( "The %s are sold for $%d…" ), plant_names[plant_index], money );
1196 player_character.cash += ( number_plants * tmp.price( true ) - number_plots * 2 ) / 100;
1197 } else {
1198 if( tmp.count_by_charges() ) {
1199 tmp.charges = 1;
1200 }
1201 for( int i = 0; i < number_plants; ++i ) {
1202 //Should be dropped at your feet once greedy companions can be controlled
1203 player_character.i_add( tmp );
1204 }
1205 add_msg( _( "You receive %d %s…" ), number_plants, plant_names[plant_index] );
1206 }
1207 tmp = item( seed_types[plant_index], calendar::turn );
1208 const islot_seed &seed_data = *tmp.type->seed;
1209 if( seed_data.spawn_seeds ) {
1210 if( tmp.count_by_charges() ) {
1211 tmp.charges = 1;
1212 }
1213 for( int i = 0; i < number_seeds; ++i ) {
1214 player_character.i_add( tmp );
1215 }
1216 add_msg( _( "You receive %d %s…" ), number_seeds, tmp.type_name( 3 ) );
1217 }
1218
1219 }
1220
scavenging_combat_skill(npc & p,int bonus,bool guns)1221 static int scavenging_combat_skill( npc &p, int bonus, bool guns )
1222 {
1223 // the following doxygen aliases do not yet exist. this is marked for future reference
1224 ///\EFFECT_MELEE_NPC affects scavenging_patrol results
1225 ///\EFFECT_SURVIVAL_NPC affects scavenging_patrol results
1226 ///\EFFECT_BASHING_NPC affects scavenging_patrol results
1227 ///\EFFECT_CUTTING_NPC affects scavenging_patrol results
1228 ///\EFFECT_GUN_NPC affects scavenging_patrol results
1229 ///\EFFECT_STABBING_NPC affects scavenging_patrol results
1230 ///\EFFECT_UNARMED_NPC affects scavenging_patrol results
1231 ///\EFFECT_DODGE_NPC affects scavenging_patrol results
1232 return bonus + p.get_skill_level( skill_melee ) + .5 * p.get_skill_level( skill_survival ) +
1233 p.get_skill_level( skill_bashing ) + p.get_skill_level( skill_cutting ) +
1234 ( guns ? p.get_skill_level( skill_gun ) : 0 ) + p.get_skill_level( skill_stabbing ) +
1235 p.get_skill_level( skill_unarmed ) + p.get_skill_level( skill_dodge );
1236 }
1237
scavenging_patrol_return(npc & p)1238 bool talk_function::scavenging_patrol_return( npc &p )
1239 {
1240 npc_ptr comp = companion_choose_return( p, "_scavenging_patrol",
1241 calendar::turn - 10_hours );
1242 if( comp == nullptr ) {
1243 return false;
1244 }
1245 int experience = rng( 5, 20 );
1246 if( one_in( 4 ) ) {
1247 popup( _( "While scavenging, %s's party suddenly found itself set upon by a large mob of "
1248 "undead…" ), comp->name );
1249 int skill = scavenging_combat_skill( *comp, 4, true );
1250 if( one_in( 6 ) ) {
1251 popup( _( "Through quick thinking the group was able to evade combat!" ) );
1252 } else {
1253 popup( _( "Combat took place in close quarters, focusing on melee skills…" ) );
1254 int monsters = rng( 8, 30 );
1255 if( skill * rng_float( .60, 1.4 ) > .35 * monsters * rng_float( .6, 1.4 ) ) {
1256 popup( _( "Through brute force the party smashed through the group of %d"
1257 " undead!" ), monsters );
1258 experience += rng( 2, 10 );
1259 } else {
1260 popup( _( "Unfortunately they were overpowered by the undead… I'm sorry." ) );
1261 overmap_buffer.remove_npc( comp->getID() );
1262 return false;
1263 }
1264 }
1265 }
1266
1267 Character &player_character = get_player_character();
1268 int money = rng( 25, 450 );
1269 player_character.cash += money * 100;
1270
1271 companion_skill_trainer( *comp, "combat", experience * 10_minutes, 10 );
1272 popup( _( "%s returns from patrol having earned $%d and a fair bit of experience…" ),
1273 comp->name, money );
1274 if( one_in( 10 ) ) {
1275 popup( _( "%s was impressed with %s's performance and gave you a small bonus ( $100 )" ),
1276 p.name, comp->name );
1277 player_character.cash += 10000;
1278 }
1279 if( one_in( 10 ) && !p.has_trait( trait_NPC_MISSION_LEV_1 ) ) {
1280 p.set_mutation( trait_NPC_MISSION_LEV_1 );
1281 popup( _( "%s feels more confident in your abilities and is willing to let you "
1282 "participate in daring raids." ), p.name );
1283 }
1284 companion_return( *comp );
1285 return true;
1286 }
1287
scavenging_raid_return(npc & p)1288 bool talk_function::scavenging_raid_return( npc &p )
1289 {
1290 npc_ptr comp = companion_choose_return( p, "_scavenging_raid",
1291 calendar::turn - 10_hours );
1292 if( comp == nullptr ) {
1293 return false;
1294 }
1295 int experience = rng( 10, 20 );
1296 if( one_in( 2 ) ) {
1297 popup( _( "While scavenging, %s's party suddenly found itself set upon by a large mob of "
1298 "undead…" ), comp->name );
1299 int skill = scavenging_combat_skill( *comp, 4, true );
1300 if( one_in( 6 ) ) {
1301 popup( _( "Through quick thinking the group was able to evade combat!" ) );
1302 } else {
1303 popup( _( "Combat took place in close quarters, focusing on melee skills…" ) );
1304 int monsters = rng( 8, 30 );
1305 if( skill * rng_float( .60, 1.4 ) > ( .35 * monsters * rng_float( .6, 1.4 ) ) ) {
1306 popup( _( "Through brute force the party smashed through the group of %d "
1307 "undead!" ), monsters );
1308 experience += rng( 2, 10 );
1309 } else {
1310 popup( _( "Unfortunately they were overpowered by the undead… I'm sorry." ) );
1311 overmap_buffer.remove_npc( comp->getID() );
1312 return false;
1313 }
1314 }
1315 }
1316 Character &player_character = get_player_character();
1317 //The loot value needs to be added to the faction - what the player is payed
1318 tripoint_abs_omt loot_location = player_character.global_omt_location();
1319 // Only check at the ground floor.
1320 loot_location.z() = 0;
1321 for( int i = 0; i < rng( 2, 3 ); i++ ) {
1322 const tripoint_abs_omt site = overmap_buffer.find_closest(
1323 loot_location, "house", 0, false, ot_match_type::prefix );
1324 overmap_buffer.reveal( site, 2 );
1325 loot_building( site );
1326 }
1327
1328 int money = rng( 200, 900 );
1329 player_character.cash += money * 100;
1330
1331 companion_skill_trainer( *comp, "combat", experience * 10_minutes, 10 );
1332 popup( _( "%s returns from the raid having earned $%d and a fair bit of experience…" ),
1333 comp->name, money );
1334 if( one_in( 20 ) ) {
1335 popup( _( "%s was impressed with %s's performance and gave you a small bonus ( $100 )" ),
1336 p.name, comp->name );
1337 player_character.cash += 10000;
1338 }
1339 if( one_in( 2 ) ) {
1340 item_group_id itemlist( "npc_misc" );
1341 if( one_in( 8 ) ) {
1342 itemlist = item_group_id( "npc_weapon_random" );
1343 }
1344 item result = item_group::item_from( itemlist );
1345 if( !result.is_null() ) {
1346 popup( _( "%s returned with a %s for you!" ), comp->name, result.tname() );
1347 player_character.i_add( result );
1348 }
1349 }
1350 companion_return( *comp );
1351 return true;
1352 }
1353
labor_return(npc & p)1354 bool talk_function::labor_return( npc &p )
1355 {
1356 npc_ptr comp = companion_choose_return( p, "_labor", calendar::turn - 1_hours );
1357 if( comp == nullptr ) {
1358 return false;
1359 }
1360
1361 Character &player_character = get_player_character();
1362 float hours = to_hours<float>( calendar::turn - comp->companion_mission_time );
1363 int money = 8 * hours;
1364 player_character.cash += money * 100;
1365
1366 companion_skill_trainer( *comp, "menial", calendar::turn - comp->companion_mission_time, 1 );
1367
1368 popup( _( "%s returns from working as a laborer having earned $%d and a bit of experience…" ),
1369 comp->name, money );
1370 companion_return( *comp );
1371 if( hours >= 8 && one_in( 8 ) && !p.has_trait( trait_NPC_MISSION_LEV_1 ) ) {
1372 p.set_mutation( trait_NPC_MISSION_LEV_1 );
1373 popup( _( "%s feels more confident in your companions and is willing to let them "
1374 "participate in advanced tasks." ), p.name );
1375 }
1376
1377 return true;
1378 }
1379
carpenter_return(npc & p)1380 bool talk_function::carpenter_return( npc &p )
1381 {
1382 npc_ptr comp = companion_choose_return( p, "_carpenter",
1383 calendar::turn - 1_hours );
1384 if( comp == nullptr ) {
1385 return false;
1386 }
1387
1388 if( one_in( 20 ) ) {
1389 // the following doxygen aliases do not yet exist. this is marked for future reference
1390
1391 ///\EFFECT_FABRICATION_NPC affects carpenter mission results
1392
1393 ///\EFFECT_DODGE_NPC affects carpenter mission results
1394
1395 ///\EFFECT_SURVIVAL_NPC affects carpenter mission results
1396 int skill_1 = comp->get_skill_level( skill_fabrication );
1397 int skill_2 = comp->get_skill_level( skill_dodge );
1398 int skill_3 = comp->get_skill_level( skill_survival );
1399 popup( _( "While %s was framing a building one of the walls began to collapse…" ),
1400 comp->name );
1401 if( skill_1 > rng( 1, 8 ) ) {
1402 popup( _( "In the blink of an eye, %s threw a brace up and averted a disaster." ),
1403 comp->name );
1404 } else if( skill_2 > rng( 1, 8 ) ) {
1405 popup( _( "Darting out a window, %s escaped the collapse." ), comp->name );
1406 } else if( skill_3 > rng( 1, 8 ) ) {
1407 popup( _( "%s didn't make it out in time…" ), comp->name );
1408 popup( _( "but %s was rescued from the debris with only minor injuries!" ),
1409 comp->name );
1410 } else {
1411 popup( _( "%s didn't make it out in time…" ), comp->name );
1412 popup( _( "Everyone who was trapped under the collapsing roof died…" ) );
1413 popup( _( "I'm sorry, there is nothing we could do." ) );
1414 overmap_buffer.remove_npc( comp->getID() );
1415 return false;
1416 }
1417 }
1418
1419 float hours = to_hours<float>( calendar::turn - comp->companion_mission_time );
1420 int money = 12 * hours;
1421 get_player_character().cash += money * 100;
1422
1423 companion_skill_trainer( *comp, "construction", calendar::turn -
1424 comp->companion_mission_time, 2 );
1425
1426 popup( _( "%s returns from working as a carpenter having earned $%d and a bit of "
1427 "experience…" ), comp->name, money );
1428 companion_return( *comp );
1429 return true;
1430 }
1431
forage_return(npc & p)1432 bool talk_function::forage_return( npc &p )
1433 {
1434 npc_ptr comp = companion_choose_return( p, "_forage", calendar::turn - 4_hours );
1435 if( comp == nullptr ) {
1436 return false;
1437 }
1438
1439 if( one_in( 10 ) ) {
1440 popup( _( "While foraging, a beast began to stalk %s…" ), comp->name );
1441 // the following doxygen aliases do not yet exist. this is marked for future reference
1442
1443 ///\EFFECT_SURVIVAL_NPC affects forage mission results
1444
1445 ///\EFFECT_DODGE_NPC affects forage mission results
1446 int skill_1 = comp->get_skill_level( skill_survival );
1447 int skill_2 = comp->get_skill_level( skill_dodge );
1448 if( skill_1 > rng( -2, 8 ) ) {
1449 popup( _( "Alerted by a rustle, %s fled to the safety of the outpost!" ), comp->name );
1450 } else if( skill_2 > rng( -2, 8 ) ) {
1451 popup( _( "As soon as the cougar sprang %s darted to the safety of the outpost!" ),
1452 comp->name );
1453 } else {
1454 popup( _( "%s was caught unaware and was forced to fight the creature at close "
1455 "range!" ), comp->name );
1456 int skill = scavenging_combat_skill( *comp, 0, false );
1457 int monsters = rng( 0, 10 );
1458 if( skill * rng_float( .80, 1.2 ) > monsters * rng_float( .8, 1.2 ) ) {
1459 if( one_in( 2 ) ) {
1460 popup( _( "%s was able to scare off the bear after delivering a nasty "
1461 "blow!" ), comp->name );
1462 } else {
1463 popup( _( "%s beat the cougar into a bloody pulp!" ), comp->name );
1464 }
1465 } else {
1466 if( one_in( 2 ) ) {
1467 popup( _( "%s was able to hold off the first wolf but the others that were "
1468 "skulking in the tree line caught up…" ), comp->name );
1469 popup( _( "I'm sorry, there wasn't anything we could do…" ) );
1470 } else {
1471 popup( _( "We… we don't know what exactly happened but we found %s's gear "
1472 "ripped and bloody…" ), comp->name );
1473 popup( _( "I fear your companion won't be returning." ) );
1474 }
1475 overmap_buffer.remove_npc( comp->getID() );
1476 return false;
1477 }
1478 }
1479 }
1480
1481 Character &player_character = get_player_character();
1482 float hours = to_hours<float>( calendar::turn - comp->companion_mission_time );
1483 int money = 10 * hours;
1484 player_character.cash += money * 100;
1485
1486 companion_skill_trainer( *comp, "gathering", calendar::turn -
1487 comp->companion_mission_time, 2 );
1488
1489 popup( _( "%s returns from working as a forager having earned $%d and a bit of "
1490 "experience…" ), comp->name, money );
1491 // the following doxygen aliases do not yet exist. this is marked for future reference
1492
1493 ///\EFFECT_SURVIVAL_NPC affects forage mission results
1494 int skill = comp->get_skill_level( skill_survival );
1495 if( skill > rng_float( -.5, 8 ) ) {
1496 item_group_id itemlist = item_group_id( "farming_seeds" );
1497 if( one_in( 2 ) ) {
1498 switch( season_of_year( calendar::turn ) ) {
1499 case SPRING:
1500 itemlist = item_group_id( "forage_spring" );
1501 break;
1502 case SUMMER:
1503 itemlist = item_group_id( "forage_summer" );
1504 break;
1505 case AUTUMN:
1506 itemlist = item_group_id( "forage_autumn" );
1507 break;
1508 case WINTER:
1509 itemlist = item_group_id( "forage_winter" );
1510 break;
1511 default:
1512 debugmsg( "Invalid season" );
1513 }
1514 }
1515 item result = item_group::item_from( itemlist );
1516 if( !result.is_null() ) {
1517 popup( _( "%s returned with a %s for you!" ), comp->name, result.tname() );
1518 player_character.i_add( result );
1519 }
1520 if( one_in( 6 ) && !p.has_trait( trait_NPC_MISSION_LEV_1 ) ) {
1521 p.set_mutation( trait_NPC_MISSION_LEV_1 );
1522 popup( _( "%s feels more confident in your companions and is willing to let them "
1523 "participate in advanced tasks." ), p.name );
1524 }
1525 }
1526 companion_return( *comp );
1527 return true;
1528 }
1529
companion_om_combat_check(const std::vector<npc_ptr> & group,const tripoint_abs_omt & om_tgt,bool try_engage)1530 bool talk_function::companion_om_combat_check( const std::vector<npc_ptr> &group,
1531 const tripoint_abs_omt &om_tgt, bool try_engage )
1532 {
1533 if( overmap_buffer.is_safe( om_tgt ) ) {
1534 //Should work but is_safe is always returning true regardless of what is there.
1535 //return true;
1536 }
1537
1538 tripoint_abs_sm sm_tgt = project_to<coords::sm>( om_tgt );
1539
1540 tinymap target_bay;
1541 target_bay.load( sm_tgt, false );
1542 std::vector< monster * > monsters_around;
1543 for( int x = 0; x < 2; x++ ) {
1544 for( int y = 0; y < 2; y++ ) {
1545 tripoint_abs_sm sm = sm_tgt + point( x, y );
1546 point_abs_om omp;
1547 tripoint_om_sm local_sm;
1548 std::tie( omp, local_sm ) = project_remain<coords::om>( sm );
1549 overmap &omi = overmap_buffer.get( omp );
1550
1551 auto monster_bucket = omi.monster_map.equal_range( local_sm );
1552 std::for_each( monster_bucket.first,
1553 monster_bucket.second, [&]( std::pair<const tripoint_om_sm, monster> &monster_entry ) {
1554 monster &this_monster = monster_entry.second;
1555 monsters_around.push_back( &this_monster );
1556 } );
1557 }
1558 }
1559 float avg_survival = 0.0f;
1560 for( const auto &guy : group ) {
1561 avg_survival += guy->get_skill_level( skill_survival );
1562 }
1563 avg_survival = avg_survival / group.size();
1564
1565 monster mon;
1566
1567 std::vector< monster * > monsters_fighting;
1568 for( monster *mons : monsters_around ) {
1569 if( mons->get_hp() <= 0 ) {
1570 continue;
1571 }
1572 int d_modifier = avg_survival - mons->type->difficulty;
1573 int roll = rng( 1, 20 ) + d_modifier;
1574 if( roll > 10 ) {
1575 if( try_engage ) {
1576 mons->death_drops = false;
1577 monsters_fighting.push_back( mons );
1578 }
1579 } else {
1580 mons->death_drops = false;
1581 monsters_fighting.push_back( mons );
1582 }
1583 }
1584
1585 if( !monsters_fighting.empty() ) {
1586 bool outcome = force_on_force( group, "Patrol", monsters_fighting, "attacking monsters",
1587 rng( -1, 2 ) );
1588 for( monster *mons : monsters_fighting ) {
1589 mons->death_drops = true;
1590 }
1591 return outcome;
1592 }
1593 return true;
1594 }
1595
force_on_force(const std::vector<npc_ptr> & defender,const std::string & def_desc,const std::vector<monster * > & monsters_fighting,const std::string & att_desc,int advantage)1596 bool talk_function::force_on_force( const std::vector<npc_ptr> &defender,
1597 const std::string &def_desc,
1598 const std::vector< monster * > &monsters_fighting,
1599 const std::string &att_desc, int advantage )
1600 {
1601 std::string adv;
1602 if( advantage < 0 ) {
1603 adv = ", attacker advantage";
1604 } else if( advantage > 0 ) {
1605 adv = ", defender advantage";
1606 }
1607 Character &player_character = get_player_character();
1608 faction *yours = player_character.get_faction();
1609 //Find out why your followers don't have your faction...
1610 popup( _( "Engagement between %d members of %s %s and %d %s%s!" ), defender.size(),
1611 yours->name, def_desc, monsters_fighting.size(), att_desc, adv );
1612 int defense = 0;
1613 int attack = 0;
1614 int att_init = 0;
1615 int def_init = 0;
1616 while( true ) {
1617 std::vector< monster * > remaining_mon;
1618 for( const auto &elem : monsters_fighting ) {
1619 if( elem->get_hp() > 0 ) {
1620 remaining_mon.push_back( elem );
1621 }
1622 }
1623 std::vector<npc_ptr> remaining_def;
1624 for( const auto &elem : defender ) {
1625 if( !elem->is_dead() && elem->get_part_hp_cur( bodypart_id( "torso" ) ) >= 0 &&
1626 elem->get_part_hp_cur( bodypart_id( "head" ) ) >= 0 ) {
1627 remaining_def.push_back( elem );
1628 }
1629 }
1630
1631 defense = combat_score( remaining_def );
1632 attack = combat_score( remaining_mon );
1633 if( attack > defense * 3 ) {
1634 attack_random( remaining_mon, remaining_def );
1635 if( defense == 0 || ( remaining_def.size() == 1 && remaining_def[0]->is_dead() ) ) {
1636 //Here too...
1637 popup( _( "%s forces are destroyed!" ), yours->name );
1638 } else {
1639 //Again, no faction for your followers
1640 popup( _( "%s forces retreat from combat!" ), yours->name );
1641 }
1642 return false;
1643 } else if( attack * 3 < defense ) {
1644 attack_random( remaining_def, remaining_mon );
1645 if( attack == 0 || ( remaining_mon.size() == 1 && remaining_mon[0]->get_hp() == 0 ) ) {
1646 popup( _( "The monsters are destroyed!" ) );
1647 } else {
1648 popup( _( "The monsters disengage!" ) );
1649 }
1650 return true;
1651 } else {
1652 def_init = rng( 1, 6 ) + advantage;
1653 att_init = rng( 1, 6 );
1654 if( def_init >= att_init ) {
1655 attack_random( remaining_mon, remaining_def );
1656 }
1657 if( def_init <= att_init ) {
1658 attack_random( remaining_def, remaining_mon );
1659 }
1660 }
1661 }
1662 }
1663
force_on_force(const std::vector<npc_ptr> & defender,const std::string & def_desc,const std::vector<npc_ptr> & attacker,const std::string & att_desc,int advantage)1664 void talk_function::force_on_force( const std::vector<npc_ptr> &defender,
1665 const std::string &def_desc,
1666 const std::vector<npc_ptr> &attacker,
1667 const std::string &att_desc, int advantage )
1668 {
1669 std::string adv;
1670 if( advantage < 0 ) {
1671 adv = ", attacker advantage";
1672 } else if( advantage > 0 ) {
1673 adv = ", defender advantage";
1674 }
1675 popup( _( "Engagement between %d members of %s %s and %d members of %s %s%s!" ),
1676 defender.size(), defender[0]->get_faction()->name, def_desc, attacker.size(),
1677 attacker[0]->get_faction()->name, att_desc, adv );
1678 int defense = 0;
1679 int attack = 0;
1680 int att_init = 0;
1681 int def_init = 0;
1682 while( true ) {
1683 std::vector<npc_ptr> remaining_att;
1684 for( const auto &elem : attacker ) {
1685 if( elem->get_part_hp_cur( bodypart_id( "torso" ) ) != 0 ) {
1686 remaining_att.push_back( elem );
1687 }
1688 }
1689 std::vector<npc_ptr> remaining_def;
1690 for( const auto &elem : defender ) {
1691 if( elem->get_part_hp_cur( bodypart_id( "torso" ) ) != 0 ) {
1692 remaining_def.push_back( elem );
1693 }
1694 }
1695
1696 defense = combat_score( remaining_def );
1697 attack = combat_score( remaining_att );
1698 if( attack > defense * 3 ) {
1699 attack_random( remaining_att, remaining_def );
1700 if( defense == 0 || ( remaining_def.size() == 1 &&
1701 remaining_def[0]->get_part_hp_cur( bodypart_id( "torso" ) ) == 0 ) ) {
1702 popup( _( "%s forces are destroyed!" ), defender[0]->get_faction()->name );
1703 } else {
1704 popup( _( "%s forces retreat from combat!" ), defender[0]->get_faction()->name );
1705 }
1706 return;
1707 } else if( attack * 3 < defense ) {
1708 attack_random( remaining_def, remaining_att );
1709 if( attack == 0 || ( remaining_att.size() == 1 &&
1710 remaining_att[0]->get_part_hp_cur( bodypart_id( "torso" ) ) == 0 ) ) {
1711 popup( _( "%s forces are destroyed!" ), attacker[0]->get_faction()->name );
1712 } else {
1713 popup( _( "%s forces retreat from combat!" ), attacker[0]->get_faction()->name );
1714 }
1715 return;
1716 } else {
1717 def_init = rng( 1, 6 ) + advantage;
1718 att_init = rng( 1, 6 );
1719 if( def_init >= att_init ) {
1720 attack_random( remaining_att, remaining_def );
1721 }
1722 if( def_init <= att_init ) {
1723 attack_random( remaining_def, remaining_att );
1724 }
1725 }
1726 }
1727 }
1728
1729 // skill training support functions
companion_skill_trainer(npc & comp,const std::string & skill_tested,time_duration time_worked,int difficulty)1730 void talk_function::companion_skill_trainer( npc &comp, const std::string &skill_tested,
1731 time_duration time_worked, int difficulty )
1732 {
1733 difficulty = std::max( 1, difficulty );
1734 int checks = 1 + to_minutes<int>( time_worked ) / 10;
1735
1736 weighted_int_list<skill_id> skill_practice;
1737 if( skill_tested == "combat" ) {
1738 const skill_id best_skill = comp.best_skill();
1739 if( best_skill ) {
1740 skill_practice.add( best_skill, 30 );
1741 }
1742 }
1743 for( Skill &sk : Skill::skills ) {
1744 skill_practice.add( sk.ident(), sk.get_companion_skill_practice( skill_tested ) );
1745 }
1746 if( skill_practice.empty() ) {
1747 comp.practice( skill_id( skill_tested ), difficulty * to_minutes<int>( time_worked ) / 10 );
1748 } else {
1749 for( int i = 0; i < checks; i++ ) {
1750 skill_id *ident = skill_practice.pick();
1751 if( ident ) {
1752 comp.practice( *ident, difficulty );
1753 }
1754 }
1755 }
1756 }
1757
companion_skill_trainer(npc & comp,const skill_id & skill_tested,time_duration time_worked,int difficulty)1758 void talk_function::companion_skill_trainer( npc &comp, const skill_id &skill_tested,
1759 time_duration time_worked, int difficulty )
1760 {
1761 difficulty = std::max( 1, difficulty );
1762 comp.practice( skill_tested, difficulty * to_minutes<int>( time_worked ) / 10 );
1763 }
1764
companion_return(npc & comp)1765 void talk_function::companion_return( npc &comp )
1766 {
1767 cata_assert( !comp.is_active() );
1768 comp.reset_companion_mission();
1769 comp.companion_mission_time = calendar::before_time_starts;
1770 comp.companion_mission_time_ret = calendar::before_time_starts;
1771 Character &player_character = get_player_character();
1772 map &here = get_map();
1773 for( size_t i = 0; i < comp.companion_mission_inv.size(); i++ ) {
1774 for( const auto &it : comp.companion_mission_inv.const_stack( i ) ) {
1775 if( !it.count_by_charges() || it.charges > 0 ) {
1776 here.add_item_or_charges( player_character.pos(), it );
1777 }
1778 }
1779 }
1780 comp.companion_mission_inv.clear();
1781 comp.companion_mission_points.clear();
1782 // npc *may* be active, or not if outside the reality bubble
1783 g->reload_npcs();
1784 }
1785
companion_list(const npc & p,const std::string & mission_id,bool contains)1786 std::vector<npc_ptr> talk_function::companion_list( const npc &p, const std::string &mission_id,
1787 bool contains )
1788 {
1789 std::vector<npc_ptr> available;
1790 const tripoint_abs_omt omt_pos = p.global_omt_location();
1791 for( const auto &elem : overmap_buffer.get_companion_mission_npcs() ) {
1792 npc_companion_mission c_mission = elem->get_companion_mission();
1793 if( c_mission.position == omt_pos && c_mission.mission_id == mission_id &&
1794 c_mission.role_id == p.companion_mission_role_id ) {
1795 available.push_back( elem );
1796 } else if( contains && c_mission.mission_id.find( mission_id ) != std::string::npos ) {
1797 available.push_back( elem );
1798 }
1799 }
1800 return available;
1801 }
1802
companion_combat_rank(const npc & p)1803 static int companion_combat_rank( const npc &p )
1804 {
1805 int combat = 2 * p.get_dex() + 3 * p.get_str() + 2 * p.get_per() + p.get_int();
1806 for( const Skill &sk : Skill::skills ) {
1807 combat += p.get_skill_level( sk.ident() ) * sk.companion_combat_rank_factor();
1808 }
1809 return combat * std::min( p.get_dex(), 32 ) * std::min( p.get_str(), 32 ) / 64;
1810 }
1811
companion_survival_rank(const npc & p)1812 static int companion_survival_rank( const npc &p )
1813 {
1814 int survival = 2 * p.get_dex() + p.get_str() + 2 * p.get_per() + 1.5 * p.get_int();
1815 for( const Skill &sk : Skill::skills ) {
1816 survival += p.get_skill_level( sk.ident() ) * sk.companion_survival_rank_factor();
1817 }
1818 return survival * std::min( p.get_dex(), 32 ) * std::min( p.get_per(), 32 ) / 64;
1819 }
1820
companion_industry_rank(const npc & p)1821 static int companion_industry_rank( const npc &p )
1822 {
1823 int industry = p.get_dex() + p.get_str() + p.get_per() + 3 * p.get_int();
1824 for( const Skill &sk : Skill::skills ) {
1825 industry += p.get_skill_level( sk.ident() ) * sk.companion_industry_rank_factor();
1826 }
1827 return industry * std::min( p.get_int(), 32 ) / 8;
1828 }
1829
companion_sort_compare(const npc_ptr & first,const npc_ptr & second)1830 static bool companion_sort_compare( const npc_ptr &first, const npc_ptr &second )
1831 {
1832 return companion_combat_rank( *first ) > companion_combat_rank( *second );
1833 }
1834
companion_sort(comp_list available,const std::map<skill_id,int> & required_skills)1835 comp_list talk_function::companion_sort( comp_list available,
1836 const std::map<skill_id, int> &required_skills )
1837 {
1838 if( required_skills.empty() ) {
1839 std::sort( available.begin(), available.end(), companion_sort_compare );
1840 return available;
1841 }
1842 skill_id hardest_skill;
1843 int hardest_diff = -1;
1844 for( const std::pair<const skill_id, int> &req_skill : required_skills ) {
1845 if( req_skill.second > hardest_diff ) {
1846 hardest_diff = req_skill.second;
1847 hardest_skill = req_skill.first;
1848 }
1849 }
1850
1851 struct companion_sort_skill {
1852 explicit companion_sort_skill( const skill_id &skill_tested ) {
1853 req_skill = skill_tested;
1854 }
1855
1856 bool operator()( const npc_ptr &first, const npc_ptr &second ) {
1857 return first->get_skill_level( req_skill ) > second->get_skill_level( req_skill );
1858 }
1859
1860 skill_id req_skill;
1861 };
1862 std::sort( available.begin(), available.end(), companion_sort_skill( hardest_skill ) );
1863
1864 return available;
1865 }
1866
companion_rank(const std::vector<npc_ptr> & available,bool adj)1867 std::vector<comp_rank> talk_function::companion_rank( const std::vector<npc_ptr> &available,
1868 bool adj )
1869 {
1870 std::vector<comp_rank> raw;
1871 int max_combat = 0;
1872 int max_survival = 0;
1873 int max_industry = 0;
1874 for( const auto &e : available ) {
1875 comp_rank r;
1876 r.combat = companion_combat_rank( *e );
1877 r.survival = companion_survival_rank( *e );
1878 r.industry = companion_industry_rank( *e );
1879 raw.push_back( r );
1880 if( r.combat > max_combat ) {
1881 max_combat = r.combat;
1882 }
1883 if( r.survival > max_survival ) {
1884 max_survival = r.survival;
1885 }
1886 if( r.industry > max_industry ) {
1887 max_industry = r.industry;
1888 }
1889 }
1890
1891 if( !adj ) {
1892 return raw;
1893 }
1894
1895 std::vector<comp_rank> adjusted;
1896 for( const auto &entry : raw ) {
1897 comp_rank r;
1898 r.combat = max_combat ? 100 * entry.combat / max_combat : 0;
1899 r.survival = max_survival ? 100 * entry.survival / max_survival : 0;
1900 r.industry = max_industry ? 100 * entry.industry / max_industry : 0;
1901 adjusted.push_back( r );
1902 }
1903 return adjusted;
1904 }
1905
companion_choose(const std::map<skill_id,int> & required_skills)1906 npc_ptr talk_function::companion_choose( const std::map<skill_id, int> &required_skills )
1907 {
1908 Character &player_character = get_player_character();
1909 std::vector<npc_ptr> available;
1910 cata::optional<basecamp *> bcp = overmap_buffer.find_camp(
1911 player_character.global_omt_location().xy() );
1912
1913 for( const character_id &elem : g->get_follower_list() ) {
1914 npc_ptr guy = overmap_buffer.find_npc( elem );
1915 if( !guy ) {
1916 continue;
1917 }
1918 npc_companion_mission c_mission = guy->get_companion_mission();
1919 // get non-assigned visible followers
1920 if( player_character.posz() == guy->posz() && !guy->has_companion_mission() &&
1921 !guy->is_travelling() &&
1922 ( rl_dist( player_character.pos(), guy->pos() ) <= SEEX * 2 ) &&
1923 player_character.sees( guy->pos() ) ) {
1924 available.push_back( guy );
1925 } else if( bcp ) {
1926 basecamp *player_camp = *bcp;
1927 std::vector<npc_ptr> camp_npcs = player_camp->get_npcs_assigned();
1928 if( std::any_of( camp_npcs.begin(), camp_npcs.end(),
1929 [guy]( const npc_ptr & i ) {
1930 return ( ( i == guy ) && ( !guy->has_companion_mission() ) );
1931 } ) ) {
1932 available.push_back( guy );
1933 }
1934 } else {
1935 const tripoint_abs_omt guy_omt_pos = guy->global_omt_location();
1936 cata::optional<basecamp *> guy_camp = overmap_buffer.find_camp( guy_omt_pos.xy() );
1937 if( guy_camp ) {
1938 // get NPCs assigned to guard a remote base
1939 basecamp *temp_camp = *guy_camp;
1940 std::vector<npc_ptr> assigned_npcs = temp_camp->get_npcs_assigned();
1941 if( std::any_of( assigned_npcs.begin(), assigned_npcs.end(),
1942 [guy]( const npc_ptr & i ) {
1943 return ( ( i == guy ) && ( !guy->has_companion_mission() ) );
1944 } ) ) {
1945 available.push_back( guy );
1946 }
1947 }
1948 }
1949 }
1950 if( available.empty() ) {
1951 popup( _( "You don't have any companions to send out…" ) );
1952 return nullptr;
1953 }
1954 std::vector<uilist_entry> npc_menu;
1955 available = companion_sort( available, required_skills );
1956 std::vector<comp_rank> rankings = companion_rank( available );
1957
1958 int x = 0;
1959 std::string menu_header = left_justify( _( "Who do you want to send?" ), 51 );
1960 if( required_skills.empty() ) {
1961 menu_header += _( "[ COMBAT : SURVIVAL : INDUSTRY ]" );
1962 }
1963 for( const npc_ptr &e : available ) {
1964 std::string npc_desc;
1965 bool can_do = true;
1966 if( e->mission == NPC_MISSION_GUARD_ALLY ) {
1967 //~ %1$s: npc name
1968 npc_desc = string_format( pgettext( "companion", "%1$s (Guarding)" ), e->name );
1969 } else {
1970 npc_desc = e->name;
1971 }
1972 if( required_skills.empty() ) {
1973 npc_desc = string_format( pgettext( "companion ranking", "%s [ %4d : %4d : %4d ]" ),
1974 left_justify( npc_desc, 51 ), rankings[x].combat,
1975 rankings[x].survival, rankings[x].industry );
1976 } else {
1977 npc_desc = left_justify( npc_desc, 51 );
1978 bool first = true;
1979 for( const std::pair<const skill_id, int> &skill_tested : required_skills ) {
1980 if( first ) {
1981 first = false;
1982 } else {
1983 npc_desc += ", ";
1984 }
1985 skill_id skill_tested_id = skill_tested.first;
1986 int skill_level = skill_tested.second;
1987 if( skill_level == 0 ) {
1988 //~ %1$s: skill name, %2$d: companion skill level
1989 npc_desc += string_format( pgettext( "companion skill", "%1$s %2$d" ),
1990 skill_tested_id.obj().name(),
1991 e->get_skill_level( skill_tested_id ) );
1992 } else {
1993 //~ %1$s: skill name, %2$d: companion skill level, %3$d: skill requirement
1994 npc_desc += string_format( pgettext( "companion skill", "%1$s %2$d/%3$d" ),
1995 skill_tested_id.obj().name(),
1996 e->get_skill_level( skill_tested_id ),
1997 skill_level );
1998 can_do &= e->get_skill_level( skill_tested_id ) >= skill_level;
1999 }
2000 }
2001 }
2002 uilist_entry npc_entry = uilist_entry( x, can_do, x, npc_desc );
2003 npc_menu.push_back( npc_entry );
2004 x++;
2005 }
2006 const size_t npc_choice = uilist( menu_header, npc_menu );
2007 if( npc_choice >= available.size() ) {
2008 popup( _( "You choose to send no one…" ), npc_choice );
2009 return nullptr;
2010 }
2011
2012 return available[npc_choice];
2013 }
2014
companion_choose_return(const npc & p,const std::string & mission_id,const time_point & deadline)2015 npc_ptr talk_function::companion_choose_return( const npc &p, const std::string &mission_id,
2016 const time_point &deadline )
2017 {
2018 const tripoint_abs_omt omt_pos = p.global_omt_location();
2019 const std::string &role_id = p.companion_mission_role_id;
2020 return companion_choose_return( omt_pos, role_id, mission_id, deadline );
2021 }
companion_choose_return(const tripoint_abs_omt & omt_pos,const std::string & role_id,const std::string & mission_id,const time_point & deadline,const bool by_mission)2022 npc_ptr talk_function::companion_choose_return( const tripoint_abs_omt &omt_pos,
2023 const std::string &role_id,
2024 const std::string &mission_id,
2025 const time_point &deadline,
2026 const bool by_mission )
2027 {
2028 std::vector<npc_ptr> available;
2029 Character &player_character = get_player_character();
2030 for( npc_ptr &guy : overmap_buffer.get_companion_mission_npcs() ) {
2031 npc_companion_mission c_mission = guy->get_companion_mission();
2032 if( c_mission.position != omt_pos ||
2033 ( by_mission && c_mission.mission_id != mission_id ) || c_mission.role_id != role_id ) {
2034 continue;
2035 }
2036 if( player_character.has_trait( trait_DEBUG_HS ) ) {
2037 available.push_back( guy );
2038 } else if( deadline == calendar::before_time_starts ) {
2039 if( guy->companion_mission_time_ret <= calendar::turn ) {
2040 available.push_back( guy );
2041 }
2042 } else if( guy->companion_mission_time <= deadline ) {
2043 available.push_back( guy );
2044 }
2045 }
2046
2047 if( available.empty() ) {
2048 popup( _( "You don't have any companions ready to return…" ) );
2049 return nullptr;
2050 }
2051
2052 if( available.size() == 1 ) {
2053 return available[0];
2054 }
2055
2056 std::vector<std::string> npcs;
2057 for( auto &elem : available ) {
2058 npcs.push_back( ( elem )->name );
2059 }
2060 const size_t npc_choice = uilist( _( "Who should return?" ), npcs );
2061 if( npc_choice < available.size() ) {
2062 return available[npc_choice];
2063 }
2064 popup( _( "No one returns to your party…" ) );
2065 return nullptr;
2066 }
2067
2068 //Smash stuff, steal valuables, and change map maker
loot_building(const tripoint_abs_omt & site)2069 void talk_function::loot_building( const tripoint_abs_omt &site )
2070 {
2071 tinymap bay;
2072 bay.load( project_to<coords::sm>( site ), false );
2073 for( const tripoint &p : bay.points_on_zlevel() ) {
2074 const ter_id t = bay.ter( p );
2075 //Open all the doors, doesn't need to be exhaustive
2076 if( t == t_door_c || t == t_door_c_peep || t == t_door_b
2077 || t == t_door_boarded || t == t_door_boarded_damaged
2078 || t == t_rdoor_boarded || t == t_rdoor_boarded_damaged
2079 || t == t_door_boarded_peep || t == t_door_boarded_damaged_peep ) {
2080 bay.ter_set( p, t_door_o );
2081 } else if( t == t_door_locked || t == t_door_locked_peep || t == t_door_locked_alarm ) {
2082 const map_bash_info &bash = bay.ter( p ).obj().bash;
2083 bay.ter_set( p, bash.ter_set );
2084 bay.spawn_items( p, item_group::items_from( bash.drop_group, calendar::turn ) );
2085 } else if( t == t_door_metal_c || t == t_door_metal_locked || t == t_door_metal_pickable ) {
2086 bay.ter_set( p, t_door_metal_o );
2087 } else if( t == t_door_glass_c ) {
2088 bay.ter_set( p, t_door_glass_o );
2089 } else if( t == t_wall && one_in( 25 ) ) {
2090 const map_bash_info &bash = bay.ter( p ).obj().bash;
2091 bay.ter_set( p, bash.ter_set );
2092 bay.spawn_items( p, item_group::items_from( bash.drop_group, calendar::turn ) );
2093 bay.collapse_at( p, false );
2094 }
2095 //Smash easily breakable stuff
2096 else if( ( t == t_window || t == t_window_taped || t == t_window_domestic ||
2097 t == t_window_boarded_noglass || t == t_window_domestic_taped ||
2098 t == t_window_alarm_taped || t == t_window_boarded ||
2099 t == t_curtains || t == t_window_alarm ||
2100 t == t_window_no_curtains || t == t_window_no_curtains_taped )
2101 && one_in( 4 ) ) {
2102 const map_bash_info &bash = bay.ter( p ).obj().bash;
2103 bay.ter_set( p, bash.ter_set );
2104 bay.spawn_items( p, item_group::items_from( bash.drop_group, calendar::turn ) );
2105 } else if( ( t == t_wall_glass || t == t_wall_glass_alarm ) && one_in( 3 ) ) {
2106 const map_bash_info &bash = bay.ter( p ).obj().bash;
2107 bay.ter_set( p, bash.ter_set );
2108 bay.spawn_items( p, item_group::items_from( bash.drop_group, calendar::turn ) );
2109 } else if( bay.has_furn( p ) && bay.furn( p ).obj().bash.str_max != -1 && one_in( 10 ) ) {
2110 const map_bash_info &bash = bay.furn( p ).obj().bash;
2111 bay.furn_set( p, bash.furn_set );
2112 bay.delete_signage( p );
2113 bay.spawn_items( p, item_group::items_from( bash.drop_group, calendar::turn ) );
2114 }
2115 //Kill zombies! Only works against pre-spawned enemies at the moment...
2116 Creature *critter = g->critter_at( p );
2117 if( critter != nullptr ) {
2118 critter->die( nullptr );
2119 }
2120 //Hoover up tasty items!
2121 map_stack items = bay.i_at( p );
2122 for( map_stack::iterator it = items.begin(); it != items.end(); ) {
2123 if( ( ( it->is_food() || it->is_food_container() ) && !one_in( 8 ) ) ||
2124 ( it->made_of( phase_id::LIQUID ) && !one_in( 8 ) ) ||
2125 ( it->price( true ) > 1000 && !one_in( 4 ) ) || one_in( 5 ) ) {
2126 it = items.erase( it );
2127 } else {
2128 ++it;
2129 }
2130 }
2131 }
2132 bay.save();
2133 overmap_buffer.ter_set( site, oter_id( "looted_building" ) );
2134 }
2135
add(const std::string & id,const std::string & name_display,const std::string & text)2136 void mission_data::add( const std::string &id, const std::string &name_display,
2137 const std::string &text )
2138 {
2139 add( id, name_display, cata::nullopt, text, false, true );
2140 }
add_return(const std::string & id,const std::string & name_display,const cata::optional<point> & dir,const std::string & text,bool possible)2141 void mission_data::add_return( const std::string &id, const std::string &name_display,
2142 const cata::optional<point> &dir, const std::string &text, bool possible )
2143 {
2144 add( id, name_display, dir, text, true, possible );
2145 }
add_start(const std::string & id,const std::string & name_display,const cata::optional<point> & dir,const std::string & text,bool possible)2146 void mission_data::add_start( const std::string &id, const std::string &name_display,
2147 const cata::optional<point> &dir, const std::string &text, bool possible )
2148 {
2149 add( id, name_display, dir, text, false, possible );
2150 }
add(const std::string & id,const std::string & name_display,const cata::optional<point> & dir,const std::string & text,bool priority,bool possible)2151 void mission_data::add( const std::string &id, const std::string &name_display,
2152 const cata::optional<point> &dir, const std::string &text,
2153 bool priority, bool possible )
2154 {
2155 mission_entry miss;
2156 miss.id = id;
2157 if( name_display.empty() ) {
2158 miss.name_display = id;
2159 } else {
2160 miss.name_display = name_display;
2161 }
2162 miss.dir = dir;
2163 miss.text = text;
2164 miss.priority = priority;
2165 miss.possible = possible;
2166
2167 if( priority ) {
2168 entries[0].push_back( miss );
2169 }
2170 if( !possible ) {
2171 entries[10].push_back( miss );
2172 }
2173 const point direction = dir ? *dir : base_camps::base_dir;
2174 const int tab_order = base_camps::all_directions.at( direction ).tab_order;
2175 entries[tab_order + 1].emplace_back( miss );
2176 }
2177