From 73275e9fa9c29964e3a9ee8fe2ac449a58324b85 Mon Sep 17 00:00:00 2001 From: Kevin Granade Date: Mon, 11 May 2020 15:47:29 +0000 Subject: [PATCH] Support for selecting a special attack via behavior tree and integrate with first special --- data/json/monsters/monster_goals.json | 22 ++++++++++++++++++---- src/behavior.cpp | 7 +++++++ src/behavior_oracle.cpp | 4 ++-- src/init.cpp | 2 +- src/monattack.cpp | 14 ++++++++++---- src/monmove.cpp | 9 +++++++++ src/monster.cpp | 14 ++++---------- src/monster.h | 4 ++-- src/monster_oracle.cpp | 16 +++++++--------- src/monster_oracle.h | 2 +- src/monstergenerator.cpp | 5 +++++ tests/behavior_test.cpp | 23 +++++++++++++++++------ 12 files changed, 83 insertions(+), 39 deletions(-) diff --git a/data/json/monsters/monster_goals.json b/data/json/monsters/monster_goals.json index 43a5d05be11b8..b0f5131ce79ab 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,22 @@ }, { "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", + "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 6e92e79ba5887..0bc65ac9d08e2 100644 --- a/src/behavior.cpp +++ b/src/behavior.cpp @@ -85,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 { diff --git a/src/behavior_oracle.cpp b/src/behavior_oracle.cpp index 36be3830b5cbf..6b76724aeba8a 100644 --- a/src/behavior_oracle.cpp +++ b/src/behavior_oracle.cpp @@ -39,10 +39,10 @@ predicate_map = {{ { "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", make_function( &monster_oracle_t::has_special ) }, { "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_adjacent_plants", make_function( &monster_oracle_t::adjacent_plants ) }, + { "monster_special_available", make_function( &monster_oracle_t::special_available ) } } }; 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 f9f60fffa780f..b7a0592da151e 100644 --- a/src/monattack.cpp +++ b/src/monattack.cpp @@ -301,13 +301,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 25df312439527..989f3ea5a6fe4 100644 --- a/src/monmove.cpp +++ b/src/monmove.cpp @@ -662,6 +662,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 cece4410620d0..46ac81a43c558 100644 --- a/src/monster.cpp +++ b/src/monster.cpp @@ -2032,17 +2032,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 406d68431c723..75d7d6cae3529 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 89ea43ae1b463..a6e9e4ec4a8a9 100644 --- a/src/monster_oracle.cpp +++ b/src/monster_oracle.cpp @@ -3,20 +3,13 @@ #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 std::string & ) const -{ - if( subject->shortest_special_cooldown() == 0 ) { - return running; - } - return failure; -} - status_t monster_oracle_t::not_hallucination( const std::string & ) const { return subject->is_hallucination() ? failure : running; @@ -33,7 +26,7 @@ status_t monster_oracle_t::items_available( const std::string & ) const // TODO: Have it select a target and stash it somewhere. status_t monster_oracle_t::adjacent_plants( const std::string & ) const { - for( const auto &p : g->m.points_in_radius( subject->pos(), 1 ) ) { + for( const tripoint &p : g->m.points_in_radius( subject->pos(), 1 ) ) { if( g->m.has_flag( "PLANT", p ) ) { return running; } @@ -41,4 +34,9 @@ status_t monster_oracle_t::adjacent_plants( const std::string & ) const return failure; } +status_t monster_oracle_t::special_available( const std::string &special_name ) const +{ + return subject->special_available( special_name ) ? running : failure; +} + } // namespace behavior diff --git a/src/monster_oracle.h b/src/monster_oracle.h index 015bbe7b7a52e..dfdc8e5d13cdd 100644 --- a/src/monster_oracle.h +++ b/src/monster_oracle.h @@ -20,10 +20,10 @@ class monster_oracle_t : public oracle_t /** * Predicates used by AI to determine goals. */ - status_t has_special( const std::string & ) 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 bddd553a75af6..a4d6a1d13ec02 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 d83dcd954e71f..c9a4add717dc8 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" @@ -183,18 +185,27 @@ TEST_CASE( "check_npc_behavior_tree", "[npc][behavior][!mayfail]" ) TEST_CASE( "check_monster_behavior_tree", "[monster][behavior]" ) { + const tripoint monster_location( 5, 5, 0 ); + 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" ); } }