chain lightning megathread
Posted: Saturday, 9th June 2018, 22:58
Here is the function for chain spells in spl-damage.cc
First of all, lol. Second of all, this damage function's weird properties are undesirable and could use revision. The power of each arc varies, getting lower by a randomized amount on each subsequent arc, and the spell stops when it runs out of power or randomly fails to arc. These properties make it very hard to understand the damage distribution (which makes the spell harder to adjust) and impossible to provide a meaningful value for the average damage per arc. Chain lightning would be cleaner if the spell calculated the number of arcs to attempt in advance and just gave every arc the same power. This will affect the damage distribution in ways that I don't really care enough about to try to understand, but has big advantages for clarity and simplicity and probably does not affect how the spell would be used. You would want to run some simulations to get good numbers for the formulas, of course.
I think it would also be desirable to make chain lightning not able to target firewood (fungi, plants, etc). It is rather annoying for your level 8 dual-school spell to suddenly become useless because there is a fungus vault onscreen, and it's neither interesting nor challenging to walk backwards away from plants in depths so that the spell does damage again.
Lastly, Nikola is a very dumb unique and chain lightning is a very dumb monster spell. Please do not respond to this with a list of tactics to use on Nikola; I have not died to Nikola ever and probably will not die to Nikola ever unless he decides to do a 200 damage chain lightning the turn I autoexplore into his line of sight.
- Code:
spret_type cast_chain_spell(spell_type spell_cast, int pow,
const actor *caster, bool fail)
{
fail_check();
bolt beam;
// initialise beam structure
switch (spell_cast)
{
case SPELL_CHAIN_LIGHTNING:
beam.name = "lightning arc";
beam.aux_source = "chain lightning";
beam.glyph = dchar_glyph(DCHAR_FIRED_ZAP);
beam.flavour = BEAM_ELECTRICITY;
break;
case SPELL_CHAIN_OF_CHAOS:
beam.name = "arc of chaos";
beam.aux_source = "chain of chaos";
beam.glyph = dchar_glyph(DCHAR_FIRED_ZAP);
beam.flavour = BEAM_CHAOS;
break;
default:
die("buggy chain spell %d cast", spell_cast);
break;
}
beam.source_id = caster->mid;
beam.thrower = caster->is_player() ? KILL_YOU_MISSILE : KILL_MON_MISSILE;
beam.range = 8;
beam.hit = AUTOMATIC_HIT;
beam.obvious_effect = true;
beam.pierce = false; // since we want to stop at our target
beam.is_explosion = false;
beam.is_tracer = false;
beam.origin_spell = spell_cast;
if (const monster* mons = caster->as_monster())
beam.source_name = mons->name(DESC_PLAIN, true);
bool first = true;
coord_def source, target;
for (source = caster->pos(); pow > 0;
pow -= 8 + random2(13), source = target)
{
// infinity as far as this spell is concerned
// (Range - 1) is used because the distance is randomised and
// may be shifted by one.
int min_dist = LOS_DEFAULT_RANGE - 1;
int dist;
int count = 0;
target.x = -1;
target.y = -1;
for (monster_iterator mi; mi; ++mi)
{
if (invalid_monster(*mi))
continue;
// Don't arc to things we cannot hit.
if (beam.ignores_monster(*mi))
continue;
dist = grid_distance(source, mi->pos());
// check for the source of this arc
if (!dist)
continue;
// randomise distance (arcs don't care about a couple of feet)
dist += (random2(3) - 1);
// always ignore targets further than current one
if (dist > min_dist)
continue;
if (!cell_see_cell(source, mi->pos(), LOS_SOLID)
|| !cell_see_cell(caster->pos(), mi->pos(), LOS_SOLID_SEE))
{
continue;
}
count++;
if (dist < min_dist)
{
// switch to looking for closer targets (but not always)
if (!one_chance_in(10))
{
min_dist = dist;
target = mi->pos();
count = 0;
}
}
else if (target.x == -1 || one_chance_in(count))
{
// either first target, or new selected target at
// min_dist == dist.
target = mi->pos();
}
}
// now check if the player is a target
dist = grid_distance(source, you.pos());
if (dist) // i.e., player was not the source
{
// distance randomised (as above)
dist += (random2(3) - 1);
// select player if only, closest, or randomly selected
if ((target.x == -1
|| dist < min_dist
|| (dist == min_dist && one_chance_in(count + 1)))
&& cell_see_cell(source, you.pos(), LOS_SOLID))
{
target = you.pos();
}
}
const bool see_source = you.see_cell(source);
const bool see_targ = you.see_cell(target);
if (target.x == -1)
{
if (see_source)
mprf("The %s grounds out.", beam.name.c_str());
break;
}
// Trying to limit message spamming here so we'll only mention
// the thunder at the start or when it's out of LoS.
switch (spell_cast)
{
case SPELL_CHAIN_LIGHTNING:
{
const char* msg = "You hear a mighty clap of thunder!";
noisy(spell_effect_noise(SPELL_CHAIN_LIGHTNING), source,
(first || !see_source) ? msg : nullptr);
break;
}
case SPELL_CHAIN_OF_CHAOS:
if (first && see_source)
mpr("A swirling arc of seething chaos appears!");
break;
default:
break;
}
first = false;
if (see_source && !see_targ)
mprf("The %s arcs out of your line of sight!", beam.name.c_str());
else if (!see_source && see_targ)
mprf("The %s suddenly appears!", beam.name.c_str());
beam.source = source;
beam.target = target;
switch (spell_cast)
{
case SPELL_CHAIN_LIGHTNING:
beam.colour = LIGHTBLUE;
beam.damage = caster->is_player()
? calc_dice(5, 10 + pow * 2 / 3)
: calc_dice(5, 46 + pow / 6);
break;
case SPELL_CHAIN_OF_CHAOS:
beam.colour = ETC_RANDOM;
beam.ench_power = pow;
beam.damage = calc_dice(3, 5 + pow / 2);
beam.real_flavour = BEAM_CHAOS;
beam.flavour = BEAM_CHAOS;
default:
break;
}
// Be kinder to the caster.
if (target == caster->pos())
{
if (!(beam.damage.num /= 2))
beam.damage.num = 1;
if ((beam.damage.size /= 2) < 3)
beam.damage.size = 3;
}
beam.fire();
}
return SPRET_SUCCESS;
}
First of all, lol. Second of all, this damage function's weird properties are undesirable and could use revision. The power of each arc varies, getting lower by a randomized amount on each subsequent arc, and the spell stops when it runs out of power or randomly fails to arc. These properties make it very hard to understand the damage distribution (which makes the spell harder to adjust) and impossible to provide a meaningful value for the average damage per arc. Chain lightning would be cleaner if the spell calculated the number of arcs to attempt in advance and just gave every arc the same power. This will affect the damage distribution in ways that I don't really care enough about to try to understand, but has big advantages for clarity and simplicity and probably does not affect how the spell would be used. You would want to run some simulations to get good numbers for the formulas, of course.
I think it would also be desirable to make chain lightning not able to target firewood (fungi, plants, etc). It is rather annoying for your level 8 dual-school spell to suddenly become useless because there is a fungus vault onscreen, and it's neither interesting nor challenging to walk backwards away from plants in depths so that the spell does damage again.
Lastly, Nikola is a very dumb unique and chain lightning is a very dumb monster spell. Please do not respond to this with a list of tactics to use on Nikola; I have not died to Nikola ever and probably will not die to Nikola ever unless he decides to do a 200 damage chain lightning the turn I autoexplore into his line of sight.