diff --git a/doc/NPCs.md b/doc/NPCs.md
index e43a706671349..12d389e6dda2c 100644
--- a/doc/NPCs.md
+++ b/doc/NPCs.md
@@ -449,6 +449,8 @@ Effect | Description
`add_debt: mod_list` | Increases the NPC's debt to the player by the values in the `mod_list`.
The following would increase the NPC's debt to the player by 1500x the NPC's altruism and 1000x the NPC's opinion of the player's value: `{ "effect": { "add_debt": [ [ "ALTRUISM", 3 ], [ "VALUE", 2 ], [ "TOTAL", 500 ] ] } }`
`u_consume_item`, `npc_consume_item: item_string`, (*optional* `count: count_num`) | You or the NPC will delete the item or `count_num` copies of the item from their inventory.
This effect will fail if the you or NPC does not have at least `count_num` copies of the item, so it should be checked with `u_has_items` or `npc_has_items`.
`u_remove_item_with`, `npc_remove_item_with: item_string` | You or the NPC will delete any instances of item in inventory.
This is an unconditional remove and will not fail if you or the NPC does not have the item.
+`u_buy_monster: monster_type_string`, (*optional* `cost: cost_num`, *optional* `count: count_num`, *optional* `name: name_string`, *optional* `pacified: pacified_bool`) | The NPC will give your character `count_num` (default 1) instances of the monster as pets and will subtract `cost_num` from `op_of_u.owed` if specified. If the `op_o_u.owed` is less than `cost_num`, the trade window will open and the player will have to trade to make up the difference; the NPC will not give the player the item unless `cost_num` is satisfied.
If cost isn't present, the NPC gives your character the item at no charge.
If `name_string` is specified the monster(s) will have the specified name. If `pacified_bool` is set to true, the monster will have the pacified effect applied.
+
#### Behaviour / AI
diff --git a/src/dialogue.h b/src/dialogue.h
index 057a2f4a4b72b..f27e65cda9050 100644
--- a/src/dialogue.h
+++ b/src/dialogue.h
@@ -121,6 +121,8 @@ struct talk_effect_fun_t {
void set_bulk_trade_accept( bool is_trade, bool is_npc = false );
void set_npc_gets_item( bool to_use );
void set_add_mission( std::string mission_id );
+ void set_u_buy_monster( const std::string &monster_id, int cost, int count, bool pacified,
+ const translation &name );
void operator()( const dialogue &d ) const {
if( !function ) {
diff --git a/src/npctalk.cpp b/src/npctalk.cpp
index 6c93075b3a788..d7e15146d5048 100644
--- a/src/npctalk.cpp
+++ b/src/npctalk.cpp
@@ -32,6 +32,7 @@
#include "json.h"
#include "line.h"
#include "map.h"
+#include "map_iterator.h"
#include "mapgen_functions.h"
#include "martialarts.h"
#include "messages.h"
@@ -2097,6 +2098,55 @@ void talk_effect_fun_t::set_add_mission( const std::string mission_id )
};
}
+void talk_effect_fun_t::set_u_buy_monster( const std::string &monster_type_id, int cost, int count,
+ bool pacified, const translation &name )
+{
+ function = [monster_type_id, cost, count, pacified, name]( const dialogue & d ) {
+ npc &p = *d.beta;
+ player &u = *d.alpha;
+ if( !npc_trading::pay_npc( p, cost ) ) {
+ popup( _( "You can't afford it!" ) );
+ return;
+ }
+
+ const mtype_id mtype( monster_type_id );
+ const efftype_id effect_pet( "pet" );
+ const efftype_id effect_pacified( "pacified" );
+ const tripoint_range points = g->m.points_in_radius( u.pos(), 3 );
+
+ for( int i = 0; i < count; i++ ) {
+ monster tmp( mtype );
+
+ // Our monster is always a pet.
+ tmp.friendly = -1;
+ tmp.add_effect( effect_pet, 1_turns, num_bp, true );
+
+ if( pacified ) {
+ tmp.add_effect( effect_pacified, 1_turns, num_bp, true );
+ }
+
+ if( !name.empty() ) {
+ tmp.unique_name = name.translated();
+ }
+
+ if( const cata::optional pos = random_point( points, [&]( const tripoint & p ) {
+ return g->is_empty( p ) && tmp.can_move_to( p );
+ } ) ) {
+ tmp.spawn( *pos );
+ g->add_zombie( tmp );
+ } else {
+ add_msg( m_debug, "Cannot place u_buy_monster, no valid placement locations." );
+ }
+ }
+
+ if( name.empty() ) {
+ popup( _( "%1$s gives you %2$d %3$s." ), p.name, count, mtype.obj().nname( count ) );
+ } else {
+ popup( _( "%1$s gives you %2$s." ), p.name, name );
+ }
+ };
+}
+
void talk_effect_t::set_effect_consequence( const talk_effect_fun_t &fun, dialogue_consequence con )
{
effects.push_back( fun );
@@ -2293,6 +2343,14 @@ void talk_effect_t::parse_sub_effect( JsonObject jo )
subeffect_fun.set_npc_cbm_recharge_rule( setting );
} else if( jo.has_member( "mapgen_update" ) ) {
subeffect_fun.set_mapgen_update( jo, "mapgen_update" );
+ } else if( jo.has_string( "u_buy_monster" ) ) {
+ const std::string &monster_type_id = jo.get_string( "u_buy_monster" );
+ const int cost = jo.get_int( "cost", 0 );
+ const int count = jo.get_int( "count", 1 );
+ const bool pacified = jo.get_bool( "pacified", false );
+ translation name;
+ jo.read( "name", name );
+ subeffect_fun.set_u_buy_monster( monster_type_id, cost, count, pacified, name );
} else {
jo.throw_error( "invalid sub effect syntax :" + jo.str() );
}