diff --git a/include/objects.h b/include/objects.h index 11de04b07..98ee04a7f 100644 --- a/include/objects.h +++ b/include/objects.h @@ -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 */ diff --git a/include/patchlevel.h b/include/patchlevel.h index 42ba716f6..0abd71576 100644 --- a/include/patchlevel.h +++ b/include/patchlevel.h @@ -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. diff --git a/src/spell.c b/src/spell.c index 940de8c35..e16367534 100644 --- a/src/spell.c +++ b/src/spell.c @@ -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); diff --git a/src/zap.c b/src/zap.c index 22c11e64d..b8d77bfff 100644 --- a/src/zap.c +++ b/src/zap.c @@ -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) diff --git a/win/share/objects.txt b/win/share/objects.txt index e8ab2335b..7ce7c8d80 100644 --- a/win/share/objects.txt +++ b/win/share/objects.txt @@ -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) { ................