From: copperwater Date: Mon, 8 Feb 2021 04:21:31 +0000 (-0500) Subject: Make engraving an occupation X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=9c6a5fc42321064e556ea716f78cda2eaec60cd2;p=nethack Make engraving an occupation Instead of inexplicably paralyzing the player for the duration of their engraving. Many a character has died by trying to engrave something and then sitting there diligently writing it while monsters surround and attack them. (This was especially prominent back in the 3.4.3 era when repeated Elbereths were viable, but it still occurs today with e.g. using a hard stone to engrave Elbereth). There were also some other oddities - for instance, if something teleported the player away while they were engraving, they would continue to "engrave" (be paralyzed) on their new location, but would not produce any text there; the full engraving would be placed on their initial position. In this commit, I have converted engraving to use the occupation framework, which treats it as an interruptible activity. This necessitated some logical restructuring, mostly involving the engraving being written out in chunks as the player spends more uninterrupted time on it. I've tried to keep this free of regressions except for those inherent to the occupation system. What has NOT changed: - The rate of engraving is still 10 characters per turn, or 1 character using slow methods. - The formulas for determining how much a bladed weapon or marker can engrave before getting exhausted are kept. Though this is a bit convoluted, and if it's not considered important to preserve the existing behavior, I would recommend simplifying it by decreasing the maximum engraving length for weapons by 1 so that each point of enchantment simply gets you 2 characters' worth of engraving (e.g. a -2 weapon will only engrave 1 or 2 characters before dulling to -3, rather than giving it a third "grace character". - The input buffer is still modified based on confusion/blindness/etc only at the time when the player inputs it (if they gain a debilitating status while engraving, it will not affect the text). My personal preference is to make the text affected in scenarios like that, but it's not strictly necessary to do here, so I didn't. - Wand messages such as "The floor is riddled by bullet holes", and blinding from engraving lightning, still appear before the hero starts to take any time engraving. As noted above, getting blinded by the wand still has no effect on accurately engraving the text, unless the hero was already blind or impaired. What has changed: - Moving off the engraving or losing the object being engraved with causes the player to stop engraving. - Wands can still engrave an arbitrary amount of text using a single charge, but if the hero is interrupted and decides to start engraving again, they will consume a second charge. As it adds a new field to g.context, this is a save-breaking change. --- diff --git a/include/context.h b/include/context.h index fe7b3ab72..10da8bd8d 100644 --- a/include/context.h +++ b/include/context.h @@ -65,6 +65,16 @@ struct victual_info { Bitfield(doreset, 1); /* stop eating at end of turn */ }; +struct engrave_info { + char text[BUFSZ]; /* actual text being engraved - doengrave() handles all + the possible mutations of this */ + char *nextc; /* next character(s) in text[] to engrave */ + struct obj *stylus; /* object doing the writing */ + xchar type; /* type of engraving (DUST, MARK, etc) */ + coord pos; /* location the engraving is being placed on */ + int actionct; /* nth turn spent engraving */ +}; + struct warntype_info { unsigned long obj; /* object warn_of_mon monster type M2 */ unsigned long polyd; /* warn_of_mon monster type M2 due to poly */ @@ -142,6 +152,7 @@ struct context_info { boolean enhance_tip; /* player is informed about #enhance */ struct dig_info digging; struct victual_info victual; + struct engrave_info engraving; struct tin_info tin; struct book_info spbook; struct takeoff_info takeoff; diff --git a/src/engrave.c b/src/engrave.c index 083698c65..9acb246ed 100644 --- a/src/engrave.c +++ b/src/engrave.c @@ -6,6 +6,7 @@ #include "hack.h" static int stylus_ok(struct obj *); +static int engrave(void); static const char *blengr(void); char * @@ -493,7 +494,6 @@ doengrave(void) const char *eloc; /* Where the engraving is (ie dust/floor/...) */ char *sp; /* Place holder for space count of engr text */ int len; /* # of nonspace chars of new engraving text */ - int maxelen; /* Max allowable length of engraving text */ struct engr *oep = engr_at(u.ux, u.uy); /* The current engraving */ struct obj *otmp; /* Object selected with which to engrave */ @@ -505,7 +505,6 @@ doengrave(void) buf[0] = (char) 0; ebuf[0] = (char) 0; post_engr_text[0] = (char) 0; - maxelen = BUFSZ - 1; if (oep) oetype = oep->engr_type; if (is_demon(g.youmonst.data) || is_vampire(g.youmonst.data)) @@ -1069,105 +1068,215 @@ doengrave(void) oep = (struct engr *) 0; } - /* Figure out how long it took to engrave, and if player has - * engraved too much. - */ - switch (type) { + Strcpy(g.context.engraving.text, ebuf); + g.context.engraving.nextc = g.context.engraving.text; + g.context.engraving.stylus = otmp; + g.context.engraving.type = type; + g.context.engraving.pos.x = u.ux; + g.context.engraving.pos.y = u.uy; + g.context.engraving.actionct = 0; + set_occupation(engrave, "engraving", 0); + + if (post_engr_text[0]) + pline("%s", post_engr_text); + if (doblind && !resists_blnd(&g.youmonst)) { + You("are blinded by the flash!"); + make_blinded((long) rnd(50), FALSE); + if (!Blind) + Your1(vision_clears); + } + + /* Engraving will always take at least one action via being run as an + * occupation, so do not count this setup as taking time. */ + return 0; +} + +/* occupation callback for engraving some text */ +static int +engrave(void) +{ + struct engr *oep; + char buf[BUFSZ]; /* holds the post-this-action engr text, including anything + * already there */ + const char *finishverb; /* "You finish [foo]." */ + struct obj * stylus; /* shorthand for g.context.engraving.stylus */ + boolean firsttime = (g.context.engraving.actionct == 0); + int rate = 10; /* # characters we are capable of engraving in this action */ + boolean truncate = FALSE; + + boolean carving = (g.context.engraving.type == ENGRAVE + || g.context.engraving.type == HEADSTONE); + boolean dulling_wep, marker; + char *endc; /* points at character 1 beyond the last character to engrave + this action */ + int i; + + if (g.context.engraving.pos.x != u.ux + || g.context.engraving.pos.y != u.uy) { /* teleported? */ + pline("You are unable to continue engraving."); + return 0; + } + /* Stylus might have been taken out of inventory and destroyed somehow. + * Not safe to dereference stylus until after this. */ + if (g.context.engraving.stylus == &cg.zeroobj) { /* bare finger */ + stylus = (struct obj *) 0; + } + else { + for (stylus = g.invent; stylus; stylus = stylus->nobj) { + if (stylus == g.context.engraving.stylus) { + break; + } + } + if (!stylus) { + pline("You are unable to continue engraving."); + return 0; + } + } + + dulling_wep = (stylus && stylus->oclass == WEAPON_CLASS + && (stylus->otyp != ATHAME || stylus->cursed)); + marker = (stylus && stylus->otyp == MAGIC_MARKER); + + g.context.engraving.actionct++; + + /* sanity checks */ + if (dulling_wep && !carving) { + impossible("using weapon for non-carve engraving"); + } + else if (g.context.engraving.type == MARK && !marker) { + impossible("making graffiti with non-marker stylus"); + } + + /* Step 1: Compute rate. */ + if (carving && stylus + && (dulling_wep || stylus->oclass == RING_CLASS + || stylus->oclass == GEM_CLASS)) { + /* slow engraving methods */ + rate = 1; + } + else if (marker) { + /* one charge / 2 letters */ + rate = min(rate, stylus->spe * 2); + } + + /* Step 2: Compute last character that can be engraved this action. */ + i = rate; + for (endc = g.context.engraving.nextc; *endc && i > 0; endc++) { + if (*endc != ' ') { + i--; + } + } + + /* Step 3: affect stylus from engraving - it might wear out. */ + if (dulling_wep) { + /* Dull the weapon at a rate of -1 enchantment per 2 characters, + * rounding down. + * The number of characters obtainable given starting enchantment: + * -2 => 3, -1 => 5, 0 => 7, +1 => 9, +2 => 11 + * Note: this does not allow a +0 anything (except an athame) to + * engrave "Elbereth" all at once. + * However, you can engrave "Elb", then "ere", then "th", by taking + * advantage of the rounding down. */ + if (firsttime) { + pline("%s dull.", Yobjnam2(stylus, "get")); + } + if (g.context.engraving.actionct % 2 == 1) { /* 1st, 3rd, ... action */ + /* deduct a point on 1st, 3rd, 5th, ... turns, unless this is the + * last character being engraved (a rather convoluted way to round + * down). + * Check for truncation *before* deducting a point - otherwise, + * attempting to e.g. engrave 3 characters with a -2 weapon will + * stop at the 1st. */ + if (stylus->spe <= -3) { + if (firsttime) { + impossible("<= -3 weapon valid for engraving"); + } + truncate = TRUE; + } + else if (*endc) { + stylus->spe -= 1; + } + } + } + else if (marker) { + int ink_cost = max(rate / 2, 1); /* Prevent infinite graffiti */ + if (stylus->spe < ink_cost) { + impossible("dry marker valid for graffiti"); + truncate = TRUE; + } + stylus->spe -= ink_cost; + if (stylus->spe == 0) { + /* can't engrave any further; truncate the string */ + Your("marker dries out."); + truncate = TRUE; + } + } + + switch (g.context.engraving.type) { default: - g.multi = -(len / 10); - if (g.multi) - g.nomovemsg = "You finish your weird engraving."; + finishverb = "your weird engraving"; break; case DUST: - g.multi = -(len / 10); - if (g.multi) - g.nomovemsg = "You finish writing in the dust."; + finishverb = "writing in the dust"; break; case HEADSTONE: case ENGRAVE: - g.multi = -(len / 10); - if (otmp->oclass == WEAPON_CLASS - && (otmp->otyp != ATHAME || otmp->cursed)) { - g.multi = -len; - maxelen = ((otmp->spe + 3) * 2) + 1; - /* -2 => 3, -1 => 5, 0 => 7, +1 => 9, +2 => 11 - * Note: this does not allow a +0 anything (except an athame) - * to engrave "Elbereth" all at once. - * However, you can engrave "Elb", then "ere", then "th". - */ - pline("%s dull.", Yobjnam2(otmp, "get")); - costly_alteration(otmp, COST_DEGRD); - if (len > maxelen) { - g.multi = -maxelen; - otmp->spe = -3; - } else if (len > 1) - otmp->spe -= len >> 1; - else - otmp->spe -= 1; /* Prevent infinite engraving */ - } else if (otmp->oclass == RING_CLASS || otmp->oclass == GEM_CLASS) { - g.multi = -len; - } - if (g.multi) - g.nomovemsg = "You finish engraving."; + finishverb = "engraving"; break; case BURN: - g.multi = -(len / 10); - if (g.multi) - g.nomovemsg = is_ice(u.ux, u.uy) - ? "You finish melting your message into the ice." - : "You finish burning your message into the floor."; + finishverb = is_ice(u.ux, u.uy) ? "melting your message into the ice" + : "burning your message into the floor"; break; case MARK: - g.multi = -(len / 10); - if (otmp->otyp == MAGIC_MARKER) { - maxelen = otmp->spe * 2; /* one charge / 2 letters */ - if (len > maxelen) { - Your("marker dries out."); - otmp->spe = 0; - g.multi = -(maxelen / 10); - } else if (len > 1) - otmp->spe -= len >> 1; - else - otmp->spe -= 1; /* Prevent infinite graffiti */ - } - if (g.multi) - g.nomovemsg = "You finish defacing the dungeon."; + finishverb = "defacing the dungeon"; break; case ENGR_BLOOD: - g.multi = -(len / 10); - if (g.multi) - g.nomovemsg = "You finish scrawling."; - break; + finishverb = "scrawling"; } - /* Chop engraving down to size if necessary */ - if (len > maxelen) { - for (sp = ebuf; maxelen && *sp; sp++) - if (!(*sp == ' ')) - maxelen--; - if (!maxelen && *sp) { - *sp = '\0'; - if (g.multi) - g.nomovemsg = "You cannot write any more."; - You("are only able to write \"%s\".", ebuf); - } + /* actions that happen at the end of every engraving action go here */ + + /* If the stylus did wear out mid-engraving, truncate the input so that we + * can't go any further. */ + if (truncate && *endc != '\0') { + *endc = '\0'; + You("are only able to write \"%s\".", g.context.engraving.text); + } + else { + /* input was not truncated; stylus may still have worn out on the last + * character, though */ + truncate = FALSE; } + Strcpy(buf, ""); + oep = engr_at(u.ux, u.uy); if (oep) /* add to existing engraving */ Strcpy(buf, oep->engr_txt); - (void) strncat(buf, ebuf, BUFSZ - (int) strlen(buf) - 1); - /* Put the engraving onto the map */ - make_engr_at(u.ux, u.uy, buf, g.moves - g.multi, type); + (void) strncat(buf, g.context.engraving.nextc, + endc - g.context.engraving.nextc); + make_engr_at(u.ux, u.uy, buf, g.moves - g.multi, g.context.engraving.type); - if (post_engr_text[0]) - pline("%s", post_engr_text); - if (doblind && !resists_blnd(&g.youmonst)) { - You("are blinded by the flash!"); - make_blinded((long) rnd(50), FALSE); - if (!Blind) - Your1(vision_clears); + if (*endc) { + g.context.engraving.nextc = endc; + return 1; /* not yet finished this turn */ + } + else { /* finished engraving */ + /* actions that happen after the engraving is finished go here */ + + if (truncate) { + /* Now that "You are only able to write 'foo'" also prints at the + * end of engraving, this might be redundant. */ + You("cannot write any more."); + } + else if (!firsttime) { + /* only print this if engraving took multiple actions */ + You("finish %s.", finishverb); + } + g.context.engraving.text[0] = '\0'; + g.context.engraving.nextc = (char *) 0; + g.context.engraving.stylus = (struct obj *) 0; } - return 1; + return 0; } /* while loading bones, clean up text which might accidentally