From 1813bfb9edc6d8206597a4ce14c51aad33f36638 Mon Sep 17 00:00:00 2001 From: Kevin Granade Date: Mon, 25 May 2020 01:04:25 -0700 Subject: [PATCH] Behavior tree special attack integration (#40555) * add an argument to behavior tree predicates * Support for selecting a special attack via behavior tree and integrate with first special --- data/json/monsters/monster_goals.json | 23 +++++++++++++++++++---- src/behavior.cpp | 20 ++++++++++++++++---- src/behavior.h | 8 +++++--- src/behavior_oracle.cpp | 24 ++++++++++++++++-------- src/behavior_oracle.h | 5 +++-- src/character_oracle.cpp | 16 ++++++++-------- src/character_oracle.h | 18 ++++++++++-------- src/init.cpp | 2 +- src/monattack.cpp | 14 ++++++++++---- src/monmove.cpp | 9 +++++++++ src/monster.cpp | 14 ++++---------- src/monster.h | 4 ++-- src/monster_oracle.cpp | 27 ++++++++++++++++++--------- src/monster_oracle.h | 9 ++++++--- src/monstergenerator.cpp | 5 +++++ tests/behavior_test.cpp | 26 +++++++++++++++++++------- 16 files changed, 151 insertions(+), 73 deletions(-) diff --git a/data/json/monsters/monster_goals.json b/data/json/monsters/monster_goals.json index 43a5d05be11b8..b33e8334559f7 100644 --- a/data/json/monsters/monster_goals.json +++ b/data/json/monsters/monster_goals.json @@ -3,7 +3,7 @@ "type": "behavior", "id": "monster_goals", "strategy": "sequential_until_done", - "children": [ "absorb_items", "monster_special" ] + "children": [ "absorb_items" ] }, { "type": "behavior", @@ -20,8 +20,23 @@ }, { "type": "behavior", - "id": "monster_special", - "predicate": "monster_has_special", - "goal": "do_special" + "id": "EAT_CROP", + "strategy": "sequential", + "predicate": "monster_not_hallucination", + "children": [ "ready_to_eat_crop" ] + }, + { + "type": "behavior", + "id": "ready_to_eat_crop", + "predicate": "monster_special_available", + "predicate_argument": "EAT_CROP", + "strategy": "sequential", + "children": [ "do_eat_crop" ] + }, + { + "type": "behavior", + "id": "do_eat_crop", + "predicate": "monster_adjacent_plants", + "goal": "EAT_CROP" } ] diff --git a/src/behavior.cpp b/src/behavior.cpp index f124a9cc81550..0bc65ac9d08e2 100644 --- a/src/behavior.cpp +++ b/src/behavior.cpp @@ -18,9 +18,11 @@ void node_t::set_strategy( const strategy_t *new_strategy ) { strategy = new_strategy; } -void node_t::set_predicate( std::function new_predicate ) +void node_t::set_predicate( std::function + new_predicate, const std::string &argument ) { predicate = new_predicate; + predicate_argument = argument; } void node_t::set_goal( const std::string &new_goal ) { @@ -35,10 +37,10 @@ behavior_return node_t::tick( const oracle_t *subject ) const { assert( predicate ); if( children.empty() ) { - return { predicate( subject ), this }; + return { predicate( subject, predicate_argument ), this }; } else { assert( strategy != nullptr ); - status_t result = predicate( subject ); + status_t result = predicate( subject, predicate_argument ); if( result == running ) { return strategy->evaluate( subject, children ); } else { @@ -83,6 +85,13 @@ generic_factory behavior_factory( "behavior" ); std::list temp_node_data; } // namespace +/** @relates string_id */ +template<> +bool string_id::is_valid() const +{ + return behavior_factory.is_valid( *this ); +} + template<> const node_t &string_id::obj() const { @@ -127,7 +136,10 @@ void node_t::load( const JsonObject &jo, const std::string & ) } else { debugmsg( "While loading %s, failed to find predicate %s.", id.str(), jo.get_string( "predicate" ) ); - jo.throw_error( "Invalid strategy in behavior." ); + jo.throw_error( "Invalid predicate in behavior." ); + } + if( jo.has_string( "predicate_argument" ) ) { + predicate_argument = jo.get_string( "predicate_argument" ); } } optional( jo, was_loaded, "goal", _goal ); diff --git a/src/behavior.h b/src/behavior.h index d6aa2f460ce61..d37ccb5441ec5 100644 --- a/src/behavior.h +++ b/src/behavior.h @@ -62,7 +62,8 @@ class node_t // Interface to construct a node. void set_strategy( const strategy_t *new_strategy ); - void set_predicate( std::function < status_t ( const oracle_t * )> new_predicate ); + void set_predicate( std::function < status_t ( const oracle_t *, const std::string & )> + new_predicate, const std::string &argument = "" ); void set_goal( const std::string &new_goal ); void add_child( const node_t *new_child ); @@ -73,8 +74,9 @@ class node_t bool was_loaded = false; private: std::vector children; - const strategy_t *strategy; - std::function predicate; + const strategy_t *strategy = nullptr; + std::function predicate; + std::string predicate_argument; // TODO: make into an ID? std::string _goal; }; diff --git a/src/behavior_oracle.cpp b/src/behavior_oracle.cpp index 297e8d93e74f7..6b76724aeba8a 100644 --- a/src/behavior_oracle.cpp +++ b/src/behavior_oracle.cpp @@ -11,19 +11,26 @@ namespace behavior { -status_t return_running( const oracle_t * ) +status_t return_running( const oracle_t *, const std::string & ) { return running; } // Just a little helper to make populating predicate_map slightly less gross. -static std::function < status_t( const oracle_t * ) > -make_function( status_t ( character_oracle_t::* fun )() const ) +static std::function < status_t( const oracle_t *, const std::string & ) > +make_function( status_t ( character_oracle_t::* fun )( const std::string & ) const ) { - return static_cast( fun ); + return static_cast( fun ); } -std::unordered_map> predicate_map = {{ +static std::function < status_t( const oracle_t *, const std::string & ) > +make_function( status_t ( monster_oracle_t::* fun )( const std::string & ) const ) +{ + return static_cast( fun ); +} + +std::unordered_map> +predicate_map = {{ { "npc_needs_warmth_badly", make_function( &character_oracle_t::needs_warmth_badly ) }, { "npc_needs_water_badly", make_function( &character_oracle_t::needs_water_badly ) }, { "npc_needs_food_badly", make_function( &character_oracle_t::needs_food_badly ) }, @@ -32,9 +39,10 @@ std::unordered_map> pre { "npc_can_take_shelter", make_function( &character_oracle_t::can_take_shelter ) }, { "npc_has_water", make_function( &character_oracle_t::has_water ) }, { "npc_has_food", make_function( &character_oracle_t::has_food ) }, - { "monster_has_special", static_cast( &monster_oracle_t::has_special ) }, - { "monster_not_hallucination", static_cast( &monster_oracle_t::not_hallucination ) }, - { "monster_items_available", static_cast( &monster_oracle_t::items_available ) } + { "monster_not_hallucination", make_function( &monster_oracle_t::not_hallucination ) }, + { "monster_items_available", make_function( &monster_oracle_t::items_available ) }, + { "monster_adjacent_plants", make_function( &monster_oracle_t::adjacent_plants ) }, + { "monster_special_available", make_function( &monster_oracle_t::special_available ) } } }; diff --git a/src/behavior_oracle.h b/src/behavior_oracle.h index 79171642bb53a..82c1c454c4eef 100644 --- a/src/behavior_oracle.h +++ b/src/behavior_oracle.h @@ -21,9 +21,10 @@ class oracle_t { }; -status_t return_running( const oracle_t * ); +status_t return_running( const oracle_t *, const std::string & ); -extern std::unordered_map> predicate_map; +extern std::unordered_map> + predicate_map; } // namespace behavior #endif // CATA_SRC_BEHAVIOR_ORACLE_H diff --git a/src/character_oracle.cpp b/src/character_oracle.cpp index aece481d2d430..5eabf56372602 100644 --- a/src/character_oracle.cpp +++ b/src/character_oracle.cpp @@ -22,7 +22,7 @@ namespace behavior // To avoid a local minima when the character has access to warmth in a shelter but gets cold // when they go outside, this method needs to only alert when travel time to known shelter // approaches time to freeze. -status_t character_oracle_t::needs_warmth_badly() const +status_t character_oracle_t::needs_warmth_badly( const std::string & ) const { const player *p = dynamic_cast( subject ); // Use player::temp_conv to predict whether the Character is "in trouble". @@ -34,7 +34,7 @@ status_t character_oracle_t::needs_warmth_badly() const return success; } -status_t character_oracle_t::needs_water_badly() const +status_t character_oracle_t::needs_water_badly( const std::string & ) const { // Check thirst threshold. if( subject->get_thirst() > 520 ) { @@ -43,7 +43,7 @@ status_t character_oracle_t::needs_water_badly() const return success; } -status_t character_oracle_t::needs_food_badly() const +status_t character_oracle_t::needs_food_badly( const std::string & ) const { // Check hunger threshold. if( subject->get_hunger() >= 300 && subject->get_starvation() > 2500 ) { @@ -52,7 +52,7 @@ status_t character_oracle_t::needs_food_badly() const return success; } -status_t character_oracle_t::can_wear_warmer_clothes() const +status_t character_oracle_t::can_wear_warmer_clothes( const std::string & ) const { const player *p = dynamic_cast( subject ); // Check inventory for wearable warmer clothes, greedily. @@ -66,7 +66,7 @@ status_t character_oracle_t::can_wear_warmer_clothes() const return failure; } -status_t character_oracle_t::can_make_fire() const +status_t character_oracle_t::can_make_fire( const std::string & ) const { // Check inventory for firemaking tools and fuel bool tool = false; @@ -88,14 +88,14 @@ status_t character_oracle_t::can_make_fire() const return success; } -status_t character_oracle_t::can_take_shelter() const +status_t character_oracle_t::can_take_shelter( const std::string & ) const { // See if we know about some shelter // Don't know how yet. return failure; } -status_t character_oracle_t::has_water() const +status_t character_oracle_t::has_water( const std::string & ) const { // Check if we know about water somewhere bool found_water = subject->inv.has_item_with( []( const item & cand ) { @@ -104,7 +104,7 @@ status_t character_oracle_t::has_water() const return found_water ? running : failure; } -status_t character_oracle_t::has_food() const +status_t character_oracle_t::has_food( const std::string & ) const { // Check if we know about food somewhere bool found_food = subject->inv.has_item_with( []( const item & cand ) { diff --git a/src/character_oracle.h b/src/character_oracle.h index 01781694f20a7..1353b4fd388b1 100644 --- a/src/character_oracle.h +++ b/src/character_oracle.h @@ -2,6 +2,8 @@ #ifndef CATA_SRC_CHARACTER_ORACLE_H #define CATA_SRC_CHARACTER_ORACLE_H +#include + #include "behavior_oracle.h" class Character; @@ -18,14 +20,14 @@ class character_oracle_t : public oracle_t /** * Predicates used by AI to determine goals. */ - status_t needs_warmth_badly() const; - status_t needs_water_badly() const; - status_t needs_food_badly() const; - status_t can_wear_warmer_clothes() const; - status_t can_make_fire() const; - status_t can_take_shelter() const; - status_t has_water() const; - status_t has_food() const; + status_t needs_warmth_badly( const std::string & ) const; + status_t needs_water_badly( const std::string & ) const; + status_t needs_food_badly( const std::string & ) const; + status_t can_wear_warmer_clothes( const std::string & ) const; + status_t can_make_fire( const std::string & ) const; + status_t can_take_shelter( const std::string & ) const; + status_t has_water( const std::string & ) const; + status_t has_food( const std::string & ) const; private: const Character *subject; }; diff --git a/src/init.cpp b/src/init.cpp index 8c7f2f54d6852..b5a4bb38188e2 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -593,6 +593,7 @@ void DynamicDataLoader::finalize_loaded_data( loading_ui &ui ) { _( "Start locations" ), &start_locations::finalize_all }, { _( "Vehicle prototypes" ), &vehicle_prototype::finalize }, { _( "Mapgen weights" ), &calculate_mapgen_weights }, + { _( "Behaviors" ), &behavior::finalize }, { _( "Monster types" ), []() { @@ -608,7 +609,6 @@ void DynamicDataLoader::finalize_loaded_data( loading_ui &ui ) { _( "Martial arts" ), &finialize_martial_arts }, { _( "NPC classes" ), &npc_class::finalize_all }, { _( "Missions" ), &mission_type::finalize }, - { _( "Behaviors" ), &behavior::finalize }, { _( "Harvest lists" ), &harvest_list::finalize_all }, { _( "Anatomies" ), &anatomy::finalize_all }, { _( "Mutations" ), &mutation_branch::finalize }, diff --git a/src/monattack.cpp b/src/monattack.cpp index c066478c7c981..a9b71e84f5294 100644 --- a/src/monattack.cpp +++ b/src/monattack.cpp @@ -323,13 +323,19 @@ bool mattack::none( monster * ) bool mattack::eat_crop( monster *z ) { + cata::optional target; + int num_targets = 1; for( const auto &p : g->m.points_in_radius( z->pos(), 1 ) ) { - if( g->m.has_flag( "PLANT", p ) && one_in( 4 ) ) { - g->m.furn_set( p, furn_str_id( g->m.furn( p )->plant->base ) ); - g->m.i_clear( p ); - return true; + if( g->m.has_flag( "PLANT", p ) && one_in( num_targets ) ) { + num_targets++; + target = p; } } + if( target ) { + g->m.furn_set( *target, furn_str_id( g->m.furn( *target )->plant->base ) ); + g->m.i_clear( *target ); + return true; + } return true; } diff --git a/src/monmove.cpp b/src/monmove.cpp index 9e45b6267907d..3838c687e8bac 100644 --- a/src/monmove.cpp +++ b/src/monmove.cpp @@ -664,6 +664,15 @@ void monster::move() } } g->m.i_clear( pos() ); + } else if( action == "eat_crop" ) { + // TODO: Create a special attacks whitelist unordered map instead of an if chain. + std::map::const_iterator attack = + type->special_attacks.find( action ); + if( attack != type->special_attacks.end() && attack->second->call( *this ) ) { + if( special_attacks.count( action ) != 0 ) { + reset_special( action ); + } + } } // record position before moving to put the player there if we're dragging tripoint drag_to = g->m.getabs( pos() ); diff --git a/src/monster.cpp b/src/monster.cpp index 1ef479bd10c6e..8fc355626f0ec 100644 --- a/src/monster.cpp +++ b/src/monster.cpp @@ -2036,17 +2036,11 @@ void monster::disable_special( const std::string &special_name ) special_attacks.at( special_name ).enabled = false; } -int monster::shortest_special_cooldown() const +bool monster::special_available( const std::string &special_name ) const { - int countdown = std::numeric_limits::max(); - for( const std::pair &sp_type : special_attacks ) { - const mon_special_attack &local_attack_data = sp_type.second; - if( !local_attack_data.enabled ) { - continue; - } - countdown = std::min( countdown, local_attack_data.cooldown ); - } - return countdown; + std::map::const_iterator iter = special_attacks.find( + special_name ); + return iter != special_attacks.end() && iter->second.enabled && iter->second.cooldown == 0; } void monster::normalize_ammo( const int old_ammo ) diff --git a/src/monster.h b/src/monster.h index 1ee92bbb9822c..1698e54544d83 100644 --- a/src/monster.h +++ b/src/monster.h @@ -392,8 +392,8 @@ class monster : public Creature void set_special( const std::string &special_name, int time ); /** Sets the enabled flag for the given special to false */ void disable_special( const std::string &special_name ); - /** Return the lowest cooldown for an enabled special */ - int shortest_special_cooldown() const; + /** Test whether the specified special is ready. */ + bool special_available( const std::string &special_name ) const; void process_turn() override; /** Resets the value of all bonus fields to 0, clears special effect flags. */ diff --git a/src/monster_oracle.cpp b/src/monster_oracle.cpp index 88ba91621bb38..a6e9e4ec4a8a9 100644 --- a/src/monster_oracle.cpp +++ b/src/monster_oracle.cpp @@ -3,31 +3,40 @@ #include "behavior.h" #include "game.h" #include "map.h" +#include "map_iterator.h" #include "monster.h" #include "monster_oracle.h" namespace behavior { -status_t monster_oracle_t::has_special() const +status_t monster_oracle_t::not_hallucination( const std::string & ) const { - if( subject->shortest_special_cooldown() == 0 ) { + return subject->is_hallucination() ? failure : running; +} + +status_t monster_oracle_t::items_available( const std::string & ) const +{ + if( !g->m.has_flag( TFLAG_SEALED, subject->pos() ) && g->m.has_items( subject->pos() ) ) { return running; } return failure; } -status_t monster_oracle_t::not_hallucination() const +// TODO: Have it select a target and stash it somewhere. +status_t monster_oracle_t::adjacent_plants( const std::string & ) const { - return subject->is_hallucination() ? failure : running; + for( const tripoint &p : g->m.points_in_radius( subject->pos(), 1 ) ) { + if( g->m.has_flag( "PLANT", p ) ) { + return running; + } + } + return failure; } -status_t monster_oracle_t::items_available() const +status_t monster_oracle_t::special_available( const std::string &special_name ) const { - if( !g->m.has_flag( TFLAG_SEALED, subject->pos() ) && g->m.has_items( subject->pos() ) ) { - return running; - } - return failure; + return subject->special_available( special_name ) ? running : failure; } } // namespace behavior diff --git a/src/monster_oracle.h b/src/monster_oracle.h index d4452d49e8205..dfdc8e5d13cdd 100644 --- a/src/monster_oracle.h +++ b/src/monster_oracle.h @@ -2,6 +2,8 @@ #ifndef CATA_SRC_MONSTER_ORACLE_H #define CATA_SRC_MONSTER_ORACLE_H +#include + #include "behavior_oracle.h" class monster; @@ -18,9 +20,10 @@ class monster_oracle_t : public oracle_t /** * Predicates used by AI to determine goals. */ - status_t has_special() const; - status_t not_hallucination() const; - status_t items_available() const; + status_t not_hallucination( const std::string & ) const; + status_t items_available( const std::string & ) const; + status_t adjacent_plants( const std::string & ) const; + status_t special_available( const std::string &special_name ) const; private: const monster *subject; }; diff --git a/src/monstergenerator.cpp b/src/monstergenerator.cpp index eeff00b4ce5b1..b62ef0b548ef6 100644 --- a/src/monstergenerator.cpp +++ b/src/monstergenerator.cpp @@ -339,6 +339,11 @@ static void build_behavior_tree( mtype &type ) if( type.has_flag( MF_ABSORBS ) || type.has_flag( MF_ABSORBS_SPLITS ) ) { type.add_goal( "absorb_items" ); } + for( const std::pair &attack : type.special_attacks ) { + if( string_id( attack.first ).is_valid() ) { + type.add_goal( attack.first ); + } /* TODO: Make this an error once all the special attacks are migrated. */ + } } void MonsterGenerator::finalize_mtypes() diff --git a/tests/behavior_test.cpp b/tests/behavior_test.cpp index 1290ebd2fa3a5..ea702fe150e83 100644 --- a/tests/behavior_test.cpp +++ b/tests/behavior_test.cpp @@ -9,6 +9,8 @@ #include "game.h" #include "item.h" #include "item_location.h" +#include "map.h" +#include "map_iterator.h" #include "monster_oracle.h" #include "mtype.h" #include "npc.h" @@ -31,7 +33,7 @@ static behavior::node_t make_test_node( std::string goal, const behavior::status if( !goal.empty() ) { node.set_goal( goal ); } - node.set_predicate( [status]( const behavior::oracle_t * ) { + node.set_predicate( [status]( const behavior::oracle_t *, const std::string & ) { return *status; } ); return node; @@ -183,18 +185,28 @@ TEST_CASE( "check_npc_behavior_tree", "[npc][behavior][!mayfail]" ) TEST_CASE( "check_monster_behavior_tree", "[monster][behavior]" ) { + const tripoint monster_location( 5, 5, 0 ); + clear_map(); + monster &test_monster = spawn_test_monster( "mon_locust", monster_location ); + + behavior::monster_oracle_t oracle( &test_monster ); behavior::tree monster_goals; - monster_goals.add( &string_id( "monster_special" ).obj() ); - monster &test_monster = spawn_test_monster( "mon_zombie", { 5, 5, 0 } ); + monster_goals.add( test_monster.type->get_goals() ); + for( const std::string &special_name : test_monster.type->special_attacks_names ) { test_monster.reset_special( special_name ); } - behavior::monster_oracle_t oracle( &test_monster ); CHECK( monster_goals.tick( &oracle ) == "idle" ); + for( const tripoint &near : g->m.points_in_radius( monster_location, 1 ) ) { + g->m.ter_set( near, ter_id( "t_grass" ) ); + g->m.furn_set( near, furn_id( "f_null" ) ); + } SECTION( "Special Attack" ) { - test_monster.set_special( "bite", 0 ); - CHECK( monster_goals.tick( &oracle ) == "do_special" ); - test_monster.set_special( "bite", 1 ); + test_monster.set_special( "EAT_CROP", 0 ); + CHECK( monster_goals.tick( &oracle ) == "idle" ); + g->m.furn_set( monster_location, furn_id( "f_plant_seedling" ) ); + CHECK( monster_goals.tick( &oracle ) == "EAT_CROP" ); + test_monster.set_special( "EAT_CROP", 1 ); CHECK( monster_goals.tick( &oracle ) == "idle" ); } }