/** * @file * @brief Misc monster related functions. **/ #include "AppHdr.h" #include #include "mon-util.h" #include "act-iter.h" #include "areas.h" #include "artefact.h" #include "attitude-change.h" #include "beam.h" #include "colour.h" #include "coordit.h" #include "database.h" #include "dgn-overview.h" #include "directn.h" #include "dungeon.h" #include "env.h" #include "errors.h" #include "fight.h" #include "food.h" #include "fprop.h" #include "ghost.h" #include "goditem.h" #include "itemname.h" #include "itemprop.h" #include "items.h" #include "libutil.h" #include "mapmark.h" #include "mgen_data.h" #include "misc.h" #include "mon-abil.h" #include "mon-behv.h" #include "mon-book.h" #include "mon-chimera.h" #include "mon-death.h" #include "mon-place.h" #include "mon-poly.h" #include "notes.h" #include "options.h" #include "random.h" #include "religion.h" #include "showsymb.h" #include "species.h" #include "spl-util.h" #include "spl-summoning.h" #include "state.h" #include "strings.h" #include "terrain.h" #include "tilepick.h" #include "tileview.h" #include "traps.h" #include "unicode.h" #include "unwind.h" #include "view.h" static FixedVector < int, NUM_MONSTERS > mon_entry; struct mon_display { ucs_t glyph; unsigned colour; monster_type detected; // What a monster of type "type" is detected as. mon_display(unsigned gly = 0, unsigned col = 0, monster_type d = MONS_NO_MONSTER) : glyph(gly), colour(col), detected(d) { } }; static mon_display monster_symbols[NUM_MONSTERS]; static bool initialised_randmons = false; static vector monsters_by_habitat[NUM_HABITATS]; static vector species_by_habitat[NUM_HABITATS]; #include "mon-spll.h" #include "mon-data.h" #define MONDATASIZE ARRAYSZ(mondata) static int _mons_exp_mod(monster_type mclass); // Macro that saves some typing, nothing more. #define smc get_monster_data(mc) // ASSERT(smc) was getting really old #define ASSERT_smc() \ do { \ if (!get_monster_data(mc)) \ die("bogus mc (no monster data): %s (%d)", \ mons_type_name(mc, DESC_PLAIN).c_str(), mc); \ } while (false) /* ******************** BEGIN PUBLIC FUNCTIONS ******************** */ static habitat_type _grid2habitat(dungeon_feature_type grid) { if (feat_is_watery(grid)) return HT_WATER; switch (grid) { case DNGN_LAVA: return HT_LAVA; case DNGN_FLOOR: default: return HT_LAND; } } dungeon_feature_type habitat2grid(habitat_type ht) { switch (ht) { case HT_WATER: return DNGN_DEEP_WATER; case HT_LAVA: return DNGN_LAVA; case HT_LAND: case HT_AMPHIBIOUS: case HT_AMPHIBIOUS_LAVA: default: return DNGN_FLOOR; } } static void _initialise_randmons() { for (int i = 0; i < NUM_HABITATS; ++i) { set tmp_species; const dungeon_feature_type grid = habitat2grid(habitat_type(i)); for (monster_type mt = MONS_0; mt < NUM_MONSTERS; ++mt) { if (invalid_monster_type(mt)) continue; if (monster_habitable_grid(mt, grid)) monsters_by_habitat[i].push_back(mt); const monster_type species = mons_species(mt); if (monster_habitable_grid(species, grid)) tmp_species.insert(species); } for (set::iterator it = tmp_species.begin(); it != tmp_species.end(); ++it) { species_by_habitat[i].push_back(*it); } } initialised_randmons = true; } monster_type random_monster_at_grid(const coord_def& p, bool species) { if (!initialised_randmons) _initialise_randmons(); const habitat_type ht = _grid2habitat(grd(p)); const vector &valid_mons = species ? species_by_habitat[ht] : monsters_by_habitat[ht]; ASSERT(!valid_mons.empty()); return valid_mons.empty() ? MONS_PROGRAM_BUG : valid_mons[ random2(valid_mons.size()) ]; } typedef map mon_name_map; static mon_name_map Mon_Name_Cache; void init_mon_name_cache() { if (!Mon_Name_Cache.empty()) return; for (unsigned i = 0; i < ARRAYSZ(mondata); ++i) { string name = mondata[i].name; lowercase(name); const int mtype = mondata[i].mc; const monster_type mon = monster_type(mtype); // Deal sensibly with duplicate entries; refuse or allow the // insert, depending on which should take precedence. Mostly we // don't care, except looking up "rakshasa" and getting _FAKE // breaks ?/M rakshasa. if (Mon_Name_Cache.count(name)) { if (mon == MONS_PLAYER_SHADOW || mon != MONS_SERPENT_OF_HELL && mons_species(mon) == MONS_SERPENT_OF_HELL) { // Keep previous entry. continue; } else die("Un-handled duplicate monster name: %s", name.c_str()); } Mon_Name_Cache[name] = mon; } } static const char *_mon_entry_name(size_t idx) { return mondata[idx].name; } monster_type get_monster_by_name(string name, bool substring) { if (name.empty()) return MONS_PROGRAM_BUG; lowercase(name); if (!substring) { mon_name_map::iterator i = Mon_Name_Cache.find(name); if (i != Mon_Name_Cache.end()) return i->second; return MONS_PROGRAM_BUG; } size_t idx = find_earliest_match(name, (size_t) 0, ARRAYSZ(mondata), _always_true, _mon_entry_name); return idx == ARRAYSZ(mondata) ? MONS_PROGRAM_BUG : (monster_type) mondata[idx].mc; } void init_monsters() { // First, fill static array with dummy values. {dlb} mon_entry.init(-1); // Next, fill static array with location of entry in mondata[]. {dlb}: for (unsigned int i = 0; i < MONDATASIZE; ++i) mon_entry[mondata[i].mc] = i; // Finally, monsters yet with dummy entries point to TTTSNB(tm). {dlb}: for (unsigned int i = 0; i < NUM_MONSTERS; ++i) if (mon_entry[i] == -1) mon_entry[i] = mon_entry[MONS_PROGRAM_BUG]; init_monster_symbols(); } void init_monster_symbols() { map base_mons; for (monster_type mc = MONS_0; mc < NUM_MONSTERS; ++mc) { mon_display &md = monster_symbols[mc]; const monsterentry *me = get_monster_data(mc); if (me) { md.glyph = me->basechar; md.colour = me->colour; map::iterator it = base_mons.find(md.glyph); if (it == base_mons.end() || it->first == MONS_PROGRAM_BUG) base_mons[md.glyph] = mc; md.detected = base_mons[md.glyph]; } } // Let those follow the feature settings, unless specifically overridden. monster_symbols[MONS_ANIMATED_TREE].glyph = get_feat_symbol(DNGN_TREE); for (monster_type mc = MONS_0; mc < NUM_MONSTERS; ++mc) if (mons_genus(mc) == MONS_STATUE) monster_symbols[mc].glyph = get_feat_symbol(DNGN_GRANITE_STATUE); for (map::iterator it = Options.mon_glyph_overrides.begin(); it != Options.mon_glyph_overrides.end(); ++it) { if (it->first == MONS_PROGRAM_BUG) continue; const cglyph_t &md = it->second; if (md.ch) monster_symbols[it->first].glyph = get_glyph_override(md.ch); if (md.col) monster_symbols[it->first].colour = md.col; } // Validate all glyphs, even those which didn't come from an override. for (monster_type i = MONS_PROGRAM_BUG; i < NUM_MONSTERS; ++i) if (wcwidth(monster_symbols[i].glyph) != 1) monster_symbols[i].glyph = mons_base_char(i); } void set_resist(resists_t &all, mon_resist_flags res, int lev) { if (res > MR_LAST_MULTI) { ASSERT_RANGE(lev, 0, 2); if (lev) all |= res; else all &= ~res; return; } ASSERT_RANGE(lev, -3, 5); all = all & ~(res * 7) | res * (lev & 7); } resists_t get_mons_class_resists(monster_type mc) { const monsterentry *me = get_monster_data(mc); return me ? me->resists : get_monster_data(MONS_PROGRAM_BUG)->resists; } resists_t get_mons_resists(const monster* mon) { get_tentacle_head(mon); resists_t resists = get_mons_class_resists(mon->type); if (mons_is_ghost_demon(mon->type)) resists |= mon->ghost->resists; if (mons_genus(mon->type) == MONS_DRACONIAN && mon->type != MONS_DRACONIAN || mon->type == MONS_TIAMAT || mons_genus(mon->type) == MONS_DEMONSPAWN && mon->type != MONS_DEMONSPAWN) { monster_type subspecies = draco_or_demonspawn_subspecies(mon); if (subspecies != mon->type) resists |= get_mons_class_resists(subspecies); } // Undead get full poison resistance. This is set from here in case // they're undead due to the MF_FAKE_UNDEAD flag. if (mon->holiness() == MH_UNDEAD) resists = resists & ~(MR_RES_POISON * 7) | MR_RES_POISON * 3; // Everything but natural creatures have full rNeg. Set here for the // benefit of the monster_info constructor. If you change this, also // change monster::res_negative_energy. if (mon->holiness() != MH_NATURAL) resists = resists & ~(MR_RES_NEG * 7) | MR_RES_NEG * 3; return resists; } int get_mons_resist(const monster* mon, mon_resist_flags res) { return get_resist(get_mons_resists(mon), res); } monster* monster_at(const coord_def &pos) { if (!in_bounds(pos)) return NULL; const int mindex = mgrd(pos); if (mindex == NON_MONSTER) return NULL; ASSERT(mindex <= MAX_MONSTERS); return &menv[mindex]; } bool mons_class_flag(monster_type mc, uint64_t bf) { const monsterentry *me = get_monster_data(mc); return me ? (me->bitfields & bf) != 0 : false; } int monster::wearing(equipment_type slot, int sub_type, bool calc_unid) const { int ret = 0; const item_def *item = 0; switch (slot) { case EQ_WEAPON: case EQ_STAFF: { const mon_inv_type end = mons_wields_two_weapons(this) ? MSLOT_ALT_WEAPON : MSLOT_WEAPON; for (int i = MSLOT_WEAPON; i <= end; i = i + 1) { item = mslot_item((mon_inv_type) i); if (item && item->base_type == (slot == EQ_WEAPON ? OBJ_WEAPONS : OBJ_STAVES) && item->sub_type == sub_type // Weapon subtypes are always known, staves not. && (slot == EQ_WEAPON || calc_unid || item_type_known(*item))) { ret++; } } } break; case EQ_ALL_ARMOUR: case EQ_CLOAK: case EQ_HELMET: case EQ_GLOVES: case EQ_BOOTS: case EQ_SHIELD: item = mslot_item(MSLOT_SHIELD); if (item && item->base_type == OBJ_ARMOUR && item->sub_type == sub_type) ret++; // Don't check MSLOT_ARMOUR for EQ_SHIELD if (slot == EQ_SHIELD) break; // intentional fall-through case EQ_BODY_ARMOUR: item = mslot_item(MSLOT_ARMOUR); if (item && item->base_type == OBJ_ARMOUR && item->sub_type == sub_type) ret++; break; case EQ_AMULET: case EQ_RINGS: case EQ_RINGS_PLUS: item = mslot_item(MSLOT_JEWELLERY); if (item && item->base_type == OBJ_JEWELLERY && item->sub_type == sub_type && (calc_unid || item_type_known(*item))) { if (slot == EQ_RINGS_PLUS) ret += item->plus; else ret++; } break; default: die("invalid slot %d for monster::wearing()", slot); } return ret; } int monster::wearing_ego(equipment_type slot, int special, bool calc_unid) const { int ret = 0; const item_def *item = 0; switch (slot) { case EQ_WEAPON: { const mon_inv_type end = mons_wields_two_weapons(this) ? MSLOT_ALT_WEAPON : MSLOT_WEAPON; for (int i = MSLOT_WEAPON; i <= end; i++) { item = mslot_item((mon_inv_type) i); if (item && item->base_type == OBJ_WEAPONS && get_weapon_brand(*item) == special && (calc_unid || item_type_known(*item))) { ret++; } } } break; case EQ_ALL_ARMOUR: case EQ_CLOAK: case EQ_HELMET: case EQ_GLOVES: case EQ_BOOTS: case EQ_SHIELD: item = mslot_item(MSLOT_SHIELD); if (item && item->base_type == OBJ_ARMOUR && get_armour_ego_type(*item) == special && (calc_unid || item_type_known(*item))) { ret++; } // Don't check MSLOT_ARMOUR for EQ_SHIELD if (slot == EQ_SHIELD) break; // intentional fall-through case EQ_BODY_ARMOUR: item = mslot_item(MSLOT_ARMOUR); if (item && item->base_type == OBJ_ARMOUR && get_armour_ego_type(*item) == special && (calc_unid || item_type_known(*item))) { ret++; } break; case EQ_AMULET: case EQ_STAFF: case EQ_RINGS: case EQ_RINGS_PLUS: // No egos. break; default: die("invalid slot %d for monster::wearing_ego()", slot); } return ret; } int monster::scan_artefacts(artefact_prop_type ra_prop, bool calc_unid) const { int ret = 0; // TODO: do we really want to prevent randarts from working for zombies? if (mons_itemuse(this) >= MONUSE_STARTING_EQUIPMENT) { const int weap = inv[MSLOT_WEAPON]; const int second = inv[MSLOT_ALT_WEAPON]; // Two-headed ogres, etc. const int armour = inv[MSLOT_ARMOUR]; const int shld = inv[MSLOT_SHIELD]; const int jewellery = inv[MSLOT_JEWELLERY]; if (weap != NON_ITEM && mitm[weap].base_type == OBJ_WEAPONS && is_artefact(mitm[weap])) { ret += artefact_wpn_property(mitm[weap], ra_prop); } if (second != NON_ITEM && mitm[second].base_type == OBJ_WEAPONS && is_artefact(mitm[second]) && mons_wields_two_weapons(this)) { ret += artefact_wpn_property(mitm[second], ra_prop); } if (armour != NON_ITEM && mitm[armour].base_type == OBJ_ARMOUR && is_artefact(mitm[armour])) { ret += artefact_wpn_property(mitm[armour], ra_prop); } if (shld != NON_ITEM && mitm[shld].base_type == OBJ_ARMOUR && is_artefact(mitm[shld])) { ret += artefact_wpn_property(mitm[shld], ra_prop); } if (jewellery != NON_ITEM && mitm[jewellery].base_type == OBJ_JEWELLERY && is_artefact(mitm[jewellery])) { ret += artefact_wpn_property(mitm[jewellery], ra_prop); } } return ret; } mon_holy_type mons_class_holiness(monster_type mc) { ASSERT_smc(); return smc->holiness; } bool mons_class_is_confusable(monster_type mc) { ASSERT_smc(); return smc->resist_magic < MAG_IMMUNE && mons_class_holiness(mc) != MH_NONLIVING && mons_class_holiness(mc) != MH_PLANT; } bool mons_class_is_stationary(monster_type mc) { return mons_class_flag(mc, M_STATIONARY); } // Monsters that are worthless obstacles: not to // be attacked by default, but may be cut down to // get to target even if coaligned. bool mons_class_is_firewood(monster_type mc) { return mons_class_is_stationary(mc) && mons_class_flag(mc, M_NO_EXP_GAIN) && !mons_is_tentacle_or_tentacle_segment(mc); } bool mons_is_firewood(const monster* mon) { return mons_class_is_firewood(mon->type); } // "body" in a purely grammatical sense. bool mons_has_body(const monster* mon) { if (mon->type == MONS_FLYING_SKULL || mon->type == MONS_CURSE_SKULL || mon->type == MONS_CURSE_TOE || mons_class_is_animated_weapon(mon->type)) { return false; } switch (mons_base_char(mon->type)) { case 'P': case 'v': case 'G': case '*': case '%': case 'J': return false; } return true; } bool mons_has_flesh(const monster* mon) { if (mon->is_skeletal() || mon->is_insubstantial()) return false; // Dictionary says: // 1. (12) flesh -- (the soft tissue of the body of a vertebrate: // mainly muscle tissue and fat) // 3. pulp, flesh -- (a soft moist part of a fruit) // yet I exclude sense 3 anyway but include arthropods and molluscs. return mon->holiness() != MH_PLANT && mon->holiness() != MH_NONLIVING && mons_genus(mon->type) != MONS_GIANT_EYEBALL && mons_genus(mon->type) != MONS_GIANT_ORANGE_BRAIN && mons_genus(mon->type) != MONS_JELLY && mon->type != MONS_DEATH_COB; // plant! } // Difference in speed between monster and the player for Cheibriados' // purposes. This is the speed difference disregarding the player's // slow status. int cheibriados_monster_player_speed_delta(const monster* mon) { // Ignore the Slow effect. unwind_var ignore_slow(you.duration[DUR_SLOW], 0); const int pspeed = 1000 / (player_movement_speed() * player_speed()); dprf("Your delay: %d, your speed: %d, mon speed: %d", player_movement_speed(), pspeed, mon->speed); return mon->speed - pspeed; } bool cheibriados_thinks_mons_is_fast(const monster* mon) { return cheibriados_monster_player_speed_delta(mon) > 0; } // Dithmenos hates halos and spells that cause illumination. bool mons_is_illuminating(const monster* mon) { if (mon->halo_radius2() >= 0) return true; if (!mon->can_use_spells()) return false; for (int i = 0; i < NUM_MONSTER_SPELL_SLOTS; ++i) { if (is_illuminating_spell(mon->spells[i])) return true; } return false; } // Dithmenos also hates fire users and generally fiery beings. bool mons_is_fiery(const monster* mon) { // This chain of checks is for fire breath weapons and special // abilities. if (mons_species(mon->type) == MONS_FIRE_DRAGON || mon->type == MONS_BURNING_BUSH || (mons_genus(mon->type) == MONS_DRACONIAN && draco_or_demonspawn_subspecies(mon) == MONS_RED_DRACONIAN) || mon->type == MONS_HELL_HOUND || mon->type == MONS_FIRE_DRAKE || mon->type == MONS_LINDWURM || mon->type == MONS_FIRE_CRAB) { return true; } return mon->has_attack_flavour(AF_FIRE) || mon->has_attack_flavour(AF_PURE_FIRE) || mon->has_attack_flavour(AF_STICKY_FLAME) || (mon->has_spell_of_type(SPTYP_FIRE) && mon->can_use_spells()); } bool mons_is_projectile(monster_type mc) { return mc == MONS_ORB_OF_DESTRUCTION; } bool mons_is_projectile(const monster* mon) { return mon->type == MONS_ORB_OF_DESTRUCTION; } bool mons_is_boulder(const monster* mon) { return mon->type == MONS_BOULDER_BEETLE && mon->rolling(); } static bool _mons_class_is_clingy(monster_type type) { return mons_genus(type) == MONS_SPIDER || type == MONS_GIANT_GECKO || type == MONS_GIANT_COCKROACH || type == MONS_GIANT_MITE || type == MONS_DEMONIC_CRAWLER; } bool mons_can_cling_to_walls(const monster* mon) { return _mons_class_is_clingy(mon->type) || mons_class_is_chimeric(mon->type) && _mons_class_is_clingy(get_chimera_legs(mon)); } // Conjuration or Hexes. Summoning and Necromancy make the monster a creature // at least in some degree, golems have a chem granting them that. bool mons_is_object(monster_type mc) { return mons_is_conjured(mc) || mc == MONS_TWISTER // unloading seeds helps the species || mc == MONS_GIANT_SPORE || mc == MONS_LURKING_HORROR || mc == MONS_DANCING_WEAPON || mc == MONS_LIGHTNING_SPIRE; } bool mons_has_blood(monster_type mc) { return mons_class_flag(mc, M_COLD_BLOOD) || mons_class_flag(mc, M_WARM_BLOOD); } bool mons_is_sensed(monster_type mc) { return mc == MONS_SENSED || mc == MONS_SENSED_FRIENDLY || mc == MONS_SENSED_TRIVIAL || mc == MONS_SENSED_EASY || mc == MONS_SENSED_TOUGH || mc == MONS_SENSED_NASTY; } bool mons_allows_beogh(const monster* mon) { if (!player_genus(GENPC_ORCISH) || you_worship(GOD_BEOGH)) return false; // no one else gives a damn return mons_genus(mon->type) == MONS_ORC && mon->is_priest() && mon->god == GOD_BEOGH; } bool mons_allows_beogh_now(const monster* mon) { // Do the expensive LOS check last. return mon && mons_allows_beogh(mon) && !mon->is_summoned() && !mon->friendly() && !silenced(mon->pos()) && !mon->has_ench(ENCH_MUTE) && !mons_is_confused(mon) && mons_is_seeking(mon) && mon->foe == MHITYOU && !mons_is_immotile(mon) && you.visible_to(mon) && you.can_see(mon); } // Returns true for monsters that obviously (to the player) feel // "thematically at home" in a branch. Currently used for native // monsters recognising traps and patrolling branch entrances. bool mons_is_native_in_branch(const monster* mons, const branch_type branch) { switch (branch) { case BRANCH_ELF: return mons_genus(mons->type) == MONS_ELF; case BRANCH_ORC: return mons_genus(mons->type) == MONS_ORC; case BRANCH_SHOALS: return mons_species(mons->type) == MONS_CYCLOPS || mons_species(mons->type) == MONS_MERFOLK || mons_genus(mons->type) == MONS_MERMAID || mons->type == MONS_HARPY; case BRANCH_SLIME: return mons_is_slime(mons); case BRANCH_SNAKE: return mons_genus(mons->type) == MONS_NAGA || mons_genus(mons->type) == MONS_SNAKE || mons_genus(mons->type) == MONS_SALAMANDER; case BRANCH_ZOT: return mons_genus(mons->type) == MONS_DRACONIAN || mons->type == MONS_ORB_GUARDIAN || mons->type == MONS_ORB_OF_FIRE || mons->type == MONS_DEATH_COB; case BRANCH_VAULTS: return mons_genus(mons->type) == MONS_HUMAN; case BRANCH_CRYPT: return mons->holiness() == MH_UNDEAD; case BRANCH_TOMB: return mons_genus(mons->type) == MONS_MUMMY; case BRANCH_SPIDER: return mons_genus(mons->type) == MONS_SPIDER; case BRANCH_ABYSS: return mons_is_abyssal_only(mons->type) || mons->type == MONS_ABOMINATION_LARGE || mons->type == MONS_ABOMINATION_SMALL || mons->type == MONS_TENTACLED_MONSTROSITY || mons->type == MONS_TENTACLED_STARSPAWN || mons->type == MONS_THRASHING_HORROR || mons->type == MONS_UNSEEN_HORROR || mons->type == MONS_WORLDBINDER; default: return false; } } bool mons_is_abyssal_only(monster_type mc) { switch (mc) { case MONS_ANCIENT_ZYME: case MONS_ELDRITCH_TENTACLE: case MONS_ELDRITCH_TENTACLE_SEGMENT: case MONS_LURKING_HORROR: case MONS_WRETCHED_STAR: return true; default: return false; } } bool mons_is_poisoner(const monster* mon) { if (chunk_is_poisonous(mons_corpse_effect(mon->type))) return true; if (mon->has_attack_flavour(AF_POISON) || mon->has_attack_flavour(AF_POISON_STRONG)) { return true; } if (mon->type == MONS_DANCING_WEAPON && mon->primary_weapon() && get_weapon_brand(*mon->primary_weapon()) == SPWPN_VENOM) { return true; } return false; } // Monsters considered as "slime" for Jiyva. bool mons_class_is_slime(monster_type mc) { return mons_genus(mc) == MONS_JELLY || mons_genus(mc) == MONS_GIANT_EYEBALL || mons_genus(mc) == MONS_GIANT_ORANGE_BRAIN; } bool mons_is_slime(const monster* mon) { return mons_class_is_slime(mon->type); } bool herd_monster(const monster* mon) { return mons_class_flag(mon->type, M_HERD); } // Plant or fungus really bool mons_class_is_plant(monster_type mc) { return mons_genus(mc) == MONS_PLANT || mons_genus(mc) == MONS_FUNGUS || mons_species(mc) == MONS_BUSH || mc == MONS_SHAMBLING_MANGROVE; } bool mons_is_plant(const monster* mon) { return mons_class_is_plant(mon->type); } bool mons_eats_items(const monster* mon) { return mons_itemeat(mon) == MONEAT_ITEMS || mon->has_ench(ENCH_EAT_ITEMS); } bool mons_eats_corpses(const monster* mon) { return mons_itemeat(mon) == MONEAT_CORPSES; } bool invalid_monster(const monster* mon) { return !mon || invalid_monster_type(mon->type); } bool invalid_monster_type(monster_type mt) { return mt < 0 || mt >= NUM_MONSTERS || mon_entry[mt] == mon_entry[MONS_PROGRAM_BUG]; } bool invalid_monster_index(int i) { return i < 0 || i >= MAX_MONSTERS; } bool mons_is_statue(monster_type mc) { return mc == MONS_ORANGE_STATUE || mc == MONS_SILVER_STATUE || mc == MONS_ICE_STATUE || mc == MONS_ROXANNE; } bool mons_is_mimic(monster_type mc) { return mons_is_item_mimic(mc) || mons_is_feat_mimic(mc); } bool mons_is_item_mimic(monster_type mc) { #if TAG_MAJOR_VERSION == 34 return mc >= MONS_INEPT_ITEM_MIMIC && mc <= MONS_MONSTROUS_ITEM_MIMIC; #else return mc >= MONS_INEPT_ITEM_MIMIC && mc <= MONS_RAVENOUS_ITEM_MIMIC; #endif } bool mons_is_feat_mimic(monster_type mc) { return mc >= MONS_INEPT_FEATURE_MIMIC && mc <= MONS_RAVENOUS_FEATURE_MIMIC; } void discover_mimic(const coord_def& pos, bool wake) { item_def* item = item_mimic_at(pos); const bool feature_mimic = !item && feature_mimic_at(pos); // Is there really a mimic here? if (!item && !feature_mimic) return; const dungeon_feature_type feat = grd(pos); // If the feature has been destroyed, don't create a floor mimic. if (feature_mimic && feat_cannot_be_mimic(feat)) { env.level_map_mask(pos) &= !MMT_MIMIC; return; } const feature_def feat_d = get_feature_def(feat); const string name = feature_mimic ? feat_type_name(feat) : item->base_type == OBJ_GOLD ? "pile of gold coins" : item->name(DESC_BASENAME); tileidx_t tile = tileidx_feature(pos); #ifdef USE_TILE apply_variations(env.tile_flv(pos), &tile, pos); #endif // If a monster is standing on top of the mimic, move it out of the way. actor* act = actor_at(pos); if (act && !act->shove(name.c_str())) { // Not a single habitable place left on the level. Possible in a Zig // or if a paranoid player covers a small Trove with summons. mpr("There is some commotion, and a hidden mimic gets squished!"); if (item) destroy_item(*item, true); else { unnotice_feature(level_pos(level_id::current(), pos)); grd(pos) = DNGN_FLOOR; env.level_map_mask(pos) &= !MMT_MIMIC; set_terrain_changed(pos); remove_markers_and_listeners_at(pos); } return; } if (feature_mimic) { // If we took a note of this feature, then note that it was a mimic. if (!is_boring_terrain(feat)) { string desc = feature_description_at(pos, false, DESC_THE, false); take_note(Note(NOTE_FEAT_MIMIC, 0, 0, desc.c_str())); } // Remove the feature and clear the flag. unnotice_feature(level_pos(level_id::current(), pos)); grd(pos) = DNGN_FLOOR; env.level_map_mask(pos) &= !MMT_MIMIC; set_terrain_changed(pos); remove_markers_and_listeners_at(pos); #if TAG_MAJOR_VERSION == 34 if (feat_is_door(feat)) env.level_map_mask(pos) |= MMT_WAS_DOOR_MIMIC; #endif } // Generate and place the monster. mgen_data mg; mg.behaviour = wake ? BEH_WANDER : BEH_LURK; mg.cls = item ? MONS_ITEM_MIMIC : MONS_FEATURE_MIMIC; mg.pos = pos; if (wake) mg.flags |= MG_DONT_COME; const int level = env.absdepth0 + 1; // Early levels get inept mimics instead if (!x_chance_in_y(level - 6, 6)) mg.cls = item ? MONS_INEPT_ITEM_MIMIC : MONS_INEPT_FEATURE_MIMIC; // Deeper, you get ravenous mimics else if (x_chance_in_y(level - 15, 6)) mg.cls = item ? MONS_RAVENOUS_ITEM_MIMIC : MONS_RAVENOUS_FEATURE_MIMIC; if (feature_mimic) { if (feat_is_stone_stair(feat)) mg.colour = feat_d.em_colour; else mg.colour = feat_d.colour; mg.props["feat_type"] = static_cast(feat); mg.props["glyph"] = static_cast(get_feat_symbol(feat)); mg.props["tile_idx"] = static_cast(tile); } else { const cglyph_t glyph = get_item_glyph(item); mg.colour = glyph.col; mg.props["glyph"] = static_cast(glyph.ch); } monster *mimic = place_monster(mg, true, true); if (!mimic) { mprf(MSGCH_ERROR, "Too many monsters on level, can't place mimic."); if (item) destroy_item(*item, true); return; } if (item && !mimic->pickup_misc(*item, 0)) die("Mimic failed to pickup its item."); if (!mimic->move_to_pos(pos)) die("Moving mimic into position failed."); if (wake) behaviour_event(mimic, ME_ALERT, &you); // Friendly monsters don't appreciate being pushed away. if (act && !act->is_player() && act->as_monster()->friendly()) behaviour_event(act->as_monster(), ME_WHACK, mimic); // Announce the mimic. if (mons_near(mimic)) { mprf(MSGCH_WARN, "The %s is a mimic!", name.c_str()); mimic->seen_context = SC_JUST_SEEN; } // Just in case there's another one. if (mimic_at(pos)) discover_mimic(pos); } void discover_shifter(monster* shifter) { shifter->flags |= MF_KNOWN_SHIFTER; } bool mons_is_demon(monster_type mc) { // Not every demonic monster is a demon (hell hog, hell hound, etc.) if (mons_class_holiness(mc) == MH_DEMONIC && (mons_demon_tier(mc) != 0 && mc != MONS_ANTAEUS || mons_species(mc) == MONS_RAKSHASA)) { return true; } return false; } int mons_demon_tier(monster_type mc) { switch (mons_base_char(mc)) { case 'C': if (mc != MONS_ANTAEUS) return 0; case '&': return -1; case '1': return 1; case '2': return 2; case '3': return 3; case '4': return 4; case '5': return 5; default: return 0; } } bool mons_is_draconian(monster_type mc) { return mc >= MONS_FIRST_DRACONIAN && mc <= MONS_LAST_DRACONIAN; } bool mons_is_base_draconian(monster_type mc) { return mc >= MONS_FIRST_DRACONIAN && mc <= MONS_LAST_BASE_DRACONIAN; } bool mons_is_demonspawn(monster_type mc) { return #if TAG_MAJOR_VERSION == 34 mc == MONS_DEMONSPAWN || #endif mc >= MONS_FIRST_DEMONSPAWN && mc <= MONS_LAST_DEMONSPAWN; } // Conjured (as opposed to summoned) monsters are actually here, even // though they're typically volatile (like, made of real fire). As such, // they should be immune to Abjuration or Recall. Also, they count as // things rather than beings. bool mons_is_conjured(monster_type mc) { return mons_is_projectile(mc) || mons_is_avatar(mc) || mc == MONS_FIRE_VORTEX || mc == MONS_SPATIAL_VORTEX || mc == MONS_BALL_LIGHTNING || mc == MONS_FULMINANT_PRISM; } // Returns true if the given monster's foe is also a monster. bool mons_foe_is_mons(const monster* mons) { const actor *foe = mons->get_foe(); return foe && foe->is_monster(); } int mons_weight(monster_type mc) { ASSERT_smc(); return smc->weight; } corpse_effect_type mons_corpse_effect(monster_type mc) { ASSERT_smc(); return smc->corpse_thingy; } monster_type mons_genus(monster_type mc) { if (mc == RANDOM_DRACONIAN || mc == RANDOM_BASE_DRACONIAN || mc == RANDOM_NONBASE_DRACONIAN || (mc == MONS_PLAYER_ILLUSION && player_genus(GENPC_DRACONIAN))) { return MONS_DRACONIAN; } if (mc == RANDOM_DEMONSPAWN || mc == RANDOM_BASE_DEMONSPAWN || mc == RANDOM_NONBASE_DEMONSPAWN) { return MONS_DEMONSPAWN; } if (mc == MONS_NO_MONSTER) return MONS_NO_MONSTER; ASSERT_smc(); return smc->genus; } monster_type mons_species(monster_type mc) { const monsterentry *me = get_monster_data(mc); return me ? me->species : MONS_PROGRAM_BUG; } monster_type draco_or_demonspawn_subspecies(const monster* mon) { ASSERT(mons_genus(mon->type) == MONS_DRACONIAN || mons_genus(mon->type) == MONS_DEMONSPAWN); if (mon->type == MONS_PLAYER_ILLUSION && mons_genus(mon->type) == MONS_DRACONIAN) { return player_species_to_mons_species(mon->ghost->species); } monster_type retval = mons_species(mon->type); if ((retval == MONS_DRACONIAN || retval == MONS_DEMONSPAWN) && mon->type != retval) { retval = mon->base_monster; } return retval; } monster_type mons_detected_base(monster_type mc) { return monster_symbols[mc].detected; } int get_shout_noise_level(const shout_type shout) { switch (shout) { case S_SILENT: return 0; case S_HISS: case S_VERY_SOFT: return 4; case S_SOFT: return 6; case S_GURGLE: case S_LOUD: return 10; case S_SHOUT2: case S_ROAR: case S_VERY_LOUD: return 12; default: return 8; } } // Only beasts and chaos spawns use S_RANDOM for noise type. // Pandemonium lords can also get here, but this is mostly used for the // "says" verb used for insults. static bool _shout_fits_monster(monster_type mc, int shout) { if (shout == NUM_SHOUTS || shout >= NUM_LOUDNESS || shout == S_SILENT) return false; // Chaos spawns can do anything but demon taunts, since they're // not coherent enough to actually say words. if (mc == MONS_CHAOS_SPAWN) return shout != S_DEMON_TAUNT; // For Pandemonium lords, almost everything is fair game. It's only // used for the shouting verb ("say", "bellow", "roar", etc.) anyway. if (mc != MONS_HELL_BEAST) return shout != S_BUZZ && shout != S_WHINE && shout != S_CROAK; switch (shout) { // 2-headed ogres, bees or mosquitos never fit. case S_SHOUT2: case S_BUZZ: case S_WHINE: // The beast cannot speak. case S_DEMON_TAUNT: return false; default: return true; } } // If demon_shout is true, we're trying to find a random verb and // loudness for a Pandemonium lord trying to shout. shout_type mons_shouts(monster_type mc, bool demon_shout) { ASSERT_smc(); shout_type u = smc->shouts; // Pandemonium lords use this to get the noises. if (u == S_RANDOM || demon_shout && u == S_DEMON_TAUNT) { const int max_shout = (u == S_RANDOM ? NUM_SHOUTS : NUM_LOUDNESS); do u = static_cast(random2(max_shout)); while (!_shout_fits_monster(mc, u)); } return u; } bool mons_is_ghost_demon(monster_type mc) { return mc == MONS_UGLY_THING || mc == MONS_VERY_UGLY_THING || mc == MONS_PLAYER_GHOST || mc == MONS_PLAYER_ILLUSION || mons_class_is_animated_weapon(mc) || mc == MONS_PANDEMONIUM_LORD || mons_class_is_chimeric(mc) || mc == MONS_SPELLFORGED_SERVITOR; } bool mons_is_pghost(monster_type mc) { return mc == MONS_PLAYER_GHOST || mc == MONS_PLAYER_ILLUSION; } bool mons_is_unique(monster_type mc) { return mons_class_flag(mc, M_UNIQUE); } bool mons_sense_invis(const monster* mon) { return mons_class_flag(mon->type, M_SENSE_INVIS); } ucs_t mons_char(monster_type mc) { return monster_symbols[mc].glyph; } char mons_base_char(monster_type mc) { const monsterentry *me = get_monster_data(mc); return me ? me->basechar : 0; } mon_itemuse_type mons_class_itemuse(monster_type mc) { ASSERT_smc(); return smc->gmon_use; } mon_itemuse_type mons_itemuse(const monster* mon) { if (mons_enslaved_soul(mon)) return mons_class_itemuse(mons_zombie_base(mon)); return mons_class_itemuse(mon->type); } static mon_itemeat_type _mons_class_itemeat(monster_type mc) { ASSERT_smc(); return smc->gmon_eat; } mon_itemeat_type mons_itemeat(const monster* mon) { if (mons_enslaved_soul(mon)) return _mons_class_itemeat(mons_zombie_base(mon)); if (mon->has_ench(ENCH_EAT_ITEMS)) return MONEAT_ITEMS; return _mons_class_itemeat(mon->type); } int mons_class_colour(monster_type mc) { ASSERT_smc(); return monster_symbols[mc].colour; } bool mons_class_can_regenerate(monster_type mc) { return !mons_class_flag(mc, M_NO_REGEN); } bool get_tentacle_head(const monster*& mon) { // For tentacle segments, find the associated tentacle. if (mon->is_child_tentacle_segment()) { if (invalid_monster_index(mon->number)) return false; if (invalid_monster(&menv[mon->number])) return false; mon = &menv[mon->number]; } // For tentacles, find the associated head. if (mon->is_child_tentacle()) { if (invalid_monster_index(mon->number)) return false; if (invalid_monster(&menv[mon->number])) return false; mon = &menv[mon->number]; } return true; } bool mons_can_regenerate(const monster* mon) { get_tentacle_head(mon); if (testbits(mon->flags, MF_NO_REGEN)) return false; return mons_class_can_regenerate(mon->type); } bool mons_class_fast_regen(monster_type mc) { return mons_class_flag(mc, M_FAST_REGEN); } bool mons_class_leaves_hide(monster_type mc) { if (mons_genus(mc) == MONS_TROLL) return true; switch (mc) { case MONS_FIRE_DRAGON: case MONS_ICE_DRAGON: case MONS_STEAM_DRAGON: case MONS_MOTTLED_DRAGON: case MONS_STORM_DRAGON: case MONS_GOLDEN_DRAGON: case MONS_SWAMP_DRAGON: case MONS_PEARL_DRAGON: return true; default: return false; } } int mons_zombie_size(monster_type mc) { ASSERT_smc(); return smc->zombie_size; } monster_type mons_zombie_base(const monster* mon) { return mon->base_monster; } bool mons_class_is_zombified(monster_type mc) { return mc == MONS_ZOMBIE || mc == MONS_SKELETON || mc == MONS_SIMULACRUM || mc == MONS_SPECTRAL_THING; } bool mons_class_is_hybrid(monster_type mc) { return mons_class_flag(mc, M_HYBRID); } bool mons_class_is_chimeric(monster_type mc) { return mc == MONS_CHIMERA; } bool mons_class_is_animated_weapon(monster_type type) { return type == MONS_DANCING_WEAPON || type == MONS_SPECTRAL_WEAPON; } bool mons_is_zombified(const monster* mon) { return mons_class_is_zombified(mon->type); } monster_type mons_base_type(const monster* mon) { if (mons_class_is_zombified(mon->type) || mons_class_is_chimeric(mon->type)) return mon->base_monster; return mon->type; } bool mons_class_can_leave_corpse(monster_type mc) { return mons_weight(mc) > 0; } bool mons_class_can_be_zombified(monster_type mc) { monster_type ms = mons_species(mc); return !invalid_monster_type(ms) && mons_zombie_size(ms) != Z_NOZOMBIE && mons_weight(ms) > 0; } bool mons_can_be_zombified(const monster* mon) { return mons_class_can_be_zombified(mon->type) && !mon->is_summoned() && !mons_enslaved_body_and_soul(mon); } bool mons_class_can_use_stairs(monster_type mc) { return (!mons_class_is_zombified(mc) || mc == MONS_SPECTRAL_THING) && !mons_is_tentacle_or_tentacle_segment(mc) && !mons_is_abyssal_only(mc) && mc != MONS_SILENT_SPECTRE && mc != MONS_PLAYER_GHOST && mc != MONS_GERYON && mc != MONS_ROYAL_JELLY; } bool mons_can_use_stairs(const monster* mon) { if (!mons_class_can_use_stairs(mon->type)) return false; // Check summon status int stype = 0; // Other permanent summons can always use stairs if (mon->is_summoned(0, &stype) && !mon->is_perm_summoned() && stype > 0 && stype < NUM_SPELLS) { // Allow uncapped summons to use stairs. This means creatures // from misc evokables, temporary god summons, etc. These tend // to be balanced by other means; however this could use a review // and perhaps needs a whilelist (or long-duration vs. short-duration). return !summons_are_capped(static_cast(stype)); } // Everything else is fine return true; } bool mons_enslaved_body_and_soul(const monster* mon) { return mon->has_ench(ENCH_SOUL_RIPE); } bool mons_enslaved_soul(const monster* mon) { return testbits(mon->flags, MF_ENSLAVED_SOUL); } bool name_zombie(monster* mon, monster_type mc, const string &mon_name) { mon->mname = mon_name; // Special case for Blork the orc: shorten his name to "Blork" to // avoid mentions of "Blork the orc the orc zombie". if (mc == MONS_BLORK_THE_ORC) mon->mname = "Blork"; // Also for the Lernaean hydra: treat Lernaean as an adjective to // avoid mentions of "the Lernaean hydra the X-headed hydra zombie". else if (mc == MONS_LERNAEAN_HYDRA) { mon->mname = "Lernaean"; mon->flags |= MF_NAME_ADJECTIVE; } // Also for the Enchantress: treat Enchantress as an adjective to // avoid mentions of "the Enchantress the spriggan zombie". else if (mc == MONS_THE_ENCHANTRESS) { mon->mname = "Enchantress"; mon->flags |= MF_NAME_ADJECTIVE; } if (starts_with(mon->mname, "shaped ")) mon->flags |= MF_NAME_SUFFIX; // It's unlikely there's a desc for "Duvessa the elf skeleton", but // we still want to allow it if overridden. if (!mon->props.exists("dbname")) mon->props["dbname"] = mons_class_name(mon->type); return true; } bool name_zombie(monster* mon, const monster* orig) { if (!mons_is_unique(orig->type) && orig->mname.empty()) return false; string name; if (!orig->mname.empty()) name = orig->mname; else name = mons_type_name(orig->type, DESC_PLAIN); return name_zombie(mon, orig->type, name); } static int _downscale_zombie_damage(int damage) { // These are cumulative, of course: {dlb} if (damage > 1) damage--; if (damage > 4) damage--; if (damage > 11) damage--; if (damage > 14) damage--; return damage; } static mon_attack_def _downscale_zombie_attack(const monster* mons, mon_attack_def attk) { switch (attk.type) { case AT_STING: case AT_SPORE: case AT_TOUCH: case AT_ENGULF: attk.type = AT_HIT; break; default: break; } if (mons->type == MONS_SIMULACRUM) attk.flavour = AF_COLD; else if (mons->type == MONS_SPECTRAL_THING && coinflip()) attk.flavour = AF_DRAIN_XP; else if (attk.flavour != AF_REACH && attk.flavour != AF_CRUSH) attk.flavour = AF_PLAIN; attk.damage = _downscale_zombie_damage(attk.damage); return attk; } /** Get the attack type, attack flavour and damage for a monster attack. * * @param mon The monster to look at. * @param attk_number Which attack number to get. * @param base_flavour If true, attack flavours that are randomised on every attack * will have their base flavour returned instead of one of the * random flavours. * @return A mon_attack_def for the specified attack. */ mon_attack_def mons_attack_spec(const monster* mon, int attk_number, bool base_flavour) { monster_type mc = mon->type; get_tentacle_head(mon); const bool zombified = mons_is_zombified(mon); if (attk_number < 0 || attk_number >= MAX_NUM_ATTACKS || mon->has_hydra_multi_attack()) attk_number = 0; if (mons_class_is_chimeric(mc)) { // Chimera get attacks 0, 1 and 2 from their base components. Attack 3 is // the secondary attack of the main base type. switch (attk_number) { case 0: mc = mon->base_monster; break; case 1: if (mon->props.exists("chimera_part_2")) { mc = static_cast(mon->props["chimera_part_2"].get_int()); attk_number = 0; } break; case 2: if (mon->props.exists("chimera_part_3")) { mc = static_cast(mon->props["chimera_part_3"].get_int()); attk_number = 0; } break; case 3: mc = mon->base_monster; attk_number = 1; break; } } else if (mons_is_ghost_demon(mc)) { if (attk_number == 0) { return mon_attack_def::attk(mon->ghost->damage, mon->ghost->att_type, mon->ghost->att_flav); } return mon_attack_def::attk(0, AT_NONE); } else if (mons_is_demonspawn(mc) && attk_number != 0) mc = draco_or_demonspawn_subspecies(mon); if (zombified && mc != MONS_KRAKEN_TENTACLE) mc = mons_zombie_base(mon); ASSERT_smc(); mon_attack_def attk = smc->attack[attk_number]; if (mons_is_demonspawn(mon->type) && attk_number == 0) { const monsterentry* mbase = get_monster_data (draco_or_demonspawn_subspecies(mon)); ASSERT(mbase); attk.flavour = mbase->attack[0].flavour; return attk; } // Nonbase draconians inherit aux attacks from their base type. // Implicit assumption: base draconian types only get one aux // attack, and it's in their second attack slot. // If that changes this code will need to be changed. if (mons_species(mon->type) == MONS_DRACONIAN && mon->type != MONS_DRACONIAN && attk.type == AT_NONE && attk_number > 0 && smc->attack[attk_number - 1].type != AT_NONE) { const monsterentry* mbase = get_monster_data (draco_or_demonspawn_subspecies(mon)); ASSERT(mbase); return mbase->attack[1]; } if (mon->type == MONS_PLAYER_SHADOW && attk_number == 0) { if (!you.weapon()) attk.damage = max(1, you.skill_rdiv(SK_UNARMED_COMBAT, 10, 20)); } if (attk.type == AT_RANDOM) attk.type = random_choose(AT_HIT, AT_GORE, -1); if (attk.type == AT_CHERUB) attk.type = random_choose(AT_HIT, AT_BITE, AT_PECK, AT_GORE, -1); if (!base_flavour) { if (attk.flavour == AF_KLOWN) { attack_flavour flavours[] = {AF_POISON_STRONG, AF_PAIN, AF_DRAIN_SPEED, AF_FIRE, AF_COLD, AF_ELEC, AF_ANTIMAGIC, AF_ACID}; attk.flavour = RANDOM_ELEMENT(flavours); } if (attk.flavour == AF_DRAIN_STAT) { attack_flavour flavours[] = {AF_DRAIN_STR, AF_DRAIN_INT, AF_DRAIN_DEX}; attk.flavour = RANDOM_ELEMENT(flavours); } } // Slime creature attacks are multiplied by the number merged. if (mon->type == MONS_SLIME_CREATURE && mon->number > 1) attk.damage *= mon->number; return zombified ? _downscale_zombie_attack(mon, attk) : attk; } static int _mons_damage(monster_type mc, int rt) { if (rt < 0 || rt > 3) rt = 0; ASSERT_smc(); return smc->attack[rt].damage; } const char* resist_margin_phrase(int margin) { return margin >= 30 ? " resists with almost no effort." : margin >= 15 ? " easily resists." : margin >= 0 ? " resists." : margin >= -14 ? " resists with significant effort.": margin >= -30 ? " struggles to resist." : " barely resists."; } bool mons_immune_magic(const monster* mon) { return get_monster_data(mon->type)->resist_magic == MAG_IMMUNE; } const char* mons_resist_string(const monster* mon, int res_margin) { if (mons_immune_magic(mon)) return " is unaffected."; else return resist_margin_phrase(res_margin); } bool mons_skeleton(monster_type mc) { return !mons_class_flag(mc, M_NO_SKELETON); } bool mons_zombifiable(monster_type mc) { return !mons_class_flag(mc, M_NO_ZOMBIE) && mons_zombie_size(mc); } flight_type mons_class_flies(monster_type mc) { const monsterentry *me = get_monster_data(mc); return me ? me->fly : FL_NONE; } flight_type mons_flies(const monster* mon, bool temp) { flight_type ret; // For dancing weapons, this function can get called before their // ghost_demon is created, so check for a NULL ghost. -cao if (mons_is_ghost_demon(mon->type) && mon->ghost.get()) ret = mon->ghost->fly; else ret = mons_class_flies(mons_base_type(mon)); // Handle the case where the zombified base monster can't fly, but // the zombified monster can (e.g. spectral things). if (mons_is_zombified(mon)) ret = max(ret, mons_class_flies(mon->type)); if (temp && ret < FL_LEVITATE) { if (mon->scan_artefacts(ARTP_FLY) > 0) return FL_LEVITATE; const int armour = mon->inv[MSLOT_ARMOUR]; if (armour != NON_ITEM && mitm[armour].base_type == OBJ_ARMOUR && mitm[armour].special == SPARM_FLYING) { return FL_LEVITATE; } const int jewellery = mon->inv[MSLOT_JEWELLERY]; if (jewellery != NON_ITEM && mitm[jewellery].base_type == OBJ_JEWELLERY && mitm[jewellery].sub_type == RING_FLIGHT) { return FL_LEVITATE; } if (mon->has_ench(ENCH_FLIGHT)) return FL_LEVITATE; } return ret; } bool mons_flattens_trees(const monster* mon) { return mons_base_type(mon) == MONS_LERNAEAN_HYDRA; } int mons_class_res_wind(monster_type mc) { // Lightning goes well with storms. if (mc == MONS_AIR_ELEMENTAL || mc == MONS_BALL_LIGHTNING || mc == MONS_TWISTER || mc == MONS_WIND_DRAKE) { return 1; } // Flyers are not immune due to buffeting -- and for airstrike, even // specially vulnerable. // Smoky humanoids may have problems staying together. // Insubstantial wisps are a toss-up between being immune and immediately // fatally dispersing. return 0; } // This nice routine we keep in exactly the way it was. int hit_points(int hit_dice, int min_hp, int rand_hp) { int hrolled = 0; for (int hroll = 0; hroll < hit_dice; ++hroll) { hrolled += random2(1 + rand_hp); hrolled += min_hp; } return hrolled; } // This function returns the standard number of hit dice for a type of // monster, not a particular monster's current hit dice. - bwr int mons_class_hit_dice(monster_type mc) { const monsterentry *me = get_monster_data(mc); return me ? me->hpdice[0] : 0; } int mons_avg_hp(monster_type mc) { // Currently, difficulty is defined as "average hp". Leaks too much info? const monsterentry* me = get_monster_data(mc); if (!me) return 0; // Hack for nonbase demonspawn: pretend it's a basic demonspawn with // a job. if (mons_is_demonspawn(mc) && mc != MONS_DEMONSPAWN && mons_species(mc) == MONS_DEMONSPAWN) { const monsterentry* mbase = get_monster_data(MONS_DEMONSPAWN); return me->hpdice[0] * (2 * (me->hpdice[1] + mbase->hpdice[1]) + me->hpdice[2] + mbase->hpdice[2]) / 2 + me->hpdice[3] + mbase->hpdice[3]; } // [ds] XXX: Use monster experience value as a better indicator of diff.? return me->hpdice[0] * (2 * me->hpdice[1] + me->hpdice[2]) / 2 + me->hpdice[3]; } int exper_value(const monster* mon, bool real) { int x_val = 0; // These four are the original arguments. const monster_type mc = mon->type; int hd = mon->get_experience_level(); int maxhp = mon->max_hit_points; // pghosts and pillusions have no reasonable base values, and you can look // up the exact value anyway. Especially for pillusions. if (real || mon->type == MONS_PLAYER_GHOST || mon->type == MONS_PLAYER_ILLUSION) { // A berserking monster is much harder, but the xp value shouldn't // depend on whether it was berserk at the moment of death. if (mon->has_ench(ENCH_BERSERK)) maxhp = (maxhp * 2 + 1) / 3; } else { const monsterentry *m = get_monster_data(mons_base_type(mon)); ASSERT(m); // Use real hd, zombies would use the basic species and lose // information known to the player ("orc warrior zombie"). Monsters // levelling up is visible (although it may happen off-screen), so // this is hardly ever a leak. Only Pan lords are unknown in the // general. if (m->mc == MONS_PANDEMONIUM_LORD) hd = m->hpdice[0]; maxhp = hd * m->hpdice[1] + (hd * (1 + m->hpdice[2])) / 2 + m->hpdice[3]; } // Hacks to make merged slime creatures not worth so much exp. We // will calculate the experience we would get for 1 blob, and then // just multiply it so that exp is linear with blobs merged. -cao if (mon->type == MONS_SLIME_CREATURE && mon->number > 1) maxhp /= mon->number; // These are some values we care about. const int speed = mons_base_speed(mon); const int modifier = _mons_exp_mod(mc); const int item_usage = mons_itemuse(mon); // XXX: Shapeshifters can qualify here, even though they can't cast. const bool spellcaster = mon->can_use_spells(); // Early out for no XP monsters. if (mons_class_flag(mc, M_NO_EXP_GAIN)) return 0; x_val = (16 + maxhp) * hd * hd / 10; // Let's calculate a simple difficulty modifier. - bwr int diff = 0; // Let's look for big spells. if (spellcaster) { const monster_spells &hspell_pass = mon->spells; for (int i = 0; i < 6; ++i) { switch (hspell_pass[i]) { case SPELL_PARALYSE: case SPELL_SMITING: case SPELL_SUMMON_EYEBALLS: case SPELL_HELLFIRE_BURST: case SPELL_HELLFIRE: case SPELL_SYMBOL_OF_TORMENT: case SPELL_GLACIATE: case SPELL_FIRE_STORM: case SPELL_SHATTER: case SPELL_CHAIN_LIGHTNING: case SPELL_TORNADO: case SPELL_LEGENDARY_DESTRUCTION: diff += 25; break; case SPELL_SUMMON_GREATER_DEMON: diff += 20; break; case SPELL_LIGHTNING_BOLT: case SPELL_STICKY_FLAME_RANGE: case SPELL_DISINTEGRATE: case SPELL_BANISHMENT: case SPELL_LEHUDIBS_CRYSTAL_SPEAR: case SPELL_IRON_SHOT: case SPELL_IOOD: case SPELL_FIREBALL: case SPELL_HASTE: case SPELL_AGONY: case SPELL_LRD: case SPELL_CHAIN_OF_CHAOS: diff += 10; break; case SPELL_HAUNT: case SPELL_SUMMON_DRAGON: case SPELL_SUMMON_HORRIBLE_THINGS: case SPELL_PLANEREND: diff += 7; break; default: break; } } } // Let's look at regeneration. if (mons_class_fast_regen(mc)) diff += 15; // Monsters at normal or fast speed with big melee damage. if (speed >= 10) { int max_melee = 0; for (int i = 0; i < 4; ++i) max_melee += _mons_damage(mc, i); if (max_melee > 30) diff += (max_melee / ((speed == 10) ? 2 : 1)); } // Monsters who can use equipment (even if only the equipment // they are given) can be considerably enhanced because of // the way weapons work for monsters. - bwr if (item_usage >= MONUSE_STARTING_EQUIPMENT) diff += 30; // Set a reasonable range on the difficulty modifier... // Currently 70% - 200%. - bwr if (diff > 100) diff = 100; else if (diff < -30) diff = -30; // Apply difficulty. x_val *= (100 + diff); x_val /= 100; // Basic speed modification. if (speed > 0) { x_val *= speed; x_val /= 10; } // Slow monsters without spells and items often have big HD which // cause the experience value to be overly large... this tries // to reduce the inappropriate amount of XP that results. - bwr if (speed < 10 && !spellcaster && item_usage < MONUSE_STARTING_EQUIPMENT) x_val /= 2; // Apply the modifier in the monster's definition. if (modifier > 0) { x_val *= modifier; x_val /= 10; } // Scale starcursed mass exp by what percentage of the whole it represents if (mon->type == MONS_STARCURSED_MASS) x_val = (x_val * mon->number) / 12; // Further reduce xp from zombies if (mons_is_zombified(mon)) x_val /= 2; // Reductions for big values. - bwr if (x_val > 100) x_val = 100 + ((x_val - 100) * 3) / 4; if (x_val > 750) x_val = 750 + (x_val - 750) / 3; // Slime creature exp hack part 2: Scale exp back up by the number // of blobs merged. -cao // Has to be after the stepdown to prevent issues with 4-5 merged slime // creatures. -pf if (mon->type == MONS_SLIME_CREATURE && mon->number > 1) x_val *= mon->number; // Guarantee the value is within limits. if (x_val <= 0) x_val = 1; else if (x_val > 15000) x_val = 15000; return x_val; } monster_type random_draconian_monster_species() { const int num_drac = MONS_LAST_BASE_DRACONIAN - MONS_FIRST_BASE_DRACONIAN + 1; return static_cast(MONS_FIRST_BASE_DRACONIAN + random2(num_drac)); } monster_type random_demonspawn_monster_species() { const int num_demons = MONS_LAST_BASE_DEMONSPAWN - MONS_FIRST_BASE_DEMONSPAWN + 1; return static_cast(MONS_FIRST_BASE_DEMONSPAWN + random2(num_demons)); } // Note: For consistent behavior in player_will_anger_monster(), all // spellbooks a given monster can get here should produce the same // return values in the following: // // (is_unholy_spell() || is_evil_spell()) // // (is_unclean_spell() || is_chaotic_spell()) // // FIXME: This is not true for one set of spellbooks; MST_WIZARD_IV // contains the unholy and chaotic Banishment spell, but the other // MST_WIZARD-type spellbooks contain no unholy, evil, unclean or // chaotic spells. // // If a monster has only one spellbook, it is specified in mon-data.h. // If it has multiple books, mon-data.h sets the book to MST_NO_SPELLS, // and the books are accounted for here. static vector _mons_spellbook_list(monster_type mon_type) { vector books; const monsterentry *m = get_monster_data(mon_type); mon_spellbook_type book = static_cast(m->sec); switch (mon_type) { case MONS_HELL_KNIGHT: books.push_back(MST_HELL_KNIGHT_I); books.push_back(MST_HELL_KNIGHT_II); break; case MONS_LICH: case MONS_ANCIENT_LICH: books.push_back(MST_LICH_I); books.push_back(MST_LICH_II); books.push_back(MST_LICH_III); books.push_back(MST_LICH_IV); break; case MONS_NECROMANCER: books.push_back(MST_NECROMANCER_I); books.push_back(MST_NECROMANCER_II); break; case MONS_ORC_WIZARD: case MONS_DEEP_ELF_FIGHTER: case MONS_DEEP_ELF_KNIGHT: books.push_back(MST_ORC_WIZARD_I); books.push_back(MST_ORC_WIZARD_II); books.push_back(MST_ORC_WIZARD_III); break; case MONS_WIZARD: case MONS_EROLCHA: books.push_back(MST_WIZARD_I); books.push_back(MST_WIZARD_II); books.push_back(MST_WIZARD_III); books.push_back(MST_WIZARD_IV); books.push_back(MST_WIZARD_V); break; case MONS_OGRE_MAGE: books.push_back(MST_OGRE_MAGE_I); books.push_back(MST_OGRE_MAGE_II); books.push_back(MST_OGRE_MAGE_III); books.push_back(MST_OGRE_MAGE_IV); books.push_back(MST_OGRE_MAGE_V); break; case MONS_DRACONIAN_KNIGHT: books.push_back(MST_DRACONIAN_KNIGHT_I); books.push_back(MST_DRACONIAN_KNIGHT_II); books.push_back(MST_DRACONIAN_KNIGHT_III); break; case MONS_ANCIENT_CHAMPION: books.push_back(MST_ANCIENT_CHAMPION_I); books.push_back(MST_ANCIENT_CHAMPION_II); books.push_back(MST_ANCIENT_CHAMPION_III); books.push_back(MST_ANCIENT_CHAMPION_IV); break; case MONS_TENGU_CONJURER: books.push_back(MST_TENGU_CONJURER_I); books.push_back(MST_TENGU_CONJURER_II); books.push_back(MST_TENGU_CONJURER_III); books.push_back(MST_TENGU_CONJURER_IV); break; case MONS_TENGU_REAVER: books.push_back(MST_TENGU_REAVER_I); books.push_back(MST_TENGU_REAVER_II); books.push_back(MST_TENGU_REAVER_III); break; case MONS_DEEP_ELF_MAGE: books.push_back(MST_DEEP_ELF_MAGE_I); books.push_back(MST_DEEP_ELF_MAGE_II); books.push_back(MST_DEEP_ELF_MAGE_III); books.push_back(MST_DEEP_ELF_MAGE_IV); books.push_back(MST_DEEP_ELF_MAGE_V); break; case MONS_FAUN: books.push_back(MST_FAUN_I); books.push_back(MST_FAUN_II); books.push_back(MST_FAUN_III); break; default: books.push_back(book); break; } return books; } vector get_spellbooks(const monster_info &mon) { vector books; // special case for vault monsters: if they have a custom book, // treat it as MST_GHOST if (mon.props.exists("custom_spells")) books.push_back(MST_GHOST); else books = _mons_spellbook_list(mon.type); return books; } // get a list of unique spells from a monster's preset spellbooks // or in the case of ghosts their actual spells. unique_books get_unique_spells(const monster_info &mi) { // No entry for MST_GHOST COMPILE_CHECK(ARRAYSZ(mspell_list) == NUM_MSTYPES - 1); const vector books = get_spellbooks(mi); const size_t num_books = books.size(); unique_books result; for (size_t i = 0; i < num_books; ++i) { const mon_spellbook_type book = books[i]; // TODO: should we build an index to speed this reverse lookup? unsigned int msidx; for (msidx = 0; msidx < ARRAYSZ(mspell_list); ++msidx) if (mspell_list[msidx].type == book) break; vector spells; for (int j = 0; j < NUM_MONSTER_SPELL_SLOTS; ++j) { spell_type spell; if (book == MST_GHOST) spell = mi.spells[j]; else { ASSERT(msidx < ARRAYSZ(mspell_list)); spell = mspell_list[msidx].spells[j]; } bool match = false; for (size_t k = 0; k < spells.size(); ++k) if (spell == spells[k]) match = true; if (!match && spell != SPELL_NO_SPELL && spell != SPELL_MELEE) spells.push_back(spell); } result.push_back(spells); } return result; } static void _mons_load_spells(monster* mon) { vector books = _mons_spellbook_list(mon->type); const mon_spellbook_type book = books[random2(books.size())]; if (book == MST_GHOST) return mon->load_ghost_spells(); mon->spells.init(SPELL_NO_SPELL); if (book == MST_NO_SPELLS) return; dprf("%s: loading spellbook #%d", mon->name(DESC_PLAIN, true).c_str(), static_cast(book)); for (unsigned int i = 0; i < ARRAYSZ(mspell_list); ++i) { if (mspell_list[i].type == book) { for (int j = 0; j < NUM_MONSTER_SPELL_SLOTS; ++j) mon->spells[j] = mspell_list[i].spells[j]; break; } } } // Never hand out DARKGREY as a monster colour, even if it is randomly // chosen. colour_t random_monster_colour() { colour_t col = DARKGREY; while (col == DARKGREY) col = random_colour(); return col; } // Butterflies static colour_t _random_butterfly_colour() { colour_t col; // Restricted to 'light' colours. do col = random_monster_colour(); while (is_low_colour(col)); return col; } bool init_abomination(monster* mon, int hd) { if (mon->type == MONS_CRAWLING_CORPSE || mon->type == MONS_MACABRE_MASS) { mon->set_hit_dice(mon->hit_points = mon->max_hit_points = hd); return true; } else if (mon->type != MONS_ABOMINATION_LARGE && mon->type != MONS_ABOMINATION_SMALL) { return false; } const int max_hd = mon->type == MONS_ABOMINATION_LARGE ? 30 : 15; const int max_ac = mon->type == MONS_ABOMINATION_LARGE ? 20 : 10; mon->set_hit_dice(min(max_hd, hd)); const monsterentry *m = get_monster_data(mon->type); int hp = hit_points(hd, m->hpdice[1], m->hpdice[2]) + m->hpdice[3]; mon->max_hit_points = hp; mon->hit_points = hp; if (mon->type == MONS_ABOMINATION_LARGE) { mon->ac = min(max_ac, 7 + hd / 2); mon->ev = min(max_ac, 2 * hd / 3); } else { mon->ac = min(max_ac, 3 + hd * 2 / 3); mon->ev = min(max_ac, 4 + hd); } return true; } // Generate a shiny, new and unscarred monster. void define_monster(monster* mons) { monster_type mcls = mons->type; int monnumber = mons->number; monster_type monbase = mons->base_monster; const monsterentry *m = get_monster_data(mcls); int col = mons_class_colour(mcls); int hd = mons_class_hit_dice(mcls); int hp = 0, hp_max, ac, ev; mons->mname.clear(); // misc ac = m->AC; ev = m->ev; mons->god = GOD_NO_GOD; switch (mcls) { case MONS_BUTTERFLY: col = _random_butterfly_colour(); break; case MONS_ABOMINATION_SMALL: init_abomination(mons, 4 + random2(4)); break; case MONS_ABOMINATION_LARGE: init_abomination(mons, 8 + random2(4)); break; case MONS_HELL_BEAST: hd = 4 + random2(4); ac = 2 + random2(5); ev = 7 + random2(5); break; case MONS_SLIME_CREATURE: // Slime creatures start off as only single un-merged blobs. monnumber = 1; break; case MONS_HYDRA: // Hydras start off with 4 to 8 heads. monnumber = random_range(4, 8); break; case MONS_LERNAEAN_HYDRA: // The Lernaean hydra starts off with 27 heads. monnumber = 27; break; case MONS_KRAKEN: if (col != BLACK) // May be overwritten by the mon_glyph option. break; col = element_colour(ETC_KRAKEN); break; case MONS_DRACONIAN_CALLER: case MONS_DRACONIAN_MONK: case MONS_DRACONIAN_ZEALOT: case MONS_DRACONIAN_SHIFTER: case MONS_DRACONIAN_ANNIHILATOR: case MONS_DRACONIAN_SCORCHER: case MONS_DRACONIAN_KNIGHT: { // Professional draconians still have a base draconian type. // White draconians will never be draconian scorchers, but // apart from that, anything goes. do monbase = random_draconian_monster_species(); while (drac_colour_incompatible(mcls, monbase)); const monsterentry* mbase = get_monster_data(monbase); ac += mbase->AC; ev += mbase->ev; break; } case MONS_TIAMAT: // Initialise to a random draconian type. draconian_change_colour(mons); monbase = mons->base_monster; col = mons->colour; break; case MONS_STARCURSED_MASS: monnumber = 12; break; // Randomize starting speed burst clock case MONS_SIXFIRHY: case MONS_JIANGSHI: monnumber = random2(360); break; case MONS_SHAMBLING_MANGROVE: monnumber = x_chance_in_y(3, 5) ? random_range(2, 3) : 0; break; case MONS_BLOOD_SAINT: case MONS_CHAOS_CHAMPION: case MONS_WARMONGER: case MONS_CORRUPTER: case MONS_BLACK_SUN: { // Some base demonspawn have more or less HP, AC, EV than their // brethren; those should be based on the base monster, // with modifiers taken from the job. monbase = mons->base_monster != MONS_NO_MONSTER ? mons->base_monster : random_demonspawn_monster_species(); const monsterentry* mbase = get_monster_data(monbase); hp = hit_points(hd, mbase->hpdice[1] + m->hpdice[1], mbase->hpdice[2] + m->hpdice[2]); hp += mbase->hpdice[3] + m->hpdice[3]; ac += mbase->AC; ev += mbase->ev; break; } default: break; } if (col == BLACK) // but never give out darkgrey to monsters col = random_monster_colour(); // Some calculations. if (hp == 0) { hp = hit_points(hd, m->hpdice[1], m->hpdice[2]); hp += m->hpdice[3]; } hp_max = hp; // So let it be written, so let it be done. mons->set_hit_dice(hd); mons->hit_points = hp; mons->max_hit_points = hp_max; mons->ac = ac; mons->ev = ev; mons->speed_increment = 70; if (mons->base_monster == MONS_NO_MONSTER) mons->base_monster = monbase; if (mons->number == 0) mons->number = monnumber; mons->flags = 0; mons->experience = 0; mons->colour = col; mons->bind_melee_flags(); _mons_load_spells(mons); mons->bind_spell_flags(); // Reset monster enchantments. mons->enchantments.clear(); mons->ench_cache.reset(); mons->ench_countdown = 0; // NOTE: For player ghosts and (very) ugly things this just ensures // that the monster instance is valid and won't crash when used, // though the (very) ugly thing generated should actually work. The // player ghost and (very) ugly thing code is currently only used // for generating a monster for MonsterMenuEntry in // _find_description() in command.cc. switch (mcls) { case MONS_PANDEMONIUM_LORD: { ghost_demon ghost; ghost.init_random_demon(); mons->set_ghost(ghost); mons->ghost_demon_init(); mons->flags |= MF_INTERESTING; mons->bind_melee_flags(); mons->bind_spell_flags(); break; } case MONS_PLAYER_GHOST: case MONS_PLAYER_ILLUSION: { ghost_demon ghost; ghost.init_player_ghost(); mons->set_ghost(ghost); mons->ghost_init(!mons->props.exists("fake")); mons->bind_melee_flags(); mons->bind_spell_flags(); if (mons->ghost->species == SP_DEEP_DWARF) mons->flags |= MF_NO_REGEN; break; } case MONS_UGLY_THING: case MONS_VERY_UGLY_THING: { ghost_demon ghost; ghost.init_ugly_thing(mcls == MONS_VERY_UGLY_THING); mons->set_ghost(ghost); mons->uglything_init(); break; } // Load with dummy values so certain monster properties can be queried // before placement without crashing (proper setup is done later here) case MONS_DANCING_WEAPON: case MONS_SPECTRAL_WEAPON: { ghost_demon ghost; mons->set_ghost(ghost); } case MONS_SPELLFORGED_SERVITOR: { ghost_demon ghost; ghost.init_spellforged_servitor(); mons->set_ghost(ghost); mons->ghost_demon_init(); mons->props["custom_spells"].get_bool() = true; break; } default: break; } mons->calc_speed(); } static const char *ugly_colour_names[] = { "red", "brown", "green", "cyan", "purple", "white" }; string ugly_thing_colour_name(colour_t colour) { int colour_offset = ugly_thing_colour_offset(colour); if (colour_offset == -1) return "buggy"; return ugly_colour_names[colour_offset]; } static const colour_t ugly_colour_values[] = { RED, BROWN, GREEN, CYAN, MAGENTA, LIGHTGREY }; colour_t ugly_thing_random_colour() { return RANDOM_ELEMENT(ugly_colour_values); } int str_to_ugly_thing_colour(const string &s) { COMPILE_CHECK(ARRAYSZ(ugly_colour_values) == ARRAYSZ(ugly_colour_names)); for (int i = 0, size = ARRAYSZ(ugly_colour_values); i < size; ++i) if (s == ugly_colour_names[i]) return ugly_colour_values[i]; return BLACK; } int ugly_thing_colour_offset(colour_t colour) { for (unsigned i = 0; i < ARRAYSZ(ugly_colour_values); ++i) { if (make_low_colour(colour) == ugly_colour_values[i]) return i; } return -1; } static const char *drac_colour_names[] = { "black", "mottled", "yellow", "green", "purple", "red", "white", "grey", "pale" }; string draconian_colour_name(monster_type mon_type) { COMPILE_CHECK(ARRAYSZ(drac_colour_names) == MONS_LAST_BASE_DRACONIAN - MONS_DRACONIAN); if (!mons_is_base_draconian(mon_type) || mon_type == MONS_DRACONIAN) return "buggy"; return drac_colour_names[mon_type - MONS_FIRST_BASE_DRACONIAN]; } monster_type draconian_colour_by_name(const string &name) { COMPILE_CHECK(ARRAYSZ(drac_colour_names) == (MONS_LAST_BASE_DRACONIAN - MONS_DRACONIAN)); for (unsigned i = 0; i < ARRAYSZ(drac_colour_names); ++i) { if (name == drac_colour_names[i]) return static_cast(i + MONS_FIRST_BASE_DRACONIAN); } return MONS_PROGRAM_BUG; } static const char *demonspawn_base_names[] = { "monstrous", "gelid", "infernal", "putrid", "torturous", }; string demonspawn_base_name(monster_type mon_type) { COMPILE_CHECK(ARRAYSZ(demonspawn_base_names) == MONS_LAST_BASE_DEMONSPAWN - MONS_FIRST_BASE_DEMONSPAWN + 1); if (mon_type < MONS_FIRST_BASE_DEMONSPAWN || mon_type > MONS_LAST_BASE_DEMONSPAWN) { return "buggy"; } return demonspawn_base_names[mon_type - MONS_FIRST_BASE_DEMONSPAWN]; } monster_type demonspawn_base_by_name(const string &name) { COMPILE_CHECK(ARRAYSZ(demonspawn_base_names) == MONS_LAST_BASE_DEMONSPAWN - MONS_FIRST_BASE_DEMONSPAWN + 1); for (unsigned i = 0; i < ARRAYSZ(demonspawn_base_names); ++i) { if (name == demonspawn_base_names[i]) return static_cast(i + MONS_FIRST_BASE_DEMONSPAWN); } return MONS_PROGRAM_BUG; } string mons_type_name(monster_type mc, description_level_type desc) { string result; if (!mons_is_unique(mc)) { switch (desc) { case DESC_THE: result = "the "; break; case DESC_A: result = "a "; break; case DESC_PLAIN: default: break; } } switch (mc) { case RANDOM_MONSTER: result += "random monster"; return result; case RANDOM_DRACONIAN: result += "random draconian"; return result; case RANDOM_BASE_DRACONIAN: result += "random base draconian"; return result; case RANDOM_NONBASE_DRACONIAN: result += "random nonbase draconian"; return result; case RANDOM_DEMONSPAWN: result += "random demonspawn"; return result; case RANDOM_BASE_DEMONSPAWN: result += "random base demonspawn"; return result; case RANDOM_NONBASE_DEMONSPAWN: result += "random nonbase demonspawn"; return result; case WANDERING_MONSTER: result += "wandering monster"; return result; default: ; } const monsterentry *me = get_monster_data(mc); if (me == NULL) { result += make_stringf("invalid monster_type %d", mc); return result; } result += me->name; // Vowel fix: Change 'a orc' to 'an orc'.. if (result.length() >= 3 && (result[0] == 'a' || result[0] == 'A') && result[1] == ' ' && is_vowel(result[2]) // XXX: Hack && !starts_with(&result[2], "one-")) { result.insert(1, "n"); } return result; } static string _get_proper_monster_name(const monster* mon) { const monsterentry *me = mon->find_monsterentry(); if (!me) return ""; string name = getRandNameString(me->name, " name"); if (!name.empty()) return name; return getRandNameString(get_monster_data(mons_genus(mon->type))->name, " name"); } // Names a previously unnamed monster. bool give_monster_proper_name(monster* mon, bool orcs_only) { // Already has a unique name. if (mon->is_named()) return false; // Since this is called from the various divine blessing routines, // don't bless non-orcs, and normally don't bless plain orcs, either. if (orcs_only) { if (mons_genus(mon->type) != MONS_ORC || mon->type == MONS_ORC && !one_chance_in(8)) { return false; } } mon->mname = _get_proper_monster_name(mon); if (mon->friendly()) take_note(Note(NOTE_NAMED_ALLY, 0, 0, mon->mname.c_str())); return mon->is_named(); } // See mons_init for initialization of mon_entry array. monsterentry *get_monster_data(monster_type mc) { if (mc >= 0 && mc < NUM_MONSTERS) return &mondata[mon_entry[mc]]; else return NULL; } static int _mons_exp_mod(monster_type mc) { ASSERT_smc(); return smc->exp_mod; } int mons_class_base_speed(monster_type mc) { ASSERT_smc(); return smc->speed; } mon_energy_usage mons_class_energy(monster_type mc) { ASSERT_smc(); return smc->energy_usage; } int mons_class_zombie_base_speed(monster_type zombie_base_mc) { return max(3, mons_class_base_speed(zombie_base_mc) - 2); } int mons_base_speed(const monster* mon) { if (mon->ghost.get()) return mon->ghost->speed; if (mon->type == MONS_SPECTRAL_THING) return mons_class_base_speed(mons_zombie_base(mon)); return mons_is_zombified(mon) ? mons_class_zombie_base_speed(mons_zombie_base(mon)) : mons_class_base_speed(mon->type); } mon_intel_type mons_class_intel(monster_type mc) { ASSERT_smc(); return smc->intel; } mon_intel_type mons_intel(const monster* mon) { get_tentacle_head(mon); if (mons_enslaved_soul(mon)) return mons_class_intel(mons_zombie_base(mon)); return mons_class_intel(mon->type); } static habitat_type _mons_class_habitat(monster_type mc, bool real_amphibious = false) { const monsterentry *me = get_monster_data(mc); habitat_type ht = (me ? me->habitat : get_monster_data(MONS_PROGRAM_BUG)->habitat); if (!real_amphibious) { // XXX: No class equivalent of monster::body_size(PSIZE_BODY)! size_type st = (me ? me->size : get_monster_data(MONS_PROGRAM_BUG)->size); if (ht == HT_LAND && st >= SIZE_GIANT || mc == MONS_GREY_DRACONIAN) ht = HT_AMPHIBIOUS; } return ht; } habitat_type mons_habitat(const monster* mon, bool real_amphibious) { return _mons_class_habitat(fixup_zombie_type(mon->type, mons_base_type(mon)), real_amphibious); } habitat_type mons_class_primary_habitat(monster_type mc) { habitat_type ht = _mons_class_habitat(mc); if (ht == HT_AMPHIBIOUS || ht == HT_AMPHIBIOUS_LAVA) ht = HT_LAND; return ht; } habitat_type mons_primary_habitat(const monster* mon) { return mons_class_primary_habitat(mons_base_type(mon)); } habitat_type mons_class_secondary_habitat(monster_type mc) { habitat_type ht = _mons_class_habitat(mc); if (ht == HT_AMPHIBIOUS) ht = HT_WATER; if (ht == HT_AMPHIBIOUS_LAVA) ht = HT_LAVA; return ht; } habitat_type mons_secondary_habitat(const monster* mon) { return mons_class_secondary_habitat(mons_base_type(mon)); } bool intelligent_ally(const monster* mon) { return mon->attitude == ATT_FRIENDLY && mons_intel(mon) >= I_NORMAL; } int mons_power(monster_type mc) { // For now, just return monster hit dice. ASSERT_smc(); return mons_class_hit_dice(mc); } bool mons_aligned(const actor *m1, const actor *m2) { mon_attitude_type fr1, fr2; const monster* mon1, *mon2; if (!m1 || !m2) return true; if (m1->is_player()) fr1 = ATT_FRIENDLY; else { mon1 = static_cast(m1); if (mons_is_projectile(mon1->type)) return true; fr1 = mons_attitude(mon1); } if (m2->is_player()) fr2 = ATT_FRIENDLY; else { mon2 = static_cast(m2); if (mons_is_projectile(mon2->type)) return true; fr2 = mons_attitude(mon2); } return mons_atts_aligned(fr1, fr2); } bool mons_atts_aligned(mon_attitude_type fr1, mon_attitude_type fr2) { if (mons_att_wont_attack(fr1) && mons_att_wont_attack(fr2)) return true; return fr1 == fr2; } bool mons_class_wields_two_weapons(monster_type mc) { return mons_class_flag(mc, M_TWO_WEAPONS); } bool mons_wields_two_weapons(const monster* mon) { if (testbits(mon->flags, MF_TWO_WEAPONS)) return true; return mons_class_wields_two_weapons(mons_base_type(mon)); } bool mons_self_destructs(const monster* m) { return m->type == MONS_GIANT_SPORE || m->type == MONS_BALL_LIGHTNING || m->type == MONS_LURKING_HORROR || m->type == MONS_ORB_OF_DESTRUCTION || m->type == MONS_FULMINANT_PRISM; } bool mons_att_wont_attack(mon_attitude_type fr) { return fr == ATT_FRIENDLY || fr == ATT_GOOD_NEUTRAL || fr == ATT_STRICT_NEUTRAL; } mon_attitude_type mons_attitude(const monster* m) { if (m->friendly()) return ATT_FRIENDLY; else if (m->good_neutral()) return ATT_GOOD_NEUTRAL; else if (m->strict_neutral()) return ATT_STRICT_NEUTRAL; else if (m->neutral()) return ATT_NEUTRAL; else return ATT_HOSTILE; } bool mons_is_confused(const monster* m, bool class_too) { return (m->has_ench(ENCH_CONFUSION) || m->has_ench(ENCH_MAD)) && (class_too || !mons_class_flag(m->type, M_CONFUSED)); } bool mons_is_wandering(const monster* m) { return m->behaviour == BEH_WANDER; } bool mons_is_seeking(const monster* m) { return m->behaviour == BEH_SEEK; } // Either running in fear, or trapped and unable to do so (but still wishing to) bool mons_is_fleeing(const monster* m) { return m->behaviour == BEH_FLEE || mons_is_cornered(m); } // Can be either an orderly withdrawal (from which the monster can stop at will) // or running in fear (from which they cannot) bool mons_is_retreating(const monster* m) { return m->behaviour == BEH_RETREAT || mons_is_fleeing(m); } bool mons_is_cornered(const monster* m) { return m->behaviour == BEH_CORNERED; } bool mons_is_lurking(const monster* m) { return m->behaviour == BEH_LURK; } bool mons_is_influenced_by_sanctuary(const monster* m) { return !m->wont_attack() && !m->is_stationary(); } bool mons_is_fleeing_sanctuary(const monster* m) { return mons_is_influenced_by_sanctuary(m) && in_bounds(env.sanctuary_pos) && (m->flags & MF_FLEEING_FROM_SANCTUARY); } bool mons_just_slept(const monster* m) { return m->flags & MF_JUST_SLEPT; } // Moving body parts, turning oklob flowers and so on counts as motile here. // So does preparing resurrect, struggling against a net, etc. bool mons_is_immotile(const monster* mons) { return mons_is_firewood(mons) || mons->petrified() || mons->asleep() || mons->paralysed(); } bool mons_is_batty(const monster* m) { return mons_class_flag(m->type, M_BATTY) || m->type == MONS_CHIMERA && chimera_is_batty(m); } bool mons_looks_stabbable(const monster* m) { const stab_type st = find_stab_type(&you, m); return !m->friendly() && (st == STAB_PARALYSED || st == STAB_SLEEPING); } bool mons_looks_distracted(const monster* m) { const stab_type st = find_stab_type(&you, m); return !m->friendly() && st != STAB_NO_STAB && st != STAB_PARALYSED && st != STAB_SLEEPING; } void mons_start_fleeing_from_sanctuary(monster* mons) { mons->flags |= MF_FLEEING_FROM_SANCTUARY; mons->target = env.sanctuary_pos; behaviour_event(mons, ME_SCARE, 0, env.sanctuary_pos); } void mons_stop_fleeing_from_sanctuary(monster* mons) { const bool had_flag = (mons->flags & MF_FLEEING_FROM_SANCTUARY); mons->flags &= (~MF_FLEEING_FROM_SANCTUARY); if (had_flag) behaviour_event(mons, ME_EVAL, &you); } void mons_pacify(monster* mon, mon_attitude_type att, bool no_xp) { // If the _real_ (non-charmed) attitude is already that or better, // don't degrade it. This can happen, for example, with a high-power // Crusade card on Pikel's slaves who would then go down from friendly // to good_neutral when you kill Pikel. if (mon->attitude >= att) return; // Must be done before attitude change, so that proper targets are affected if (mon->type == MONS_FLAYED_GHOST) end_flayed_effect(mon); // Make the monster permanently neutral. mon->attitude = att; mon->flags |= MF_WAS_NEUTRAL; if (!testbits(mon->flags, MF_GOT_HALF_XP) && !no_xp && !mon->is_summoned() && !testbits(mon->flags, MF_NO_REWARD)) { // Give the player half of the monster's XP. gain_exp((exper_value(mon) + 1) / 2); mon->flags |= MF_GOT_HALF_XP; } if (mon->type == MONS_GERYON) { simple_monster_message(mon, make_stringf(" discards %s horn.", mon->pronoun(PRONOUN_POSSESSIVE).c_str()).c_str()); monster_drop_things(mon, false, item_is_horn_of_geryon); } // End constriction. mon->stop_constricting_all(false); mon->stop_being_constricted(); // Cancel fleeing and such. mon->behaviour = BEH_WANDER; // Remove haunting, which would otherwise cause monster to continue attacking mon->del_ench(ENCH_HAUNTING, true, true); // Make the monster begin leaving the level. behaviour_event(mon, ME_EVAL); if (mons_is_pikel(mon)) pikel_band_neutralise(); if (mons_is_elven_twin(mon)) elven_twins_pacify(mon); if (mons_is_kirke(mon)) hogs_to_humans(); if (mon->type == MONS_VAULT_WARDEN) timeout_terrain_changes(0, true); mons_att_changed(mon); } static bool _mons_should_fire_beneficial(bolt &beam) { // Should monster heal other, haste other or might other be able to // target the player? Saying no for now. -cao if (beam.target == you.pos()) return false; // Assuming all beneficial beams are enchantments if a foe is in // the path the beam will definitely hit them so we shouldn't fire // in that case. if (beam.friend_info.count == 0 || beam.foe_info.count != 0) { return false; } // Should beneficial monster enchantment beams be allowed in a // sanctuary? -cao if (is_sanctuary(you.pos()) || is_sanctuary(beam.source)) return false; return true; } static bool _beneficial_beam_flavour(beam_type flavour) { switch (flavour) { case BEAM_HASTE: case BEAM_HEALING: case BEAM_INVISIBILITY: case BEAM_MIGHT: case BEAM_AGILITY: return true; default: return false; } } bool mons_should_fire(bolt &beam) { dprf("tracer: foes %d (pow: %d), friends %d (pow: %d), " "foe_ratio: %d, smart: %s", beam.foe_info.count, beam.foe_info.power, beam.friend_info.count, beam.friend_info.power, beam.foe_ratio, beam.smart_monster ? "yes" : "no"); // Use different evaluation criteria if the beam is a beneficial // enchantment (haste other). if (_beneficial_beam_flavour(beam.flavour)) return _mons_should_fire_beneficial(beam); // Friendly monsters shouldn't be targeting you: this will happen // often because the default behaviour for charmed monsters is to // have you as a target. While foe_ratio will handle this, we // don't want a situation where a friendly dragon breathes through // you to hit other creatures... it should target the other // creatures, and coincidentally hit you. // // FIXME: this can cause problems with reflection, bounces, etc. // It would be better to have the monster fire logic never reach // this point for friendlies. if (!invalid_monster_index(beam.beam_source)) { monster& m = menv[beam.beam_source]; if (m.alive() && m.friendly() && beam.target == you.pos()) return false; } // Use of foeRatio: // The higher this number, the more monsters will _avoid_ collateral // damage to their friends. // Setting this to zero will in fact have all monsters ignore their // friends when considering collateral damage. // Quick check - did we in fact get any foes? if (beam.foe_info.count == 0) return false; if (is_sanctuary(you.pos()) || is_sanctuary(beam.source)) return false; // If we hit no friends, fire away. if (beam.friend_info.count == 0) return true; // Only fire if they do acceptably low collateral damage. return beam.foe_info.power >= div_round_up(beam.foe_ratio * (beam.foe_info.power + beam.friend_info.power), 100); } static bool _ms_los_spell(spell_type monspell) { // True, the tentacles _are_ summoned, but they are restricted to // water just like the kraken is, so it makes more sense not to // count them here. if (monspell == SPELL_CREATE_TENTACLES) return false; if (monspell == SPELL_SMITING || monspell == SPELL_AIRSTRIKE || monspell == SPELL_HAUNT || monspell == SPELL_SUMMON_SPECTRAL_ORCS || monspell == SPELL_CHAOTIC_MIRROR || spell_typematch(monspell, SPTYP_SUMMONING)) { return true; } return false; } static bool _ms_ranged_spell(spell_type monspell, bool attack_only = false, bool ench_too = true) { // Check for Smiting specially, so it's not filtered along // with the summon spells. if (attack_only && (monspell == SPELL_SMITING || monspell == SPELL_AIRSTRIKE || monspell == SPELL_PORTAL_PROJECTILE || monspell == SPELL_CHAOTIC_MIRROR)) { return true; } // These spells are ranged, but aren't direct attack spells. if (_ms_los_spell(monspell)) return !attack_only; switch (monspell) { case SPELL_NO_SPELL: case SPELL_CANTRIP: case SPELL_FRENZY: case SPELL_HASTE: case SPELL_MIGHT: case SPELL_MINOR_HEALING: case SPELL_MAJOR_HEALING: case SPELL_TELEPORT_SELF: case SPELL_INVISIBILITY: case SPELL_BLINK: case SPELL_BLINK_CLOSE: case SPELL_BLINK_RANGE: case SPELL_BERSERKER_RAGE: case SPELL_SWIFTNESS: return false; // The animation spells don't work through transparent walls and // thus are listed here instead of above. case SPELL_ANIMATE_DEAD: case SPELL_ANIMATE_SKELETON: return !attack_only; // XXX: can this list not be hard-coded to prevent problems in the future? case SPELL_CORONA: case SPELL_CONFUSE: case SPELL_SLOW: case SPELL_PARALYSE: case SPELL_SLEEP: case SPELL_HIBERNATION: case SPELL_CAUSE_FEAR: case SPELL_LEDAS_LIQUEFACTION: case SPELL_MESMERISE: case SPELL_MASS_CONFUSION: case SPELL_ENGLACIATION: case SPELL_TELEPORT_OTHER: case SPELL_BLINK_OTHER_CLOSE: case SPELL_BLINK_AWAY: case SPELL_BLINK_OTHER: return ench_too; default: // All conjurations count as ranged spells. return true; } } // Returns true if the monster has an ability that only needs LOS to // affect the target. bool mons_has_los_ability(monster_type mon_type) { // These eyes only need LOS, as well. (The other eyes use spells.) if (mon_type == MONS_GIANT_EYEBALL || mon_type == MONS_EYE_OF_DRAINING || mon_type == MONS_GOLDEN_EYE || mon_type == MONS_MOTH_OF_WRATH || mon_type == MONS_GHOST_MOTH) { return true; } // Beholding just needs LOS. if (mons_genus(mon_type) == MONS_MERMAID) return true; return false; } bool mons_has_ranged_spell(const monster* mon, bool attack_only, bool ench_too) { // Monsters may have spell-like abilities. if (mons_has_los_ability(mon->type)) return true; if (mon->can_use_spells()) { for (int i = 0; i < NUM_MONSTER_SPELL_SLOTS; ++i) if (_ms_ranged_spell(mon->spells[i], attack_only, ench_too)) return true; } return false; } // Returns whether the monster has a spell which is theoretically capable of // causing an incapacitation state in the target foe (ie: it can cast sleep // and the foe is not immune to being put to sleep) // // Note that this only current checks for inherent obvious immunity (ie: sleep // immunity from being undead) and not immunity that might be granted by gear // (such as an amulet of clarity or stasis) bool mons_has_incapacitating_spell(const monster* mon, const actor* foe) { if (!mon->can_use_spells()) return false; for (int i = 0; i < NUM_MONSTER_SPELL_SLOTS; ++i) { switch (mon->spells[i]) { case SPELL_SLEEP: if (foe->can_sleep()) return true; break; case SPELL_HIBERNATION: if (foe->can_hibernate(false, true)) return true; break; case SPELL_CONFUSE: case SPELL_MASS_CONFUSION: case SPELL_PARALYSE: return true; case SPELL_PETRIFY: if (foe->res_petrify()) return true; break; default: break; } } return false; } static bool _mons_has_ranged_ability(const monster* mon) { // [ds] FIXME: Get rid of special abilities and remove this. switch (mon->type) { case MONS_ACID_BLOB: case MONS_BURNING_BUSH: case MONS_DRACONIAN: case MONS_FIRE_DRAGON: case MONS_ICE_DRAGON: case MONS_HELL_HOUND: case MONS_LINDWURM: case MONS_FIRE_DRAKE: case MONS_XTAHUA: case MONS_FIRE_CRAB: case MONS_APOCALYPSE_CRAB: case MONS_GHOST_CRAB: case MONS_ELECTRIC_EEL: case MONS_LAVA_SNAKE: case MONS_MANTICORE: case MONS_OKLOB_PLANT: case MONS_OKLOB_SAPLING: case MONS_LIGHTNING_SPIRE: return true; default: return false; } } static bool _mons_has_usable_ranged_weapon(const monster* mon) { // Ugh. monster* mnc = const_cast(mon); const item_def *weapon = mnc->launcher(); const item_def *primary = mnc->mslot_item(MSLOT_WEAPON); const item_def *missile = mnc->missiles(); // We don't have a usable ranged weapon if a different cursed weapon // is presently equipped. if (weapon != primary && primary && primary->cursed()) return false; if (!missile) return false; return is_launched(mnc, weapon, *missile); } bool mons_has_ranged_attack(const monster* mon) { return mons_has_ranged_spell(mon, true) || _mons_has_ranged_ability(mon) || _mons_has_usable_ranged_weapon(mon); } bool mons_has_incapacitating_ranged_attack(const monster* mon, const actor* foe) { if (!_mons_has_usable_ranged_weapon(mon)) return false; monster* mnc = const_cast(mon); const item_def *missile = mnc->missiles(); if (missile && missile->sub_type == MI_THROWING_NET) return true; else if (missile && missile->sub_type == MI_NEEDLE) { switch (get_ammo_brand(*missile)) { // Not actually incapacitating, but marked as such so that // assassins will prefer using it while ammo remains case SPMSL_CURARE: if (foe->res_poison() <= 0) return true; break; case SPMSL_SLEEP: if (foe->can_sleep()) return true; break; case SPMSL_CONFUSION: case SPMSL_PARALYSIS: return true; default: break; } } return false; } static bool _mons_starts_with_ranged_weapon(monster_type mc) { switch (mc) { case MONS_JOSEPH: case MONS_DEEP_ELF_MASTER_ARCHER: case MONS_CENTAUR: case MONS_CENTAUR_WARRIOR: case MONS_NESSOS: case MONS_YAKTAUR: case MONS_YAKTAUR_CAPTAIN: case MONS_CHERUB: case MONS_SONJA: case MONS_HAROLD: case MONS_POLYPHEMUS: case MONS_CYCLOPS: case MONS_STONE_GIANT: case MONS_CHUCK: case MONS_MERFOLK_JAVELINEER: case MONS_URUG: case MONS_FAUN: case MONS_SATYR: case MONS_NAGA_SHARPSHOOTER: return true; default: return false; } } bool mons_has_known_ranged_attack(const monster* mon) { return _mons_has_ranged_ability(mon) || mon->flags & MF_SEEN_RANGED || _mons_starts_with_ranged_weapon(mon->type) && !(mon->flags & MF_KNOWN_SHIFTER); } bool mons_can_attack(const monster* mon) { const actor* foe = mon->get_foe(); if (!foe || !mon->can_see(foe)) return false; if (mons_has_ranged_attack(mon) && mon->see_cell_no_trans(foe->pos())) return true; return adjacent(mon->pos(), foe->pos()); } static gender_type _mons_class_gender(monster_type mc) { gender_type gender = GENDER_NEUTER; if (mons_genus(mc) == MONS_MERMAID || mc == MONS_QUEEN_ANT || mc == MONS_QUEEN_BEE || mc == MONS_HARPY || mc == MONS_SPHINX || mc == MONS_WATER_NYMPH) { gender = GENDER_FEMALE; } else if (mc == MONS_HELLBINDER || mc == MONS_CLOUD_MAGE) { gender = GENDER_MALE; } else if (mons_is_unique(mc) && !mons_is_pghost(mc)) { if (mons_species(mc) == MONS_SERPENT_OF_HELL) mc = MONS_SERPENT_OF_HELL; switch (mc) { case MONS_JESSICA: case MONS_PSYCHE: case MONS_JOSEPHINE: case MONS_AGNES: case MONS_MAUD: case MONS_LOUISE: case MONS_FRANCES: case MONS_MARGERY: case MONS_EROLCHA: case MONS_ERICA: case MONS_TIAMAT: case MONS_ERESHKIGAL: case MONS_ROXANNE: case MONS_SONJA: case MONS_ILSUIW: case MONS_NERGALLE: case MONS_KIRKE: case MONS_DUVESSA: case MONS_THE_ENCHANTRESS: case MONS_NELLIE: case MONS_ARACHNE: case MONS_NATASHA: case MONS_VASHNIA: gender = GENDER_FEMALE; break; case MONS_ROYAL_JELLY: case MONS_LERNAEAN_HYDRA: case MONS_IRON_GIANT: case MONS_SERPENT_OF_HELL: gender = GENDER_NEUTER; break; default: gender = GENDER_MALE; break; } } return gender; } static const char * const _pronoun_declension[][NUM_PRONOUN_CASES] = { // subj poss refl obj { "it", "its", "itself", "it" }, // neuter { "he", "his", "himself", "him" }, // masculine { "she", "her", "herself", "her" }, // feminine { "you", "your", "yourself", "you" }, // 2nd person }; const char *decline_pronoun(gender_type gender, pronoun_type variant) { ASSERT_RANGE(gender, 0, NUM_GENDERS); ASSERT_RANGE(variant, 0, NUM_PRONOUN_CASES); return _pronoun_declension[gender][variant]; } // Use of variant (case is irrelevant here): // PRONOUN_SUBJECTIVE : _She_ is tap dancing. // PRONOUN_POSSESSIVE : _Its_ sword explodes! // PRONOUN_REFLEXIVE : The wizard mumbles to _herself_. // PRONOUN_OBJECTIVE : You miss _him_. const char *mons_pronoun(monster_type mon_type, pronoun_type variant, bool visible) { COMPILE_CHECK(ARRAYSZ(_pronoun_declension) == NUM_GENDERS); const gender_type gender = !visible ? GENDER_NEUTER : _mons_class_gender(mon_type); return decline_pronoun(gender, variant); } // Checks if the monster can use smiting/torment to attack without // unimpeded LOS to the player. static bool _mons_has_smite_attack(const monster* mons) { const monster_spells &hspell_pass = mons->spells; for (unsigned i = 0; i < hspell_pass.size(); ++i) if (hspell_pass[i] == SPELL_SYMBOL_OF_TORMENT || hspell_pass[i] == SPELL_SMITING || hspell_pass[i] == SPELL_HELLFIRE_BURST || hspell_pass[i] == SPELL_FIRE_STORM || hspell_pass[i] == SPELL_AIRSTRIKE || hspell_pass[i] == SPELL_CHAOTIC_MIRROR) { return true; } return false; } // Determines if a monster is smart and pushy enough to displace other // monsters. A shover should not cause damage to the shovee by // displacing it, so monsters that trail clouds of badness are // ineligible. The shover should also benefit from shoving, so monsters // that can smite/torment are ineligible. // // (Smiters would be eligible for shoving when fleeing if the AI allowed // for smart monsters to flee.) bool monster_shover(const monster* m) { const monsterentry *me = get_monster_data(m->type); if (!me) return false; // Efreet and fire elementals are disqualified because they leave behind // clouds of flame. Curse toes are disqualified because they trail // clouds of miasma. if (mons_genus(m->type) == MONS_EFREET || m->type == MONS_FIRE_ELEMENTAL || m->type == MONS_CURSE_TOE) { return false; } // Monsters too stupid to use stairs (e.g. non-spectral zombified undead) // are also disqualified. if (!mons_can_use_stairs(m)) return false; // Smiters profit from staying back and smiting. if (_mons_has_smite_attack(m)) return false; char mchar = me->basechar; // Somewhat arbitrary: giants and dragons are too big to get past anything, // beetles are too dumb (arguable), dancing weapons can't communicate, eyes // aren't pushers and shovers. Worms and elementals are on the list because // all 'w' are currently unrelated. return mchar != 'C' && mchar != 'B' && mchar != '(' && mchar != 'D' && mchar != 'G' && mchar != 'w' && mchar != 'E'; } // Returns true if m1 and m2 are related, and m1 is higher up the totem pole // than m2. The criteria for being related are somewhat loose, as you can see // below. bool monster_senior(const monster* m1, const monster* m2, bool fleeing) { const monsterentry *me1 = get_monster_data(m1->type), *me2 = get_monster_data(m2->type); if (!me1 || !me2) return false; // Fannar's ice beasts can push past Fannar, who benefits from this. // Similarly, he refuses to push past them unless he's fleeing. if (m1->type == MONS_FANNAR && m2->type == MONS_ICE_BEAST) return fleeing; if (m1->type == MONS_ICE_BEAST && m2->type == MONS_FANNAR) return true; // Band leaders can displace followers regardless of type considerations. // -cao if (m2->props.exists("band_leader")) { unsigned leader_mid = m2->props["band_leader"].get_int(); if (leader_mid == m1->mid) return true; } // And prevent followers to displace the leader to avoid constant swapping. else if (m1->props.exists("band_leader")) { unsigned leader_mid = m1->props["band_leader"].get_int(); if (leader_mid == m2->mid) return false; } char mchar1 = me1->basechar; char mchar2 = me2->basechar; // If both are demons, the smaller number is the nastier demon. if (isadigit(mchar1) && isadigit(mchar2)) return fleeing || mchar1 < mchar2; // &s are the evillest demons of all, well apart from Geryon, who really // profits from *not* pushing past beasts. if (mchar1 == '&' && isadigit(mchar2) && m1->type != MONS_GERYON) return fleeing || m1->get_hit_dice() > m2->get_hit_dice(); // If they're the same holiness, monsters smart enough to use stairs can // push past monsters too stupid to use stairs (so that e.g. non-zombified // or spectral zombified undead can push past non-spectral zombified // undead). if (m1->holiness() == m2->holiness() && mons_class_can_use_stairs(m1->type) && !mons_class_can_use_stairs(m2->type)) { return true; } if (mons_genus(m1->type) == MONS_KILLER_BEE && mons_genus(m2->type) == MONS_KILLER_BEE) { if (fleeing) return true; if (m1->type == MONS_QUEEN_BEE && m2->type != MONS_QUEEN_BEE) return true; } // Special-case gnolls, so they can't get past (hob)goblins. if (mons_species(m1->type) == MONS_GNOLL && mons_species(m2->type) != MONS_GNOLL) { return false; } // Special-case (non-enslaved soul) spectral things to push past revenants. if ((m1->type == MONS_SPECTRAL_THING && !mons_enslaved_soul(m1)) && m2->type == MONS_REVENANT) { return true; } return mchar1 == mchar2 && (fleeing || m1->get_hit_dice() > m2->get_hit_dice()); } bool mons_class_can_pass(monster_type mc, const dungeon_feature_type grid) { if (grid == DNGN_MALIGN_GATEWAY) { return mc == MONS_ELDRITCH_TENTACLE || mc == MONS_ELDRITCH_TENTACLE_SEGMENT; } return !feat_is_solid(grid); } static bool _mons_can_open_doors(const monster* mon) { return mons_itemuse(mon) >= MONUSE_OPEN_DOORS; } // Some functions that check whether a monster can open/eat/pass a // given door. These all return false if there's no closed door there. bool mons_can_open_door(const monster* mon, const coord_def& pos) { if (!_mons_can_open_doors(mon)) return false; // Creatures allied with the player can't open sealed doors either if (mon->friendly() && grd(pos) == DNGN_SEALED_DOOR) return false; if (env.markers.property_at(pos, MAT_ANY, "door_restrict") == "veto") return false; return true; } // Monsters that eat items (currently only jellies) also eat doors. bool mons_can_eat_door(const monster* mon, const coord_def& pos) { if (mons_itemeat(mon) != MONEAT_ITEMS) return false; if (env.markers.property_at(pos, MAT_ANY, "door_restrict") == "veto") return false; return true; } static bool _mons_can_pass_door(const monster* mon, const coord_def& pos) { return mon->can_pass_through_feat(DNGN_FLOOR) && (mons_can_open_door(mon, pos) || mons_can_eat_door(mon, pos)); } bool mons_can_traverse(const monster* mon, const coord_def& p, bool only_in_sight, bool checktraps) { // Friendly summons should avoid pathing out of the player's sight // (especially as attempting this may make them ignore valid, but longer // paths). if (only_in_sight && !you.see_cell_no_trans(p)) return false; if ((grd(p) == DNGN_CLOSED_DOOR || grd(p) == DNGN_SEALED_DOOR) && _mons_can_pass_door(mon, p)) { return true; } if (!mon->is_habitable(p)) return false; const trap_def* ptrap = find_trap(p); if (checktraps && ptrap) { const trap_type tt = ptrap->type; // Don't allow allies to pass over known (to them) Zot traps. if (tt == TRAP_ZOT && ptrap->is_known(mon) && mon->friendly()) { return false; } // Monsters cannot travel over teleport traps. if (!can_place_on_trap(mons_base_type(mon), tt)) return false; } return true; } void mons_remove_from_grid(const monster* mon) { const coord_def pos = mon->pos(); if (map_bounds(pos) && mgrd(pos) == mon->mindex()) mgrd(pos) = NON_MONSTER; } mon_inv_type equip_slot_to_mslot(equipment_type eq) { switch (eq) { case EQ_WEAPON: return MSLOT_WEAPON; case EQ_BODY_ARMOUR: return MSLOT_ARMOUR; case EQ_SHIELD: return MSLOT_SHIELD; case EQ_RINGS: case EQ_AMULET: return MSLOT_JEWELLERY; default: return NUM_MONSTER_SLOTS; } } mon_inv_type item_to_mslot(const item_def &item) { switch (item.base_type) { case OBJ_WEAPONS: case OBJ_STAVES: case OBJ_RODS: return MSLOT_WEAPON; case OBJ_MISSILES: return MSLOT_MISSILE; case OBJ_ARMOUR: return equip_slot_to_mslot(get_armour_slot(item)); case OBJ_WANDS: return MSLOT_WAND; case OBJ_SCROLLS: return MSLOT_SCROLL; case OBJ_POTIONS: return MSLOT_POTION; case OBJ_MISCELLANY: return MSLOT_MISCELLANY; case OBJ_GOLD: return MSLOT_GOLD; default: return NUM_MONSTER_SLOTS; } } monster_type royal_jelly_ejectable_monster() { return random_choose(MONS_ACID_BLOB, MONS_AZURE_JELLY, MONS_DEATH_OOZE, -1); } // Replaces @foe_god@ and @god_is@ with foe's god name. // // Atheists get "You"/"you", and worshippers of nameless gods get "Your // god"/"your god". static string _replace_god_name(god_type god, bool need_verb = false, bool capital = false) { string result; if (god == GOD_NO_GOD) result = capital ? "You" : "you"; else if (god == GOD_NAMELESS) result = capital ? "Your god" : "your god"; else { const string godname = god_name(god, false); result = capital ? uppercase_first(godname) : godname; } if (need_verb) { result += ' '; result += (god == GOD_NO_GOD) ? "are" : "is"; } return result; } static string _get_species_insult(const string &species, const string &type) { string insult; string lookup; // Get species genus. if (!species.empty()) { lookup = "insult "; lookup += species; lookup += " "; lookup += type; insult = getSpeakString(lowercase(lookup)); } if (insult.empty()) // Species too specific? { lookup = "insult general "; lookup += type; insult = getSpeakString(lookup); } return insult; } // Replaces the "@foo@" strings in monster shout and monster speak // definitions. string do_mon_str_replacements(const string &in_msg, const monster* mons, int s_type) { string msg = in_msg; const actor* foe = (mons->wont_attack() && invalid_monster_index(mons->foe)) ? &you : mons->get_foe(); if (s_type < 0 || s_type >= NUM_LOUDNESS || s_type == NUM_SHOUTS) s_type = mons_shouts(mons->type); msg = maybe_pick_random_substring(msg); // FIXME: Handle player_genus in case it was not generalised to foe_genus. msg = replace_all(msg, "@a_player_genus@", article_a(species_name(you.species, true))); msg = replace_all(msg, "@player_genus@", species_name(you.species, true)); msg = replace_all(msg, "@player_genus_plural@", pluralise(species_name(you.species, true))); string foe_species; if (foe == NULL) ; else if (foe->is_player()) { foe_species = species_name(you.species, true); msg = replace_all(msg, " @to_foe@", ""); msg = replace_all(msg, " @at_foe@", ""); msg = replace_all(msg, "@player_only@", ""); msg = replace_all(msg, " @foe,@", ","); msg = replace_all(msg, "@player", "@foe"); msg = replace_all(msg, "@Player", "@Foe"); msg = replace_all(msg, "@foe_possessive@", "your"); msg = replace_all(msg, "@foe@", "you"); msg = replace_all(msg, "@Foe@", "You"); msg = replace_all(msg, "@foe_name@", you.your_name); msg = replace_all(msg, "@foe_species@", species_name(you.species)); msg = replace_all(msg, "@foe_genus@", foe_species); msg = replace_all(msg, "@Foe_genus@", uppercase_first(foe_species)); msg = replace_all(msg, "@foe_genus_plural@", pluralise(species_name(you.species, true))); } else { string foe_name; const monster* m_foe = foe->as_monster(); if (m_foe->attitude == ATT_FRIENDLY && !mons_is_unique(m_foe->type) && !crawl_state.game_is_arena()) { foe_name = foe->name(DESC_YOUR); const string::size_type pos = foe_name.find("'"); if (pos != string::npos) foe_name = foe_name.substr(0, pos); } else foe_name = foe->name(DESC_THE); string prep = "at"; if (s_type == S_SILENT || s_type == S_SHOUT || s_type == S_NORMAL) prep = "to"; msg = replace_all(msg, "@says@ @to_foe@", "@says@ " + prep + " @foe@"); msg = replace_all(msg, " @to_foe@", " to @foe@"); msg = replace_all(msg, " @at_foe@", " at @foe@"); msg = replace_all(msg, "@foe,@", "@foe@,"); msg = replace_all(msg, "@foe_possessive@", "@foe@'s"); msg = replace_all(msg, "@foe@", foe_name); msg = replace_all(msg, "@Foe@", uppercase_first(foe_name)); if (m_foe->is_named()) msg = replace_all(msg, "@foe_name@", foe->name(DESC_PLAIN, true)); string species = mons_type_name(mons_species(m_foe->type), DESC_PLAIN); msg = replace_all(msg, "@foe_species@", species); string genus = mons_type_name(mons_genus(m_foe->type), DESC_PLAIN); msg = replace_all(msg, "@foe_genus@", genus); msg = replace_all(msg, "@Foe_genus@", uppercase_first(genus)); msg = replace_all(msg, "@foe_genus_plural@", pluralise(genus)); foe_species = genus; } description_level_type nocap = DESC_THE, cap = DESC_THE; if (mons->is_named() && you.can_see(mons)) { const string name = mons->name(DESC_THE); msg = replace_all(msg, "@the_something@", name); msg = replace_all(msg, "@The_something@", name); msg = replace_all(msg, "@the_monster@", name); msg = replace_all(msg, "@The_monster@", name); } else if (mons->attitude == ATT_FRIENDLY && !mons_is_unique(mons->type) && !crawl_state.game_is_arena() && you.can_see(mons)) { nocap = DESC_PLAIN; cap = DESC_PLAIN; msg = replace_all(msg, "@the_something@", "your @the_something@"); msg = replace_all(msg, "@The_something@", "Your @The_something@"); msg = replace_all(msg, "@the_monster@", "your @the_monster@"); msg = replace_all(msg, "@The_monster@", "Your @the_monster@"); } if (you.see_cell(mons->pos())) { dungeon_feature_type feat = grd(mons->pos()); if (feat < DNGN_MINMOVE || feat >= NUM_FEATURES) msg = replace_all(msg, "@surface@", "buggy surface"); else if (feat == DNGN_LAVA) msg = replace_all(msg, "@surface@", "lava"); else if (feat_is_water(feat)) msg = replace_all(msg, "@surface@", "water"); else if (feat_is_altar(feat)) msg = replace_all(msg, "@surface@", "altar"); else msg = replace_all(msg, "@surface@", "ground"); msg = replace_all(msg, "@feature@", raw_feature_description(mons->pos())); } else { msg = replace_all(msg, "@surface@", "buggy unseen surface"); msg = replace_all(msg, "@feature@", "buggy unseen feature"); } string something = mons->name(DESC_PLAIN); msg = replace_all(msg, "@something@", something); msg = replace_all(msg, "@a_something@", mons->name(DESC_A)); msg = replace_all(msg, "@the_something@", mons->name(nocap)); something[0] = toupper(something[0]); msg = replace_all(msg, "@Something@", something); msg = replace_all(msg, "@A_something@", mons->name(DESC_A)); msg = replace_all(msg, "@The_something@", mons->name(cap)); // Player name. msg = replace_all(msg, "@player_name@", you.your_name); string plain = mons->name(DESC_PLAIN); msg = replace_all(msg, "@monster@", plain); msg = replace_all(msg, "@a_monster@", mons->name(DESC_A)); msg = replace_all(msg, "@the_monster@", mons->name(nocap)); plain[0] = toupper(plain[0]); msg = replace_all(msg, "@Monster@", plain); msg = replace_all(msg, "@A_monster@", mons->name(DESC_A)); msg = replace_all(msg, "@The_monster@", mons->name(cap)); msg = replace_all(msg, "@Subjective@", mons->pronoun(PRONOUN_SUBJECTIVE)); msg = replace_all(msg, "@subjective@", mons->pronoun(PRONOUN_SUBJECTIVE)); msg = replace_all(msg, "@Possessive@", mons->pronoun(PRONOUN_POSSESSIVE)); msg = replace_all(msg, "@possessive@", mons->pronoun(PRONOUN_POSSESSIVE)); msg = replace_all(msg, "@reflexive@", mons->pronoun(PRONOUN_REFLEXIVE)); msg = replace_all(msg, "@objective@", mons->pronoun(PRONOUN_OBJECTIVE)); // Body parts. bool can_plural = false; string part_str = mons->hand_name(false, &can_plural); msg = replace_all(msg, "@hand@", part_str); msg = replace_all(msg, "@Hand@", uppercase_first(part_str)); if (!can_plural) part_str = "NO PLURAL HANDS"; else part_str = mons->hand_name(true); msg = replace_all(msg, "@hands@", part_str); msg = replace_all(msg, "@Hands@", uppercase_first(part_str)); can_plural = false; part_str = mons->arm_name(false, &can_plural); msg = replace_all(msg, "@arm@", part_str); msg = replace_all(msg, "@Arm@", uppercase_first(part_str)); if (!can_plural) part_str = "NO PLURAL ARMS"; else part_str = mons->arm_name(true); msg = replace_all(msg, "@arms@", part_str); msg = replace_all(msg, "@Arms@", uppercase_first(part_str)); can_plural = false; part_str = mons->foot_name(false, &can_plural); msg = replace_all(msg, "@foot@", part_str); msg = replace_all(msg, "@Foot@", uppercase_first(part_str)); if (!can_plural) part_str = "NO PLURAL FEET"; else part_str = mons->foot_name(true); msg = replace_all(msg, "@feet@", part_str); msg = replace_all(msg, "@Feet@", uppercase_first(part_str)); if (foe != NULL) { const god_type god = foe->deity(); // Replace with "you are" for atheists. msg = replace_all(msg, "@god_is@", _replace_god_name(god, true, false)); msg = replace_all(msg, "@God_is@", _replace_god_name(god, true, true)); // No verb needed. msg = replace_all(msg, "@foe_god@", _replace_god_name(god, false, false)); msg = replace_all(msg, "@Foe_god@", _replace_god_name(god, false, true)); } // The monster's god, not the player's. Atheists get // "NO GOD"/"NO GOD"/"NO_GOD"/"NO_GOD", and worshippers of nameless // gods get "a god"/"its god/my God/My God". // // XXX: Crawl currently has no first-person possessive pronoun; // if it gets one, it should be used for the last two entries. if (mons->god == GOD_NO_GOD) { msg = replace_all(msg, "@a_God@", "NO GOD"); msg = replace_all(msg, "@A_God@", "NO GOD"); msg = replace_all(msg, "@possessive_God@", "NO GOD"); msg = replace_all(msg, "@Possessive_God@", "NO GOD"); msg = replace_all(msg, "@my_God@", "NO GOD"); msg = replace_all(msg, "@My_God@", "NO GOD"); } else if (mons->god == GOD_NAMELESS) { msg = replace_all(msg, "@a_God@", "a god"); msg = replace_all(msg, "@A_God@", "A god"); const string possessive = mons->pronoun(PRONOUN_POSSESSIVE) + " god"; msg = replace_all(msg, "@possessive_God@", possessive); msg = replace_all(msg, "@Possessive_God@", uppercase_first(possessive)); msg = replace_all(msg, "@my_God@", "my God"); msg = replace_all(msg, "@My_God@", "My God"); } else { const string godname = god_name(mons->god); const string godcap = uppercase_first(godname); msg = replace_all(msg, "@a_God@", godname); msg = replace_all(msg, "@A_God@", godcap); msg = replace_all(msg, "@possessive_God@", godname); msg = replace_all(msg, "@Possessive_God@", godcap); msg = replace_all(msg, "@my_God@", godname); msg = replace_all(msg, "@My_God@", godcap); } // Replace with species specific insults. if (msg.find("@species_insult_") != string::npos) { msg = replace_all(msg, "@species_insult_adj1@", _get_species_insult(foe_species, "adj1")); msg = replace_all(msg, "@species_insult_adj2@", _get_species_insult(foe_species, "adj2")); msg = replace_all(msg, "@species_insult_noun@", _get_species_insult(foe_species, "noun")); } static const char * sound_list[] = { "says", // actually S_SILENT "shouts", "barks", "shouts", "roars", "screams", "bellows", "trumpets", "screeches", "buzzes", "moans", "gurgles", "whines", "croaks", "growls", "hisses", "sneers", // S_DEMON_TAUNT "caws", "says", // S_CHERUB -- they just speak normally. "buggily says", // NUM_SHOUTS "breathes", // S_VERY_SOFT "whispers", // S_SOFT "says", // S_NORMAL "shouts", // S_LOUD "screams", // S_VERY_LOUD }; COMPILE_CHECK(ARRAYSZ(sound_list) == NUM_LOUDNESS); if (s_type < 0 || s_type >= NUM_LOUDNESS || s_type == NUM_SHOUTS) { mprf(MSGCH_DIAGNOSTICS, "Invalid @says@ type."); msg = replace_all(msg, "@says@", "buggily says"); } else msg = replace_all(msg, "@says@", sound_list[s_type]); msg = apostrophise_fixup(msg); msg = maybe_capitalise_substring(msg); return msg; } static mon_body_shape _get_ghost_shape(const monster* mon) { const ghost_demon &ghost = *(mon->ghost); switch (ghost.species) { case SP_NAGA: return MON_SHAPE_NAGA; case SP_CENTAUR: return MON_SHAPE_CENTAUR; case SP_RED_DRACONIAN: case SP_WHITE_DRACONIAN: case SP_GREEN_DRACONIAN: case SP_YELLOW_DRACONIAN: case SP_GREY_DRACONIAN: case SP_BLACK_DRACONIAN: case SP_PURPLE_DRACONIAN: case SP_MOTTLED_DRACONIAN: case SP_PALE_DRACONIAN: case SP_BASE_DRACONIAN: return MON_SHAPE_HUMANOID_TAILED; default: return MON_SHAPE_HUMANOID; } } /** * Get the monster body shape of the given monster. * @param mon The monster in question. * @return The mon_body_shape type of this monster. */ mon_body_shape get_mon_shape(const monster* mon) { if (mons_is_pghost(mon->type)) return _get_ghost_shape(mon); else if (mons_is_zombified(mon)) return get_mon_shape(mon->base_monster); else return get_mon_shape(mon->type); } /** * Get the monster body shape of the given monster type. * @param mon The monster type in question. * @return The mon_body_shape type of this monster type. */ mon_body_shape get_mon_shape(const monster_type mc) { if (mc == MONS_CHAOS_SPAWN) return static_cast(random2(MON_SHAPE_MISC + 1)); ASSERT_smc(); return smc->shape; } /** * Get a DB lookup string for the given monster body shape. * @param mon The monster body shape type in question. * @return A DB lookup string for the monster body shape. */ string get_mon_shape_str(const mon_body_shape shape) { ASSERT_RANGE(shape, MON_SHAPE_HUMANOID, MON_SHAPE_MISC + 1); static const char *shape_names[] = { "humanoid", "winged humanoid", "tailed humanoid", "winged tailed humanoid", "centaur", "naga", "quadruped", "tailless quadruped", "winged quadruped", "bat", "snake", "fish", "insect", "winged insect", "arachnid", "centipede", "snail", "plant", "fungus", "orb", "blob", "misc" }; COMPILE_CHECK(ARRAYSZ(shape_names) == MON_SHAPE_MISC + 1); return shape_names[shape]; } bool player_or_mon_in_sanct(const monster* mons) { return is_sanctuary(you.pos()) || is_sanctuary(mons->pos()); } bool mons_landlubbers_in_reach(const monster* mons) { if (mons_has_ranged_attack(mons)) return true; actor *act; for (radius_iterator ai(mons->pos(), mons->reach_range(), C_CIRCLE, true); ai; ++ai) if ((act = actor_at(*ai)) && !mons_aligned(mons, act)) return true; return false; } int get_dist_to_nearest_monster() { int minRange = LOS_RADIUS_SQ + 1; for (radius_iterator ri(you.pos(), LOS_NO_TRANS, true); ri; ++ri) { const monster* mon = monster_at(*ri); if (mon == NULL) continue; if (!mon->visible_to(&you)) continue; // Plants/fungi don't count. if (mons_class_flag(mon->type, M_NO_EXP_GAIN) && (mon->type != MONS_BALLISTOMYCETE || mon->number == 0)) { continue; } if (mon->wont_attack()) continue; int dist = distance2(you.pos(), *ri); if (dist < minRange) minRange = dist; } return minRange; } bool monster_nearby() { for (radius_iterator ri(you.pos(), LOS_DEFAULT); ri; ++ri) if (monster_at(*ri)) return true; return false; } actor *actor_by_mid(mid_t m) { if (m == MID_PLAYER) return &you; return monster_by_mid(m); } monster *monster_by_mid(mid_t m) { if (m == MID_ANON_FRIEND) return &menv[ANON_FRIENDLY_MONSTER]; if (m == MID_YOU_FAULTLESS) return &menv[YOU_FAULTLESS]; map::const_iterator mc = env.mid_cache.find(m); if (mc != env.mid_cache.end()) return &menv[mc->second]; return 0; } bool mons_is_tentacle_head(monster_type mc) { return mc == MONS_KRAKEN || mc == MONS_TENTACLED_STARSPAWN; } bool mons_is_child_tentacle(monster_type mc) { return mc == MONS_KRAKEN_TENTACLE || mc == MONS_STARSPAWN_TENTACLE || mc == MONS_SNAPLASHER_VINE; } bool mons_is_child_tentacle_segment(monster_type mc) { return mc == MONS_KRAKEN_TENTACLE_SEGMENT || mc == MONS_STARSPAWN_TENTACLE_SEGMENT || mc == MONS_SNAPLASHER_VINE_SEGMENT; } bool mons_is_tentacle(monster_type mc) { return mc == MONS_ELDRITCH_TENTACLE || mons_is_child_tentacle(mc); } bool mons_is_tentacle_segment(monster_type mc) { return mc == MONS_ELDRITCH_TENTACLE_SEGMENT || mons_is_child_tentacle_segment(mc); } bool mons_is_tentacle_or_tentacle_segment(monster_type mc) { return mons_is_tentacle(mc) || mons_is_tentacle_segment(mc); } monster* mons_get_parent_monster(monster* mons) { for (monster_iterator mi; mi; ++mi) { if (mi->is_parent_monster_of(mons)) return mi->as_monster(); } return 0; } void init_anon() { monster &mon = menv[ANON_FRIENDLY_MONSTER]; mon.reset(); mon.type = MONS_PROGRAM_BUG; mon.mid = MID_ANON_FRIEND; mon.attitude = ATT_FRIENDLY; mon.hit_points = mon.max_hit_points = 1000; monster &yf = menv[YOU_FAULTLESS]; yf.reset(); yf.type = MONS_PROGRAM_BUG; yf.mid = MID_YOU_FAULTLESS; yf.attitude = ATT_FRIENDLY; // higher than this, actually yf.hit_points = mon.max_hit_points = 1000; } actor *find_agent(mid_t m, kill_category kc) { actor *agent = actor_by_mid(m); if (agent) return agent; switch (kc) { case KC_YOU: // shouldn't happen, there ought to be a valid mid return &you; case KC_FRIENDLY: return &menv[ANON_FRIENDLY_MONSTER]; case KC_OTHER: // currently hostile dead/gone monsters are no different from env return 0; case KC_NCATEGORIES: default: die("invalid kill category"); } } const char* mons_class_name(monster_type mc) { // Usually, all invalids return "program bug", but since it has value of 0, // it's good to tell them apart in error messages. if (invalid_monster_type(mc) && mc != MONS_PROGRAM_BUG) return "INVALID"; return get_monster_data(mc)->name; } monster_type mons_tentacle_parent_type(const monster* mons) { switch (mons_base_type(mons)) { case MONS_KRAKEN_TENTACLE: return MONS_KRAKEN; case MONS_KRAKEN_TENTACLE_SEGMENT: return MONS_KRAKEN_TENTACLE; case MONS_STARSPAWN_TENTACLE: return MONS_TENTACLED_STARSPAWN; case MONS_STARSPAWN_TENTACLE_SEGMENT: return MONS_STARSPAWN_TENTACLE; case MONS_ELDRITCH_TENTACLE_SEGMENT: return MONS_ELDRITCH_TENTACLE; case MONS_SNAPLASHER_VINE_SEGMENT: return MONS_SNAPLASHER_VINE; default: return MONS_PROGRAM_BUG; } } monster_type mons_tentacle_child_type(const monster* mons) { switch (mons_base_type(mons)) { case MONS_KRAKEN: return MONS_KRAKEN_TENTACLE; case MONS_KRAKEN_TENTACLE: return MONS_KRAKEN_TENTACLE_SEGMENT; case MONS_TENTACLED_STARSPAWN: return MONS_STARSPAWN_TENTACLE; case MONS_STARSPAWN_TENTACLE: return MONS_STARSPAWN_TENTACLE_SEGMENT; case MONS_ELDRITCH_TENTACLE: return MONS_ELDRITCH_TENTACLE_SEGMENT; case MONS_SNAPLASHER_VINE: return MONS_SNAPLASHER_VINE_SEGMENT; default: return MONS_PROGRAM_BUG; } } //Returns whether a given monster is a tentacle segment immediately attached //to the parent monster bool mons_tentacle_adjacent(const monster* parent, const monster* child) { return mons_is_tentacle_head(mons_base_type(parent)) && mons_is_tentacle_segment(child->type) && child->props.exists("inwards") && child->props["inwards"].get_int() == parent->mindex(); } mon_threat_level_type mons_threat_level(const monster *mon, bool real) { const double factor = sqrt(exp_needed(you.experience_level) / 30.0); const int tension = exper_value(mon, real) / (1 + factor); if (tension <= 0) { // Conjurators use melee to conserve mana, MDFis switch plates... return MTHRT_TRIVIAL; } else if (tension <= 5) { // An easy fight but not ignorable. return MTHRT_EASY; } else if (tension <= 32) { // Hard but reasonable. return MTHRT_TOUGH; } else { // Check all wands/jewels several times, wear brown pants... return MTHRT_NASTY; } } bool mons_foe_is_marked(const monster* mon) { if (mon->foe == MHITYOU) return you.duration[DUR_SENTINEL_MARK]; else return false; } void debug_mondata() { string fails; for (monster_type mc = MONS_0; mc < NUM_MONSTERS; ++mc) { if (invalid_monster_type(mc)) continue; const char* name = mons_class_name(mc); const monsterentry *md = get_monster_data(mc); int MR = md->resist_magic; if (MR < 0) MR = md->hpdice[0] * -MR * 4 / 3; if (md->resist_magic > 200 && md->resist_magic != MAG_IMMUNE) fails += make_stringf("%s has MR %d > 200\n", name, MR); // Tests below apply only to corpses. if (md->species != mc || md->bitfields & M_CANT_SPAWN) continue; if (md->weight && !md->corpse_thingy) fails += make_stringf("%s has a corpse but no corpse_type\n", name); if (md->weight && !md->zombie_size) fails += make_stringf("%s has a corpse but no zombie_size\n", name); } if (!fails.empty()) { FILE *f = fopen("mon-data.out", "w"); if (!f) sysfail("can't write test output"); fprintf(f, "%s", fails.c_str()); fclose(f); fail("mon-data errors (dumped to mon-data.out)"); } } // Used when clearing level data, to ensure any additional reset quirks // are handled properly. void reset_all_monsters() { for (int i = 0; i < MAX_MONSTERS; i++) { // The monsters here have already been saved or discarded, so this // is the only place when a constricting monster can legitimately // be reset. Thus, clear constriction manually. if (!invalid_monster(&menv[i])) { delete menv[i].constricting; menv[i].constricting = nullptr; menv[i].clear_constricted(); } menv[i].reset(); } env.mid_cache.clear(); } bool mons_is_recallable(actor* caller, monster* targ) { // For player, only recall friendly monsters if (caller == &you) { if (!targ->friendly()) return false; } // Monster recall requires same attitude and at least normal intelligence else if (mons_intel(targ) < I_NORMAL || (!caller && targ->friendly()) || (caller && !mons_aligned(targ, caller->as_monster()))) { return false; } return targ->alive() && !mons_class_is_stationary(targ->type) && !mons_is_conjured(targ->type); } vector get_on_level_followers() { vector mon_list; for (monster_iterator mi; mi; ++mi) if (mons_is_recallable(&you, *mi) && mi->foe == MHITYOU) mon_list.push_back(*mi); return mon_list; } // Return the number of monsters of the specified type. // If friendly_only is true, only count friendly // monsters, otherwise all of them int count_monsters(monster_type mtyp, bool friendly_only) { int count = 0; for (int mon = 0; mon < MAX_MONSTERS; mon++) { monster *mons = &menv[mon]; if (mons->alive() && mons->type == mtyp && (!friendly_only || mons->friendly())) { count++; } } return count; } int count_allies() { int count = 0; for (int mon = 0; mon < MAX_MONSTERS; mon++) if (menv[mon].alive() && menv[mon].friendly()) count++; return count; } bool mons_stores_tracking_data(const monster* mons) { return mons->type == MONS_THORN_HUNTER || mons->type == MONS_SIREN; } bool mons_is_beast(monster_type mc) { if (mons_class_holiness(mc) != MH_NATURAL && mc != MONS_APIS || mons_class_intel(mc) < I_INSECT || mons_class_intel(mc) > I_ANIMAL) { return false; } else if ((mons_genus(mc) == MONS_DRAGON && mc != MONS_SWAMP_DRAGON) || mons_genus(mc) == MONS_UGLY_THING || mc == MONS_ICE_BEAST || mc == MONS_SKY_BEAST || mc == MONS_BUTTERFLY) { return false; } else return true; } bool mons_is_avatar(monster_type mc) { return mc == MONS_SPECTRAL_WEAPON || mc == MONS_BATTLESPHERE || mc == MONS_GRAND_AVATAR; } bool mons_is_player_shadow(const monster* mon) { return mon->type == MONS_PLAYER_SHADOW && mon->mname.empty(); } bool mons_antimagic_affected(const monster* mons) { return mons->can_use_spells() && !mons->is_priest() && !mons_class_flag(mons->type, M_FAKE_SPELLS); } // The default suitable() function for choose_random_nearby_monster(). bool choose_any_monster(const monster* mon) { return !mons_is_projectile(mon->type); } // Find a nearby monster and return its index, including you as a // possibility with probability weight. suitable() should return true // for the type of monster wanted. // If prefer_named is true, named monsters (including uniques) are twice // as likely to get chosen compared to non-named ones. // If prefer_priest is true, priestly monsters (including uniques) are // twice as likely to get chosen compared to non-priestly ones. monster* choose_random_nearby_monster(int weight, bool (*suitable)(const monster* mon), bool prefer_named_or_priest) { monster* chosen = NULL; for (radius_iterator ri(you.pos(), LOS_NO_TRANS); ri; ++ri) { monster* mon = monster_at(*ri); if (!mon || !suitable(mon)) continue; // FIXME: if the intent is to favour monsters // named by $DEITY, we should set a flag on the // monster (something like MF_DEITY_PREFERRED) and // use that instead of checking the name, given // that other monsters can also have names. // True, but it's currently only used for orcs, and // Blork and Urug also being preferred to non-named orcs // is fine, I think. Once more gods name followers (and // prefer them) that should be changed, of course. (jpeg) int mon_weight = 1; if (prefer_named_or_priest) mon_weight += mon->is_named() + mon->is_priest(); if (x_chance_in_y(mon_weight, weight += mon_weight)) chosen = mon; } return chosen; } monster* choose_random_monster_on_level(int weight, bool (*suitable)(const monster* mon)) { monster* chosen = NULL; for (rectangle_iterator ri(1); ri; ++ri) { monster* mon = monster_at(*ri); if (!mon || !suitable(mon)) continue; // Named or priestly monsters have doubled chances. int mon_weight = 1 + mon->is_named() + mon->is_priest(); if (x_chance_in_y(mon_weight, weight += mon_weight)) chosen = mon; } return chosen; }