Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannibals are pariahs 2 #77825

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,13 @@
},
{
"text": "Let's trade.",
"topic": "TALK_FREE_MERCHANTS_MERCHANT_DoneTrading",
"condition": { "compare_string": [ "yes", { "u_val": "general_meeting_u_met_Gavin" } ] },
"effect": "start_trade"
"trial": {
"type": "CONDITION",
"condition": { "math": [ "faction_food_supply('free_merchants', 'vitamin':'human_flesh_vitamin')", "<", "1" ] }
},
"success": { "effect": "start_trade", "topic": "TALK_FREE_MERCHANTS_MERCHANT_DoneTrading" },
"failure": { "topic": "TALK_FREE_MERCHANTS_MERCHANT_CANNIBAL_GTFO" }
},
{
"text": "Just saying hello. Keep safe.",
Expand All @@ -55,7 +59,15 @@
"speaker_effect": [ { "effect": "distribute_food_auto" } ],
"responses": [
{ "text": "About other things…", "topic": "TALK_FREE_MERCHANTS_MERCHANT_Talk" },
{ "text": "Let's trade.", "topic": "TALK_FREE_MERCHANTS_MERCHANT_DoneTrading", "effect": "start_trade" },
{
"text": "Let's trade.",
"trial": {
"type": "CONDITION",
"condition": { "math": [ "faction_food_supply('free_merchants', 'vitamin':'human_flesh_vitamin')", "<", "1" ] }
},
"success": { "effect": "start_trade", "topic": "TALK_FREE_MERCHANTS_MERCHANT_DoneTrading" },
"failure": { "topic": "TALK_FREE_MERCHANTS_MERCHANT_CANNIBAL_GTFO" }
},
{ "text": "When will you have new stuff in stock?", "topic": "TALK_FREE_MERCHANTS_MERCHANT_AskedRestock" },
{ "text": "That's all for now. <end_talking_bye>", "topic": "TALK_DONE" }
]
Expand Down Expand Up @@ -94,7 +106,15 @@
],
"responses": [
{ "text": "Got some time to talk?", "topic": "TALK_FREE_MERCHANTS_MERCHANT_Talk" },
{ "text": "Let's trade.", "topic": "TALK_FREE_MERCHANTS_MERCHANT_DoneTrading", "effect": "start_trade" },
{
"text": "Let's trade.",
"trial": {
"type": "CONDITION",
"condition": { "math": [ "faction_food_supply('free_merchants', 'vitamin':'human_flesh_vitamin')", "<", "1" ] }
},
"success": { "effect": "start_trade", "topic": "TALK_FREE_MERCHANTS_MERCHANT_DoneTrading" },
"failure": { "topic": "TALK_FREE_MERCHANTS_MERCHANT_CANNIBAL_GTFO" }
},
{ "text": "I just wanted to look around. <end_talking_bye>", "topic": "TALK_DONE" }
]
},
Expand Down Expand Up @@ -162,7 +182,15 @@
"topic": "TALK_FREE_MERCHANTS_MERCHANT_OutsideWorld"
},
{ "text": "I'm looking for work. Can I do anything for the center?", "topic": "TALK_MISSION_LIST" },
{ "text": "Let's trade.", "topic": "TALK_FREE_MERCHANTS_MERCHANT_DoneTrading", "effect": "start_trade" },
{
"text": "Let's trade.",
"trial": {
"type": "CONDITION",
"condition": { "math": [ "faction_food_supply('free_merchants', 'vitamin':'human_flesh_vitamin')", "<", "1" ] }
},
"success": { "effect": "start_trade", "topic": "TALK_FREE_MERCHANTS_MERCHANT_DoneTrading" },
"failure": { "topic": "TALK_FREE_MERCHANTS_MERCHANT_CANNIBAL_GTFO" }
},
{
"text": "Are you interested in buying any processed lumber?",
"topic": "TALK_EVAC_MERCHANT_DEAL_NEGOTIATE",
Expand Down Expand Up @@ -303,8 +331,12 @@
},
{
"text": "Mind if I take a look at your stock?",
"topic": "TALK_FREE_MERCHANTS_MERCHANT_DoneTrading",
"effect": "start_trade"
"trial": {
"type": "CONDITION",
"condition": { "math": [ "faction_food_supply('free_merchants', 'vitamin':'human_flesh_vitamin')", "<", "1" ] }
},
"success": { "effect": "start_trade", "topic": "TALK_FREE_MERCHANTS_MERCHANT_DoneTrading" },
"failure": { "topic": "TALK_FREE_MERCHANTS_MERCHANT_CANNIBAL_GTFO" }
},
{ "text": "Radios? That seems awfully specific.", "topic": "TALK_FREE_MERCHANTS_MERCHANT_SellingHardware1" },
{ "text": "Good to know.", "topic": "TALK_FREE_MERCHANTS_MERCHANT_Talk" }
Expand All @@ -329,6 +361,17 @@
{ "text": "I'll keep that in mind.", "topic": "TALK_FREE_MERCHANTS_MERCHANT_Talk" }
]
},
{
"id": "TALK_FREE_MERCHANTS_MERCHANT_CANNIBAL_GTFO",
"type": "talk_topic",
"dynamic_line": "Listen, buddy. We were putting away some of the last food you gave us and noticed it was… off. Okay I'll cut the shit- It was people. I don't know how you got it, or what you did, but we aren't taking any chances. You are not welcome here anymore. <color_red>I'll give you one minute</color> to fuck off and never come back.",
"speaker_effect": [
{
"effect": [ "end_conversation", { "run_eocs": [ "EOC_GAVE_BEGGAR_CANNIBAL_PROOF" ], "time_in_future": "1 minutes" } ]
}
],
"responses": [ { "text": "But I-", "topic": "TALK_DONE" } ]
},
{
"id": "TALK_EVAC_MERCHANT_DEAL_NEGOTIATE",
"type": "talk_topic",
Expand Down
1 change: 1 addition & 0 deletions data/json/vitamin.json
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@
"type": "vitamin",
"vit_type": "counter",
"name": { "str": "Consumed human flesh" },
"flags": [ "NO_SELL" ],
"min": 0,
"max": 10000,
"rate": "1 h"
Expand Down
2 changes: 1 addition & 1 deletion doc/NPCs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1350,7 +1350,7 @@ _some functions support array arguments or kwargs, denoted with square brackets
| effect_duration(`s`/`v`) | ✅ | ✅ | u, n | Return the characters duration of effect.<br/>Argument is effect ID.<br/><br/>Optional kwargs:<br/>`bodypart`: `s`/`v` - Specify the bodypart to get/set duration of effect.<br/>`unit`: `s`/`v` - Specify the unit of the duration. Omitting will use seconds.<br/><br/> Example:<br/>`"condition": { "math": [ "u_effect_duration('bite', 'bodypart': 'torso')", ">", "1"] }`<br/>`{ "math": [ "_thing", "=", "u_effect_duration('yrax_overcharged', 'bodypart': 'torso', 'unit': 'hours')" ] }`|
| encumbrance(`s`/`v`) | ✅ | ❌ | u, n | Return the characters total encumbrance of a body part.<br/>Argument is bodypart ID. <br/> For items, returns typical encumbrance of the item. <br/><br/>Example:<br/>`"condition": { "math": [ "u_encumbrance('torso')", ">", "0"] }`|
| energy(`s`/`v`) | ✅ | ❌ | u, n | Return a numeric value (in millijoules) for an energy string (see [Units](JSON_INFO.md#units)).<br/><br/>Example:<br/>`{ "math": [ "u_val('power')", "-=", "energy('25 kJ')" ] }`|
| faction_like(`s`/`v`)<br/>faction_respect(`s`/`v`)<br/>faction_trust(`s`/`v`)<br/>faction_food_supply(`s`/`v`)<br/>faction_wealth(`s`/`v`)<br/>faction_power(`s`/`v`)<br/>faction_size(`s`/`v`) | ✅ | ✅ | N/A<br/>(global) | Return the like/respect/trust/fac_food_supply/wealth/power/size value a faction has for the avatar.<br/>Argument is faction ID.<br/><br/>Example:<br/>`"condition": { "math": [ "faction_like('hells_raiders') < -60" ] }`|
| faction_like(`s`/`v`)<br/>faction_respect(`s`/`v`)<br/>faction_trust(`s`/`v`)<br/>faction_food_supply(`s`/`v`)<br/>faction_wealth(`s`/`v`)<br/>faction_power(`s`/`v`)<br/>faction_size(`s`/`v`) | ✅ | ✅ | N/A<br/>(global) | Return the like/respect/trust/fac_food_supply/wealth/power/size value a faction has for the avatar.<br/>Argument is faction ID.<br/>`faction_food_supply` has an optional second argument(kwarg) for getting/setting vitamins.<br/><br/>Example:<br/>`"condition": { "math": [ "faction_like('hells_raiders') < -60" ] }`<br/><br/>`{ "math": [ "calcium_amount", "=", "faction_food_supply('your_followers', 'vitamin':'calcium')" ] },`<br/>`{ "u_message": "Calcium stored is <global_val:calcium_amount>", "type": "good" },`|
| field_strength(`s`/`v`) | ✅ | ❌ | u, n, global | Return the strength of a field on the tile.<br/>Argument is field ID.<br/><br/>Optional kwargs:<br/> `location`: `v` - center search on this location<br/><br/>The `location` kwarg is mandatory in the global scope.<br/><br/>Examples:<br/>`"condition": { "math": [ "u_field_strength('fd_blood')", ">", "5" ] }`<br/><br/>`"condition": { "math": [ "field_strength('fd_blood_insect', 'location': u_search_loc)", ">", "5" ] }`|
| has_flag(`s`/`v`) | ✅ | ❌ | u, n | Check whether the actor has a flag. Meant to be used as condition for ternaries. Argument is trait ID.<br/><br/> Example:<br/>`"condition": { "math": [ "u_blorg", "=", "u_has_flag('MUTATION_TRESHOLD') ? 100 : 15" ] }`|
| has_trait(`s`/`v`) | ✅ | ❌ | u, n | Check whether the actor has a trait. Meant to be used as condition for ternaries. Argument is trait ID.<br/><br/> Example:<br/>`"condition": { "math": [ "u_blorg", "=", "u_has_trait('FEEBLE') ? 100 : 15" ] }`|
Expand Down
39 changes: 31 additions & 8 deletions src/activity_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,10 @@

static const json_character_flag json_flag_ASOCIAL1( "ASOCIAL1" );
static const json_character_flag json_flag_ASOCIAL2( "ASOCIAL2" );
static const json_character_flag json_flag_CANNIBAL( "CANNIBAL" );

Check failure on line 210 in src/activity_handlers.cpp

View workflow job for this annotation

GitHub Actions / build (src)

Variable 'json_flag_CANNIBAL' declared but not used. [cata-unused-statics,-warnings-as-errors]
static const json_character_flag json_flag_PAIN_IMMUNE( "PAIN_IMMUNE" );
static const json_character_flag json_flag_PSYCHOPATH( "PSYCHOPATH" );

Check failure on line 212 in src/activity_handlers.cpp

View workflow job for this annotation

GitHub Actions / build (src)

Variable 'json_flag_PSYCHOPATH' declared but not used. [cata-unused-statics,-warnings-as-errors]
static const json_character_flag json_flag_SAPIOVORE( "SAPIOVORE" );

Check failure on line 213 in src/activity_handlers.cpp

View workflow job for this annotation

GitHub Actions / build (src)

Variable 'json_flag_SAPIOVORE' declared but not used. [cata-unused-statics,-warnings-as-errors]
static const json_character_flag json_flag_SILENT_SPELL( "SILENT_SPELL" );
static const json_character_flag json_flag_SOCIAL1( "SOCIAL1" );
static const json_character_flag json_flag_SOCIAL2( "SOCIAL2" );
Expand Down Expand Up @@ -479,6 +479,32 @@
corpse_item.set_var( butcher_progress_var( action ), progress );
}

static bool do_cannibalism_piss_people_off( Character &you )
{
if( !you.is_avatar() ) {
return true; // NPCs dont accidentally cause player hate
}

if( !query_yn(
_( "Really desecrate the mortal remains of a fellow human being by butchering them for meat?" ) ) ) {
return false; // player cancels
}

for( npc &guy : g->all_npcs() ) {
if( guy.is_active() && guy.sees( you ) && !guy.okay_with_eating_humans() ) {
guy.say( _( "<swear!>? Are you butchering them? That's not okay, <fuck_you>." ) );
// massive opinion penalty
guy.op_of_u.trust -= 5;
guy.op_of_u.value -= 5;
guy.op_of_u.anger += 5;
if( guy.turned_hostile() ) {
guy.make_angry();
}
}
}
return true;
}

static void set_up_butchery( player_activity &act, Character &you, butcher_type action )
{
const int factor = you.max_quality( action == butcher_type::DISSECT ? qual_CUT_FINE : qual_BUTCHER,
Expand Down Expand Up @@ -597,20 +623,18 @@
}
}


// TODO: Extract this bool into a function
const bool is_human = corpse.id == mtype_id::NULL_ID() || ( ( corpse.in_species( species_HUMAN ) ||
corpse.in_species( species_FERAL ) ) &&
!corpse.in_species( species_ZOMBIE ) );

// applies to all butchery actions except for dissections
if( is_human && action != butcher_type::DISSECT && !( you.has_flag( json_flag_CANNIBAL ) ||
you.has_flag( json_flag_PSYCHOPATH ) || you.has_flag( json_flag_SAPIOVORE ) ) ) {
if( is_human && action != butcher_type::DISSECT && !you.okay_with_eating_humans() ) {
//first determine if the butcherer has the dissect_humans proficiency.
if( you.has_proficiency( proficiency_prof_dissect_humans ) ) {
//if it's player doing the butchery, ask them first.
if( you.is_avatar() ) {
if( query_yn(
_( "Really desecrate the mortal remains of a fellow human being by butchering them for meat?" ) ) ) {
if( do_cannibalism_piss_people_off( you ) ) {
//give the player a random message showing their disgust and cause morale penalty.
switch( rng( 1, 3 ) ) {
case 1:
Expand Down Expand Up @@ -638,7 +662,7 @@
} else {
//this runs if the butcherer does NOT have prof_dissect_humans
if( you.is_avatar() ) {
if( query_yn( _( "Would you dare desecrate the mortal remains of a fellow human being?" ) ) ) {
if( do_cannibalism_piss_people_off( you ) ) {
//random message and morale penalty
switch( rng( 1, 3 ) ) {
case 1:
Expand Down Expand Up @@ -667,8 +691,7 @@
}

// applies to only dissections, so that dissect_humans training makes a difference.
if( is_human && action == butcher_type::DISSECT && !( you.has_flag( json_flag_CANNIBAL ) ||
you.has_flag( json_flag_PSYCHOPATH ) || you.has_flag( json_flag_SAPIOVORE ) ) ) {
if( is_human && action == butcher_type::DISSECT && !you.okay_with_eating_humans() ) {
if( you.has_proficiency( proficiency_prof_dissect_humans ) ) {
//you're either trained for this, densensitized, or both. doesn't bother you.
if( you.is_avatar() ) {
Expand Down
6 changes: 4 additions & 2 deletions src/activity_handlers.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ enum class do_activity_reason : int {
NEEDS_MOP, // This spot can be mopped, if a mop is present.
NEEDS_FISHING, // This spot can be fished, if the right tool is present.
NEEDS_CRAFT, // There is at least one item to craft.
NEEDS_DISASSEMBLE // There is at least one item to disassemble.
NEEDS_DISASSEMBLE, // There is at least one item to disassemble.
REFUSES_THIS_WORK // Character refuses to do this for some reason, maybe against their beliefs or needs danger prompt.

};

Expand Down Expand Up @@ -122,7 +123,8 @@ const std::vector<std::string> do_activity_reason_string = {
"NEEDS_MOP",
"NEEDS_FISHING",
"NEEDS_CRAFT",
"NEEDS_DISASSEMBLE"
"NEEDS_DISASSEMBLE",
"REFUSES_THIS_WORK"
};

struct activity_reason_info {
Expand Down
22 changes: 22 additions & 0 deletions src/activity_item_handling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ static const quality_id qual_WELD( "WELD" );

static const requirement_id requirement_data_mining_standard( "mining_standard" );

static const species_id species_FERAL( "FERAL" );
static const species_id species_HUMAN( "HUMAN" );
static const species_id species_ZOMBIE( "ZOMBIE" );

static const ter_str_id ter_t_stump( "t_stump" );
static const ter_str_id ter_t_trunk( "t_trunk" );

Expand Down Expand Up @@ -1213,6 +1217,16 @@ static activity_reason_info can_do_activity_there( const activity_id &act, Chara
}
}
if( !corpses.empty() ) {
for( item &body : corpses ) {
const mtype &corpse = *body.get_mtype();
// TODO: Extract this bool into a function
const bool is_human = corpse.id == mtype_id::NULL_ID() || ( ( corpse.in_species( species_HUMAN ) ||
corpse.in_species( species_FERAL ) ) &&
!corpse.in_species( species_ZOMBIE ) );
if( is_human && !you.okay_with_eating_humans() ) {
return activity_reason_info::fail( do_activity_reason::REFUSES_THIS_WORK );
}
}
if( big_count > 0 && small_count == 0 ) {
if( !b_rack_present ) {
return activity_reason_info::fail( do_activity_reason::NO_ZONE );
Expand Down Expand Up @@ -2742,6 +2756,14 @@ static requirement_check_result generic_multi_activity_check_requirement(
if( can_do_it ) {
return requirement_check_result::CAN_DO_LOCATION;
}
if( reason == do_activity_reason::REFUSES_THIS_WORK ) {
you.add_msg_if_player( m_info,
_( "There's a human corpse there. You wouldn't want to butcher it by accident." ) );
if( you.is_npc() ) {
add_msg_if_player_sees( you, m_info, _( "%s refuses to butcher a human corpse." ),
you.disp_name() );
}
}
if( reason == do_activity_reason::DONT_HAVE_SKILL ||
reason == do_activity_reason::NO_ZONE ||
reason == do_activity_reason::ALREADY_DONE ||
Expand Down
2 changes: 2 additions & 0 deletions src/character.h
Original file line number Diff line number Diff line change
Expand Up @@ -3386,6 +3386,8 @@ class Character : public Creature, public visitable
int nutrition_for( const item &comest ) const;
/** Can the food be [theoretically] eaten no matter the consequences? */
ret_val<edible_rating> can_eat( const item &food ) const;
/** Would this character be normally willing to consume human flesh? */
bool okay_with_eating_humans() const;
/**
* Same as @ref can_eat, but takes consequences into account.
* Asks about them if @param interactive is true, refuses otherwise.
Expand Down
9 changes: 7 additions & 2 deletions src/consumption.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,12 @@ ret_val<edible_rating> Character::can_eat( const item &food ) const
return ret_val<edible_rating>::make_success();
}

bool Character::okay_with_eating_humans() const
{
return has_flag( STATIC( json_character_flag( "CANNIBAL" ) ) ) ||
has_flag( json_flag_PSYCHOPATH ) || has_flag( json_flag_SAPIOVORE );
}

ret_val<edible_rating> Character::will_eat( const item &food, bool interactive ) const
{
ret_val<edible_rating> ret = can_eat( food );
Expand Down Expand Up @@ -989,8 +995,7 @@ ret_val<edible_rating> Character::will_eat( const item &food, bool interactive )
const bool food_is_human_flesh = food.has_vitamin( vitamin_human_flesh_vitamin ) ||
( food.has_flag( flag_STRICT_HUMANITARIANISM ) &&
!has_flag( json_flag_STRICT_HUMANITARIAN ) );
if( ( food_is_human_flesh && !has_flag( STATIC( json_character_flag( "CANNIBAL" ) ) ) &&
!has_flag( json_flag_PSYCHOPATH ) && !has_flag( json_flag_SAPIOVORE ) ) &&
if( ( food_is_human_flesh && !okay_with_eating_humans() ) &&
( !food.has_flag( flag_HEMOVORE_FUN ) || ( !has_flag( json_flag_BLOODFEEDER ) ) ) ) {
add_consequence( _( "The thought of eating human flesh makes you feel sick." ), CANNIBALISM );
}
Expand Down
10 changes: 10 additions & 0 deletions src/item.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ static const vitamin_id vitamin_human_flesh_vitamin( "human_flesh_vitamin" );

// vitamin flags
static const std::string flag_NO_DISPLAY( "NO_DISPLAY" );
static const std::string flag_NO_SELL( "NO_SELL" );

// fault flags
static const std::string flag_BLACKPOWDER_FOULING_DAMAGE( "BLACKPOWDER_FOULING_DAMAGE" );
Expand Down Expand Up @@ -7115,6 +7116,15 @@ int item::price_no_contents( bool practical, std::optional<int> price_override )
price *= fault->price_mod();
}

if( is_food() && get_comestible() ) {
const nutrients &nutrients_value = default_character_compute_effective_nutrients( *this );
for( const std::pair<const vitamin_id, int> &vit_pair : nutrients_value.vitamins() ) {
if( vit_pair.second > 0 && vit_pair.first->has_flag( flag_NO_SELL ) ) {
price = 0.0;
}
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract this to a function and use it in npc::value(), npc::wants_to_buy(), and npc::wants_to_sell() rather than here. That way the item is untradeable rather than just worthless, thieving NPCs won't try to pick it up, and you can display a refusal reason in the trade UI.

return price;
}

Expand Down
17 changes: 13 additions & 4 deletions src/math_parser_diag.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -268,19 +268,28 @@ diag_assign_dbl_f faction_trust_ass( char /* scope */, std::vector<diag_value> c
}

diag_eval_dbl_f faction_food_supply_eval( char /* scope */,
std::vector<diag_value> const &params, diag_kwargs const &/* kwargs */ )
std::vector<diag_value> const &params, diag_kwargs const &kwargs )
{
return [fac_val = params[0]]( const_dialogue const & d ) {
diag_value vit_val = kwargs.kwarg_or( "vitamin" );
return [fac_val = params[0], vit_val]( const_dialogue const & d ) {
faction *fac = g->faction_manager_ptr->get( faction_id( fac_val.str( d ) ) );
if( !vit_val.is_empty() ) {
return static_cast<double>( fac->food_supply.get_vitamin( vitamin_id( vit_val.str( d ) ) ) );
}
return static_cast<double>( fac->food_supply.calories );
};
}

diag_assign_dbl_f faction_food_supply_ass( char /* scope */,
std::vector<diag_value> const &params, diag_kwargs const &/* kwargs */ )
std::vector<diag_value> const &params, diag_kwargs const &kwargs )
{
return [fac_val = params[0]]( dialogue const & d, double val ) {
diag_value vit_val = kwargs.kwarg_or( "vitamin" );
return [fac_val = params[0], vit_val]( dialogue const & d, double val ) {
faction *fac = g->faction_manager_ptr->get( faction_id( fac_val.str( d ) ) );
if( !vit_val.is_empty() ) {
fac->food_supply.add_vitamin( vitamin_id( vit_val.str( d ) ), val );
return;
}
fac->food_supply.calories = val;
};
}
Expand Down
Loading