/** * @file * @brief Functions related to clouds. * * Creating a cloud module so all the cloud stuff can be isolated. **/ #include "AppHdr.h" #include #include "externs.h" #include "areas.h" #include "cloud.h" #include "colour.h" #include "coord.h" #include "coordit.h" #include "dungeon.h" #include "env.h" #include "fprop.h" #include "godconduct.h" #include "losglobal.h" #include "mapmark.h" #include "melee_attack.h" #include "message.h" #include "mgen_data.h" #include "misc.h" #include "mon-behv.h" #include "mon-place.h" #include "mutation.h" #include "ouch.h" #include "random.h" #include "religion.h" #include "shout.h" #include "state.h" #include "terrain.h" #include "tiledef-gui.h" #include "tiledef-main.h" #include "unwind.h" static int _actor_cloud_damage(actor *act, const cloud_struct &cloud, bool maximum_damage); static int _actual_spread_rate(cloud_type type, int spread_rate) { if (spread_rate >= 0) return spread_rate; switch (type) { #if TAG_MAJOR_VERSION == 34 case CLOUD_GLOOM: return 50; #endif case CLOUD_STEAM: case CLOUD_GREY_SMOKE: case CLOUD_BLACK_SMOKE: return 22; case CLOUD_RAIN: case CLOUD_INK: return 11; default: return 0; } } static beam_type _cloud2beam(cloud_type flavour) { switch (flavour) { default: case CLOUD_NONE: return BEAM_NONE; case CLOUD_FIRE: return BEAM_FIRE; case CLOUD_FOREST_FIRE: return BEAM_FIRE; case CLOUD_MEPHITIC: return BEAM_MEPHITIC; case CLOUD_COLD: return BEAM_COLD; case CLOUD_POISON: return BEAM_POISON; case CLOUD_STEAM: return BEAM_STEAM; case CLOUD_MIASMA: return BEAM_MIASMA; case CLOUD_CHAOS: return BEAM_CHAOS; #if TAG_MAJOR_VERSION == 34 case CLOUD_GLOOM: return BEAM_GLOOM; #endif case CLOUD_INK: return BEAM_INK; case CLOUD_HOLY_FLAMES: return BEAM_HOLY_FLAME; case CLOUD_PETRIFY: return BEAM_PETRIFYING_CLOUD; case CLOUD_RANDOM: return BEAM_RANDOM; case CLOUD_ACID: return BEAM_ACID; case CLOUD_NEGATIVE_ENERGY: return BEAM_NEG; } } #ifdef ASSERTS static bool _killer_whose_match(kill_category whose, killer_type killer) { switch (whose) { case KC_YOU: return killer == KILL_YOU_MISSILE || killer == KILL_YOU_CONF; case KC_FRIENDLY: return killer == KILL_MON_MISSILE || killer == KILL_YOU_CONF || killer == KILL_MON; case KC_OTHER: return killer == KILL_MON_MISSILE || killer == KILL_MISCAST || killer == KILL_MISC || killer == KILL_MON; case KC_NCATEGORIES: die("kill category not matching killer type"); } return false; } #endif static bool _is_opaque_cloud(cloud_type ctype); static void _los_cloud_changed(const coord_def& p, cloud_type t) { if (_is_opaque_cloud(t)) invalidate_los_around(p); } static void _new_cloud(int cloud, cloud_type type, const coord_def& p, int decay, kill_category whose, killer_type killer, mid_t source, uint8_t spread_rate, int colour, string name, string tile, int excl_rad) { ASSERT(env.cloud[cloud].type == CLOUD_NONE); ASSERT(_killer_whose_match(whose, killer)); cloud_struct& c = env.cloud[cloud]; c.type = type; c.decay = decay; c.pos = p; c.whose = whose; c.killer = killer; c.source = source; c.spread_rate = spread_rate; c.colour = colour; c.name = name; c.excl_rad = excl_rad; if (!tile.empty()) { tileidx_t index; if (!tile_main_index(tile.c_str(), &index)) { mprf(MSGCH_ERROR, "Invalid tile requested for cloud: '%s'.", tile.c_str()); tile = ""; } } c.tile = tile; env.cgrid(p) = cloud; env.cloud_no++; _los_cloud_changed(p, type); } static void _place_new_cloud(cloud_type cltype, const coord_def& p, int decay, kill_category whose, killer_type killer, mid_t source, int spread_rate = -1, int colour = -1, string name = "", string tile = "", int excl_rad = -1) { if (env.cloud_no >= MAX_CLOUDS) return; ASSERT(!cell_is_solid(p)); // Find slot for cloud. for (int ci = 0; ci < MAX_CLOUDS; ci++) { if (env.cloud[ci].type == CLOUD_NONE) // i.e., is empty { _new_cloud(ci, cltype, p, decay, whose, killer, source, spread_rate, colour, name, tile, excl_rad); break; } } } static int _spread_cloud(const cloud_struct &cloud) { const int spreadch = cloud.decay > 30? 80 : cloud.decay > 20? 50 : 30; int extra_decay = 0; for (adjacent_iterator ai(cloud.pos); ai; ++ai) { if (random2(100) >= spreadch) continue; if (!in_bounds(*ai) || env.cgrid(*ai) != EMPTY_CLOUD || cell_is_solid(*ai) || is_sanctuary(*ai) && !is_harmless_cloud(cloud.type)) { continue; } if (cloud.type == CLOUD_INK && !feat_is_watery(grd(*ai))) continue; int newdecay = cloud.decay / 2 + 1; if (newdecay >= cloud.decay) newdecay = cloud.decay - 1; _place_new_cloud(cloud.type, *ai, newdecay, cloud.whose, cloud.killer, cloud.source, cloud.spread_rate, cloud.colour, cloud.name, cloud.tile, cloud.excl_rad); extra_decay += 8; } return extra_decay; } static void _spread_fire(const cloud_struct &cloud) { int make_flames = one_chance_in(5); for (adjacent_iterator ai(cloud.pos); ai; ++ai) { if (!in_bounds(*ai) || env.cgrid(*ai) != EMPTY_CLOUD || is_sanctuary(*ai)) { continue; } // burning trees produce flames all around if (!cell_is_solid(*ai) && make_flames) { _place_new_cloud(CLOUD_FIRE, *ai, cloud.decay/2+1, cloud.whose, cloud.killer, cloud.source, cloud.spread_rate, cloud.colour, cloud.name, cloud.tile, cloud.excl_rad); } // forest fire doesn't spread in all directions at once, // every neighbouring square gets a separate roll if (feat_is_tree(grd(*ai)) && one_chance_in(20)) { if (env.markers.property_at(*ai, MAT_ANY, "veto_fire") == "veto") continue; if (you.see_cell(*ai)) mpr("The forest fire spreads!"); destroy_wall(*ai); _place_new_cloud(cloud.type, *ai, random2(30)+25, cloud.whose, cloud.killer, cloud.source, cloud.spread_rate, cloud.colour, cloud.name, cloud.tile, cloud.excl_rad); if (cloud.whose == KC_YOU) { did_god_conduct(DID_KILL_PLANT, 1); did_god_conduct(DID_FIRE, 6); } else if (cloud.whose == KC_FRIENDLY && !crawl_state.game_is_arena()) did_god_conduct(DID_PLANT_KILLED_BY_SERVANT, 1); } } } static void _cloud_interacts_with_terrain(const cloud_struct &cloud) { if (cloud.type == CLOUD_FIRE || cloud.type == CLOUD_FOREST_FIRE) { for (adjacent_iterator ai(cloud.pos); ai; ++ai) { const coord_def p(*ai); if (in_bounds(p) && feat_is_watery(grd(p)) && !cell_is_solid(p) && env.cgrid(p) == EMPTY_CLOUD && one_chance_in(7)) { _place_new_cloud(CLOUD_STEAM, p, cloud.decay / 2 + 1, cloud.whose, cloud.killer, cloud.source, 22); } } } } static void _dissipate_cloud(int cloudidx, int dissipate) { cloud_struct &cloud = env.cloud[cloudidx]; // Apply calculated rate to the actual cloud. cloud.decay -= dissipate; if (cloud.type == CLOUD_FOREST_FIRE) _spread_fire(cloud); else if (x_chance_in_y(cloud.spread_rate, 100)) { cloud.spread_rate -= div_rand_round(cloud.spread_rate, 10); cloud.decay -= _spread_cloud(cloud); } // Check for total dissipation and handle accordingly. if (cloud.decay < 1) delete_cloud(cloudidx); } static void _handle_ghostly_flame(const cloud_struct& cloud) { if (actor_at(cloud.pos) || !actor_by_mid(cloud.source)) return; int countn = 0; for (distance_iterator di(cloud.pos, false, false, 2); di; ++di) { if (monster_at(*di) && monster_at(*di)->type == MONS_SPECTRAL_THING) countn++; } int rate[5] = {650, 175, 45, 20, 0}; int chance = rate[(min(4, countn))]; if (!x_chance_in_y(chance, you.time_taken * 600)) return; monster_type basetype = random_choose_weighted(4, MONS_ANACONDA, 6, MONS_HYDRA, 3, MONS_SNAPPING_TURTLE, 2, MONS_ALLIGATOR_SNAPPING_TURTLE, 100, RANDOM_MONSTER, 0); if (basetype == RANDOM_MONSTER && one_chance_in(4)) { do basetype = pick_random_zombie(); while (mons_class_flag(basetype, M_NO_GEN_DERIVED) || !monster_habitable_grid(basetype, grd(cloud.pos))); } monster* agent = monster_by_mid(cloud.source); monster *mon = create_monster(mgen_data(MONS_SPECTRAL_THING, (cloud.whose == KC_OTHER ? BEH_HOSTILE : BEH_FRIENDLY), NULL, 1, SPELL_GHOSTLY_FLAMES, cloud.pos, (agent ? agent->foe : MHITYOU), MG_FORCE_PLACE, GOD_NO_GOD, basetype)); if (mon) mon->summoner = cloud.source; } void manage_clouds() { for (int i = 0; i < MAX_CLOUDS; ++i) { cloud_struct& cloud = env.cloud[i]; if (cloud.type == CLOUD_NONE) continue; #ifdef ASSERTS if (cell_is_solid(cloud.pos)) { die("cloud %s in %s at (%d,%d)", cloud_type_name(cloud.type).c_str(), dungeon_feature_name(grd(cloud.pos)), cloud.pos.x, cloud.pos.y); } #endif int dissipate = you.time_taken; // If a player-created cloud is out of LOS, it dissipates much faster. if (cloud.source == MID_PLAYER && !you.see_cell_no_trans(cloud.pos)) dissipate *= 4; // Fire clouds dissipate faster over water, // rain and cold clouds dissipate faster over lava. if (cloud.type == CLOUD_FIRE && grd(cloud.pos) == DNGN_DEEP_WATER) dissipate *= 4; else if (cloud.type == CLOUD_STORM) { // This was initially 40, but that was far too spammy. if (x_chance_in_y(dissipate, 400) && !actor_at(cloud.pos)) { bool you_see = you.see_cell(cloud.pos); if (you_see && !you_worship(GOD_QAZLAL)) mpr("Lightning arcs down from a storm cloud!"); noisy(25, cloud.pos, you_see || you_worship(GOD_QAZLAL) ? NULL : "You hear a mighty clap of thunder!"); } if (grd(cloud.pos) == DNGN_LAVA) dissipate *= 4; } else if ((cloud.type == CLOUD_COLD || cloud.type == CLOUD_RAIN) && grd(cloud.pos) == DNGN_LAVA) dissipate *= 4; // Ink cloud doesn't appear outside of water. else if (cloud.type == CLOUD_INK && !feat_is_watery(grd(cloud.pos))) dissipate *= 40; else if (cloud.type == CLOUD_GHOSTLY_FLAME) _handle_ghostly_flame(cloud); _cloud_interacts_with_terrain(cloud); _dissipate_cloud(i, dissipate); } } static void _maybe_leave_water(const cloud_struct& c) { ASSERT_IN_BOUNDS(c.pos); // Rain clouds can occasionally leave shallow water or deepen it: // If we're near lava, chance of leaving water is lower; // if we're near deep water already, chance of leaving water // is slightly higher. if (one_chance_in((5 + count_neighbours(c.pos, DNGN_LAVA)) - count_neighbours(c.pos, DNGN_DEEP_WATER))) { dungeon_feature_type feat; if (grd(c.pos) == DNGN_FLOOR) feat = DNGN_SHALLOW_WATER; else if (grd(c.pos) == DNGN_SHALLOW_WATER && you.pos() != c.pos && one_chance_in(3) && !crawl_state.game_is_zotdef() && !crawl_state.game_is_sprint()) // Don't drown the player! feat = DNGN_DEEP_WATER; else feat = grd(c.pos); if (grd(c.pos) != feat) { if (you.pos() == c.pos && you.ground_level()) mpr("The rain has left you waist-deep in water!"); temp_change_terrain(c.pos, feat, random_range(500, 1000), TERRAIN_CHANGE_FLOOD); } } } void delete_cloud_at(coord_def p) { const int cloudno = env.cgrid(p); if (cloudno != EMPTY_CLOUD) delete_cloud(cloudno); } void delete_cloud(int cloud) { cloud_struct& c = env.cloud[cloud]; if (c.type != CLOUD_NONE) { cloud_type t = c.type; if (c.type == CLOUD_RAIN) _maybe_leave_water(c); c.type = CLOUD_NONE; c.decay = 0; c.whose = KC_OTHER; c.killer = KILL_NONE; c.spread_rate = 0; c.colour = -1; c.name = ""; c.tile = ""; env.cgrid(c.pos) = EMPTY_CLOUD; _los_cloud_changed(c.pos, t); c.pos.reset(); env.cloud_no--; } } void move_cloud_to(coord_def src, coord_def dst) { const int cloudno = env.cgrid(src); move_cloud(cloudno, dst); } // The current use of this function is for shifting in the abyss, so // that clouds get moved along with the rest of the map. void move_cloud(int cloud, const coord_def& newpos) { ASSERT(!cell_is_solid(newpos)); if (cloud != EMPTY_CLOUD) { const coord_def oldpos = env.cloud[cloud].pos; env.cgrid(oldpos) = EMPTY_CLOUD; env.cgrid(newpos) = cloud; env.cloud[cloud].pos = newpos; _los_cloud_changed(oldpos, env.cloud[cloud].type); _los_cloud_changed(newpos, env.cloud[cloud].type); } } #if 0 static void _validate_clouds() { for (rectangle_iterator ri(0); ri; ri++) { int c = env.cgrid(*ri); if (c == EMPTY_CLOUD) continue; ASSERT(env.cloud[c].pos == *ri); } for (int c = 0; c < MAX_CLOUDS; c++) if (env.cloud[c].type != CLOUD_NONE) ASSERT(env.cgrid(env.cloud[c].pos) == c); } #endif void swap_clouds(coord_def p1, coord_def p2) { if (p1 == p2) return; int c1 = env.cgrid(p1); int c2 = env.cgrid(p2); bool affects_los = false; if (c1 != EMPTY_CLOUD) { env.cloud[c1].pos = p2; if (is_opaque_cloud(env.cloud[c1].type)) affects_los = true; } if (c2 != EMPTY_CLOUD) { env.cloud[c2].pos = p1; if (is_opaque_cloud(env.cloud[c2].type)) affects_los = true; } env.cgrid(p1) = c2; env.cgrid(p2) = c1; if (affects_los) { invalidate_los_around(p1); invalidate_los_around(p2); } } // Places a cloud with the given stats assuming one doesn't already // exist at that point. void check_place_cloud(cloud_type cl_type, const coord_def& p, int lifetime, const actor *agent, int spread_rate, int colour, string name, string tile, int excl_rad) { if (!in_bounds(p) || env.cgrid(p) != EMPTY_CLOUD) return; if (cl_type == CLOUD_INK && !feat_is_watery(grd(p))) return; place_cloud(cl_type, p, lifetime, agent, spread_rate, colour, name, tile, excl_rad); } static int _steam_cloud_damage(int decay) { decay = min(decay, 60); decay = max(decay, 10); // Damage in range 3 - 16. return (decay * 13 + 20) / 50; } static bool _is_weak_cloud(int cl) { if (cl == EMPTY_CLOUD) return true; cloud_struct& cloud = env.cloud[cl]; return cloud.type >= CLOUD_GREY_SMOKE && cloud.type <= CLOUD_STEAM || cloud.type == CLOUD_BLACK_SMOKE || cloud.type == CLOUD_MIST || cloud.decay <= 20; // soon gone } static bool cloud_is_stronger(cloud_type ct, int cl) { if (_is_weak_cloud(cl)) return true; cloud_struct& cloud = env.cloud[cl]; if (ct == CLOUD_POISON && cloud.type == CLOUD_MEPHITIC) return true; // allow upgrading meph if (ct == CLOUD_TORNADO) return true; // visual/AI only return false; } // Places a cloud with the given stats. May delete old clouds to // make way if there are too many on level. Will overwrite an old // cloud under some circumstances. void place_cloud(cloud_type cl_type, const coord_def& ctarget, int cl_range, const actor *agent, int _spread_rate, int colour, string name, string tile, int excl_rad) { if (is_sanctuary(ctarget) && !is_harmless_cloud(cl_type)) return; if (cl_type == CLOUD_INK && !feat_is_watery(grd(ctarget))) return; ASSERT(!cell_is_solid(ctarget)); kill_category whose = KC_OTHER; killer_type killer = KILL_MISC; mid_t source = 0; if (agent && agent->is_player()) whose = KC_YOU, killer = KILL_YOU_MISSILE, source = MID_PLAYER; else if (agent && agent->is_monster()) { if (agent->as_monster()->friendly()) whose = KC_FRIENDLY; else whose = KC_OTHER; killer = KILL_MON_MISSILE; source = agent->mid; } int cl_new = -1; const int target_cgrid = env.cgrid(ctarget); if (target_cgrid != EMPTY_CLOUD) { // There's already a cloud here. See if we can overwrite it. if (cloud_is_stronger(cl_type, target_cgrid)) { // Delete this cloud and replace it. cl_new = target_cgrid; delete_cloud(target_cgrid); } else // Guess not. return; } const int spread_rate = _actual_spread_rate(cl_type, _spread_rate); // Too many clouds. if (env.cloud_no >= MAX_CLOUDS) { // Default to random in case there's no low quality clouds. int cl_del = random2(MAX_CLOUDS); for (int ci = 0; ci < MAX_CLOUDS; ci++) if (_is_weak_cloud(ci)) { cl_del = ci; break; } delete_cloud(cl_del); cl_new = cl_del; } // Create new cloud. if (cl_new != -1) { _new_cloud(cl_new, cl_type, ctarget, cl_range * 10, whose, killer, source, spread_rate, colour, name, tile, excl_rad); } else { // Find slot for cloud. for (int ci = 0; ci < MAX_CLOUDS; ci++) { if (env.cloud[ci].type == CLOUD_NONE) // ie is empty { _new_cloud(ci, cl_type, ctarget, cl_range * 10, whose, killer, source, spread_rate, colour, name, tile, excl_rad); break; } } } } static bool _is_opaque_cloud(cloud_type ctype) { return ctype >= CLOUD_OPAQUE_FIRST && ctype <= CLOUD_OPAQUE_LAST; } bool is_opaque_cloud(int cloud_idx) { if (cloud_idx == EMPTY_CLOUD) return false; return _is_opaque_cloud(env.cloud[cloud_idx].type); } cloud_type cloud_type_at(const coord_def &c) { const int cloudno = env.cgrid(c); return cloudno == EMPTY_CLOUD ? CLOUD_NONE : env.cloud[cloudno].type; } cloud_type random_smoke_type() { // including black to keep variety switch (random2(4)) { case 0: return CLOUD_GREY_SMOKE; case 1: return CLOUD_BLUE_SMOKE; case 2: return CLOUD_BLACK_SMOKE; case 3: return CLOUD_PURPLE_SMOKE; } return CLOUD_DEBUGGING; } int max_cloud_damage(cloud_type cl_type, int power) { cloud_struct cloud; cloud.type = cl_type; cloud.decay = power * 10; return _actor_cloud_damage(&you, cloud, true); } // Returns true if the cloud type has negative side effects beyond // plain damage and inventory destruction effects. static bool _cloud_has_negative_side_effects(cloud_type cloud) { switch (cloud) { case CLOUD_MEPHITIC: case CLOUD_MIASMA: case CLOUD_MUTAGENIC: case CLOUD_CHAOS: case CLOUD_PETRIFY: case CLOUD_ACID: case CLOUD_NEGATIVE_ENERGY: return true; default: return false; } } static int _cloud_damage_calc(int size, int n_average, int extra, bool maximum_damage) { return maximum_damage? extra + size - 1 : random2avg(size, n_average) + extra; } // Calculates the base damage that the cloud does to an actor without // considering resistances and time spent in the cloud. static int _cloud_base_damage(const actor *act, const cloud_struct &cloud, bool maximum_damage) { switch (cloud.type) { case CLOUD_RAIN: // Only applies to fiery actors: see _actor_cloud_resist. return _cloud_damage_calc(9, 1, 0, maximum_damage); case CLOUD_FIRE: case CLOUD_FOREST_FIRE: case CLOUD_COLD: case CLOUD_HOLY_FLAMES: case CLOUD_ACID: case CLOUD_NEGATIVE_ENERGY: // Yes, we really hate players, damn their guts. // // XXX: Some superior way of linking cloud damage to cloud // power would be nice, so we can dispense with these hacky // special cases. if (act->is_player()) return _cloud_damage_calc(23, 3, 10, maximum_damage); else return _cloud_damage_calc(16, 3, 6, maximum_damage); case CLOUD_STORM: // Four times the damage, because it's a quarter as often. if (act->is_player()) return _cloud_damage_calc(92, 3, 40, maximum_damage); else return _cloud_damage_calc(64, 3, 24, maximum_damage); case CLOUD_MEPHITIC: return _cloud_damage_calc(3, 1, 0, maximum_damage); case CLOUD_POISON: return _cloud_damage_calc(10, 1, 0, maximum_damage); case CLOUD_MIASMA: return _cloud_damage_calc(12, 3, 0, maximum_damage); case CLOUD_STEAM: return _cloud_damage_calc(_steam_cloud_damage(cloud.decay), 2, 0, maximum_damage); case CLOUD_GHOSTLY_FLAME: return _cloud_damage_calc(15, 3, 4, maximum_damage); default: return 0; } } // Returns true if the actor is immune to cloud damage, inventory item // destruction, and all other cloud-type-specific side effects (i.e. // apart from cloud interaction with invisibility). // // Note that actor_cloud_immune may be false even if the actor will // not be harmed by the cloud. The cloud may have positive // side-effects on the actor. bool actor_cloud_immune(const actor *act, const cloud_struct &cloud) { if (is_harmless_cloud(cloud.type)) return true; const bool player = act->is_player(); if (!player && you_worship(GOD_FEDHAS) && fedhas_protects(act->as_monster()) && (cloud.whose == KC_YOU || cloud.whose == KC_FRIENDLY) && (act->as_monster()->friendly() || act->as_monster()->neutral())) { return true; } // Qazlalites get immunity to their own clouds. if (player && cloud.whose == KC_YOU && you_worship(GOD_QAZLAL) && !player_under_penance()) { return true; } #if TAG_MAJOR_VERSION == 34 if (player && you.species == SP_DJINNI && (cloud.type == CLOUD_FIRE || cloud.type == CLOUD_FOREST_FIRE || cloud.type == CLOUD_HOLY_FLAMES)) { return true; } #endif switch (cloud.type) { case CLOUD_FIRE: case CLOUD_FOREST_FIRE: return act->is_fiery() || player && (you.duration[DUR_FIRE_SHIELD] || you.mutation[MUT_FLAME_CLOUD_IMMUNITY]); case CLOUD_HOLY_FLAMES: return act->res_holy_energy(cloud.agent()) > 0; case CLOUD_COLD: return act->is_icy() || (player && you.mutation[MUT_FREEZING_CLOUD_IMMUNITY]); case CLOUD_MEPHITIC: return act->res_poison() > 0 || act->is_unbreathing(); case CLOUD_POISON: return act->res_poison() > 0; case CLOUD_STEAM: // Players get steam cloud immunity from any res steam, which is hardly // fair, but this is what the old code did. return player && act->res_steam() > 0; case CLOUD_MIASMA: return act->res_rotting() > 0; case CLOUD_PETRIFY: return act->res_petrify(); case CLOUD_GHOSTLY_FLAME: return act->holiness() == MH_UNDEAD // Don't let these guys kill themselves. || act->type == MONS_GHOST_CRAB; case CLOUD_ACID: return act->res_acid() > 0; case CLOUD_STORM: return act->res_elec() >= 3; case CLOUD_NEGATIVE_ENERGY: return act->res_negative_energy() > 0; default: return false; } } // Returns a numeric resistance value for the actor's resistance to // the cloud's effects. If the actor is immune to the cloud's damage, // returns MAG_IMMUNE. static int _actor_cloud_resist(const actor *act, const cloud_struct &cloud) { if (actor_cloud_immune(act, cloud)) return MAG_IMMUNE; switch (cloud.type) { case CLOUD_RAIN: return act->is_fiery()? 0 : MAG_IMMUNE; case CLOUD_FIRE: case CLOUD_FOREST_FIRE: return act->res_fire(); case CLOUD_STEAM: return act->res_steam(); case CLOUD_HOLY_FLAMES: return act->res_holy_energy(cloud.agent()); case CLOUD_COLD: return act->res_cold(); case CLOUD_PETRIFY: return act->res_petrify(); case CLOUD_ACID: return act->res_acid(); case CLOUD_STORM: return act->res_elec(); case CLOUD_NEGATIVE_ENERGY: return act->res_negative_energy(); default: return 0; } } static bool _mephitic_cloud_roll(const monster* mons) { const int meph_hd_cap = 21; return mons->get_hit_dice() >= meph_hd_cap? one_chance_in(50) : !x_chance_in_y(mons->get_hit_dice(), meph_hd_cap); } // Applies cloud messages and side-effects and returns true if the // cloud had a side-effect. This function does not check for cloud immunity. static bool _actor_apply_cloud_side_effects(actor *act, const cloud_struct &cloud, int final_damage) { const bool player = act->is_player(); monster *mons = !player? act->as_monster() : NULL; switch (cloud.type) { case CLOUD_RAIN: case CLOUD_STORM: if (act->is_fiery() && final_damage > 0) { if (you.can_see(act)) { mprf("%s %s in the rain.", act->name(DESC_THE).c_str(), act->conj_verb(silenced(act->pos())? "steam" : "sizzle").c_str()); } } if (player) { if (you.duration[DUR_FIRE_SHIELD] > 1) { you.duration[DUR_FIRE_SHIELD] = 1; return true; } } break; case CLOUD_MEPHITIC: { if (player) { if (1 + random2(27) >= you.experience_level) { mpr("You choke on the stench!"); // effectively one or two turns, since it will be // decremented right away confuse_player(coinflip() ? 3 : 2); return true; } } else { bolt beam; beam.flavour = BEAM_CONFUSION; beam.thrower = cloud.killer; if (cloud.whose == KC_FRIENDLY) beam.beam_source = ANON_FRIENDLY_MONSTER; if (mons_class_is_confusable(mons->type) && _mephitic_cloud_roll(mons)) { beam.apply_enchantment_to_monster(mons); return true; } } break; } case CLOUD_PETRIFY: { if (player) { if (random2(55) - 13 >= you.experience_level) { you.petrify(act); return true; } } else { bolt beam; beam.flavour = BEAM_PETRIFY; beam.thrower = cloud.killer; if (cloud.whose == KC_FRIENDLY) beam.beam_source = ANON_FRIENDLY_MONSTER; beam.apply_enchantment_to_monster(mons); return true; } break; } case CLOUD_POISON: if (player) { const actor* agent = cloud.agent(); poison_player(5 + roll_dice(3, 8), agent ? agent->name(DESC_A) : "", cloud.cloud_name()); } else poison_monster(mons, cloud.agent()); return true; case CLOUD_MIASMA: if (player) { const actor* agent = cloud.agent(); if (agent) miasma_player(agent->name(DESC_A), cloud.cloud_name()); else miasma_player(cloud.cloud_name()); } else miasma_monster(mons, cloud.agent()); break; case CLOUD_MUTAGENIC: if (player) { mpr("The mutagenic energy flows into you."); // It's possible that you got trampled into the mutagenic cloud // and it's not your fault... so we'll say it's not intentional. // (it's quite bad in any case, so players won't scum, probably.) contaminate_player(1300 + random2(1250), false); // min 2 turns to yellow, max 4 return true; } else if (coinflip() && mons->malmutate("mutagenic cloud")) { if (you_worship(GOD_ZIN) && cloud.whose == KC_YOU) did_god_conduct(DID_DELIBERATE_MUTATING, 5 + random2(3)); return true; } return false; case CLOUD_CHAOS: if (coinflip()) { // TODO: Not have this in melee_attack melee_attack::chaos_affect_actor(act); return true; } break; case CLOUD_ACID: { const actor* agent = cloud.agent(); if (player) splash_with_acid(5, agent ? agent->mindex() : NON_MONSTER, true); else mons->splash_with_acid(agent); return true; } case CLOUD_NEGATIVE_ENERGY: { actor* agent = cloud.agent(); if (act->drain_exp(agent)) { if (cloud.whose == KC_YOU) did_god_conduct(DID_NECROMANCY, 5 + random2(3)); return true; } break; } default: break; } return false; } static int _actor_cloud_base_damage(actor *act, const cloud_struct &cloud, int resist, bool maximum_damage) { if (actor_cloud_immune(act, cloud)) return 0; const int cloud_raw_base_damage = _cloud_base_damage(act, cloud, maximum_damage); const int cloud_base_damage = (resist == MAG_IMMUNE ? 0 : cloud_raw_base_damage); return cloud_base_damage; } static int _cloud_damage_output(actor *actor, beam_type flavour, int resist, int base_damage, bool maximum_damage = false) { const int resist_adjusted_damage = resist_adjust_damage(actor, flavour, resist, base_damage, true); if (maximum_damage) return resist_adjusted_damage; return max(0, resist_adjusted_damage - random2(actor->armour_class())); } static int _actor_cloud_damage(actor *act, const cloud_struct &cloud, bool maximum_damage) { const int resist = _actor_cloud_resist(act, cloud); const int cloud_base_damage = _actor_cloud_base_damage(act, cloud, resist, maximum_damage); int final_damage = cloud_base_damage; switch (cloud.type) { case CLOUD_FIRE: case CLOUD_FOREST_FIRE: case CLOUD_HOLY_FLAMES: case CLOUD_COLD: case CLOUD_STEAM: case CLOUD_GHOSTLY_FLAME: case CLOUD_ACID: case CLOUD_NEGATIVE_ENERGY: final_damage = _cloud_damage_output(act, _cloud2beam(cloud.type), resist, cloud_base_damage, maximum_damage); break; case CLOUD_STORM: { if (maximum_damage || you.turn_is_over && you.time_taken > 0) { if (!maximum_damage) cloud.announce_actor_engulfed(act); if (maximum_damage || x_chance_in_y(you.time_taken, 40)) { if (!maximum_damage) { if (act->is_player()) mpr("You are struck by lightning!"); else if (you.can_see(act)) { simple_monster_message(act->as_monster(), " is struck by lightning."); } else if (you.see_cell(act->pos())) { mpr("Lightning from the thunderstorm strikes something" " you cannot see."); } noisy(25, act->pos(), act->is_player() || you.see_cell(act->pos()) || you_worship(GOD_QAZLAL) ? NULL : "You hear a clap of thunder!"); return _cloud_damage_output(act, _cloud2beam(cloud.type), resist, cloud_base_damage, maximum_damage); } else { // Average maximum damage over time. int dam = _cloud_damage_output(act, _cloud2beam(cloud.type), resist, cloud_base_damage, maximum_damage) / 4; if (dam > 0) return dam; } } } cloud_struct dupl = cloud; dupl.type = CLOUD_RAIN; return _actor_cloud_damage(act, dupl, maximum_damage); } default: break; } return timescale_damage(act, final_damage); } // Applies damage and side effects for an actor in a cloud and returns // the damage dealt. int actor_apply_cloud(actor *act) { const int cl = env.cgrid(act->pos()); if (cl == EMPTY_CLOUD) return 0; const cloud_struct &cloud(env.cloud[cl]); const bool player = act->is_player(); monster *mons = !player? act->as_monster() : NULL; const beam_type cloud_flavour = _cloud2beam(cloud.type); if (actor_cloud_immune(act, cloud)) return 0; const int resist = _actor_cloud_resist(act, cloud); const int cloud_max_base_damage = _actor_cloud_base_damage(act, cloud, resist, true); const int final_damage = _actor_cloud_damage(act, cloud, false); if ((player || final_damage > 0 || _cloud_has_negative_side_effects(cloud.type)) && cloud.type != CLOUD_STORM) // handled elsewhere { cloud.announce_actor_engulfed(act); } if (player && cloud_max_base_damage > 0 && resist > 0 && (cloud.type != CLOUD_STORM || final_damage > 0)) { canned_msg(MSG_YOU_RESIST); } if (cloud_flavour != BEAM_NONE) act->expose_to_element(cloud_flavour, 7); const bool side_effects = _actor_apply_cloud_side_effects(act, cloud, final_damage); if (!player && (side_effects || final_damage > 0)) behaviour_event(mons, ME_DISTURB, 0, act->pos()); if (final_damage) { #ifdef DEBUG_DIAGNOSTICS mprf(MSGCH_DIAGNOSTICS, "%s %s %d damage from cloud: %s.", act->name(DESC_THE).c_str(), act->conj_verb("take").c_str(), final_damage, cloud.cloud_name().c_str()); #endif actor *oppressor = cloud.agent(); if (player) { ouch(final_damage, oppressor? oppressor->mindex() : NON_MONSTER, KILLED_BY_CLOUD, cloud.cloud_name("", true).c_str()); } else mons->hurt(oppressor, final_damage, BEAM_MISSILE); } return final_damage; } static bool _cloud_is_harmful(actor *act, cloud_struct &cloud, int maximum_negligible_damage) { return !actor_cloud_immune(act, cloud) && (_cloud_has_negative_side_effects(cloud.type) || (_actor_cloud_damage(act, cloud, true) > maximum_negligible_damage)); } bool is_damaging_cloud(cloud_type type, bool accept_temp_resistances) { // A nasty hack; map_knowledge doesn't preserve whom the cloud belongs to. if (type == CLOUD_TORNADO) return !you.duration[DUR_TORNADO] && !you.duration[DUR_TORNADO_COOLDOWN]; if (accept_temp_resistances) { cloud_struct cloud; cloud.type = type; cloud.decay = 100; return _cloud_is_harmful(&you, cloud, 0); } else { // [ds] Yes, this is an ugly kludge: temporarily hide // durations and transforms. unwind_var old_durations(you.duration); unwind_var old_form(you.form, TRAN_NONE); you.duration.init(0); return is_damaging_cloud(type, true); } } static bool _mons_avoids_cloud(const monster* mons, const cloud_struct& cloud, bool placement) { bool extra_careful = placement; cloud_type cl_type = cloud.type; if (placement) extra_careful = true; // Berserk monsters are less careful and will blindly plow through any // dangerous cloud, just to kill you. {due} if (!extra_careful && mons->berserk_or_insane()) return false; if (you_worship(GOD_FEDHAS) && fedhas_protects(mons) && (cloud.whose == KC_YOU || cloud.whose == KC_FRIENDLY) && (mons->friendly() || mons->neutral())) { return false; } switch (cl_type) { case CLOUD_MIASMA: // Even the dumbest monsters will avoid miasma if they can. return !mons->res_rotting(); case CLOUD_FIRE: case CLOUD_FOREST_FIRE: if (mons->res_fire() > 1) return false; if (extra_careful) return true; if (mons_intel(mons) >= I_ANIMAL && mons->res_fire() < 0) return true; if (mons->hit_points >= 15 + random2avg(46, 5)) return false; break; case CLOUD_MEPHITIC: if (mons->res_poison() > 0) return false; if (extra_careful) return true; if (mons_intel(mons) >= I_ANIMAL && mons->res_poison() < 0) return true; if (x_chance_in_y(mons->get_hit_dice() - 1, 5)) return false; if (mons->hit_points >= random2avg(19, 2)) return false; break; case CLOUD_COLD: if (mons->res_cold() > 1) return false; if (extra_careful) return true; if (mons_intel(mons) >= I_ANIMAL && mons->res_cold() < 0) return true; if (mons->hit_points >= 15 + random2avg(46, 5)) return false; break; case CLOUD_POISON: if (mons->res_poison() > 0) return false; if (extra_careful) return true; if (mons_intel(mons) >= I_ANIMAL && mons->res_poison() < 0) return true; if (mons->hit_points >= random2avg(37, 4)) return false; break; case CLOUD_RAIN: // Fiery monsters dislike the rain. if (mons->is_fiery() && extra_careful) return true; // We don't care about what's underneath the rain cloud if we can fly. if (mons->flight_mode()) return false; // These don't care about deep water. if (monster_habitable_grid(mons, DNGN_DEEP_WATER)) return false; // This position could become deep water, and they might drown. if (grd(cloud.pos) == DNGN_SHALLOW_WATER) return true; break; case CLOUD_TORNADO: // Ball lightnings are not afraid of a _storm_, duh. Or elementals. if (mons->res_wind()) return false; // Locust swarms are too stupid to avoid winds. if (mons_intel(mons) >= I_ANIMAL) return true; break; case CLOUD_PETRIFY: if (mons->res_petrify()) return false; if (extra_careful) return true; if (mons_intel(mons) >= I_ANIMAL) return true; break; case CLOUD_GHOSTLY_FLAME: if (mons->res_negative_energy() > 2) return false; if (extra_careful) return true; if (mons->hit_points >= random2avg(25, 3)) return false; break; default: break; } // Exceedingly dumb creatures will wander into harmful clouds. if (is_harmless_cloud(cl_type) || mons_intel(mons) == I_PLANT && !extra_careful) { return false; } // If we get here, the cloud is potentially harmful. return true; } // Like the above, but allow a monster to move from one damaging cloud // to another, even if they're of different types. bool mons_avoids_cloud(const monster* mons, int cloud_num, bool placement) { if (cloud_num == EMPTY_CLOUD) return false; const cloud_struct &cloud = env.cloud[cloud_num]; // Is the target cloud okay? if (!_mons_avoids_cloud(mons, cloud, placement)) return false; // If we're already in a cloud that we'd want to avoid then moving // from one to the other is okay. if (!in_bounds(mons->pos()) || mons->pos() == cloud.pos) return true; const int our_cloud_num = env.cgrid(mons->pos()); if (our_cloud_num == EMPTY_CLOUD) return true; const cloud_struct &our_cloud = env.cloud[our_cloud_num]; return !_mons_avoids_cloud(mons, our_cloud, true); } // Is the cloud purely cosmetic with no gameplay effect? If so, // is engulfed in messages will be suppressed. static bool _cloud_is_cosmetic(cloud_type type) { switch (type) { case CLOUD_BLACK_SMOKE: case CLOUD_GREY_SMOKE: case CLOUD_BLUE_SMOKE: case CLOUD_PURPLE_SMOKE: case CLOUD_MIST: return true; default: return false; } } bool is_harmless_cloud(cloud_type type) { switch (type) { case CLOUD_NONE: case CLOUD_TLOC_ENERGY: case CLOUD_MAGIC_TRAIL: case CLOUD_DUST_TRAIL: #if TAG_MAJOR_VERSION == 34 case CLOUD_GLOOM: #endif case CLOUD_INK: case CLOUD_DEBUGGING: return true; default: return _cloud_is_cosmetic(type); } } bool in_what_cloud(cloud_type type) { int cl = env.cgrid(you.pos()); if (env.cgrid(you.pos()) == EMPTY_CLOUD) return false; if (env.cloud[cl].type == type) return true; return false; } string cloud_name_at_index(int cloudno) { if (!env.cloud[cloudno].name.empty()) return env.cloud[cloudno].name; else return cloud_type_name(env.cloud[cloudno].type); } // [ds] XXX: Some of these aren't so terse and some of the verbose // names aren't very verbose. Be warned that names in // _terse_cloud_names may be referenced by fog machines. static const char *_terse_cloud_names[] = { "?", "flame", "noxious fumes", "freezing vapour", "poison gas", "black smoke", "grey smoke", "blue smoke", "purple smoke", "translocational energy", "fire", "steam", #if TAG_MAJOR_VERSION == 34 "gloom", #endif "ink", "calcifying dust", "blessed fire", "foul pestilence", "thin mist", "seething chaos", "rain", "mutagenic fog", "magical condensation", "raging winds", "sparse dust", "ghostly flame", "acidic fog", "thunder", "negative energy" }; static const char *_verbose_cloud_names[] = { "?", "roaring flames", "noxious fumes", "freezing vapours", "poison gas", "black smoke", "grey smoke", "blue smoke", "purple smoke", "translocational energy", "roaring flames", "a cloud of scalding steam", #if TAG_MAJOR_VERSION == 34 "thick gloom", #endif "ink", "calcifying dust", "blessed fire", "dark miasma", "thin mist", "seething chaos", "the rain", "mutagenic fog", "magical condensation", "raging winds", "sparse dust", "ghostly flame", "acidic fog", "a thunderstorm", "negative energy" }; string cloud_type_name(cloud_type type, bool terse) { COMPILE_CHECK(ARRAYSZ(_terse_cloud_names) == NUM_CLOUD_TYPES); COMPILE_CHECK(ARRAYSZ(_verbose_cloud_names) == NUM_CLOUD_TYPES); return type <= CLOUD_NONE || type >= NUM_CLOUD_TYPES ? "buggy goodness" : (terse? _terse_cloud_names : _verbose_cloud_names)[type]; } //////////////////////////////////////////////////////////////////////// // cloud_struct kill_category cloud_struct::killer_to_whose(killer_type _killer) { switch (_killer) { case KILL_YOU: case KILL_YOU_MISSILE: case KILL_YOU_CONF: return KC_YOU; case KILL_MON: case KILL_MON_MISSILE: case KILL_MISC: return KC_OTHER; default: die("invalid killer type"); } return KC_OTHER; } killer_type cloud_struct::whose_to_killer(kill_category _whose) { switch (_whose) { case KC_YOU: return KILL_YOU_MISSILE; case KC_FRIENDLY: return KILL_MON_MISSILE; case KC_OTHER: return KILL_MISC; case KC_NCATEGORIES: die("invalid kill category"); } return KILL_NONE; } void cloud_struct::set_whose(kill_category _whose) { whose = _whose; killer = whose_to_killer(whose); } void cloud_struct::set_killer(killer_type _killer) { killer = _killer; whose = killer_to_whose(killer); switch (killer) { case KILL_YOU: killer = KILL_YOU_MISSILE; break; case KILL_MON: killer = KILL_MON_MISSILE; break; default: break; } } actor *cloud_struct::agent() const { return find_agent(source, whose); } string cloud_struct::cloud_name(const string &defname, bool terse) const { return !name.empty() ? name : !defname.empty() ? defname : cloud_type_name(type, terse); } void cloud_struct::announce_actor_engulfed(const actor *act, bool beneficial) const { if (_cloud_is_cosmetic(type)) return; if (you.can_see(act)) { // Special message for unmodified rain clouds: if ((type == CLOUD_RAIN || type == CLOUD_STORM) && cloud_name() == cloud_type_name(type, false)) { // Don't produce monster-in-rain messages in the interests // of spam reduction. if (act->is_player()) { mprf("%s %s standing in %s.", act->name(DESC_THE).c_str(), act->conj_verb("are").c_str(), type == CLOUD_STORM ? "a thunderstorm" : "the rain"); } } else { mprf("%s %s in %s.", act->name(DESC_THE).c_str(), beneficial ? act->conj_verb("bask").c_str() : (act->conj_verb("are") + " engulfed").c_str(), cloud_name().c_str()); } } } int get_cloud_colour(int cloudno) { int which_colour = LIGHTGREY; if (env.cloud[cloudno].colour != -1) return env.cloud[cloudno].colour; switch (env.cloud[cloudno].type) { case CLOUD_FIRE: case CLOUD_FOREST_FIRE: if (env.cloud[cloudno].decay <= 20) which_colour = RED; else if (env.cloud[cloudno].decay <= 40) which_colour = LIGHTRED; else if (one_chance_in(4)) which_colour = RED; else if (one_chance_in(4)) which_colour = LIGHTRED; else which_colour = YELLOW; break; case CLOUD_MEPHITIC: which_colour = GREEN; break; case CLOUD_COLD: if (env.cloud[cloudno].decay <= 20) which_colour = BLUE; else if (env.cloud[cloudno].decay <= 40) which_colour = LIGHTBLUE; else if (one_chance_in(4)) which_colour = BLUE; else if (one_chance_in(4)) which_colour = LIGHTBLUE; else which_colour = WHITE; break; case CLOUD_POISON: which_colour = LIGHTGREEN; break; case CLOUD_BLUE_SMOKE: which_colour = LIGHTBLUE; break; case CLOUD_PURPLE_SMOKE: case CLOUD_TLOC_ENERGY: #if TAG_MAJOR_VERSION == 34 case CLOUD_GLOOM: #endif which_colour = MAGENTA; break; case CLOUD_MIASMA: case CLOUD_BLACK_SMOKE: case CLOUD_INK: which_colour = DARKGREY; break; case CLOUD_RAIN: case CLOUD_MIST: which_colour = ETC_MIST; break; case CLOUD_CHAOS: which_colour = ETC_RANDOM; break; case CLOUD_MUTAGENIC: which_colour = ETC_MUTAGENIC; break; case CLOUD_MAGIC_TRAIL: which_colour = ETC_MAGIC; break; case CLOUD_DUST_TRAIL: which_colour = ETC_EARTH; break; case CLOUD_HOLY_FLAMES: which_colour = ETC_HOLY; break; case CLOUD_TORNADO: which_colour = ETC_TORNADO; break; case CLOUD_PETRIFY: which_colour = WHITE; break; case CLOUD_GHOSTLY_FLAME: which_colour = ETC_ELECTRICITY; break; case CLOUD_ACID: which_colour = YELLOW; break; case CLOUD_STORM: which_colour = ETC_DARK; break; case CLOUD_NEGATIVE_ENERGY: which_colour = ETC_DEATH; break; default: which_colour = LIGHTGREY; break; } return which_colour; } coord_def get_cloud_originator(const coord_def& pos) { int cl; if (!in_bounds(pos) || (cl = env.cgrid(pos)) == EMPTY_CLOUD) return coord_def(); const actor *agent = actor_by_mid(env.cloud[cl].source); if (!agent) return coord_def(); return agent->pos(); } void remove_tornado_clouds(mid_t whose) { // Needed to clean up after the end of tornado cooldown, so we can again // assume all "raging winds" clouds are harmful. This is needed only // because map_knowledge doesn't preserve the knowledge about whom the // cloud belongs to. If this changes, please remove this function. For // example, this approach doesn't work if we ever make Tornado a monster // spell (excluding immobile and mindless casters). for (int i = 0; i < MAX_CLOUDS; i++) if (env.cloud[i].type == CLOUD_TORNADO && env.cloud[i].source == whose) delete_cloud(i); } static void _spread_cloud(coord_def pos, cloud_type type, int radius, int pow, int &remaining, int ratio = 10, mid_t agent_mid = NON_ENTITY, kill_category kcat = KC_OTHER) { bolt beam; beam.target = pos; beam.use_target_as_pos = true; explosion_map exp_map; exp_map.init(INT_MAX); beam.determine_affected_cells(exp_map, coord_def(), 0, radius, true, true); coord_def centre(9,9); for (distance_iterator di(pos, true, false); di; ++di) { if (di.radius() > radius) return; if ((exp_map(*di - pos + centre) < INT_MAX) && env.cgrid(*di) == EMPTY_CLOUD && (di.radius() < radius || x_chance_in_y(ratio, 100))) { place_cloud(type, *di, pow + random2(pow), NULL); --remaining; // Setting this way since the agent of the cloud may be dead before // cloud is placed, so no agent exists to pass to place_cloud (though // proper blame should still be assigned) if (env.cgrid(*di) != EMPTY_CLOUD) { env.cloud[env.cgrid(*di)].source = agent_mid; env.cloud[env.cgrid(*di)].whose = kcat; } } // Placed all clouds for this spreader if (remaining == 0) return; } } void run_cloud_spreaders(int dur) { if (!dur) return; vector markers = env.markers.get_all(MAT_CLOUD_SPREADER); for (int i = 0, size = markers.size(); i < size; ++i) { map_cloud_spreader_marker *mark = dynamic_cast(markers[i]); mark->speed_increment += dur; int rad = min(mark->speed_increment / mark->speed, mark->max_rad - 1) + 1; int ratio = (mark->speed_increment - ((rad - 1) * mark->speed)) * 100 / mark->speed; if (ratio == 0) { rad--; ratio = 100; } _spread_cloud(mark->pos, mark->ctype, rad, mark->duration, mark->remaining, ratio, mark->agent_mid, mark->kcat); if ((rad >= mark->max_rad && ratio >= 100) || mark->remaining == 0) { env.markers.remove(mark); break; } } }