Implement the spellbook of chain lightning

Prior to this commit, there was no good way to deal with swarms of
weak, good-AC enemies using magic; trying to play a wizard as a
pure spellcaster would make bees and ants very difficult to deal
with (because they would usually dodge force bolts).

This commit adds a new spell designed to be very good against
swarms of weak enemies: "chain lightning", which does 2d6 lightning
damage to every monster around you. It has an initially short range,
but can chain from monster to monster to cover potentially large
distances (as long as none of the monsters en route are shock
resistant or tame/peaceful; the spell can't chain past shock
resistant monsters and avoids peacefuls). Monsters within one
space of the visible lightning bolts are affected. Unlike other
lightning effects, this one does only 2d6 damage, not enough to
blind affected monsters.

This commit breaks existing save and bones files (thus the
EDITLEVEL increase).
This commit is contained in:
Alex Smith
2023-12-06 19:57:44 +00:00
parent 45856e3b5b
commit 0d508cc936
5 changed files with 208 additions and 7 deletions

View File

@@ -1287,10 +1287,10 @@ SPELL("healing", "white",
P_HEALING_SPELL, 40, 2, 1, 1, IMMEDIATE, CLR_WHITE,
SPE_HEALING),
SPELL("knock", "pink",
P_MATTER_SPELL, 35, 1, 1, 1, IMMEDIATE, CLR_BRIGHT_MAGENTA,
P_MATTER_SPELL, 25, 1, 1, 1, IMMEDIATE, CLR_BRIGHT_MAGENTA,
SPE_KNOCK),
SPELL("force bolt", "red",
P_ATTACK_SPELL, 35, 2, 1, 1, IMMEDIATE, CLR_RED,
P_ATTACK_SPELL, 30, 2, 1, 1, IMMEDIATE, CLR_RED,
SPE_FORCE_BOLT),
SPELL("confuse monster", "orange",
P_ENCHANTMENT_SPELL, 49, 2, 1, 1, IMMEDIATE, CLR_ORANGE,
@@ -1305,7 +1305,7 @@ SPELL("slow monster", "light green",
P_ENCHANTMENT_SPELL, 30, 2, 2, 1, IMMEDIATE, CLR_BRIGHT_GREEN,
SPE_SLOW_MONSTER),
SPELL("wizard lock", "dark green",
P_MATTER_SPELL, 30, 3, 2, 1, IMMEDIATE, CLR_GREEN,
P_MATTER_SPELL, 25, 3, 2, 1, IMMEDIATE, CLR_GREEN,
SPE_WIZARD_LOCK),
SPELL("create monster", "turquoise",
P_CLERIC_SPELL, 35, 3, 2, 1, NODIR, CLR_BRIGHT_CYAN,
@@ -1341,7 +1341,7 @@ SPELL("restore ability", "light brown",
P_HEALING_SPELL, 25, 5, 4, 1, NODIR, CLR_BROWN,
SPE_RESTORE_ABILITY),
SPELL("invisibility", "dark brown",
P_ESCAPE_SPELL, 25, 5, 4, 1, NODIR, CLR_BROWN,
P_ESCAPE_SPELL, 20, 5, 4, 1, NODIR, CLR_BROWN,
SPE_INVISIBILITY),
SPELL("detect treasure", "gray",
P_DIVINATION_SPELL, 20, 5, 4, 1, NODIR, CLR_GRAY,
@@ -1379,6 +1379,10 @@ SPELL("jumping", "thin",
SPELL("stone to flesh", "thick",
P_HEALING_SPELL, 15, 1, 3, 1, IMMEDIATE, HI_PAPER,
SPE_STONE_TO_FLESH),
SPELL("chain lightning", "checkered",
P_ATTACK_SPELL, 25, 4, 2, 1, NODIR, CLR_GRAY,
SPE_CHAIN_LIGHTNING),
#if 0 /* DEFERRED */
/* from slash'em, create a tame critter which explodes when attacking,
damaging adjacent creatures--friend or foe--and dying in the process */

View File

@@ -17,7 +17,7 @@
* Incrementing EDITLEVEL can be used to force invalidation of old bones
* and save files.
*/
#define EDITLEVEL 91
#define EDITLEVEL 92
/*
* Development status possibilities.

View File

@@ -41,6 +41,7 @@ static int percent_success(int);
static char *spellretention(int, char *);
static int throwspell(void);
static void cast_protection(void);
static void cast_chain_lightning(void);
static void spell_backfire(int);
static boolean spelleffects_check(int, int *, int *);
static const char *spelltypemnemonic(int);
@@ -865,6 +866,178 @@ skill_based_spellbook_id(void)
}
}
/* Limit the total area chain lightning can cover; this is both for
technical reasons (making it possible to limit the size of arrays
here and in the display code) and for gameplay balance reasons;
this value should be smaller than TMP_AT_MAX_GLYPHS (display.c) in
order for chain lightning to display properly */
#define CHAIN_LIGHTNING_LIMIT 100
/* Unlike most zaps, chain lightning can't hit solid terrain (it
doesn't have enough power), it only covers open space; this also
means that it can't hit monsters inside walls, which makes sense as
they would be earthed */
#define CHAIN_LIGHTNING_TYP(typ) (IS_POOL(typ) || SPACE_POS(typ))
#define CHAIN_LIGHTNING_POS(x, y) \
(isok(x, y) && (CHAIN_LIGHTNING_TYP(levl[x][y].typ) || \
(IS_DOOR(levl[x][y].typ) && \
!(levl[x][y].doormask & (D_CLOSED | D_LOCKED)))))
struct chain_lightning_zap {
/* direction in which this zap is currently moving; this is an
enum movementdirs, clamped to the range 0 inclusive to N_DIRS
exclusive */
uchar dir;
/* current location of the zap */
coordxy x, y;
/* distance this zap can cover without chaining */
char strength;
};
struct chain_lightning_queue {
struct chain_lightning_zap q[CHAIN_LIGHTNING_LIMIT];
int head;
int tail;
int displayed_beam;
};
/* Given a potential chain lightning zap, moves it one square forward in
the given direction, then adds it to the queue unless it would hit an
invalid square or is out of power.
zap is passed by value, so the move-forward doesn't change the passed
argument. */
static void
propagate_chain_lightning(
struct chain_lightning_queue *clq,
struct chain_lightning_zap zap)
{
struct monst *mon;
zap.x += xdir[zap.dir];
zap.y += ydir[zap.dir];
if (clq->tail >= CHAIN_LIGHTNING_LIMIT)
return; /* zap has covered too many squares */
if (!CHAIN_LIGHTNING_POS(zap.x, zap.y))
return; /* zap can't go to this square */
mon = m_at(zap.x, zap.y);
if (mon && mon->mpeaceful)
return; /* chain lightning avoids peaceful and tame monsters */
/* When hitting a monster that isn't electricity-resistant, a
particular chain lightning zap regains all its power, allowing it to
chain to other monsters; upon hitting a shock-resistant monster it
can't continue any further, but we let it hit the monster to show
the shield effect */
if (mon && !resists_elec(mon) && !defended(mon, AD_ELEC))
zap.strength = 3;
else if (mon)
zap.strength = 0;
/* Unless it hits a monster, the last square of a zap isn't drawn on
screen and can't propagate further, so it may as well be discarded
now */
if (!mon && !zap.strength)
return;
/* The same square can't be chained to twice. */
for (int i = 0; i < clq->tail; i++) {
if (clq->q[i].x == zap.x && clq->q[i].y == zap.y)
return;
}
/* This array access must be inbounds due to the CHAIN_LIGHTNING_LIMIT
check earlier. */
clq->q[clq->tail++] = zap;
/* Draw it. */
tmp_at(DISP_CHANGE, zapdir_to_glyph(
xdir[zap.dir], ydir[zap.dir], clq->displayed_beam));
tmp_at(zap.x, zap.y);
}
static void
cast_chain_lightning(void)
{
struct chain_lightning_queue clq = {
{{0}}, 0, 0, Hallucination ? rn2_on_display_rng(6) : (AD_ELEC - 1)
};
if (u.uswallow) {
// TODO: damage the engulfer
return;
}
/* set the type of beam we're using; the direction here is arbitrary
because we change the beam direction just before drawing the beam
anyway */
tmp_at(DISP_BEAM, zapdir_to_glyph(0, 1, clq.displayed_beam));
/* start by propagating in all directions from the caster */
for (int dir = 0; dir < N_DIRS; dir++) {
struct chain_lightning_zap zap = { dir, u.ux, u.uy, 2 };
propagate_chain_lightning(&clq, zap);
}
nh_delay_output();
while (clq.head < clq.tail) {
int delay_tail = clq.tail;
while (clq.head < delay_tail) {
struct chain_lightning_zap zap = clq.q[clq.head++];
/* damage any monster that was hit */
struct monst *mon = m_at(zap.x, zap.y);
if (mon) {
struct obj *unused; /* AD_ELEC can't destroy armor */
int dmg = zhitm(
mon, BZ_U_SPELL(AD_ELEC - 1), 2, &unused);
if (dmg) {
/* mon has been damaged, but we haven't yet printed the
messages or given kill credit; assume the hero can
sense their spell hitting monsters, because they can
steer it away from peacefuls */
if (DEADMONSTER(mon))
xkilled(mon, XKILL_GIVEMSG);
else
pline("You shock %s%s", mon_nam(mon), exclam(dmg));
} else if (canseemon(mon)) {
pline("%s resists.", Monnam(mon));
}
}
/* each zap propagates forwards with 1 less strength, and
diagonally with 0 strength (thus the diagonal zaps aren't
drawn and don't spread unless they hit a monster);
exception: if the zap just hit a monster, the diagonals have
as much strength as the forwards zap */
if (!zap.strength)
continue; /* happens upon hitting a shock-resistant monster */
zap.strength--;
propagate_chain_lightning(&clq, zap);
if (zap.strength < 2)
zap.strength = 0;
else if (u.uen)
u.uen--; /* propagating past monsters increases Pw cost a bit */
zap.dir = DIR_LEFT(zap.dir);
propagate_chain_lightning(&clq, zap);
zap.dir = DIR_RIGHT2(zap.dir);
propagate_chain_lightning(&clq, zap);
}
nh_delay_output();
}
nh_delay_output();
nh_delay_output();
tmp_at(DISP_END, 0);
}
static void
cast_protection(void)
{
@@ -1336,6 +1509,9 @@ spelleffects(int spell_otyp, boolean atme, boolean force)
if (!(jump(max(role_skill, 1)) & ECMD_TIME))
pline1(nothing_happens);
break;
case SPE_CHAIN_LIGHTNING:
cast_chain_lightning();
break;
default:
impossible("Unknown spell %d attempted.", spell);
obfree(pseudo, (struct obj *) 0);

View File

@@ -4198,10 +4198,12 @@ zhitm(
/* can still blind the monster */
} else
tmp = d(nd, 6);
if (spellcaster)
if (spellcaster && tmp)
tmp = spell_damage_bonus(tmp);
if (!resists_blnd(mon)
&& !(type > 0 && engulfing_u(mon))) {
&& !(type > 0 && engulfing_u(mon))
&& nd > 2) {
/* sufficiently powerful lightning blinds monsters */
register unsigned rnd_tmp = rnd(50);
mon->mcansee = 0;
if ((mon->mblinded + rnd_tmp) > 127)

View File

@@ -7745,6 +7745,25 @@ Z = (195, 195, 195)
.......JJJAA....
................
}
# tile 405 (checkered / chain lightning)
{
................
................
................
....AAAA........
....AAAAAA......
...AAADDDANNP...
...AADDAADNNN...
..PNAADDNNNNOA..
..NNDNADDNNNOAA.
.PNNNDDDAAAONA..
.NNNNNNAAAAAAA..
.NOONNAAAAAOA...
..PNOOOAAAOA....
....PAAOOOA.....
.......AAA......
................
}
# tile 405 (plain / blank paper)
{
................