From 07fc4904c67d472c5e0930697d57e9bdea318554 Mon Sep 17 00:00:00 2001 From: Alex Smith Date: Thu, 19 Mar 2026 01:16:30 +0000 Subject: [PATCH] Add a new wand, the wand of stasis A wand of stasis prevents teleportation (even in some cases where it would normally not be prevented, e.g. the hero teleporting a monster, or covetous monsters teleporting). This is intended to provide an alternative tactic against covetous monsters (and their AI has been adjusted to handle being under a stasis effect), but might also be useful in other situations. It does not prevent teleportation of objects, only the hero / monsters, and does not at present prevent level teleportation (although I'm not sure about this and it might well change in the future). This breaks save compatibility, but is being pushed together with other save-breaking changes to avoid the need for multiple bumps to EDITLEVEL. --- doc/fixes3-7-0.txt | 1 + include/objects.h | 4 +++- include/rm.h | 1 + src/apply.c | 5 +++++ src/do.c | 3 ++- src/engrave.c | 1 + src/mklev.c | 1 + src/mkobj.c | 4 ++++ src/monmove.c | 7 +++---- src/teleport.c | 21 +++++++++++++++++---- src/wizard.c | 20 +++++++++++++------- src/zap.c | 5 +++++ win/share/objects.txt | 19 +++++++++++++++++++ 13 files changed, 75 insertions(+), 17 deletions(-) diff --git a/doc/fixes3-7-0.txt b/doc/fixes3-7-0.txt index 464e6e701..0b9220403 100644 --- a/doc/fixes3-7-0.txt +++ b/doc/fixes3-7-0.txt @@ -2907,6 +2907,7 @@ the game now automatically tracks which sell prices and buy prices you have seen for each type of item; these are visible in the discoveries list, and can also be shown elsewhere using the new 'price_quotes' option new shields: shield of shock resistance, shield of drain resistance +new wand: wand of stasis Platform- and/or Interface-Specific New Features diff --git a/include/objects.h b/include/objects.h index 932d063f6..faa630a50 100644 --- a/include/objects.h +++ b/include/objects.h @@ -1457,9 +1457,11 @@ WAND("create monster", "maple", 45, 200, 1, NODIR, WOOD, HI_WOOD, WAN_CREATE_MONSTER), WAND("wishing", "pine", 5, 500, 1, NODIR, WOOD, HI_WOOD, WAN_WISHING), +WAND("stasis", "redwood", 45, 150, 1, NODIR, WOOD, CLR_RED, + WAN_STASIS), WAND("nothing", "oak", 25, 100, 0, IMMEDIATE, WOOD, HI_WOOD, WAN_NOTHING), -WAND("striking", "ebony", 75, 150, 1, IMMEDIATE, WOOD, HI_WOOD, +WAND("striking", "ebony", 30, 150, 1, IMMEDIATE, WOOD, HI_WOOD, WAN_STRIKING), WAND("make invisible", "marble", 45, 150, 1, IMMEDIATE, MINERAL, HI_MINERAL, WAN_MAKE_INVISIBLE), diff --git a/include/rm.h b/include/rm.h index 1c9f49f14..8811deae1 100644 --- a/include/rm.h +++ b/include/rm.h @@ -454,6 +454,7 @@ struct levelflags { Bitfield(stormy, 1); /* clouds create lightning bolts at random */ schar temperature; /* +1 == hot, -1 == cold */ + long stasis_until; /* wand of stasis effect lasts until when? */ }; typedef struct { diff --git a/src/apply.c b/src/apply.c index 6cbcb30a8..3abfc5b6c 100644 --- a/src/apply.c +++ b/src/apply.c @@ -526,6 +526,10 @@ magic_whistled(struct obj *obj) already_discovered = objects[obj->otyp].oc_name_known != 0; int omx, omy, shift = 0, appear = 0, disappear = 0, trapped = 0; + /* stasis prevents magic-whistling */ + if (svl.level.flags.stasis_until >= svm.moves) + return; + /* need to copy (up to 3) names as they're collected rather than just save pointers to them, otherwise churning through every mbuf[] might clobber the ones we care about */ @@ -3984,6 +3988,7 @@ do_break_wand(struct obj *obj) case WAN_PROBING: case WAN_ENLIGHTENMENT: case WAN_SECRET_DOOR_DETECTION: + case WAN_STASIS: pline(nothing_else_happens); discard_broken_wand(); return ECMD_TIME; diff --git a/src/do.c b/src/do.c index 875d0c62f..26485069c 100644 --- a/src/do.c +++ b/src/do.c @@ -2248,7 +2248,8 @@ revive_mon(anything *arg, long timeout UNUSED) /* corpse will revive somewhere else if there is a monster in the way; Riders get a chance to try to bump the obstacle out of their way */ if (is_displacer(mptr) && body->where == OBJ_FLOOR - && get_obj_location(body, &x, &y, 0) && (mtmp = m_at(x, y)) != 0) { + && get_obj_location(body, &x, &y, 0) && (mtmp = m_at(x, y)) != 0 && + svl.level.flags.stasis_until < svm.moves) { boolean notice_it = canseemon(mtmp); /* before rloc() */ char *monname = Monnam(mtmp); diff --git a/src/engrave.c b/src/engrave.c index c84908cf1..22ca30aaf 100644 --- a/src/engrave.c +++ b/src/engrave.c @@ -589,6 +589,7 @@ doengrave_sfx_item_WAN(struct _doengrave_ctx *de) /* NODIR wands */ case WAN_LIGHT: case WAN_SECRET_DOOR_DETECTION: + case WAN_STASIS: case WAN_CREATE_MONSTER: case WAN_WISHING: case WAN_ENLIGHTENMENT: diff --git a/src/mklev.c b/src/mklev.c index 0350562f3..703133ca4 100644 --- a/src/mklev.c +++ b/src/mklev.c @@ -891,6 +891,7 @@ clear_level_structures(void) svl.level.flags.noautosearch = 0; svl.level.flags.fumaroles = 0; svl.level.flags.stormy = 0; + svl.level.flags.stasis_until = 0L; svn.nroom = 0; svr.rooms[0].hx = -1; diff --git a/src/mkobj.c b/src/mkobj.c index f2adf2185..ab0f98fb3 100644 --- a/src/mkobj.c +++ b/src/mkobj.c @@ -1115,6 +1115,10 @@ mksobj_init(struct obj **obj, boolean artif) case WAND_CLASS: if (otmp->otyp == WAN_WISHING) otmp->spe = 1; + else if (otmp->otyp == WAN_STASIS) + /* just as easy to recharge as other NODIR wands, but starts with + fewer charges */ + otmp->spe = rn1(4, 3); else otmp->spe = rn1(5, (objects[otmp->otyp].oc_dir == NODIR) ? 11 : 4); diff --git a/src/monmove.c b/src/monmove.c index 3830e3713..6489d19d4 100644 --- a/src/monmove.c +++ b/src/monmove.c @@ -1794,11 +1794,10 @@ m_move(struct monst *mtmp, int after) if (covetousattack & M_ATTK_AGR_DIED) return MMOVE_DIED; mmoved = MMOVE_MOVED; - } else { - mmoved = MMOVE_NOTHING; + return postmov(mtmp, ptr, omx, omy, mmoved, + seenflgs, can_tunnel, can_unlock, can_open); } - return postmov(mtmp, ptr, omx, omy, mmoved, - seenflgs, can_tunnel, can_unlock, can_open); + /* otherwise continue with normal AI routine */ } /* likewise for shopkeeper, guard, or priest */ diff --git a/src/teleport.c b/src/teleport.c index 9858dfe49..b235cb238 100644 --- a/src/teleport.c +++ b/src/teleport.c @@ -34,8 +34,13 @@ noteleport_level(struct monst *mon) if (get_iter_mons(m_blocks_teleporting)) return TRUE; - /* natural no-teleport level */ - if (svl.level.flags.noteleport) + /* natural no-teleport level; covetous monsters can bypass these */ + if (svl.level.flags.noteleport && !is_covetous(mon->data)) + return TRUE; + + /* wand of stasis prevents teleportation while the effect is active + (even for covetous monsters) */ + if (svl.level.flags.stasis_until >= svm.moves) return TRUE; return FALSE; @@ -1958,8 +1963,11 @@ mtele_trap(struct monst *mtmp, struct trap *trap, int in_sight) { char *monname; - if (tele_restrict(mtmp)) + /* don't print feedback here: a monster stepping on a trap and not + teleporting from it isn't visible */ + if (noteleport_level(mtmp)) return; + if (teleport_pet(mtmp, FALSE)) { /* save name with pre-movement visibility */ monname = Monnam(mtmp); @@ -2257,7 +2265,12 @@ u_teleport_mon( { coord cc; - if (mtmp->ispriest && *in_rooms(mtmp->mx, mtmp->my, TEMPLE)) { + if (svl.level.flags.stasis_until >= svm.moves) { + if (give_feedback) + pline("A mysterious force prevents you teleporting %s!", + mon_nam(mtmp)); + return FALSE; + } else if (mtmp->ispriest && *in_rooms(mtmp->mx, mtmp->my, TEMPLE)) { if (give_feedback) pline("%s resists your magic!", Monnam(mtmp)); return FALSE; diff --git a/src/wizard.c b/src/wizard.c index 674b4db6e..c89e3296f 100644 --- a/src/wizard.c +++ b/src/wizard.c @@ -386,10 +386,12 @@ tactics(struct monst *mtmp) mtmp->mavenge = 1; /* covetous monsters attack while fleeing */ if (In_W_tower(mx, my, &u.uz) || (mtmp->iswiz && !sx && !mon_has_amulet(mtmp))) { - if (!rn2(3 + mtmp->mhp / 10)) + if (!noteleport_level(mtmp) && + !rn2(3 + mtmp->mhp / 10)) (void) rloc(mtmp, RLOC_MSG); } else if (sx && (mx != sx || my != sy)) { - if (!mnearto(mtmp, sx, sy, TRUE, RLOC_MSG)) { + if (!noteleport_level(mtmp) && + !mnearto(mtmp, sx, sy, TRUE, RLOC_MSG)) { /* couldn't move to the target spot for some reason, so stay where we are (don't actually need rloc_to() because mtmp is still on the map at ... */ @@ -408,7 +410,7 @@ tactics(struct monst *mtmp) /*FALLTHRU*/ case STRAT_NONE: /* harass */ - if (!rn2(!mtmp->mflee ? 5 : 33)) + if (!noteleport_level(mtmp) && !rn2(!mtmp->mflee ? 5 : 33)) mnexto(mtmp, RLOC_MSG); return 0; @@ -419,13 +421,16 @@ tactics(struct monst *mtmp) int targ = (int) (strat & STRAT_GOAL); struct obj *otmp; - if (!targ) { /* simply wants you to close */ + if (!targ || !isok(tx, ty)) { /* simply wants you to close */ return 0; } + if (noteleport_level(mtmp) && !monnear(mtmp, tx, ty)) + return 0; if (u_at(tx, ty) || where == STRAT_PLAYER) { /* player is standing on it (or has it) */ mx = mtmp->mx, my = mtmp->my; - if (!mnearto(mtmp, tx, ty, FALSE, RLOC_MSG)) + if (noteleport_level(mtmp) || + !mnearto(mtmp, tx, ty, FALSE, RLOC_MSG)) rloc_to(mtmp, mx, my); /* no room? stay put */ return 0; } @@ -445,13 +450,14 @@ tactics(struct monst *mtmp) return 0; } else { /* a monster is standing on it - cause some trouble */ - if (!rn2(5)) + if (!rn2(5) && !noteleport_level(mtmp)) mnexto(mtmp, RLOC_MSG); return 0; } } else { /* a monster has it - 'port beside it. */ mx = mtmp->mx, my = mtmp->my; - if (!mnearto(mtmp, tx, ty, FALSE, RLOC_MSG)) + if (!noteleport_level(mtmp) && + !mnearto(mtmp, tx, ty, FALSE, RLOC_MSG)) rloc_to(mtmp, mx, my); /* no room? stay put */ return 0; } diff --git a/src/zap.c b/src/zap.c index 96dc3d44b..adecdbeca 100644 --- a/src/zap.c +++ b/src/zap.c @@ -2555,6 +2555,11 @@ zapnodir(struct obj *obj) known = !!obj->dknown; (void) findit(); break; + case WAN_STASIS: + /* no immediately obvious effect, and no message so that it isn't + distinguishable from other NODIR wands that produce no message */ + svl.level.flags.stasis_until = svm.moves + rn1(21, 10); + break; case WAN_CREATE_MONSTER: /* create_critters() returns True iff hero sees a new monster appear */ if (create_critters(rn2(23) ? 1 : rn1(7, 2), diff --git a/win/share/objects.txt b/win/share/objects.txt index 64a53c16a..986d88e6f 100644 --- a/win/share/objects.txt +++ b/win/share/objects.txt @@ -7973,6 +7973,25 @@ Z = (195, 195, 195) ................ ................ } +# tile 413 (redwood / stasis) +{ + ................ + ................ + ................ + ...........NO... + ..........DDAA.. + .........DDAA... + ........DDAA.... + .......DDAA..... + ......DDAA...... + .....DDAA....... + ....DDAA........ + ...NOAA......... + ....AA.......... + ................ + ................ + ................ +} # tile 414 (oak / nothing) { ................