forked from CleverRaven/Cataclysm-DDA
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcharacter_ammo.cpp
549 lines (474 loc) · 20.2 KB
/
character_ammo.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
#include "ammo.h"
#include "character.h"
#include "character_modifier.h"
#include "flag.h"
#include "game.h"
#include "itype.h"
#include "output.h"
static const character_modifier_id character_modifier_reloading_move_mod( "reloading_move_mod" );
static const itype_id itype_battery( "battery" );
static const skill_id skill_gun( "gun" );
int Character::ammo_count_for( const item &gun )
{
int ret = item::INFINITE_CHARGES;
if( !gun.is_gun() ) {
return ret;
}
int required = gun.ammo_required();
if( required > 0 ) {
int total_ammo = 0;
total_ammo += gun.ammo_remaining();
bool has_mag = gun.magazine_integral();
const auto found_ammo = find_ammo( gun, true, -1 );
int loose_ammo = 0;
for( const auto &ammo : found_ammo ) {
if( ammo->is_magazine() ) {
has_mag = true;
total_ammo += ammo->ammo_remaining();
} else if( ammo->is_ammo() ) {
loose_ammo += ammo->charges;
}
}
if( has_mag ) {
total_ammo += loose_ammo;
}
ret = std::min( ret, total_ammo / required );
}
int ups_drain = gun.get_gun_ups_drain();
if( ups_drain > 0 ) {
ret = std::min( ret, available_ups() / ups_drain );
}
return ret;
}
bool Character::can_reload( const item &it, const item *ammo ) const
{
if( ammo && !it.can_reload_with( *ammo, false ) ) {
return false;
}
if( it.is_ammo_belt() ) {
const cata::optional<itype_id> &linkage = it.type->magazine->linkage;
if( linkage && !has_charges( *linkage, 1 ) ) {
return false;
}
}
return true;
}
bool Character::list_ammo( const item &base, std::vector<item::reload_option> &ammo_list,
bool empty ) const
{
// Associate the destination with "parent"
// Useful for handling gun mods with magazines
std::vector<std::pair<const item *, const item *>> opts;
opts.emplace_back( std::make_pair( &base, &base ) );
if( base.magazine_current() ) {
opts.emplace_back( std::make_pair( base.magazine_current(), &base ) );
}
for( const item *mod : base.gunmods() ) {
opts.emplace_back( std::make_pair( mod, mod ) );
if( mod->magazine_current() ) {
opts.emplace_back( std::make_pair( mod->magazine_current(), mod ) );
}
}
bool ammo_match_found = false;
int ammo_search_range = is_mounted() ? -1 : 1;
for( auto p : opts ) {
for( item_location &ammo : find_ammo( *p.first, empty, ammo_search_range ) ) {
if( p.first->can_reload_with( *ammo.get_item(), false ) ) {
// Record that there's a matching ammo type,
// even if something is preventing reloading at the moment.
ammo_match_found = true;
} else if( ammo->has_flag( flag_SPEEDLOADER ) && p.first->allows_speedloader( ammo->typeId() ) &&
ammo->ammo_remaining() > 1 && p.first->ammo_remaining() < 1 ) {
// Again, this is "are they compatible", later check handles "can we do it now".
ammo_match_found = p.first->can_reload_with( *ammo.get_item(), false );
}
if( can_reload( *p.first, ammo.get_item() ) ) {
ammo_list.emplace_back( this, p.first, p.second, std::move( ammo ) );
}
}
}
return ammo_match_found;
}
item::reload_option Character::select_ammo( const item &base,
std::vector<item::reload_option> opts ) const
{
if( opts.empty() ) {
add_msg_if_player( m_info, _( "Never mind." ) );
return item::reload_option();
}
uilist menu;
menu.text = string_format( base.is_watertight_container() ? _( "Refill %s" ) :
base.has_flag( flag_RELOAD_AND_SHOOT ) ? _( "Select ammo for %s" ) : _( "Reload %s" ),
base.tname() );
// Construct item names
std::vector<std::string> names;
std::transform( opts.begin(), opts.end(),
std::back_inserter( names ), [&]( const item::reload_option & e ) {
if( e.ammo->is_magazine() && e.ammo->ammo_data() ) {
if( e.ammo->ammo_current() == itype_battery ) {
// This battery ammo is not a real object that can be recovered but pseudo-object that represents charge
//~ battery storage (charges)
return string_format( pgettext( "magazine", "%1$s (%2$d)" ), e.ammo->type_name(),
e.ammo->ammo_remaining() );
} else {
//~ magazine with ammo (count)
return string_format( pgettext( "magazine", "%1$s with %2$s (%3$d)" ), e.ammo->type_name(),
e.ammo->ammo_data()->nname( e.ammo->ammo_remaining() ), e.ammo->ammo_remaining() );
}
} else if( e.ammo->is_watertight_container() ||
( e.ammo->is_ammo_container() && is_worn( *e.ammo ) ) ) {
// worn ammo containers should be named by their ammo contents with their location also updated below
return e.ammo->first_ammo().display_name();
} else {
return ( ammo_location && ammo_location == e.ammo ? "* " : "" ) + e.ammo->display_name();
}
} );
// Get location descriptions
std::vector<std::string> where;
std::transform( opts.begin(), opts.end(),
std::back_inserter( where ), [this]( const item::reload_option & e ) {
bool is_ammo_container = e.ammo->is_ammo_container();
Character &player_character = get_player_character();
if( is_ammo_container || e.ammo->is_container() ) {
if( is_ammo_container && is_worn( *e.ammo ) ) {
return e.ammo->type_name();
}
return string_format( _( "%s, %s" ), e.ammo->type_name(), e.ammo.describe( &player_character ) );
}
return e.ammo.describe( &player_character );
} );
// Get destination names
std::vector<std::string> destination;
std::transform( opts.begin(), opts.end(),
std::back_inserter( destination ), [&]( const item::reload_option & e ) {
if( e.target == e.getParent() ) {
return e.target->tname( 1, false, 0, false );
} else {
return e.target->tname( 1, false, 0, false ) + " in " + e.getParent()->tname( 1, false, 0, false );
}
} );
// Pads elements to match longest member and return length
auto pad = []( std::vector<std::string> &vec, int n, int t ) -> int {
for( const auto &e : vec )
{
n = std::max( n, utf8_width( e, true ) + t );
}
for( auto &e : vec )
{
e += std::string( n - utf8_width( e, true ), ' ' );
}
return n;
};
// Pad the first column including 4 trailing spaces
int w = pad( names, utf8_width( menu.text, true ), 6 );
menu.text.insert( 0, 2, ' ' ); // add space for UI hotkeys
menu.text += std::string( w + 2 - utf8_width( menu.text, true ), ' ' );
// Pad the location similarly (excludes leading "| " and trailing " ")
w = pad( where, utf8_width( _( "| Location " ) ) - 3, 6 );
menu.text += _( "| Location " );
menu.text += std::string( w + 3 - utf8_width( _( "| Location " ) ), ' ' );
// Pad the names of target
w = pad( destination, utf8_width( _( "| Destination " ) ) - 3, 6 );
menu.text += _( "| Destination " );
menu.text += std::string( w + 3 - utf8_width( _( "| Destination " ) ), ' ' );
menu.text += _( "| Amount " );
menu.text += _( "| Moves " );
// We only show ammo statistics for guns and magazines
if( base.is_gun() || base.is_magazine() ) {
menu.text += _( "| Damage | Pierce " );
}
auto draw_row = [&]( int idx ) {
const auto &sel = opts[ idx ];
std::string row = string_format( "%s| %s | %s |", names[ idx ], where[ idx ], destination[ idx ] );
row += string_format( ( sel.ammo->is_ammo() ||
sel.ammo->is_ammo_container() ) ? " %-7d |" : " |", sel.qty() );
row += string_format( " %-7d ", sel.moves() );
if( base.is_gun() || base.is_magazine() ) {
const itype *ammo = sel.ammo->is_ammo_container() ? sel.ammo->first_ammo().ammo_data() :
sel.ammo->ammo_data();
if( ammo ) {
const damage_instance &dam = ammo->ammo->damage;
row += string_format( "| %-7d | %-7d", static_cast<int>( dam.total_damage() ),
static_cast<int>( dam.empty() ? 0.0f : ( *dam.begin() ).res_pen ) );
} else {
row += "| | ";
}
}
return row;
};
const ammotype base_ammotype( base.ammo_default().str() );
itype_id last = uistate.lastreload[ base_ammotype ];
// We keep the last key so that pressing the key twice (for example, r-r for reload)
// will always pick the first option on the list.
int last_key = inp_mngr.get_previously_pressed_key();
bool last_key_bound = false;
// This is the entry that has out default
int default_to = 0;
// If last_key is RETURN, don't use that to override hotkey
if( last_key == '\n' ) {
last_key_bound = true;
default_to = -1;
}
for( int i = 0; i < static_cast<int>( opts.size() ); ++i ) {
const item &ammo = opts[ i ].ammo->is_ammo_container() ? opts[ i ].ammo->first_ammo() :
*opts[ i ].ammo;
char hotkey = -1;
if( has_item( ammo ) ) {
// if ammo in player possession and either it or any container has a valid invlet use this
if( ammo.invlet ) {
hotkey = ammo.invlet;
} else {
for( const item *obj : parents( ammo ) ) {
if( obj->invlet ) {
hotkey = obj->invlet;
break;
}
}
}
}
if( last == ammo.typeId() ) {
if( !last_key_bound && hotkey == -1 ) {
// If this is the first occurrence of the most recently used type of ammo and the hotkey
// was not already set above then set it to the keypress that opened this prompt
hotkey = last_key;
last_key_bound = true;
}
if( !last_key_bound ) {
// Pressing the last key defaults to the first entry of compatible type
default_to = i;
last_key_bound = true;
}
}
if( hotkey == last_key ) {
last_key_bound = true;
// Prevent the default from being used: key is bound to something already
default_to = -1;
}
menu.addentry( i, true, hotkey, draw_row( i ) );
}
struct reload_callback : public uilist_callback {
public:
std::vector<item::reload_option> &opts;
const std::function<std::string( int )> draw_row;
int last_key;
const int default_to;
const bool can_partial_reload;
reload_callback( std::vector<item::reload_option> &_opts,
std::function<std::string( int )> _draw_row,
int _last_key, int _default_to, bool _can_partial_reload ) :
opts( _opts ), draw_row( _draw_row ),
last_key( _last_key ), default_to( _default_to ),
can_partial_reload( _can_partial_reload )
{}
bool key( const input_context &, const input_event &event, int idx, uilist *menu ) override {
int cur_key = event.get_first_input();
if( default_to != -1 && cur_key == last_key ) {
// Select the first entry on the list
menu->ret = default_to;
return true;
}
if( idx < 0 || idx >= static_cast<int>( opts.size() ) ) {
return false;
}
auto &sel = opts[ idx ];
switch( cur_key ) {
case KEY_LEFT:
if( can_partial_reload ) {
sel.qty( sel.qty() - 1 );
menu->entries[ idx ].txt = draw_row( idx );
}
return true;
case KEY_RIGHT:
if( can_partial_reload ) {
sel.qty( sel.qty() + 1 );
menu->entries[ idx ].txt = draw_row( idx );
}
return true;
}
return false;
}
} cb( opts, draw_row, last_key, default_to, !base.has_flag( flag_RELOAD_ONE ) );
menu.callback = &cb;
menu.query();
if( menu.ret < 0 || static_cast<size_t>( menu.ret ) >= opts.size() ) {
add_msg_if_player( m_info, _( "Never mind." ) );
return item::reload_option();
}
const item_location &sel = opts[ menu.ret ].ammo;
uistate.lastreload[ base_ammotype ] = sel->is_ammo_container() ?
// get first item in all magazine pockets
sel->first_ammo().typeId() :
sel->typeId();
return opts[ menu.ret ];
}
item::reload_option Character::select_ammo( const item &base, bool prompt, bool empty ) const
{
std::vector<item::reload_option> ammo_list;
bool ammo_match_found = list_ammo( base, ammo_list, empty );
if( ammo_list.empty() ) {
if( !is_npc() ) {
if( !base.magazine_integral() && !base.magazine_current() ) {
add_msg_if_player( m_info, _( "You need a compatible magazine to reload the %s!" ),
base.tname() );
} else if( ammo_match_found ) {
add_msg_if_player( m_info, _( "You can't reload anything with the ammo you have on hand." ) );
} else {
std::string name;
if( base.ammo_data() ) {
name = base.ammo_data()->nname( 1 );
} else if( base.is_watertight_container() ) {
name = base.is_container_empty() ? "liquid" : base.legacy_front().tname();
} else {
const std::set<ammotype> types_of_ammo = base.ammo_types();
name = enumerate_as_string( types_of_ammo.begin(),
types_of_ammo.end(), []( const ammotype & at ) {
return at->name();
}, enumeration_conjunction::none );
}
if( base.is_magazine_full() ) {
add_msg_if_player( m_info, _( "The %s is already full!" ),
base.tname() );
} else {
add_msg_if_player( m_info, _( "You don't have any %s to reload your %s!" ),
name, base.tname() );
}
}
}
return item::reload_option();
}
// sort in order of move cost (ascending), then remaining ammo (descending) with empty magazines always last
std::stable_sort( ammo_list.begin(), ammo_list.end(), []( const item::reload_option & lhs,
const item::reload_option & rhs ) {
return lhs.ammo->ammo_remaining() > rhs.ammo->ammo_remaining();
} );
std::stable_sort( ammo_list.begin(), ammo_list.end(), []( const item::reload_option & lhs,
const item::reload_option & rhs ) {
return lhs.moves() < rhs.moves();
} );
std::stable_sort( ammo_list.begin(), ammo_list.end(), []( const item::reload_option & lhs,
const item::reload_option & rhs ) {
return ( lhs.ammo->ammo_remaining() != 0 ) > ( rhs.ammo->ammo_remaining() != 0 );
} );
if( is_npc() ) {
return ammo_list[ 0 ];
}
if( !prompt && ammo_list.size() == 1 ) {
// unconditionally suppress the prompt if there's only one option
return ammo_list[ 0 ];
}
return select_ammo( base, std::move( ammo_list ) );
}
int Character::item_reload_cost( const item &it, const item &ammo, int qty ) const
{
if( ammo.is_ammo() || ammo.is_frozen_liquid() || ammo.made_of_from_type( phase_id::LIQUID ) ) {
qty = std::max( std::min( ammo.charges, qty ), 1 );
} else if( ammo.is_ammo_container() ) {
int min_clamp = 0;
// find the first ammo in the container to get its charges
ammo.visit_items( [&min_clamp]( const item * it, item * ) {
if( it->is_ammo() ) {
min_clamp = it->charges;
return VisitResponse::ABORT;
}
return VisitResponse::NEXT;
} );
qty = clamp( qty, min_clamp, 1 );
} else {
// Handle everything else as magazines
qty = 1;
}
// If necessary create duplicate with appropriate number of charges
item obj = ammo;
obj = obj.split( qty );
if( obj.is_null() ) {
obj = ammo;
}
// No base cost for handling ammo - that's already included in obtain cost
// We have the ammo in our hands right now
int mv = item_handling_cost( obj, true, 0 );
if( ammo.has_flag( flag_MAG_BULKY ) ) {
mv *= 1.5; // bulky magazines take longer to insert
}
if( !it.is_gun() && !it.is_magazine() ) {
return mv + 100; // reload a tool or sealable container
}
/** @EFFECT_GUN decreases the time taken to reload a magazine */
/** @EFFECT_PISTOL decreases time taken to reload a pistol */
/** @EFFECT_SMG decreases time taken to reload an SMG */
/** @EFFECT_RIFLE decreases time taken to reload a rifle */
/** @EFFECT_SHOTGUN decreases time taken to reload a shotgun */
/** @EFFECT_LAUNCHER decreases time taken to reload a launcher */
int cost = 0;
if( it.is_gun() ) {
cost = it.get_reload_time();
} else if( it.type->magazine ) {
cost = it.type->magazine->reload_time * qty;
} else {
cost = it.obtain_cost( ammo );
}
skill_id sk = it.is_gun() ? it.type->gun->skill_used : skill_gun;
mv += cost / ( 1.0f + std::min( get_skill_level( sk ) * 0.1f, 1.0f ) );
if( it.has_flag( flag_STR_RELOAD ) ) {
/** @EFFECT_STR reduces reload time of some weapons */
mv -= get_str() * 20;
}
return std::max( static_cast<int>( std::round( mv * get_modifier(
character_modifier_reloading_move_mod ) ) ), 25 );
}
std::vector<item_location> Character::find_reloadables()
{
std::vector<item_location> reloadables;
visit_items( [this, &reloadables]( item * node, item * ) {
if( node->is_reloadable() ) {
reloadables.emplace_back( *this, node );
}
return VisitResponse::NEXT;
} );
return reloadables;
}
hint_rating Character::rate_action_reload( const item &it ) const
{
hint_rating res = hint_rating::cant;
// Guns may contain additional reloadable mods so check these first
for( const item *mod : it.gunmods() ) {
switch( rate_action_reload( *mod ) ) {
case hint_rating::good:
return hint_rating::good;
case hint_rating::cant:
continue;
case hint_rating::iffy:
res = hint_rating::iffy;
}
}
if( !it.is_reloadable() ) {
return res;
}
return can_reload( it ) ? hint_rating::good : hint_rating::iffy;
}
hint_rating Character::rate_action_unload( const item &it ) const
{
if( it.is_container() && !it.empty() &&
it.can_unload_liquid() ) {
return hint_rating::good;
}
if( it.has_flag( flag_NO_UNLOAD ) ) {
return hint_rating::cant;
}
if( it.magazine_current() ) {
return hint_rating::good;
}
for( const item *e : it.gunmods() ) {
if( ( e->is_gun() && !e->has_flag( flag_NO_UNLOAD ) &&
( e->magazine_current() || e->ammo_remaining() > 0 || e->casings_count() > 0 ) ) ||
( e->has_flag( flag_BRASS_CATCHER ) && !e->is_container_empty() ) ) {
return hint_rating::good;
}
}
if( it.ammo_types().empty() ) {
return hint_rating::cant;
}
if( it.ammo_remaining() > 0 || it.casings_count() > 0 ) {
return hint_rating::good;
}
return hint_rating::iffy;
}