diff --git a/data/json/monsters/monster_goals.json b/data/json/monsters/monster_goals.json new file mode 100644 index 0000000000000..43a5d05be11b8 --- /dev/null +++ b/data/json/monsters/monster_goals.json @@ -0,0 +1,27 @@ +[ + { + "type": "behavior", + "id": "monster_goals", + "strategy": "sequential_until_done", + "children": [ "absorb_items", "monster_special" ] + }, + { + "type": "behavior", + "id": "absorb_items", + "strategy": "sequential", + "predicate": "monster_not_hallucination", + "children": [ "do_absorb" ] + }, + { + "type": "behavior", + "id": "do_absorb", + "predicate": "monster_items_available", + "goal": "consume_items" + }, + { + "type": "behavior", + "id": "monster_special", + "predicate": "monster_has_special", + "goal": "do_special" + } +] diff --git a/src/behavior_oracle.cpp b/src/behavior_oracle.cpp index a2f49f82e643e..297e8d93e74f7 100644 --- a/src/behavior_oracle.cpp +++ b/src/behavior_oracle.cpp @@ -31,7 +31,10 @@ std::unordered_map> pre { "npc_can_make_fire", make_function( &character_oracle_t::can_make_fire ) }, { "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 ) } + { "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 ) } } }; diff --git a/src/monmove.cpp b/src/monmove.cpp index a133d78a82d67..60646005406a7 100644 --- a/src/monmove.cpp +++ b/src/monmove.cpp @@ -13,6 +13,7 @@ #include #include "avatar.h" +#include "behavior.h" #include "bionics.h" #include "cata_utility.h" #include "creature_tracker.h" @@ -30,6 +31,7 @@ #include "memory_fast.h" #include "messages.h" #include "monfaction.h" +#include "monster_oracle.h" #include "mtype.h" #include "npc.h" #include "pathfinding.h" @@ -629,10 +631,14 @@ void monster::move() return; } + behavior::monster_oracle_t oracle( this ); + behavior::tree goals; + goals.add( type->get_goals() ); + std::string action = goals.tick( &oracle ); //The monster can consume objects it stands on. Check if there are any. //If there are. Consume them. - if( !is_hallucination() && ( has_flag( MF_ABSORBS ) || has_flag( MF_ABSORBS_SPLITS ) ) && - !g->m.has_flag( TFLAG_SEALED, pos() ) && g->m.has_items( pos() ) ) { + // TODO: Stick this in a map and dispatch to it via the action string. + if( action == "consume_items" ) { if( g->u.sees( *this ) ) { add_msg( _( "The %s flows around the objects on the floor and they are quickly dissolved!" ), name() ); diff --git a/src/monster.cpp b/src/monster.cpp index 46b17bea90f5f..e8f40fb63b6bd 100644 --- a/src/monster.cpp +++ b/src/monster.cpp @@ -2014,6 +2014,19 @@ void monster::disable_special( const std::string &special_name ) special_attacks.at( special_name ).enabled = false; } +int monster::shortest_special_cooldown() 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; +} + void monster::normalize_ammo( const int old_ammo ) { int total_ammo = 0; diff --git a/src/monster.h b/src/monster.h index 455a6acee525e..9d2df09381110 100644 --- a/src/monster.h +++ b/src/monster.h @@ -390,6 +390,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; 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 new file mode 100644 index 0000000000000..88ba91621bb38 --- /dev/null +++ b/src/monster_oracle.cpp @@ -0,0 +1,33 @@ +#include + +#include "behavior.h" +#include "game.h" +#include "map.h" +#include "monster.h" +#include "monster_oracle.h" + +namespace behavior +{ + +status_t monster_oracle_t::has_special() const +{ + if( subject->shortest_special_cooldown() == 0 ) { + return running; + } + return failure; +} + +status_t monster_oracle_t::not_hallucination() const +{ + return subject->is_hallucination() ? failure : running; +} + +status_t monster_oracle_t::items_available() const +{ + if( !g->m.has_flag( TFLAG_SEALED, subject->pos() ) && g->m.has_items( subject->pos() ) ) { + return running; + } + return failure; +} + +} // namespace behavior diff --git a/src/monster_oracle.h b/src/monster_oracle.h new file mode 100644 index 0000000000000..d4452d49e8205 --- /dev/null +++ b/src/monster_oracle.h @@ -0,0 +1,29 @@ +#pragma once +#ifndef CATA_SRC_MONSTER_ORACLE_H +#define CATA_SRC_MONSTER_ORACLE_H + +#include "behavior_oracle.h" + +class monster; + +namespace behavior +{ + +class monster_oracle_t : public oracle_t +{ + public: + monster_oracle_t( const monster *subject ) { + this->subject = subject; + } + /** + * Predicates used by AI to determine goals. + */ + status_t has_special() const; + status_t not_hallucination() const; + status_t items_available() const; + private: + const monster *subject; +}; + +} // namespace behavior +#endif // CATA_SRC_MONSTER_ORACLE_H diff --git a/src/monstergenerator.cpp b/src/monstergenerator.cpp index 50fa7c11664ad..3331b85a98edc 100644 --- a/src/monstergenerator.cpp +++ b/src/monstergenerator.cpp @@ -328,6 +328,14 @@ void load_monster_adjustment( const JsonObject &jsobj ) adjustments.push_back( adj ); } +static void build_behavior_tree( mtype &type ) +{ + type.set_strategy(); + if( type.has_flag( MF_ABSORBS ) || type.has_flag( MF_ABSORBS_SPLITS ) ) { + type.add_goal( "absorb_items" ); + } +} + void MonsterGenerator::finalize_mtypes() { mon_templates->finalize(); @@ -368,6 +376,7 @@ void MonsterGenerator::finalize_mtypes() // Lower bound for hp scaling mon.hp = std::max( mon.hp, 1 ); + build_behavior_tree( mon ); finalize_pathfinding_settings( mon ); } diff --git a/src/mtype.cpp b/src/mtype.cpp index 452e348c8d9b7..b5f102d6f8fa2 100644 --- a/src/mtype.cpp +++ b/src/mtype.cpp @@ -3,6 +3,7 @@ #include #include +#include "behavior_strategy.h" #include "creature.h" #include "field_type.h" #include "item.h" @@ -235,3 +236,18 @@ std::string mtype::get_footsteps() const } return _( "footsteps." ); } + +void mtype::set_strategy() +{ + goals.set_strategy( behavior::strategy_map[ "sequential_until_done" ] ); +} + +void mtype::add_goal( const std::string &goal_id ) +{ + goals.add_child( &string_id( goal_id ).obj() ); +} + +const behavior::node_t *mtype::get_goals() const +{ + return &goals; +} diff --git a/src/mtype.h b/src/mtype.h index 3f1fad40b8031..ff41fad5143f1 100644 --- a/src/mtype.h +++ b/src/mtype.h @@ -7,6 +7,7 @@ #include #include +#include "behavior.h" #include "calendar.h" #include "color.h" #include "damage.h" @@ -213,6 +214,8 @@ struct mtype { enum_bitset fear; enum_bitset placate; + behavior::node_t goals; + void add_special_attacks( const JsonObject &jo, const std::string &member_name, const std::string &src ); void remove_special_attacks( const JsonObject &jo, const std::string &member_name, @@ -385,6 +388,9 @@ struct mtype { int get_meat_chunks_count() const; std::string get_description() const; std::string get_footsteps() const; + void set_strategy(); + void add_goal( const std::string &goal_id ); + const behavior::node_t *get_goals() const; // Historically located in monstergenerator.cpp void load( const JsonObject &jo, const std::string &src ); diff --git a/src/panels.cpp b/src/panels.cpp index bf455076ba0af..3e29eebf7bbb2 100644 --- a/src/panels.cpp +++ b/src/panels.cpp @@ -19,6 +19,7 @@ #include "cata_utility.h" #include "catacharset.h" #include "character.h" +#include "character_oracle.h" #include "character_martial_arts.h" #include "color.h" #include "compatibility.h" diff --git a/tests/behavior_test.cpp b/tests/behavior_test.cpp index 8aa66fd8f7175..65212cbc49566 100644 --- a/tests/behavior_test.cpp +++ b/tests/behavior_test.cpp @@ -5,9 +5,11 @@ #include "behavior_oracle.h" #include "behavior_strategy.h" #include "catch/catch.hpp" +#include "character_oracle.h" #include "game.h" #include "item.h" #include "item_location.h" +#include "monster_oracle.h" #include "npc.h" #include "player.h" #include "map_helpers.h" @@ -176,3 +178,18 @@ TEST_CASE( "check_npc_behavior_tree", "[npc][behavior]" ) CHECK( npc_needs.tick( &oracle ) == "idle" ); } } + +TEST_CASE( "check_monster_behavior_tree", "[monster][behavior]" ) +{ + behavior::tree monster_goals; + monster_goals.add( &string_id( "monster_special" ).obj() ); + monster &test_monster = spawn_test_monster( "mon_zombie", { 5, 5, 0 } ); + behavior::monster_oracle_t oracle( &test_monster ); + CHECK( monster_goals.tick( &oracle ) == "idle" ); + SECTION( "Special Attack" ) { + test_monster.set_special( "bite", 0 ); + CHECK( monster_goals.tick( &oracle ) == "do_special" ); + test_monster.set_special( "bite", 1 ); + CHECK( monster_goals.tick( &oracle ) == "idle" ); + } +}