Skip to content

Commit 8b4e7d3

Browse files
committed
Learned teleport away now follows the wand of teleportation AI rules.
Monsters that learn teleport away from a spellbook cast it with none of the tactical rules muse.c applies to the wand of teleportation: they handed a free controlled teleport to players with teleport control, wasted casts on noteleport levels with no way to learn better, fired it as a random attack instead of using it to dislodge the hero from a protected spot, and never used it to escape. Review of the new code surfaced four pre-existing bugs in the same beam and kill-attribution paths, fixed here as well. * mcastu.c: add teleport away to target_resists_learned_spell(), the shared filter behind both the learned-spell meta selection and the spell picker. Against the hero the spell now requires the hero to be on a scary square or the branch stairs and is skipped outright under teleport control; against monster targets it skips sure no-ops (noteleport level, temple priest). A caster that has seen teleportation fail on the level (the same mtrapseen knowledge bit the wand checks) skips it entirely. * muse.c: - badly hurt spellcasters that know the spell now cast it to escape. New defensive action MUSE_SPE_TELEPORT_AWAY_SELF reuses the wand's mon_tele restriction and relocation code; Amulet carriers select MUSE_SPE_TELEPORT_AWAY and cast at the hero instead, since the Amulet blocks their own teleport. Shopkeepers in their shops, guards and priests never self-teleport, and casting charges the standard spell cooldown. - the defensive wand of teleportation zap (an Amulet carrier sending the hero away) fired in a stale direction: mbhit() aims via the tbx/tby globals and this path never set them. With the common 0,0 leftover the beam never advanced and teleported the Amulet carrier itself - exactly what the case exists to prevent. Aim at the hero's tracked position before the zap, restoring the pre-tbx/tby behavior. - mcast_immediate_spell() never set buzzer around its beam, unlike mcast_ray_spell() and the wand mbhit() sites, so a pet's learned force bolt, turn undead, drain life, dispel evil or cancellation kill through resist() earned no livelog credit. The polymorph system-shock death in mbhitm() likewise called mondied() with no attribution; both now credit the caster the way the adjacent drain life and dispel evil cases do. - the scroll-of-fire bystander kill (dead code upstream, enabled in EvilHack) called mondied() bare; set_pet_killer() first so a pet reading the scroll gets credit. * trap.c: the magic beam trap fires dobuzz() with a negative zap type, so its kills read the buzzer attribution global; triggered nested inside another monster's beam (knockback or teleport landing a victim on the trigger), it inherited the outer zapper for pet credit and the hero's death reason. Save, zero and restore buzzer around both MAGIC_BEAM_TRAP_SET dobuzz() calls, the same discipline exploder uses for chained explosions.
1 parent 7d5f112 commit 8b4e7d3

4 files changed

Lines changed: 96 additions & 2 deletions

File tree

‎doc/evilhack-changelog.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5547,4 +5547,5 @@ The following changes to date are:
55475547
- Fix: pets never re-equipped gear after uncursing their own items
55485548
- Farlook reports whether the selected spot is lit or dark, for all
55495549
things
5550+
- Learned teleport away now follows the wand of teleportation AI rules
55505551

‎src/mcastu.c‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2404,6 +2404,19 @@ short spell_otyp;
24042404
if (youdefend)
24052405
return (u.ualign.type != A_NONE);
24062406
return !is_evil(target);
2407+
case SPE_TELEPORT_AWAY:
2408+
/* mirror the wand of teleportation tactics from muse.c */
2409+
if (level.flags.noteleport
2410+
&& (caster->mtrapseen & (1 << (TELEP_TRAP_SET - 1))))
2411+
return TRUE; /* knows teleporting fails on this level */
2412+
if (youdefend)
2413+
return (Teleport_control
2414+
|| !(onscary(u.ux, u.uy, caster)
2415+
|| (u.ux == sstairs.sx && u.uy == sstairs.sy)));
2416+
/* monster target: filter sure-thing no-ops */
2417+
return (level.flags.noteleport
2418+
|| (target->ispriest
2419+
&& *in_rooms(target->mx, target->my, TEMPLE)));
24072420
default:
24082421
return FALSE;
24092422
}

‎src/muse.c‎

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ STATIC_DCL boolean FDECL(mcan_learn_spell, (struct monst *, struct obj *));
121121
#define MUSE_WAN_UNDEAD_TURNING 24 /* also an offensive item */
122122
#define MUSE_POT_RESTORE_ABILITY 25
123123
#define MUSE_POT_VAMPIRE_BLOOD 26
124+
#define MUSE_SPE_TELEPORT_AWAY_SELF 27 /* learned spell, no item */
125+
#define MUSE_SPE_TELEPORT_AWAY 28 /* learned spell, no item */
124126
/* MUSE_* constants for offensive items */
125127
#define MUSE_WAN_DEATH 1
126128
#define MUSE_WAN_SLEEP 2
@@ -811,6 +813,22 @@ struct monst *mtmp;
811813
|| t->ttyp == BEAR_TRAP))
812814
t = 0; /* ok for monster to dig here */
813815

816+
/* spellcasters that have learned teleport away can cast it to
817+
escape without needing a wand or scroll; same noteleport
818+
knowledge check as the wand, same minimum-level rule as
819+
mchoose_learned_spell() */
820+
if (!mtmp->mcan && !mtmp->mspec_used && !mtmp->mconf
821+
&& mknows_spell(mtmp, SPE_TELEPORT_AWAY)
822+
&& mtmp->m_lev >= objects[SPE_TELEPORT_AWAY].oc_level * 3
823+
&& (!level.flags.noteleport
824+
|| !(mtmp->mtrapseen & (1 << (TELEP_TRAP_SET - 1))))) {
825+
if (mon_has_amulet(mtmp))
826+
m.has_defense = MUSE_SPE_TELEPORT_AWAY;
827+
else if (!(mtmp->isshk && inhishop(mtmp)) && !mtmp->isgd
828+
&& !mtmp->ispriest)
829+
m.has_defense = MUSE_SPE_TELEPORT_AWAY_SELF;
830+
}
831+
814832
#define nomore(x) if (m.has_defense == x) continue;
815833
/* selection could be improved by collecting all possibilities
816834
into an array and then picking one at random */
@@ -1560,13 +1578,53 @@ struct monst *mtmp;
15601578
mzapwand(mtmp, otmp, FALSE);
15611579
m_using = TRUE;
15621580
buzzer = mtmp; /* for kill attribution (pet livelog credit) */
1563-
mbhit(mtmp, rn1(8, 6), mbhitm, bhito, otmp);
1581+
/* mbhit() aims via the tbx/tby globals; nothing on the
1582+
defensive path sets them, so aim at the hero's tracked
1583+
position (skip the beam if that is somehow the caster's
1584+
own spot) */
1585+
tbx = mtmp->mux - mtmp->mx;
1586+
tby = mtmp->muy - mtmp->my;
1587+
if (tbx || tby)
1588+
mbhit(mtmp, rn1(8, 6), mbhitm, bhito, otmp);
15641589
buzzer = 0;
15651590
/* monster learns that teleportation isn't useful here */
15661591
if (level.flags.noteleport)
15671592
mtmp->mtrapseen |= (1 << (TELEP_TRAP_SET - 1));
15681593
m_using = FALSE;
15691594
return (DEADMONSTER(mtmp)) ? 1 : 2;
1595+
case MUSE_SPE_TELEPORT_AWAY_SELF:
1596+
if ((mtmp->isshk && inhishop(mtmp)) || mtmp->isgd || mtmp->ispriest)
1597+
return 2;
1598+
m_flee(mtmp);
1599+
/* casting-act messages use the wider castmu() visibility test */
1600+
if (canseemon(mtmp) || tp_sensemon(mtmp) || Detect_monsters)
1601+
pline("%s casts a spell!", Monnam(mtmp));
1602+
else if (!Deaf)
1603+
You_hear("something cast a spell!");
1604+
/* castmu()'s casting cost formula floors at 2 for any monster
1605+
of high enough level to cast this spell */
1606+
mtmp->mspec_used = 2;
1607+
goto mon_tele;
1608+
case MUSE_SPE_TELEPORT_AWAY:
1609+
/* carrying the Amulet, so it can't teleport itself; cast at
1610+
the hero to send them away instead, as with the wand */
1611+
pline("%s casts a spell%s!",
1612+
(canseemon(mtmp) || tp_sensemon(mtmp) || Detect_monsters)
1613+
? Monnam(mtmp)
1614+
: "Something",
1615+
(Invis && !mon_prop(mtmp, SEE_INVIS)
1616+
&& (mtmp->mux != u.ux || mtmp->muy != u.uy))
1617+
? " at a spot near you"
1618+
: (Displaced && (mtmp->mux != u.ux || mtmp->muy != u.uy))
1619+
? " at your displaced image"
1620+
: " at you");
1621+
mtmp->mspec_used = 2;
1622+
(void) mcast_immediate_spell(mtmp, mtmp->mux, mtmp->muy,
1623+
SPE_TELEPORT_AWAY);
1624+
/* monster learns that teleportation isn't useful here */
1625+
if (level.flags.noteleport)
1626+
mtmp->mtrapseen |= (1 << (TELEP_TRAP_SET - 1));
1627+
return (DEADMONSTER(mtmp)) ? 1 : 2;
15701628
case MUSE_SCR_TELEPORTATION: {
15711629
int obj_is_cursed = otmp->cursed;
15721630

@@ -2506,8 +2564,11 @@ struct obj *otmp;
25062564
if (canseemon(mtmp))
25072565
pline("%s is killed!", Monnam(mtmp));
25082566
mtmp->mhp = 0;
2509-
if (DEADMONSTER(mtmp))
2567+
if (DEADMONSTER(mtmp)) {
2568+
if (mcarried(otmp) && otmp->ocarry->mtame)
2569+
set_pet_killer(otmp->ocarry);
25102570
mondied(mtmp);
2571+
}
25112572
} else if (newcham(mtmp, (struct permonst *) 0, TRUE, FALSE)) {
25122573
if (!Hallucination && zap_oseen && otmp->otyp == WAN_POLYMORPH)
25132574
makeknown(otmp->otyp);
@@ -3087,6 +3148,8 @@ struct monst *mtmp;
30873148
if (resists_cold(mtmp2))
30883149
damage_mon(mtmp2, 3 * num, AD_FIRE, FALSE);
30893150
if (DEADMONSTER(mtmp2)) {
3151+
if (mtmp->mtame)
3152+
set_pet_killer(mtmp);
30903153
mondied(mtmp2);
30913154
break;
30923155
}
@@ -5425,7 +5488,9 @@ int spell_otyp;
54255488

54265489
/* Trace the ray and affect targets along the path */
54275490
m_using = TRUE;
5491+
buzzer = caster; /* for kill attribution (pet livelog credit) */
54285492
mbhit(caster, rn1(8, 6), mbhitm, bhito, pseudo);
5493+
buzzer = 0;
54295494
m_using = FALSE;
54305495

54315496
/* Capture caster mortality before cleaning up pseudo (cleanup must

‎src/trap.c‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,10 +1293,17 @@ unsigned trflags;
12931293
seetrap(trap);
12941294
if (isok(trap->launch.x, trap->launch.y)
12951295
&& IS_STWALL(levl[trap->launch.x][trap->launch.y].typ)) {
1296+
/* nested firing (knockback or teleport during a monster's
1297+
beam can trigger this trap) must not inherit the outer
1298+
actor; a trap kill is unattributed */
1299+
struct monst *save_buzzer = buzzer;
1300+
1301+
buzzer = (struct monst *) 0;
12961302
dobuzz(trap->launch_otyp, 8,
12971303
trap->launch.x, trap->launch.y,
12981304
sgn(trap->tx - trap->launch.x),
12991305
sgn(trap->ty - trap->launch.y), FALSE);
1306+
buzzer = save_buzzer;
13001307
} else {
13011308
deltrap(trap);
13021309
newsym(u.ux, u.uy);
@@ -3431,12 +3438,20 @@ struct monst *mtmp;
34313438
seetrap(trap);
34323439
if (isok(trap->launch.x, trap->launch.y)
34333440
&& IS_STWALL(levl[trap->launch.x][trap->launch.y].typ)) {
3441+
/* nested firing (knockback or teleport during a
3442+
monster's beam can trigger this trap) must not
3443+
inherit the outer actor; a trap kill is
3444+
unattributed */
3445+
struct monst *save_buzzer = buzzer;
3446+
34343447
trap->once = 1; /* set before dobuzz; it may free
34353448
trap via melt_ice -> deltrap */
3449+
buzzer = (struct monst *) 0;
34363450
dobuzz(trap->launch_otyp, 8,
34373451
trap->launch.x, trap->launch.y,
34383452
sgn(trap->tx - trap->launch.x),
34393453
sgn(trap->ty - trap->launch.y), FALSE);
3454+
buzzer = save_buzzer;
34403455
/* trap may be freed; don't dereference */
34413456
if (DEADMONSTER(mtmp))
34423457
trapkilled = TRUE;

0 commit comments

Comments
 (0)