From 8f7258dc35a92daaf530e9f5858c95ee114a0c99 Mon Sep 17 00:00:00 2001 From: Pasi Kallinen Date: Sat, 12 Apr 2025 19:23:20 +0300 Subject: [PATCH] Buff Grimtooth with poison Grimtooth is now permanently poisoned, protects the wielder from poison, and can be invoked to throw poison. Permapoison code comes from xNetHack by copperwater . --- doc/fixes3-7-0.txt | 1 + include/artifact.h | 1 + include/artilist.h | 7 +++-- include/extern.h | 1 + include/obj.h | 9 +++--- src/artifact.c | 22 +++++++++++++++ src/mkobj.c | 2 ++ src/objnam.c | 3 ++ src/potion.c | 7 +++-- src/uhitm.c | 70 +++++++++++++++++++++++++++++++++++----------- 10 files changed, 96 insertions(+), 27 deletions(-) diff --git a/doc/fixes3-7-0.txt b/doc/fixes3-7-0.txt index bc3d300a5..0f54d8dde 100644 --- a/doc/fixes3-7-0.txt +++ b/doc/fixes3-7-0.txt @@ -1507,6 +1507,7 @@ since introduction in 3.1.0, the definition for Mitre of Holiness has specified if a tree and a boulder or statue were at the same location, applying an axe would break the boulder or statue rather than chop the tree avoid premapping outside Sokoban map to prevent showing stone glyphs +Grimtooth is permanently poisoned, protects from poison, and can fling poison Fixes to 3.7.0-x General Problems Exposed Via git Repository diff --git a/include/artifact.h b/include/artifact.h index 4199a7feb..efd0a4992 100644 --- a/include/artifact.h +++ b/include/artifact.h @@ -67,6 +67,7 @@ enum invoke_prop_types { ENLIGHTENING, CREATE_AMMO, BANISH, + FLING_POISON, BLINDING_RAY }; diff --git a/include/artilist.h b/include/artilist.h index f3ecf2e72..8d55c6789 100644 --- a/include/artilist.h +++ b/include/artilist.h @@ -43,6 +43,7 @@ static const char *const artifact_names[] = { #define FIRE(a,b) {0,AD_FIRE,a,b} #define ELEC(a,b) {0,AD_ELEC,a,b} /* electrical shock */ #define STUN(a,b) {0,AD_STUN,a,b} /* magical attack */ +#define POIS(a,b) {0,AD_DRST,a,b} /* poison */ /* clang-format on */ static NEARDATA struct artifact artilist[] = { @@ -120,9 +121,9 @@ static NEARDATA struct artifact artilist[] = { * (handled as special case in spec_dbon()). */ A("Grimtooth", ORCISH_DAGGER, (SPFX_RESTR | SPFX_WARN | SPFX_DFLAG2), - 0, M2_ELF, PHYS(2, 6), NO_DFNS, - NO_CARY, 0, A_CHAOTIC, NON_PM, PM_ORC, - 0, 5, 300L, CLR_RED, GRIMTOOTH), + 0, M2_ELF, PHYS(2, 6), POIS(0,0), + NO_CARY, FLING_POISON, A_CHAOTIC, NON_PM, PM_ORC, + 0, 5, 1200L, CLR_RED, GRIMTOOTH), /* * Orcrist and Sting have same alignment as elves. * diff --git a/include/extern.h b/include/extern.h index 2290f9711..f6bfe2e43 100644 --- a/include/extern.h +++ b/include/extern.h @@ -183,6 +183,7 @@ extern void mkot_trap_warn(void); extern boolean is_magic_key(struct monst *, struct obj *); extern struct obj *has_magic_key(struct monst *); extern boolean is_art(struct obj *, int); +extern boolean permapoisoned(struct obj *); /* ### attrib.c ### */ diff --git a/include/obj.h b/include/obj.h index 56a29a358..3b1524b17 100644 --- a/include/obj.h +++ b/include/obj.h @@ -254,10 +254,11 @@ struct obj { (otmp->oclass == WEAPON_CLASS \ && objects[otmp->otyp].oc_skill >= -P_SHURIKEN \ && objects[otmp->otyp].oc_skill <= -P_BOW) -#define is_poisonable(otmp) \ - (otmp->oclass == WEAPON_CLASS \ - && objects[otmp->otyp].oc_skill >= -P_SHURIKEN \ - && objects[otmp->otyp].oc_skill <= -P_BOW) +#define is_poisonable(otmp) \ + ((otmp->oclass == WEAPON_CLASS \ + && objects[otmp->otyp].oc_skill >= -P_SHURIKEN \ + && objects[otmp->otyp].oc_skill <= -P_BOW) \ + || permapoisoned(otmp)) #define uslinging() (uwep && objects[uwep->otyp].oc_skill == P_SLING) /* 'is_quest_artifact()' only applies to the current role's artifact */ #define any_quest_artifact(o) ((o)->oartifact >= ART_ORB_OF_DETECTION) diff --git a/src/artifact.c b/src/artifact.c index 2254b3976..3cb92fac5 100644 --- a/src/artifact.c +++ b/src/artifact.c @@ -283,6 +283,8 @@ mk_artifact( otmp = 0; } /* otherwise, otmp has not changed; just fallthrough to return it */ } + if (permapoisoned(otmp)) + otmp->opoisoned = 1; return otmp; } @@ -2009,6 +2011,19 @@ arti_invoke(struct obj *obj) } break; } + case FLING_POISON: + if (getdir((char *) 0)) { + int venom = rn2(2) ? BLINDING_VENOM : ACID_VENOM; + struct obj *otmp = mksobj(venom, TRUE, FALSE); + + otmp->spe = 1; /* the poison is yours */ + throwit(otmp, 0L, FALSE, (struct obj *) 0); + } else { + /* no direction picked */ + pline("%s", Never_mind); + obj->age = svm.moves; + } + break; case BLINDING_RAY: if (getdir((char *) 0)) { if (u.dx || u.dy) { @@ -2699,4 +2714,11 @@ get_artifact(struct obj *obj) } return &artilist[ART_NONARTIFACT]; } + +/* is object permanently poisoned? (currently only Grimtooth) */ +boolean +permapoisoned(struct obj *obj) +{ + return (obj && is_art(obj, ART_GRIMTOOTH)); +} /*artifact.c*/ diff --git a/src/mkobj.c b/src/mkobj.c index d76e65f1b..4713e9d29 100644 --- a/src/mkobj.c +++ b/src/mkobj.c @@ -1160,6 +1160,8 @@ mksobj_init(struct obj **obj, boolean artif) } mkobj_erosions(otmp); + if (permapoisoned(otmp)) + otmp->opoisoned = 1; } /* mksobj(): create a specific type of object; result is always non-Null */ diff --git a/src/objnam.c b/src/objnam.c index 334e383dd..0816a4fd2 100644 --- a/src/objnam.c +++ b/src/objnam.c @@ -5338,6 +5338,9 @@ readobjnam(char *bp, struct obj *no_wish) } } + if (permapoisoned(d.otmp)) + d.otmp->opoisoned = 1; + /* more wishing abuse: don't allow wishing for certain artifacts */ /* and make them pay; charge them for the wish anyway! */ if ((is_quest_artifact(d.otmp) diff --git a/src/potion.c b/src/potion.c index 86a0545a9..3ff57c360 100644 --- a/src/potion.c +++ b/src/potion.c @@ -2595,9 +2595,10 @@ potion_dip(struct obj *obj, struct obj *potion) obj->opoisoned = TRUE; poof(potion); return ECMD_TIME; - } else if (obj->opoisoned && (potion->otyp == POT_HEALING - || potion->otyp == POT_EXTRA_HEALING - || potion->otyp == POT_FULL_HEALING)) { + } else if (obj->opoisoned && !permapoisoned(obj) + && (potion->otyp == POT_HEALING + || potion->otyp == POT_EXTRA_HEALING + || potion->otyp == POT_FULL_HEALING)) { pline("A coating wears off %s.", the(xname(obj))); obj->opoisoned = 0; poof(potion); diff --git a/src/uhitm.c b/src/uhitm.c index e68f1a60f..581c6287e 100644 --- a/src/uhitm.c +++ b/src/uhitm.c @@ -13,6 +13,8 @@ staticfn boolean mhitm_mgc_atk_negated(struct monst *, struct monst *, staticfn boolean known_hitum(struct monst *, struct obj *, int *, int, int, struct attack *, int) NONNULLARG13; staticfn boolean theft_petrifies(struct obj *) NONNULLARG1; +staticfn void mhitm_really_poison(struct monst *, struct attack *, + struct monst *, struct mhitm_data *); staticfn void steal_it(struct monst *, struct attack *) NONNULLARG1; /* hitum_cleave() has contradictory information. There's a comment * beside the 1st arg 'target' stating non-null, but later on there @@ -1036,6 +1038,9 @@ hmon_hitmon_weapon_melee( if (obj->opoisoned && is_poisonable(obj)) hmd->ispoisoned = TRUE; } + /* permapoisoned is non-ammo/missile, limit the poison */ + if (permapoisoned(obj) && hmd->dieroll <= 5) + hmd->ispoisoned = TRUE; } staticfn void @@ -1484,7 +1489,7 @@ hmon_hitmon_poison( You_feel("like an evil coward for using a poisoned weapon."); adjalign(-1); } - if (!rn2(nopoison)) { + if (!permapoisoned(obj) && !rn2(nopoison)) { /* remove poison now in case obj ends up in a bones file */ obj->opoisoned = FALSE; /* defer "obj is no longer poisoned" until after hit message */ @@ -3026,6 +3031,29 @@ mhitm_ad_curs( } } +/* Helper for mhitm_ad_drst(), containing some code that is also called from + * mhitm_ad_phys (for poisoned weapons) and shouldn't be subject to magic + * cancellation or a 1/8 chance roll. + * In this specific case, the "mhitm" in the name ACTUALLY means just that - + * this should be called only for monster versus monster situations. */ +staticfn void +mhitm_really_poison(struct monst *magr, struct attack *mattk, + struct monst *mdef, struct mhitm_data *mhm) +{ + if (gv.vis && canspotmon(magr)) + pline("%s %s was poisoned!", s_suffix(Monnam(magr)), + mpoisons_subj(magr, mattk)); + if (resists_poison(mdef)) { + if (gv.vis && canspotmon(mdef) && canspotmon(magr)) + pline_The("poison doesn't seem to affect %s.", + mon_nam(mdef)); + } else { + mhm->damage += rn1(10, 6); + if (mhm->damage >= mdef->mhp && gv.vis && canspotmon(mdef)) + pline_The("poison was deadly..."); + } +} + void mhitm_ad_drst( struct monst *magr, struct attack *mattk, @@ -3067,22 +3095,7 @@ mhitm_ad_drst( } else { /* mhitm */ if (!negated && !rn2(8)) { - if (gv.vis && canspotmon(magr)) - pline("%s %s was poisoned!", s_suffix(Monnam(magr)), - mpoisons_subj(magr, mattk)); - if (resists_poison(mdef)) { - if (gv.vis && canspotmon(mdef) && canspotmon(magr)) - pline_The("poison doesn't seem to affect %s.", - mon_nam(mdef)); - } else { - if (rn2(10)) { - mhm->damage += rn1(10, 6); - } else { - if (gv.vis && canspotmon(mdef)) - pline_The("poison was deadly..."); - mhm->damage = mdef->mhp; - } - } + mhitm_really_poison(magr, mattk, mdef, mhm); } } } @@ -3957,6 +3970,7 @@ mhitm_ad_phys( if (mattk->aatyp == AT_WEAP && otmp) { struct obj *marmg; int tmp; + boolean was_poisoned = (otmp->opoisoned || permapoisoned(otmp)); if (otmp->otyp == CORPSE && touch_petrifies(&mons[otmp->corpsenm])) { @@ -4015,6 +4029,21 @@ mhitm_ad_phys( You("divide as %s hits you!", mon_nam(magr)); } rustm(&gy.youmonst, otmp); + if (was_poisoned && gm.mhitu_dieroll <= 5) { + char buf[BUFSZ]; + + /* similar to mhitm_really_poison, but we don't use the + * exact same values, nor do we want the same 1/8 chance of + * the poison taking (use 1/4, same as in the mhitm case). */ + Sprintf(buf, "%s %s", s_suffix(Monnam(magr)), + mpoisons_subj(magr, mattk)); + /* arbitrary, but most poison sources in the game are + * strength-based. With hpdamchance = 10, HP damage occurs + * 1/2 of the time and it will hit Str the rest of the time. + * (This is the same as poisoned ammo.) */ + poisoned(buf, A_STR, pmname(magr->data, Mgender(magr)), + 10, FALSE); + } } else if (mattk->aatyp != AT_TUCH || mhm->damage != 0 || magr != u.ustuck) { hitmsg(magr, mattk); @@ -4077,6 +4106,13 @@ mhitm_ad_phys( } if (mhm->damage) rustm(mdef, mwep); + if ((mwep->opoisoned || permapoisoned(mwep)) && !rn2(4)) { + /* 1/4 chance of weapon poison applying is the same as in + * uhitm and mhitu cases. But since we don't need to call + * any special functions or go through tangled hmon_hitmon + * code, we can just jump straight to the poisoning. */ + mhitm_really_poison(magr, mattk, mdef, mhm); + } } else if (pa == &mons[PM_PURPLE_WORM] && pd == &mons[PM_SHRIEKER]) { /* hack to enhance mm_aggression(); we don't want purple worm's bite attack to kill a shrieker because then it