diff --git a/data/json/monsters/bird.json b/data/json/monsters/bird.json index 6b3e2e45d8ba..dd178c578865 100644 --- a/data/json/monsters/bird.json +++ b/data/json/monsters/bird.json @@ -108,11 +108,16 @@ "type": "MONSTER", "copy-from": "mon_duck", "name": { "str": "goose", "str_pl": "geese" }, + "aggression": 0, + "morale": 15, + "aggro_character": false, "description": "A Canadian goose, a common waterfowl that regrets leaving Canada.", "volume": "5 L", "weight": "3750 g", "hp": 15, "dodge": 3, + "anger_triggers": [ "PLAYER_CLOSE", "FRIEND_ATTACKED" ], + "fear_triggers": [ "FRIEND_DIED", "FIRE" ], "reproduction": { "baby_egg": "egg_goose_canadian", "baby_count": 3, "baby_timer": 10 }, "baby_flags": [ "SPRING" ] }, diff --git a/data/json/monsters/fish.json b/data/json/monsters/fish.json index 7fe0066845d2..5b6544579c95 100644 --- a/data/json/monsters/fish.json +++ b/data/json/monsters/fish.json @@ -15,6 +15,7 @@ "material": [ "flesh" ], "symbol": "Y", "color": "red", + "aggro_character": false, "aggression": 15, "morale": 100, "melee_skill": 6, @@ -645,6 +646,7 @@ "material": [ "flesh" ], "symbol": "Y", "color": "magenta", + "aggro_character": false, "aggression": 4, "morale": 100, "melee_skill": 6, diff --git a/data/json/monsters/insect_spider.json b/data/json/monsters/insect_spider.json index 796c4b305840..23a8c156c99c 100644 --- a/data/json/monsters/insect_spider.json +++ b/data/json/monsters/insect_spider.json @@ -326,6 +326,7 @@ "material": [ "iflesh" ], "symbol": "a", "color": "yellow", + "aggro_character": false, "aggression": -10, "morale": 20, "melee_skill": 4, @@ -367,6 +368,7 @@ "hp": 15, "speed": 115, "attack_cost": 115, + "aggro_character": false, "aggression": 0, "morale": 0, "melee_skill": 5, @@ -406,6 +408,7 @@ "hp": 35, "speed": 110, "attack_cost": 110, + "aggro_character": false, "aggression": 9, "morale": 20, "melee_skill": 6, @@ -451,6 +454,7 @@ "volume": "300 L", "weight": "400 kg", "speed": 60, + "aggro_character": false, "aggression": 0, "morale": 10, "melee_skill": 3, @@ -499,6 +503,7 @@ "hp": 125, "speed": 110, "attack_cost": 110, + "aggro_character": false, "aggression": 100, "regen_morale": true, "melee_skill": 6, @@ -746,6 +751,7 @@ "material": [ "iflesh" ], "symbol": "s", "color": "brown", + "aggro_character": false, "aggression": -10, "morale": 100, "melee_skill": 6, @@ -815,6 +821,7 @@ "material": [ "iflesh" ], "symbol": "s", "color": "white", + "aggro_character": false, "aggression": -20, "morale": 80, "melee_skill": 5, @@ -882,6 +889,7 @@ "material": [ "iflesh" ], "symbol": "s", "color": "yellow", + "aggro_character": false, "aggression": 20, "morale": 100, "melee_skill": 4, @@ -914,6 +922,7 @@ "material": [ "iflesh" ], "symbol": "c", "color": "yellow", + "aggro_character": false, "aggression": 20, "morale": 100, "melee_skill": 1, @@ -946,6 +955,7 @@ "material": [ "iflesh" ], "symbol": "s", "color": "dark_gray", + "aggro_character": false, "aggression": -10, "morale": 100, "melee_skill": 6, @@ -980,6 +990,7 @@ "material": [ "iflesh" ], "symbol": "c", "color": "dark_gray", + "aggro_character": false, "aggression": -10, "morale": 100, "melee_skill": 4, @@ -1013,6 +1024,7 @@ "material": [ "iflesh" ], "symbol": "s", "color": "brown", + "aggro_character": false, "aggression": 20, "morale": 100, "melee_skill": 5, @@ -1086,6 +1098,7 @@ "material": [ "iflesh" ], "symbol": "a", "color": "dark_gray", + "aggro_character": false, "aggression": -10, "melee_skill": 5, "melee_dice": 2, @@ -1171,6 +1184,7 @@ "material": [ "iflesh" ], "symbol": "a", "color": "dark_gray", + "aggro_character": false, "aggression": -5, "morale": 15, "melee_skill": 7, @@ -1421,6 +1435,7 @@ "material": [ "iflesh" ], "symbol": "a", "color": "brown", + "aggro_character": false, "morale": 60, "melee_skill": 5, "melee_dice": 1, @@ -1456,6 +1471,7 @@ "material": [ "iflesh" ], "symbol": "a", "color": "green", + "aggro_character": false, "morale": 60, "melee_skill": 5, "melee_dice": 1, @@ -1548,6 +1564,7 @@ "material": [ "iflesh" ], "symbol": "a", "color": "brown", + "aggro_character": false, "aggression": 40, "morale": 100, "melee_skill": 7, @@ -1638,6 +1655,7 @@ "material": [ "iflesh" ], "symbol": "a", "color": "red", + "aggro_character": false, "aggression": 30, "morale": 100, "melee_skill": 7, diff --git a/data/json/monsters/mammal.json b/data/json/monsters/mammal.json index b59aee9c4edc..3f5e84c60709 100644 --- a/data/json/monsters/mammal.json +++ b/data/json/monsters/mammal.json @@ -67,6 +67,7 @@ "material": [ "flesh" ], "symbol": "B", "color": "dark_gray", + "aggro_character": false, "aggression": -10, "morale": 60, "melee_skill": 6, @@ -107,6 +108,7 @@ "material": [ "flesh" ], "symbol": "r", "color": "brown", + "aggro_character": false, "aggression": -35, "morale": 15, "melee_dice": 1, @@ -135,6 +137,7 @@ "material": [ "flesh" ], "symbol": "r", "color": "dark_gray", + "aggro_character": false, "aggression": 5, "morale": 5, "melee_skill": 5, @@ -169,6 +172,7 @@ "symbol": "b", "color": "brown", "looks_like": "mon_pig_piglet", + "aggro_character": false, "aggression": -10, "morale": 10, "melee_skill": 1, @@ -202,6 +206,7 @@ "material": [ "flesh" ], "symbol": "b", "color": "brown", + "aggro_character": false, "aggression": 20, "morale": 40, "melee_skill": 5, @@ -239,6 +244,7 @@ "material": [ "flesh" ], "symbol": "c", "color": "brown", + "aggro_character": false, "aggression": -25, "morale": 1, "melee_skill": 6, @@ -661,6 +667,7 @@ "material": [ "flesh" ], "symbol": "C", "color": "light_gray", + "aggro_character": false, "aggression": -50, "morale": 60, "melee_skill": 6, @@ -835,6 +842,7 @@ "material": [ "flesh" ], "symbol": "d", "color": "light_gray", + "aggro_character": false, "aggression": -10, "morale": 15, "melee_skill": 6, @@ -881,6 +889,7 @@ "weight": "1 kg", "hp": 8, "speed": 98, + "aggro_character": false, "aggression": -13, "morale": 8, "melee_skill": 2, @@ -1699,6 +1708,7 @@ "material": [ "flesh" ], "symbol": "d", "color": "light_gray", + "aggro_character": false, "aggression": -5, "morale": 30, "melee_skill": 3, @@ -1730,6 +1740,7 @@ "material": [ "flesh" ], "symbol": "d", "color": "red", + "aggro_character": false, "aggression": -5, "morale": 60, "melee_skill": 4, @@ -1848,6 +1859,7 @@ "material": [ "flesh" ], "symbol": "H", "color": "brown", + "aggro_character": false, "melee_skill": 6, "melee_dice": 2, "melee_dice_sides": 12, @@ -1937,6 +1949,7 @@ "material": [ "flesh" ], "symbol": "M", "color": "brown", + "aggro_character": false, "aggression": 5, "morale": 80, "melee_skill": 6, @@ -2131,6 +2144,7 @@ "material": [ "flesh" ], "symbol": "p", "color": "pink", + "aggro_character": false, "aggression": 10, "morale": 30, "melee_skill": 4, @@ -2248,6 +2262,7 @@ "material": [ "flesh" ], "symbol": "r", "color": "light_gray", + "aggro_character": false, "aggression": 20, "morale": 40, "melee_skill": 5, @@ -2425,6 +2440,7 @@ "material": [ "flesh" ], "symbol": "w", "color": "light_gray", + "aggro_character": false, "morale": 20, "melee_skill": 7, "melee_dice": 2, diff --git a/data/json/monsters/mutant_animal.json b/data/json/monsters/mutant_animal.json index e65b82c32a4a..663c06a37c90 100644 --- a/data/json/monsters/mutant_animal.json +++ b/data/json/monsters/mutant_animal.json @@ -6,6 +6,7 @@ "description": "As it moves, this cat's colors shift unnaturally in the light. You can't make out the breed, but its fur pattern seems to change with every step.", "copy-from": "mon_cat", "luminance": 5, + "aggro_character": false, "//": "Higher dodge due to the constantly changing fur pattern", "dodge": 9, "reproduction": { "baby_monster": "mon_cat_mutant_kitten_prism", "baby_count": 2, "baby_timer": 80 }, @@ -109,6 +110,7 @@ "symbol": "B", "color": "dark_gray", "looks_like": "mon_bear", + "aggro_character": false, "morale": 60, "melee_skill": 6, "melee_dice": 4, @@ -151,6 +153,7 @@ "symbol": "r", "color": "brown", "looks_like": "mon_beaver", + "aggro_character": false, "aggression": -10, "morale": 20, "melee_dice": 2, @@ -181,6 +184,7 @@ "symbol": "r", "color": "brown", "looks_like": "mon_beaver", + "aggro_character": false, "aggression": -50, "morale": 5, "melee_dice": 1, @@ -407,6 +411,7 @@ "symbol": "d", "color": "light_gray", "looks_like": "mon_dog", + "aggro_character": false, "aggression": -15, "morale": 15, "melee_skill": 6, diff --git a/data/json/monsters/mutant_human.json b/data/json/monsters/mutant_human.json index a0442be37f23..9a358652ccc0 100644 --- a/data/json/monsters/mutant_human.json +++ b/data/json/monsters/mutant_human.json @@ -47,6 +47,7 @@ "symbol": "M", "color": "brown", "morale": 50, + "aggro_character": false, "melee_skill": 6, "melee_dice": 3, "melee_dice_sides": 6, diff --git a/data/json/monsters/reptile_amphibian.json b/data/json/monsters/reptile_amphibian.json index 0d46cb9dd04f..41782581b36b 100644 --- a/data/json/monsters/reptile_amphibian.json +++ b/data/json/monsters/reptile_amphibian.json @@ -14,6 +14,7 @@ "material": [ "flesh" ], "symbol": "M", "color": "light_green", + "aggro_character": false, "aggression": 10, "morale": 100, "melee_skill": 5, @@ -95,6 +96,7 @@ "material": [ "flesh" ], "symbol": "s", "color": "brown", + "aggro_character": false, "aggression": -50, "morale": 60, "melee_skill": 5, @@ -129,6 +131,7 @@ "material": [ "flesh" ], "symbol": "s", "color": "brown", + "aggro_character": false, "aggression": -10, "morale": 60, "melee_skill": 6, diff --git a/doc/src/content/docs/en/mod/json/reference/creatures/monsters.md b/doc/src/content/docs/en/mod/json/reference/creatures/monsters.md index fa32a2afc808..184acc03f23f 100644 --- a/doc/src/content/docs/en/mod/json/reference/creatures/monsters.md +++ b/doc/src/content/docs/en/mod/json/reference/creatures/monsters.md @@ -218,6 +218,14 @@ hostility on detection) Monster morale. Defines how low monster HP can get before it retreats. This number is treated as % of their max HP. +## "aggro_character" + +(bool, optional, default true) + +If the monster will differentiate between monsters and characters (NPC, Player) when deciding on +targets - if false the monster will ignore characters regardless of current anger/morale until a +character trips and anger trigger. Resets randomly when the monster is at its base anger level. + ## "speed" (integer) diff --git a/doc/src/content/docs/en/mod/json/reference/json_flags.md b/doc/src/content/docs/en/mod/json/reference/json_flags.md index c96ddd250869..f3ff937630d1 100644 --- a/doc/src/content/docs/en/mod/json/reference/json_flags.md +++ b/doc/src/content/docs/en/mod/json/reference/json_flags.md @@ -940,12 +940,13 @@ Flags used to describe monsters and define their properties and abilities. ### Anger, Fear and Placation Triggers - `FIRE` There's a fire nearby. -- `FRIEND_ATTACKED` A monster of the same type was attacked. -- `FRIEND_DIED` A monster of the same type died. -- `HURT` The monster is hurt. +- `FRIEND_ATTACKED` A monster of the same type was attacked. Always triggers character aggro. +- `FRIEND_DIED` A monster of the same type died. Always triggers character aggro. +- `HURT` The monster is hurt. Always triggers character aggro. - `MEAT` Meat or a corpse is nearby. - `NULL` Source use only? -- `PLAYER_CLOSE` The player gets within a few tiles distance. +- `PLAYER_CLOSE` The player gets within a few tiles distance. Triggers character aggro `%` of + the time. - `PLAYER_WEAK` The player is hurt. - `SOUND` Heard a sound. - `STALK` Increases when following the player. diff --git a/src/mattack_actors.cpp b/src/mattack_actors.cpp index 9234b133e15e..fe240468d8c0 100644 --- a/src/mattack_actors.cpp +++ b/src/mattack_actors.cpp @@ -579,7 +579,7 @@ bool gun_actor::call( monster &z ) const aim_at = target->pos(); } else { target = z.attack_target(); - if( !target || !z.sees( *target ) ) { + if( !target || ( !target->is_monster() && !z.aggro_character ) || !z.sees( *target ) ) { if( !target_moving_vehicles ) { return false; } diff --git a/src/monmove.cpp b/src/monmove.cpp index b526d5c79d98..674496a6080d 100644 --- a/src/monmove.cpp +++ b/src/monmove.cpp @@ -354,6 +354,9 @@ void monster::plan() target = &g->u; if( dist <= 5 ) { anger += angers_hostile_near; + if( angers_hostile_near ) { + trigger_character_aggro_chance( anger, "proximity" ); + } morale -= fears_hostile_near; if( angers_mating_season > 0 ) { bool mating_angry = false; @@ -369,6 +372,7 @@ void monster::plan() } if( mating_angry ) { anger += angers_mating_season; + trigger_character_aggro_chance( anger, "mating season" ); } } } @@ -381,6 +385,7 @@ void monster::plan() //proximity to baby; monster gets furious and less likely to flee anger += angers_cub_threatened; morale += angers_cub_threatened / 2; + trigger_character_aggro( "threatening cub" ); } } } @@ -444,6 +449,7 @@ void monster::plan() } if( mating_angry ) { anger += angers_mating_season; + trigger_character_aggro_chance( anger, "mating season" ); } } } @@ -586,6 +592,9 @@ void monster::plan() int hp_per = target->hp_percentage(); if( hp_per <= 70 ) { anger += 10 - ( hp_per / 10 ); + if( anger <= 40 ) { + trigger_character_aggro_chance( anger, "weakness" ); + } } } } else if( friendly > 0 && one_in( 3 ) ) { diff --git a/src/monster.cpp b/src/monster.cpp index 85e368dab082..b8bf6545e6b8 100644 --- a/src/monster.cpp +++ b/src/monster.cpp @@ -222,6 +222,7 @@ monster::monster() : corpse_components( new monster_component_item_location( thi last_updated = calendar::start_of_cataclysm; udder_timer = calendar::turn; horde_attraction = MHA_NULL; + aggro_character = true; set_anatomy( anatomy_id( "default_anatomy" ) ); set_body(); } @@ -242,6 +243,7 @@ monster::monster( const mtype_id &id ) : monster() ammo = type->starting_ammo; upgrades = type->upgrades && ( type->half_life || type->age_grow ); reproduces = type->reproduces && type->baby_timer && !monster::has_flag( MF_NO_BREED ); + aggro_character = type->aggro_character; if( monster::has_flag( MF_AQUATIC ) ) { fish_population = dice( 1, 20 ); } @@ -1452,13 +1454,13 @@ monster_attitude monster::attitude( const Character *u ) const } } + if( effective_morale < 0 ) { if( effective_morale + effective_anger > 0 && get_hp() > get_hp_max() / 3 ) { return MATT_FOLLOW; } return MATT_FLEE; } - if( effective_anger <= 0 ) { if( get_hp() <= 0.6 * get_hp_max() ) { return MATT_FLEE; @@ -1471,6 +1473,10 @@ monster_attitude monster::attitude( const Character *u ) const return MATT_FOLLOW; } + if( u != nullptr && !aggro_character && !u->is_monster() ) { + return MATT_IGNORE; + } + return MATT_ATTACK; } @@ -1513,6 +1519,12 @@ void monster::process_triggers() } } + // If we got angry at characters have a chance at calming down + if( anger == type->agro && aggro_character && !type->aggro_character && !x_in_y( anger, 100 ) ) { + add_msg( m_debug, "%s's character aggro reset", get_name() ); + aggro_character = false; + } + // Cap values at [-100, 100] to prevent perma-angry moose etc. morale = std::min( 100, std::max( -100, morale ) ); anger = std::min( 100, std::max( -100, anger ) ); @@ -1545,6 +1557,22 @@ void monster::process_trigger( mon_trigger trig, const std::function &amo } } + +// hopefully a good spot for these functions +// eg. reason = "mating season" +void monster::trigger_character_aggro( const char *reason ) +{ + add_msg( m_debug, "%s's character aggro is triggered by %s", get_name(), reason ); + aggro_character = true; +} + +void monster::trigger_character_aggro_chance( int chance, const char *reason ) +{ + if( x_in_y( chance, 100 ) ) { + trigger_character_aggro( reason ); + } +} + bool monster::is_underwater() const { return Creature::is_underwater() && can_submerge(); @@ -1947,6 +1975,10 @@ void monster::apply_damage( Creature *source, item *source_weapon, item *source_ } } else if( dam > 0 ) { process_trigger( mon_trigger::HURT, 1 + ( dam / 3 ) ); + // Get angry at characters if hurt by one + if( source != nullptr && !aggro_character && !source->is_monster() && !source->is_fake() ) { + trigger_character_aggro( "hurt" ); + } } } void monster::apply_damage( Creature *source, item *source_weapon, bodypart_id bp, int dam, @@ -2679,6 +2711,10 @@ void monster::die( Creature *nkiller ) int morale_adjust = 0; if( type->has_anger_trigger( mon_trigger::FRIEND_DIED ) ) { anger_adjust += 15; + if( nkiller != nullptr && !nkiller->is_monster() && !nkiller->is_fake() ) { + // A character killed our friend + trigger_character_aggro( "killing a friendly creature" ); + } } if( type->has_fear_trigger( mon_trigger::FRIEND_DIED ) ) { morale_adjust -= 15; @@ -3178,6 +3214,10 @@ void monster::on_hit( Creature *source, bodypart_id, dealt_projectile_attack con int morale_adjust = 0; if( type->has_anger_trigger( mon_trigger::FRIEND_ATTACKED ) ) { anger_adjust += 15; + if( source != nullptr && !aggro_character && !source->is_monster() && !source->is_fake() ) { + // A character attacked our friend + trigger_character_aggro( "killing a friendly creature" ); + } } if( type->has_fear_trigger( mon_trigger::FRIEND_ATTACKED ) ) { morale_adjust -= 15; @@ -3344,6 +3384,36 @@ void monster::on_load() if( dt <= 0_turns ) { return; } + + if( anger != type->agro ) { + int dt_left_a = to_turns( dt ); + + if( std::abs( anger - type->agro ) > 15 ) { + const int adjust_by_a = std::min( ( dt_left_a / 4 ), + ( std::abs( anger - type->agro ) - 15 ) ); + dt_left_a -= adjust_by_a * 4; + if( anger < type->agro ) { + anger += adjust_by_a; + } else { + anger -= adjust_by_a; + } + } + + if( anger > type->agro ) { + anger -= std::min( static_cast( std::ceil( dt_left_a / 8.0 ) ), + std::abs( anger - type->agro ) ); + } else { + anger += std::min( ( dt_left_a / 8 ), + std::abs( anger - type->agro ) ); + } + // If we got angry at characters have a chance at calming down + if( aggro_character && !type->aggro_character && !x_in_y( anger, 100 ) ) { + add_msg( m_debug, "%s's character aggro reset", name() ); + aggro_character = false; + } + } + + float regen = type->regenerates; if( regen <= 0 ) { if( has_flag( MF_REVIVES ) ) { diff --git a/src/monster.h b/src/monster.h index 18d6cb72e161..8f339a558ad6 100644 --- a/src/monster.h +++ b/src/monster.h @@ -532,6 +532,9 @@ class monster : public Creature, public location_visitable } short ignoring; + + bool aggro_character = true; + std::optional lastseen_turn; // Stair data. @@ -601,6 +604,9 @@ class monster : public Creature, public location_visitable void process_trigger( mon_trigger trig, int amount ); void process_trigger( mon_trigger trig, const std::function &amount_func ); + void trigger_character_aggro( const char *reason ); + void trigger_character_aggro_chance( int chance, const char *reason ); + location_vector corpse_components; // Hack to make bionic corpses generate CBMs on death private: @@ -624,6 +630,7 @@ class monster : public Creature, public location_visitable std::bitset effect_cache; std::optional summon_time_limit = std::nullopt; + player *find_dragged_foe(); void nursebot_operate( player *dragged_foe ); diff --git a/src/monstergenerator.cpp b/src/monstergenerator.cpp index 01f1c5300cb1..cead2bf257f9 100644 --- a/src/monstergenerator.cpp +++ b/src/monstergenerator.cpp @@ -821,6 +821,7 @@ void mtype::load( const JsonObject &jo, const std::string &src ) optional( jo, was_loaded, "mech_weapon", mech_weapon, itype_id() ); optional( jo, was_loaded, "mech_str_bonus", mech_str_bonus, 0 ); optional( jo, was_loaded, "mech_battery", mech_battery, itype_id() ); + optional( jo, was_loaded, "aggro_character", aggro_character, true ); // TODO: make this work with `was_loaded` if( jo.has_array( "melee_damage" ) ) { diff --git a/src/mtype.cpp b/src/mtype.cpp index ac37d6aa9556..fc340a804403 100644 --- a/src/mtype.cpp +++ b/src/mtype.cpp @@ -56,6 +56,8 @@ mtype::mtype() luminance = 0; bash_skill = 0; + aggro_character = true; + flags .set( MF_HUMAN ) .set( MF_BONES ) diff --git a/src/mtype.h b/src/mtype.h index 4127cd5ba011..2345ffc27776 100644 --- a/src/mtype.h +++ b/src/mtype.h @@ -370,6 +370,9 @@ struct mtype { bool upgrades; bool reproduces; + // Do we indiscriminately attack characters, or should we wait until one annoys us? + bool aggro_character = true; + mtype(); /** * Check if this type is of the same species as the other one, because diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index a1be2df9c56d..c32f349db388 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -1843,6 +1843,7 @@ void monster::load( const JsonObject &data ) data.read( "anger", anger ); data.read( "morale", morale ); data.read( "hallucination", hallucination ); + data.read( "aggro_character", aggro_character ); data.read( "stairscount", staircount ); // really? data.read( "fish_population", fish_population ); // Load legacy plans. @@ -1935,6 +1936,7 @@ void monster::store( JsonOut &json ) const json.member( "anger", anger ); json.member( "morale", morale ); json.member( "hallucination", hallucination ); + json.member( "aggro_character", aggro_character ); json.member( "stairscount", staircount ); if( tied_item ) { json.member( "tied_item", *tied_item );