void monfactions::load_monster_faction( JsonObject &jo ) { // Factions inherit values from their parent factions - this is set during finalization std::set< std::string > by_mood, neutral, friendly; by_mood = jo.get_tags( "by_mood" ); neutral = jo.get_tags( "neutral" ); friendly = jo.get_tags( "friendly" ); // Need to make sure adding new factions won't invalidate our current faction's reference // That +1 is for base faction faction_list.reserve( faction_list.size() + by_mood.size() + neutral.size() + friendly.size() + 1 ); prealloc( by_mood ); prealloc( neutral ); prealloc( friendly ); std::string name = jo.get_string( "name" ); mfaction_id cur_id = get_or_add_faction( mfaction_str_id( name ) ); std::string base_faction = jo.get_string( "base_faction", "" ); mfaction_id base_id = get_or_add_faction( mfaction_str_id( base_faction ) ); // Don't get the reference until here (to avoid vector reallocation messing it up) monfaction &faction = faction_list[cur_id]; faction.base_faction = base_id; add_to_attitude_map( by_mood, faction.attitude_map, MFA_BY_MOOD ); add_to_attitude_map( neutral, faction.attitude_map, MFA_NEUTRAL ); add_to_attitude_map( friendly, faction.attitude_map, MFA_FRIENDLY ); }
// Get pointers to factions from 'keys' and add them to 'map' with value == 'value' void add_to_attitude_map( const std::set< std::string > &keys, mfaction_att_map &map, mf_attitude value ) { for( const auto &k : keys ) { const auto &faction = mfaction_str_id( k ).id(); map[faction] = value; } }
void mtype::load( JsonObject &jo ) { MonsterGenerator &gen = MonsterGenerator::generator(); // Name and name plural are not translated here, but when needed in // combination with the actual count in `mtype::nname`. mandatory( jo, was_loaded, "name", name ); // default behaviour: Assume the regular plural form (appending an “s”) optional( jo, was_loaded, "name_plural", name_plural, name + "s" ); mandatory( jo, was_loaded, "description", description, translated_string_reader ); // Have to overwrite the default { "hflesh" } here if( !was_loaded || jo.has_member( "material" ) ) { mat = { jo.get_string( "material" ) }; } optional( jo, was_loaded, "species", species, auto_flags_reader<species_id> {} ); optional( jo, was_loaded, "categories", categories, auto_flags_reader<> {} ); // See monfaction.cpp if( !was_loaded || jo.has_member( "default_faction" ) ) { const auto faction = mfaction_str_id( jo.get_string( "default_faction" ) ); default_faction = monfactions::get_or_add_faction( faction ); } if( !was_loaded || jo.has_member( "symbol" ) ) { sym = jo.get_string( "symbol" ); if( utf8_wrapper( sym ).display_width() != 1 ) { jo.throw_error( "monster symbol should be exactly one console cell width", "symbol" ); } } mandatory( jo, was_loaded, "color", color, color_reader{} ); const typed_flag_reader<decltype( Creature::size_map )> size_reader{ Creature::size_map, "invalid creature size" }; optional( jo, was_loaded, "size", size, size_reader, MS_MEDIUM ); const typed_flag_reader<decltype( gen.phase_map )> phase_reader{ gen.phase_map, "invalid phase id" }; optional( jo, was_loaded, "phase", phase, phase_reader, SOLID ); optional( jo, was_loaded, "diff", difficulty, 0 ); optional( jo, was_loaded, "aggression", agro, 0 ); optional( jo, was_loaded, "morale", morale, 0 ); optional( jo, was_loaded, "speed", speed, 0 ); optional( jo, was_loaded, "attack_cost", attack_cost, 100 ); optional( jo, was_loaded, "melee_skill", melee_skill, 0 ); optional( jo, was_loaded, "melee_dice", melee_dice, 0 ); optional( jo, was_loaded, "melee_dice_sides", melee_sides, 0 ); optional( jo, was_loaded, "melee_cut", melee_cut, 0 ); optional( jo, was_loaded, "dodge", sk_dodge, 0 ); optional( jo, was_loaded, "armor_bash", armor_bash, 0 ); optional( jo, was_loaded, "armor_cut", armor_cut, 0 ); optional( jo, was_loaded, "armor_acid", armor_acid, armor_cut / 2 ); optional( jo, was_loaded, "armor_fire", armor_fire, 0 ); optional( jo, was_loaded, "hp", hp, 0 ); optional( jo, was_loaded, "starting_ammo", starting_ammo ); optional( jo, was_loaded, "luminance", luminance, 0 ); optional( jo, was_loaded, "revert_to_itype", revert_to_itype, "" ); optional( jo, was_loaded, "vision_day", vision_day, 40 ); optional( jo, was_loaded, "vision_night", vision_night, 1 ); optional( jo, was_loaded, "armor_stab", armor_stab, 0.8f * armor_cut ); // TODO: allow adding/removing specific entries if `was_loaded` is true if( jo.has_array( "attack_effs" ) ) { JsonArray jsarr = jo.get_array( "attack_effs" ); while( jsarr.has_more() ) { JsonObject e = jsarr.next_object(); mon_effect_data new_eff( efftype_id( e.get_string( "id" ) ), e.get_int( "duration", 0 ), get_body_part_token( e.get_string( "bp", "NUM_BP" ) ), e.get_bool( "permanent", false ), e.get_int( "chance", 100 ) ); atk_effs.push_back( new_eff ); } } if( jo.has_member( "death_drops" ) ) { JsonIn &stream = *jo.get_raw( "death_drops" ); death_drops = item_group::load_item_group( stream, "distribution" ); } const typed_flag_reader<decltype( gen.death_map )> death_reader{ gen.death_map, "invalid monster death function" }; optional( jo, was_loaded, "death_function", dies, death_reader ); if( dies.empty() ) { // TODO: really needed? Is an empty `dies` container not allowed? dies.push_back( mdeath::normal ); } // TODO: allow overriding/adding/removing those if `was_loaded` is true gen.load_special_defense( this, jo, "special_when_hit" ); gen.load_special_attacks( this, jo, "special_attacks" ); // Disable upgrading when JSON contains `"upgrades": false`, but fallback to the // normal behavior (including error checking) if "upgrades" is not boolean or not `false`. if( jo.has_bool( "upgrades" ) && !jo.get_bool( "upgrades" ) ) { upgrade_group = mongroup_id::NULL_ID; upgrade_into = mtype_id::NULL_ID; upgrades = false; } else if( jo.has_member( "upgrades" ) ) { JsonObject up = jo.get_object( "upgrades" ); optional( up, was_loaded, "half_life", half_life, -1 ); optional( up, was_loaded, "into_group", upgrade_group, auto_flags_reader<mongroup_id> {}, mongroup_id::NULL_ID ); optional( up, was_loaded, "into", upgrade_into, auto_flags_reader<mtype_id> {}, mtype_id::NULL_ID ); upgrades = true; } const typed_flag_reader<decltype( gen.flag_map )> flag_reader{ gen.flag_map, "invalid monster flag" }; optional( jo, was_loaded, "flags", flags, flag_reader ); const typed_flag_reader<decltype( gen.trigger_map )> trigger_reader{ gen.trigger_map, "invalid monster trigger" }; optional( jo, was_loaded, "anger_triggers", anger, trigger_reader ); optional( jo, was_loaded, "placate_triggers", placate, trigger_reader ); optional( jo, was_loaded, "fear_triggers", fear, trigger_reader ); }
// Ensures all those factions exist void prealloc( const std::set< std::string > &facs ) { for( const auto &f : facs ) { monfactions::get_or_add_faction( mfaction_str_id( f ) ); } }
void mtype::load( JsonObject &jo, const std::string &src ) { bool strict = src == "dda"; MonsterGenerator &gen = MonsterGenerator::generator(); // Name and name plural are not translated here, but when needed in // combination with the actual count in `mtype::nname`. mandatory( jo, was_loaded, "name", name ); // default behavior: Assume the regular plural form (appending an “s”) optional( jo, was_loaded, "name_plural", name_plural, name + "s" ); optional( jo, was_loaded, "description", description ); optional( jo, was_loaded, "material", mat, auto_flags_reader<material_id> {} ); optional( jo, was_loaded, "species", species, auto_flags_reader<species_id> {} ); optional( jo, was_loaded, "categories", categories, auto_flags_reader<> {} ); // See monfaction.cpp if( !was_loaded || jo.has_member( "default_faction" ) ) { const auto faction = mfaction_str_id( jo.get_string( "default_faction" ) ); default_faction = monfactions::get_or_add_faction( faction ); } if( !was_loaded || jo.has_member( "symbol" ) ) { sym = jo.get_string( "symbol" ); if( utf8_wrapper( sym ).display_width() != 1 ) { jo.throw_error( "monster symbol should be exactly one console cell width", "symbol" ); } } if( was_loaded && jo.has_member( "copy-from" ) && looks_like.empty() ) { looks_like = jo.get_string( "copy-from" ); } if( jo.has_member( "looks_like" ) ) { looks_like = jo.get_string( "looks_like" ); } assign( jo, "color", color ); const typed_flag_reader<decltype( Creature::size_map )> size_reader{ Creature::size_map, "invalid creature size" }; optional( jo, was_loaded, "size", size, size_reader, MS_MEDIUM ); const typed_flag_reader<decltype( gen.phase_map )> phase_reader{ gen.phase_map, "invalid phase id" }; optional( jo, was_loaded, "phase", phase, phase_reader, SOLID ); assign( jo, "diff", difficulty, strict, 0 ); assign( jo, "hp", hp, strict, 1 ); assign( jo, "speed", speed, strict, 0 ); assign( jo, "aggression", agro, strict, -100, 100 ); assign( jo, "morale", morale, strict ); assign( jo, "attack_cost", attack_cost, strict, 0 ); assign( jo, "melee_skill", melee_skill, strict, 0 ); assign( jo, "melee_dice", melee_dice, strict, 0 ); assign( jo, "melee_dice_sides", melee_sides, strict, 0 ); assign( jo, "dodge", sk_dodge, strict, 0 ); assign( jo, "armor_bash", armor_bash, strict, 0 ); assign( jo, "armor_cut", armor_cut, strict, 0 ); assign( jo, "armor_stab", armor_stab, strict, 0 ); assign( jo, "armor_acid", armor_acid, strict, 0 ); assign( jo, "armor_fire", armor_fire, strict, 0 ); assign( jo, "vision_day", vision_day, strict, 0 ); assign( jo, "vision_night", vision_night, strict, 0 ); optional( jo, was_loaded, "starting_ammo", starting_ammo ); optional( jo, was_loaded, "luminance", luminance, 0 ); optional( jo, was_loaded, "revert_to_itype", revert_to_itype, "" ); optional( jo, was_loaded, "attack_effs", atk_effs, mon_attack_effect_reader{} ); // TODO: make this work with `was_loaded` if( jo.has_array( "melee_damage" ) ) { JsonArray arr = jo.get_array( "melee_damage" ); melee_damage = load_damage_instance( arr ); } else if( jo.has_object( "melee_damage" ) ) { melee_damage = load_damage_instance( jo ); } if( jo.has_int( "melee_cut" ) ) { int bonus_cut = jo.get_int( "melee_cut" ); melee_damage.add_damage( DT_CUT, bonus_cut ); } if( jo.has_member( "death_drops" ) ) { JsonIn &stream = *jo.get_raw( "death_drops" ); death_drops = item_group::load_item_group( stream, "distribution" ); } assign( jo, "harvest", harvest, strict ); const typed_flag_reader<decltype( gen.death_map )> death_reader{ gen.death_map, "invalid monster death function" }; optional( jo, was_loaded, "death_function", dies, death_reader ); if( dies.empty() ) { // TODO: really needed? Is an empty `dies` container not allowed? dies.push_back( mdeath::normal ); } assign( jo, "emit_fields", emit_fields ); if( jo.has_member( "special_when_hit" ) ) { JsonArray jsarr = jo.get_array( "special_when_hit" ); const auto iter = gen.defense_map.find( jsarr.get_string( 0 ) ); if( iter == gen.defense_map.end() ) { jsarr.throw_error( "Invalid monster defense function" ); } sp_defense = iter->second; def_chance = jsarr.get_int( 1 ); } else if( !was_loaded ) { sp_defense = &mdefense::none; def_chance = 0; } if( !was_loaded || jo.has_member( "special_attacks" ) ) { special_attacks.clear(); special_attacks_names.clear(); add_special_attacks( jo, "special_attacks", src ); } else { // Note: special_attacks left as is, new attacks are added to it! // Note: member name prefixes are compatible with those used by generic_typed_reader if( jo.has_object( "extend" ) ) { auto tmp = jo.get_object( "extend" ); add_special_attacks( tmp, "special_attacks", src ); } if( jo.has_object( "delete" ) ) { auto tmp = jo.get_object( "delete" ); remove_special_attacks( tmp, "special_attacks", src ); } } // Disable upgrading when JSON contains `"upgrades": false`, but fallback to the // normal behavior (including error checking) if "upgrades" is not boolean or not `false`. if( jo.has_bool( "upgrades" ) && !jo.get_bool( "upgrades" ) ) { upgrade_group = mongroup_id::NULL_ID(); upgrade_into = mtype_id::NULL_ID(); upgrades = false; } else if( jo.has_member( "upgrades" ) ) { JsonObject up = jo.get_object( "upgrades" ); optional( up, was_loaded, "half_life", half_life, -1 ); optional( up, was_loaded, "age_grow", age_grow, -1 ); optional( up, was_loaded, "into_group", upgrade_group, auto_flags_reader<mongroup_id> {}, mongroup_id::NULL_ID() ); optional( up, was_loaded, "into", upgrade_into, auto_flags_reader<mtype_id> {}, mtype_id::NULL_ID() ); upgrades = true; } //Reproduction if( jo.has_member( "reproduction" ) ) { JsonObject repro = jo.get_object( "reproduction" ); optional( repro, was_loaded, "baby_count", baby_count, -1 ); optional( repro, was_loaded, "baby_timer", baby_timer, -1 ); optional( repro, was_loaded, "baby_monster", baby_monster, auto_flags_reader<mtype_id> {}, mtype_id::NULL_ID() ); optional( repro, was_loaded, "baby_egg", baby_egg, auto_flags_reader<itype_id> {}, "null" ); if( jo.has_member( "baby_flags" ) ) { baby_flags.clear(); JsonArray baby_tags = jo.get_array( "baby_flags" ); while( baby_tags.has_more() ) { baby_flags.push_back( baby_tags.next_string() ); } } reproduces = true; } if( jo.has_member( "biosignature" ) ) { JsonObject biosig = jo.get_object( "biosignature" ); optional( biosig, was_loaded, "biosig_timer", biosig_timer, -1 ); optional( biosig, was_loaded, "biosig_item", biosig_item, auto_flags_reader<itype_id> {}, "null" ); biosignatures = true; } optional( jo, was_loaded, "burn_into", burn_into, auto_flags_reader<mtype_id> {}, mtype_id::NULL_ID() ); const typed_flag_reader<decltype( gen.flag_map )> flag_reader{ gen.flag_map, "invalid monster flag" }; optional( jo, was_loaded, "flags", flags, flag_reader ); // Can't calculate yet - we want all flags first optional( jo, was_loaded, "bash_skill", bash_skill, -1 ); const typed_flag_reader<decltype( gen.trigger_map )> trigger_reader{ gen.trigger_map, "invalid monster trigger" }; optional( jo, was_loaded, "anger_triggers", anger, trigger_reader ); optional( jo, was_loaded, "placate_triggers", placate, trigger_reader ); optional( jo, was_loaded, "fear_triggers", fear, trigger_reader ); if( jo.has_member( "path_settings" ) ) { auto jop = jo.get_object( "path_settings" ); // Here rather than in pathfinding.cpp because we want monster-specific defaults and was_loaded optional( jop, was_loaded, "max_dist", path_settings.max_dist, 0 ); optional( jop, was_loaded, "max_length", path_settings.max_length, -1 ); optional( jop, was_loaded, "bash_strength", path_settings.bash_strength, -1 ); optional( jop, was_loaded, "allow_open_doors", path_settings.allow_open_doors, false ); optional( jop, was_loaded, "avoid_traps", path_settings.avoid_traps, false ); optional( jop, was_loaded, "allow_climb_stairs", path_settings.allow_climb_stairs, true ); } }
void MonsterGenerator::load_monster(JsonObject &jo) { const mtype_id mid = mtype_id( jo.get_string("id") ); if (mon_templates.count(mid) > 0) { delete mon_templates[mid]; } mtype *newmon = new mtype; newmon->id = mid; newmon->name = jo.get_string("name").c_str(); if(jo.has_member("name_plural")) { newmon->name_plural = jo.get_string("name_plural"); } else { // default behaviour: Assume the regular plural form (appending an “s”) newmon->name_plural = newmon->name + "s"; } newmon->description = _(jo.get_string("description").c_str()); // Have to overwrite the default { "hflesh" } here newmon->mat = { jo.get_string("material") }; for( auto &s : jo.get_tags( "species" ) ) { newmon->species.insert( species_id( s ) ); } newmon->categories = jo.get_tags("categories"); // See monfaction.cpp newmon->default_faction = monfactions::get_or_add_faction( mfaction_str_id( jo.get_string("default_faction") ) ); newmon->sym = jo.get_string("symbol"); if( utf8_wrapper( newmon->sym ).display_width() != 1 ) { jo.throw_error( "monster symbol should be exactly one console cell width", "symbol" ); } newmon->color = color_from_string(jo.get_string("color")); newmon->size = get_from_string(jo.get_string("size", "MEDIUM"), Creature::size_map, MS_MEDIUM); newmon->phase = get_from_string(jo.get_string("phase", "SOLID"), phase_map, SOLID); newmon->difficulty = jo.get_int("diff", 0); newmon->agro = jo.get_int("aggression", 0); newmon->morale = jo.get_int("morale", 0); newmon->speed = jo.get_int("speed", 0); newmon->attack_cost = jo.get_int("attack_cost", 100); newmon->melee_skill = jo.get_int("melee_skill", 0); newmon->melee_dice = jo.get_int("melee_dice", 0); newmon->melee_sides = jo.get_int("melee_dice_sides", 0); newmon->melee_cut = jo.get_int("melee_cut", 0); newmon->sk_dodge = jo.get_int("dodge", 0); newmon->armor_bash = jo.get_int("armor_bash", 0); newmon->armor_cut = jo.get_int("armor_cut", 0); newmon->hp = jo.get_int("hp", 0); jo.read("starting_ammo", newmon->starting_ammo); newmon->luminance = jo.get_float("luminance", 0); newmon->revert_to_itype = jo.get_string( "revert_to_itype", "" ); newmon->vision_day = jo.get_int("vision_day", 40); newmon->vision_night = jo.get_int("vision_night", 1); if (jo.has_array("attack_effs")) { JsonArray jsarr = jo.get_array("attack_effs"); while (jsarr.has_more()) { JsonObject e = jsarr.next_object(); mon_effect_data new_eff(e.get_string("id", "null"), e.get_int("duration", 0), get_body_part_token( e.get_string("bp", "NUM_BP") ), e.get_bool("permanent", false), e.get_int("chance", 100)); newmon->atk_effs.push_back(new_eff); } } if( jo.has_member( "death_drops" ) ) { JsonIn& stream = *jo.get_raw( "death_drops" ); newmon->death_drops = item_group::load_item_group( stream, "distribution" ); } newmon->dies = get_death_functions(jo, "death_function"); load_special_defense(newmon, jo, "special_when_hit"); load_special_attacks(newmon, jo, "special_attacks"); if (jo.has_member("upgrades")) { JsonObject upgrades = jo.get_object("upgrades"); newmon->half_life = upgrades.get_int("half_life", -1); newmon->upgrade_group = mongroup_id( upgrades.get_string("into_group", mongroup_id::NULL_ID.str() ) ); newmon->upgrade_into = mtype_id( upgrades.get_string("into", mtype_id::NULL_ID.str() ) ); newmon->upgrades = true; } std::set<std::string> flags, anger_trig, placate_trig, fear_trig; flags = jo.get_tags("flags"); anger_trig = jo.get_tags("anger_triggers"); placate_trig = jo.get_tags("placate_triggers"); fear_trig = jo.get_tags("fear_triggers"); newmon->flags = get_set_from_tags(flags, flag_map, MF_NULL); newmon->anger = get_set_from_tags(anger_trig, trigger_map, MTRIG_NULL); newmon->fear = get_set_from_tags(fear_trig, trigger_map, MTRIG_NULL); newmon->placate = get_set_from_tags(placate_trig, trigger_map, MTRIG_NULL); mon_templates[mid] = newmon; }
void monster::plan( const mfactions &factions ) { // Bots are more intelligent than most living stuff bool smart_planning = has_flag( MF_PRIORITIZE_TARGETS ); Creature *target = nullptr; // 8.6f is rating for tank drone 60 tiles away, moose 16 or boomer 33 float dist = !smart_planning ? 1000 : 8.6f; bool fleeing = false; bool docile = friendly != 0 && has_effect( effect_docile ); bool angers_hostile_weak = type->anger.find( MTRIG_HOSTILE_WEAK ) != type->anger.end(); int angers_hostile_near = ( type->anger.find( MTRIG_HOSTILE_CLOSE ) != type->anger.end() ) ? 5 : 0; int fears_hostile_near = ( type->fear.find( MTRIG_HOSTILE_CLOSE ) != type->fear.end() ) ? 5 : 0; bool group_morale = has_flag( MF_GROUP_MORALE ) && morale < type->morale; bool swarms = has_flag( MF_SWARMS ); auto mood = attitude(); // If we can see the player, move toward them or flee. if( friendly == 0 && sees( g->u ) ) { dist = rate_target( g->u, dist, smart_planning ); fleeing = fleeing || is_fleeing( g->u ); target = &g->u; if( dist <= 5 ) { anger += angers_hostile_near; morale -= fears_hostile_near; } } else if( friendly != 0 && !docile ) { // Target unfriendly monsters, only if we aren't interacting with the player. for( int i = 0, numz = g->num_zombies(); i < numz; i++ ) { monster &tmp = g->zombie( i ); if( tmp.friendly == 0 ) { float rating = rate_target( tmp, dist, smart_planning ); if( rating < dist ) { target = &tmp; dist = rating; } } } } if( docile ) { if( friendly != 0 && target != nullptr ) { set_dest( target->pos() ); } return; } for( size_t i = 0; i < g->active_npc.size(); i++ ) { npc &who = *g->active_npc[i]; auto faction_att = faction.obj().attitude( who.get_monster_faction() ); if( faction_att == MFA_NEUTRAL || faction_att == MFA_FRIENDLY ) { continue; } float rating = rate_target( who, dist, smart_planning ); bool fleeing_from = is_fleeing( who ); // Switch targets if closer and hostile or scarier than current target if( ( rating < dist && fleeing ) || ( rating < dist && attitude( &who ) == MATT_ATTACK ) || ( !fleeing && fleeing_from ) ) { target = &who; dist = rating; } fleeing = fleeing || fleeing_from; if( rating <= 5 ) { anger += angers_hostile_near; morale -= fears_hostile_near; } } fleeing = fleeing || ( mood == MATT_FLEE ); if( friendly == 0 ) { for( const auto &fac : factions ) { auto faction_att = faction.obj().attitude( fac.first ); if( faction_att == MFA_NEUTRAL || faction_att == MFA_FRIENDLY ) { continue; } for( int i : fac.second ) { // mon indices monster &mon = g->zombie( i ); float rating = rate_target( mon, dist, smart_planning ); if( rating < dist ) { target = &mon; dist = rating; } if( rating <= 5 ) { anger += angers_hostile_near; morale -= fears_hostile_near; } } } } // Friendly monsters here // Avoid for hordes of same-faction stuff or it could get expensive const auto actual_faction = friendly == 0 ? faction : mfaction_str_id( "player" ); auto const &myfaction_iter = factions.find( actual_faction ); if( myfaction_iter == factions.end() ) { DebugLog( D_ERROR, D_GAME ) << disp_name() << " tried to find faction " << actual_faction.id().str() << " which wasn't loaded in game::monmove"; swarms = false; group_morale = false; } swarms = swarms && target == nullptr; // Only swarm if we have no target if( group_morale || swarms ) { for( const int i : myfaction_iter->second ) { monster &mon = g->zombie( i ); float rating = rate_target( mon, dist, smart_planning ); if( group_morale && rating <= 10 ) { morale += 10 - rating; } if( swarms ) { if( rating < 5 ) { // Too crowded here wander_pos.x = posx() * rng( 1, 3 ) - mon.posx(); wander_pos.y = posy() * rng( 1, 3 ) - mon.posy(); wandf = 2; target = nullptr; // Swarm to the furthest ally you can see } else if( rating < INT_MAX && rating > dist && wandf <= 0 ) { target = &mon; dist = rating; } } } } if( target != nullptr ) { tripoint dest = target->pos(); auto att_to_target = attitude_to( *target ); if( att_to_target == Attitude::A_HOSTILE && !fleeing ) { set_dest( dest ); } else if( fleeing ) { set_dest( tripoint( posx() * 2 - dest.x, posy() * 2 - dest.y, posz() ) ); } if( angers_hostile_weak && att_to_target != Attitude::A_FRIENDLY ) { int hp_per = target->hp_percentage(); if( hp_per <= 70 ) { anger += 10 - int( hp_per / 10 ); } } } else if( friendly > 0 && one_in( 3 ) ) { // Grow restless with no targets friendly--; } else if( friendly < 0 && sees( g->u ) ) { if( rl_dist( pos(), g->u.pos() ) > 2 ) { set_dest( g->u.pos() ); } else { unset_dest(); } } }
void monster::plan( const mfactions &factions ) { // Bots are more intelligent than most living stuff bool electronic = has_flag( MF_ELECTRONIC ); Creature *target = nullptr; // 8.6f is rating for tank drone 60 tiles away, moose 16 or boomer 33 float dist = !electronic ? 1000 : 8.6f; int bresenham_slope = 0; int bresen2 = 0; // Unused until FoV update int selected_slope = 0; bool fleeing = false; bool docile = has_flag( MF_VERMIN ) || ( friendly != 0 && has_effect( "docile" ) ); bool angers_hostile_weak = type->anger.find( MTRIG_HOSTILE_WEAK ) != type->anger.end(); int angers_hostile_near = ( type->anger.find( MTRIG_HOSTILE_CLOSE ) != type->anger.end() ) ? 5 : 0; int fears_hostile_near = ( type->fear.find( MTRIG_HOSTILE_CLOSE ) != type->fear.end() ) ? 5 : 0; bool group_morale = has_flag( MF_GROUP_MORALE ) && morale < type->morale; bool swarms = has_flag( MF_SWARMS ); auto mood = attitude(); // If we can see the player, move toward them or flee. if( friendly == 0 && sees( g->u, bresenham_slope ) ) { dist = rate_target( g->u, bresenham_slope, bresen2, dist, electronic ); fleeing = fleeing || is_fleeing( g->u ); target = &g->u; selected_slope = bresenham_slope; if( dist <= 5 ) { anger += angers_hostile_near; morale -= fears_hostile_near; } } else if( friendly != 0 && !docile ) { // Target unfriendly monsters, only if we aren't interacting with the player. for( int i = 0, numz = g->num_zombies(); i < numz; i++ ) { monster &tmp = g->zombie( i ); if( tmp.friendly == 0 ) { float rating = rate_target( tmp, bresenham_slope, bresen2, dist, electronic ); if( rating < dist ) { target = &tmp; dist = rating; selected_slope = bresenham_slope; } } } } if( docile ) { if( friendly != 0 && target != nullptr ) { int slope = rng( 0, 1 ); set_dest( target->pos(), slope ); } return; } for( size_t i = 0; i < g->active_npc.size(); i++ ) { npc *me = g->active_npc[i]; float rating = rate_target( *me, bresenham_slope, bresen2, dist, electronic ); bool fleeing_from = is_fleeing( *me ); // Switch targets if closer and hostile or scarier than current target if( ( rating < dist && fleeing ) || ( rating < dist && attitude( me ) == MATT_ATTACK ) || ( !fleeing && fleeing_from ) ) { target = me; dist = rating; selected_slope = bresenham_slope; } fleeing = fleeing || fleeing_from; if( rating <= 5 ) { anger += angers_hostile_near; morale -= fears_hostile_near; } } fleeing = fleeing || ( mood == MATT_FLEE ); if( friendly == 0 ) { for( const auto &fac : factions ) { auto faction_att = faction.obj().attitude( fac.first ); if( faction_att == MFA_NEUTRAL || faction_att == MFA_FRIENDLY ) { continue; } for( int i : fac.second ) { // mon indices monster &mon = g->zombie( i ); float rating = rate_target( mon, bresenham_slope, bresen2, dist, electronic ); if( rating < dist ) { target = &mon; dist = rating; selected_slope = bresenham_slope; } if( rating <= 5 ) { anger += angers_hostile_near; morale -= fears_hostile_near; } } } } // Friendly monsters here // Avoid for hordes of same-faction stuff or it could get expensive const auto actual_faction = friendly == 0 ? faction : mfaction_str_id( "player" ); auto const &myfaction_iter = factions.find( actual_faction ); if( myfaction_iter == factions.end() ) { DebugLog( D_ERROR, D_GAME ) << disp_name() << " tried to find faction " << actual_faction.id().str() << " which wasn't loaded in game::monmove"; swarms = false; group_morale = false; } swarms = swarms && target == nullptr; // Only swarm if we have no target if( group_morale || swarms ) { for( const int i : myfaction_iter->second ) { monster &mon = g->zombie( i ); float rating = rate_target( mon, bresenham_slope, bresen2, dist, electronic ); if( group_morale && rating <= 10 ) { morale += 10 - rating; } if( swarms ) { if( rating < 5 ) { // Too crowded here wander_pos.x = posx() * rng( 1, 3 ) - mon.posx(); wander_pos.y = posy() * rng( 1, 3 ) - mon.posy(); wandf = 2; target = nullptr; // Swarm to the furthest ally you can see } else if( rating < INT_MAX && rating > dist && wandf <= 0 ) { target = &mon; dist = rating; selected_slope = bresenham_slope; } } } } if( target != nullptr ) { if( one_in( 2 ) ) { // Random for the diversity of the trajectory ++selected_slope; } else { --selected_slope; } tripoint dest = target->pos(); auto att_to_target = attitude_to( *target ); if( att_to_target == Attitude::A_HOSTILE && !fleeing ) { set_dest( dest, selected_slope ); } else if( fleeing ) { set_dest( tripoint( posx() * 2 - dest.x, posy() * 2 - dest.y, posz() ), selected_slope ); } if( angers_hostile_weak && att_to_target != Attitude::A_FRIENDLY ) { int hp_per = target->hp_percentage(); if( hp_per <= 70 ) { anger += 10 - int( hp_per / 10 ); } } } else if( friendly > 0 && one_in( 3 ) ) { // Grow restless with no targets friendly--; } else if( friendly < 0 && sees( g->u, bresenham_slope ) ) { if( rl_dist( pos3(), g->u.pos3() ) > 2 ) { set_dest( g->u.pos3(), bresenham_slope ); } else { plans.clear(); } } // If we're not adjacent to the start of our plan path, don't act on it. // This is to catch when we had pre-existing invalid plans and // made it through the function without changing them. if( !plans.empty() && square_dist( pos3(), plans.front() ) > 1 ) { plans.clear(); } }
void mtype::load( JsonObject &jo ) { MonsterGenerator &gen = MonsterGenerator::generator(); // Name and name plural are not translated here, but when needed in // combination with the actual count in `mtype::nname`. mandatory( jo, was_loaded, "name", name ); // default behaviour: Assume the regular plural form (appending an “s”) optional( jo, was_loaded, "name_plural", name_plural, name + "s" ); mandatory( jo, was_loaded, "description", description, translated_string_reader ); optional( jo, was_loaded, "material", mat, auto_flags_reader<material_id> {} ); optional( jo, was_loaded, "species", species, auto_flags_reader<species_id> {} ); optional( jo, was_loaded, "categories", categories, auto_flags_reader<> {} ); // See monfaction.cpp if( !was_loaded || jo.has_member( "default_faction" ) ) { const auto faction = mfaction_str_id( jo.get_string( "default_faction" ) ); default_faction = monfactions::get_or_add_faction( faction ); } if( !was_loaded || jo.has_member( "symbol" ) ) { sym = jo.get_string( "symbol" ); if( utf8_wrapper( sym ).display_width() != 1 ) { jo.throw_error( "monster symbol should be exactly one console cell width", "symbol" ); } } mandatory( jo, was_loaded, "color", color, color_reader{} ); const typed_flag_reader<decltype( Creature::size_map )> size_reader{ Creature::size_map, "invalid creature size" }; optional( jo, was_loaded, "size", size, size_reader, MS_MEDIUM ); const typed_flag_reader<decltype( gen.phase_map )> phase_reader{ gen.phase_map, "invalid phase id" }; optional( jo, was_loaded, "phase", phase, phase_reader, SOLID ); optional( jo, was_loaded, "diff", difficulty, 0 ); optional( jo, was_loaded, "aggression", agro, 0 ); optional( jo, was_loaded, "morale", morale, 0 ); optional( jo, was_loaded, "speed", speed, 0 ); optional( jo, was_loaded, "attack_cost", attack_cost, 100 ); optional( jo, was_loaded, "melee_skill", melee_skill, 0 ); optional( jo, was_loaded, "melee_dice", melee_dice, 0 ); optional( jo, was_loaded, "melee_dice_sides", melee_sides, 0 ); optional( jo, was_loaded, "dodge", sk_dodge, 0 ); optional( jo, was_loaded, "armor_bash", armor_bash, 0 ); optional( jo, was_loaded, "armor_cut", armor_cut, 0 ); optional( jo, was_loaded, "armor_acid", armor_acid, armor_cut / 2 ); optional( jo, was_loaded, "armor_fire", armor_fire, 0 ); optional( jo, was_loaded, "hp", hp, 0 ); optional( jo, was_loaded, "starting_ammo", starting_ammo ); optional( jo, was_loaded, "luminance", luminance, 0 ); optional( jo, was_loaded, "revert_to_itype", revert_to_itype, "" ); optional( jo, was_loaded, "vision_day", vision_day, 40 ); optional( jo, was_loaded, "vision_night", vision_night, 1 ); optional( jo, was_loaded, "armor_stab", armor_stab, 0.8f * armor_cut ); optional( jo, was_loaded, "attack_effs", atk_effs, mon_attack_effect_reader{} ); // TODO: make this work with `was_loaded` if( jo.has_array( "melee_damage" ) ) { JsonArray arr = jo.get_array( "melee_damage" ); melee_damage = load_damage_instance( arr ); } else if( jo.has_object( "melee_damage" ) ) { melee_damage = load_damage_instance( jo ); } if( jo.has_int( "melee_cut" ) ) { int bonus_cut = jo.get_int( "melee_cut" ); melee_damage.add_damage( DT_CUT, bonus_cut ); } if( jo.has_member( "death_drops" ) ) { JsonIn &stream = *jo.get_raw( "death_drops" ); death_drops = item_group::load_item_group( stream, "distribution" ); } const typed_flag_reader<decltype( gen.death_map )> death_reader{ gen.death_map, "invalid monster death function" }; optional( jo, was_loaded, "death_function", dies, death_reader ); if( dies.empty() ) { // TODO: really needed? Is an empty `dies` container not allowed? dies.push_back( mdeath::normal ); } if( jo.has_member( "special_when_hit" ) ) { JsonArray jsarr = jo.get_array( "special_when_hit" ); const auto iter = gen.defense_map.find( jsarr.get_string( 0 ) ); if( iter == gen.defense_map.end() ) { jsarr.throw_error( "Invalid monster defense function" ); } sp_defense = iter->second; def_chance = jsarr.get_int( 1 ); } else if( !was_loaded ) { sp_defense = &mdefense::none; def_chance = 0; } if( !was_loaded || jo.has_member( "special_attacks" ) ) { special_attacks.clear(); special_attacks_names.clear(); add_special_attacks( jo, "special_attacks" ); } else { // Note: special_attacks left as is, new attacks are added to it! // Note: member name prefixes are compatible with those used by generic_typed_reader remove_special_attacks( jo, "remove:special_attacks" ); add_special_attacks( jo, "add:special_attacks" ); } // Disable upgrading when JSON contains `"upgrades": false`, but fallback to the // normal behavior (including error checking) if "upgrades" is not boolean or not `false`. if( jo.has_bool( "upgrades" ) && !jo.get_bool( "upgrades" ) ) { upgrade_group = mongroup_id::NULL_ID; upgrade_into = mtype_id::NULL_ID; upgrades = false; } else if( jo.has_member( "upgrades" ) ) { JsonObject up = jo.get_object( "upgrades" ); optional( up, was_loaded, "half_life", half_life, -1 ); optional( up, was_loaded, "into_group", upgrade_group, auto_flags_reader<mongroup_id> {}, mongroup_id::NULL_ID ); optional( up, was_loaded, "into", upgrade_into, auto_flags_reader<mtype_id> {}, mtype_id::NULL_ID ); upgrades = true; } optional( jo, was_loaded, "burn_into", burn_into, auto_flags_reader<mtype_id> {}, mtype_id::NULL_ID ); const typed_flag_reader<decltype( gen.flag_map )> flag_reader{ gen.flag_map, "invalid monster flag" }; optional( jo, was_loaded, "flags", flags, flag_reader ); const typed_flag_reader<decltype( gen.trigger_map )> trigger_reader{ gen.trigger_map, "invalid monster trigger" }; optional( jo, was_loaded, "anger_triggers", anger, trigger_reader ); optional( jo, was_loaded, "placate_triggers", placate, trigger_reader ); optional( jo, was_loaded, "fear_triggers", fear, trigger_reader ); }