]> granicus.if.org Git - nethack/commitdiff
spell retention feedback (trunk only)
authornethack.rankin <nethack.rankin>
Sat, 11 Feb 2006 06:35:41 +0000 (06:35 +0000)
committernethack.rankin <nethack.rankin>
Sat, 11 Feb 2006 06:35:41 +0000 (06:35 +0000)
     Instead of just marking forgotten spells with an asterisk, add a new
column to the '+' spell menu which gives an estimate of how well the spell
is remembered.  Precision of the estimate depends upon the hero's skill in
the category of spell; expert gets the most detail with a low-to-high range
spanning just 2%, such as 19%-20% or 75%-76%; skilled sees 5% ranges; basic
10%, and unskilled the least detail with 25% ranges (ie, one of 1-25, 26-50,
51-75, or 76-100 for percentage of time left available for the spell).
[The tab-separator variant for the menu is untested.]

     This fixes an off by one bug for spell retention.  It got set to 20000
as intended when a spellbook was read, but then it would be decremented to
19999 before the player had a chance to do anything else, cheating him out
of 1 turn of memory.  Spells known at game start last exactly 20000 turns.

     Also, adjust the point at which you're allowed to reread a spellbook
and refresh your memory of the spell.  When the current spell system was
instituted, that was 10% of the retention period (1000 turns or less left
out of 10000); when retention got doubled to 20000 turns, the relearn point
was left as is.  Increasing it to 10% (ie, doubling to 2000) makes it fit
better with the displayed retention percentages.  Otherwise expert casters
would be left to guess when they've hit that point (1000 is 5%, falling
within their 5%-6% range, which really indicates (4%+1) to 6%).  At 10%,
it's the threshold for the 9-10 range for experts, 5-10 range for skilled,
and 1-10 range for basic so the player can see when it has been reached;
only unskilled, with a bottom range of 1-25, is left to guess.  Players
will likely prefer to wait until later, when the spell is nearly expired,
to refresh, but if they're about to abandon their books on the way to the
endgame then being able to re-read sooner is beneficial.

doc/fixes35.0
src/spell.c

index d566d4e852238eeac61afe5d3eb6f3c68e480261..3668be89fced7a1e2f039b9e9d2490661096703e 100644 (file)
@@ -176,6 +176,7 @@ some intelligent pets will avoid cannibalism
 keep track of which monsters were cloned from other monsters
 number_pad:3 run-time option to use inverted phone keypad layout for movement
 number_pad:-1 to swap function of y and z keys; z to move NW, y to zap wands
+display spell retention information in the spell menu
 
 
 Platform- and/or Interface-Specific New Features
index e85abb6b7fc880b429bed04a62e33a8be935ea65..b161c78c9448044d182c779b392691c32331cb18 100644 (file)
@@ -1,4 +1,4 @@
-/*     SCCS Id: @(#)spell.c    3.5     2006/02/03      */
+/*     SCCS Id: @(#)spell.c    3.5     2006/02/10      */
 /*     Copyright (c) M. Stephenson 1988                          */
 /* NetHack may be freely redistributed.  See license for details. */
 
@@ -8,8 +8,16 @@
 #define SPELLMENU_CAST (-2)
 #define SPELLMENU_VIEW (-1)
 
+/* spell retention period, in turns; at 10% of this value, player becomes
+   eligible to reread the spellbook and regain 100% retention (the threshold
+   used to be 1000 turns, which was 10% of the original 10000 turn retention
+   period but didn't get adjusted when that period got doubled to 20000) */
 #define KEEN 20000
-#define incrnknow(spell)        spl_book[spell].sp_know = KEEN
+/* x: need to add 1 when used for reading a spellbook rather than for hero
+   initialization; spell memory is decremented at the end of each turn,
+   including the turn on which the spellbook is read; without the extra
+   increment, the hero used to get cheated out of 1 turn of retention */
+#define incrnknow(spell,x)     (spl_book[spell].sp_know = KEEN + (x))
 
 #define spellev(spell)         spl_book[spell].sp_lev
 #define spellname(spell)       OBJ_NAME(objects[spellid(spell)])
@@ -24,6 +32,7 @@ STATIC_PTR int NDECL(learn);
 STATIC_DCL boolean FDECL(getspell, (int *));
 STATIC_DCL boolean FDECL(dospellmenu, (const char *,int,int *));
 STATIC_DCL int FDECL(percent_success, (int));
+STATIC_DCL char *FDECL(spellretention, (int,char *));
 STATIC_DCL int NDECL(throwspell);
 STATIC_DCL void NDECL(cast_protection);
 STATIC_DCL void FDECL(spell_backfire, (int));
@@ -358,14 +367,15 @@ learn()
                book->otyp = booktype = SPE_BLANK_PAPER;
                /* reset spestudied as if polymorph had taken place */
                book->spestudied = rn2(book->spestudied);
-           } else if (spellknow(i) <= 1000) {
-               Your("knowledge of %s is keener.", splname);
-               incrnknow(i);
-               book->spestudied++;
-               exercise(A_WIS,TRUE);       /* extra study */
-           } else { /* 1000 < spellknow(i) <= KEEN */
+           } else if (spellknow(i) > KEEN / 10) {
                You("know %s quite well already.", splname);
                costly = FALSE;
+           } else { /* spellknow(i) <= KEEN/10 */
+               Your("knowledge of %s is %s.", splname,
+                    spellknow(i) ? "keener" : "restored");
+               incrnknow(i, 1);
+               book->spestudied++;
+               exercise(A_WIS,TRUE);   /* extra study */
            }
            /* make book become known even when spell is already
               known, in case amnesia made you forget the book */
@@ -383,7 +393,7 @@ learn()
            } else {
                spl_book[i].sp_id = booktype;
                spl_book[i].sp_lev = objects[booktype].oc_level;
-               incrnknow(i);
+               incrnknow(i, 1);
                book->spestudied++;
                You(i > 0 ? "add %s to your repertoire." : "learn %s.",
                    splname);
@@ -584,22 +594,24 @@ getspell(spell_no)
            if (nspells == 1)  Strcpy(lets, "a");
            else if (nspells < 27)  Sprintf(lets, "a-%c", 'a' + nspells - 1);
            else if (nspells == 27)  Sprintf(lets, "a-zA");
+           /* this assumes that there are at most 52 spells... */
            else Sprintf(lets, "a-zA-%c", 'A' + nspells - 27);
 
-           for(;;)  {
-               Sprintf(qbuf, "Cast which spell? [%s ?]", lets);
-               if ((ilet = yn_function(qbuf, (char *)0, '\0')) == '?')
-                   break;
-
+           for (;;) {
+               Sprintf(qbuf, "Cast which spell? [%s *?]", lets);
+               ilet = yn_function(qbuf, (char *)0, '\0');
+               if (ilet == '*' || ilet == '?')
+                   break;              /* use menu mode */
                if (index(quitchars, ilet))
                    return FALSE;
 
                idx = spell_let_to_idx(ilet);
-               if (idx >= 0 && idx < nspells) {
-                   *spell_no = idx;
-                   return TRUE;
-               } else
+               if (idx < 0 || idx >= nspells) {
                    You("don't know that spell.");
+                   continue;           /* ask again */
+               }
+               *spell_no = idx;
+               return TRUE;
            }
        }
        return dospellmenu("Choose which spell to cast",
@@ -1086,7 +1098,8 @@ int *spell_no;
 {
        winid tmpwin;
        int i, n, how;
-       char buf[BUFSZ];
+       char buf[BUFSZ], retentionbuf[24];
+       const char *fmt;
        menu_item *selected;
        anything any;
 
@@ -1095,26 +1108,27 @@ int *spell_no;
        any.a_void = 0;         /* zero out all bits */
 
        /*
-        * The correct spacing of the columns depends on the
-        * following that (1) the font is monospaced and (2)
-        * that selection letters are pre-pended to the given
-        * string and are of the form "a - ".
-        *
-        * To do it right would require that we implement columns
-        * in the window-ports (say via a tab character).
+        * The correct spacing of the columns when not using
+        * tab separation depends on the following:
+        * (1) that the font is monospaced, and
+        * (2) that selection letters are pre-pended to the
+        * given string and are of the form "a - ".
         */
-       if (!iflags.menu_tab_sep)
-               Sprintf(buf, "%-20s     Level  %-12s Fail", "    Name", "Category");
-       else
-               Sprintf(buf, "Name\tLevel\tCategory\tFail");
+       if (!iflags.menu_tab_sep) {
+               Sprintf(buf, "%-20s     Level %-12s Fail Retention",
+                       "    Name", "Category");
+               fmt = "%-20s  %2d   %-12s %3d%% %9s";
+       } else {
+               Sprintf(buf, "Name\tLevel\tCategory\tFail\tRetention");
+               fmt = "%s\t%-d\t%s\t%-d%%\t%s";
+       }
        add_menu(tmpwin, NO_GLYPH, &any, 0, 0, ATR_BOLD, buf, MENU_UNSELECTED);
        for (i = 0; i < MAXSPELL && spellid(i) != NO_SPELL; i++) {
-               Sprintf(buf, iflags.menu_tab_sep ?
-                       "%s\t%-d%s\t%s\t%-d%%" : "%-20s  %2d%s   %-12s %3d%%",
+               Sprintf(buf, fmt,
                        spellname(i), spellev(i),
-                       spellknow(i) ? " " : "*",
                        spelltypemnemonic(spell_skilltype(spellid(i))),
-                       100 - percent_success(i));
+                       100 - percent_success(i),
+                       spellretention(i, retentionbuf));
 
                any.a_int = i+1;        /* must be non-zero */
                add_menu(tmpwin, NO_GLYPH, &any,
@@ -1278,28 +1292,70 @@ int spell;
        return chance;
 }
 
+STATIC_OVL char *
+spellretention(idx, outbuf)
+int idx;
+char *outbuf;
+{
+       long turnsleft, percent, accuracy;
+       int skill;
+
+       skill = P_SKILL(spell_skilltype(spellid(idx)));
+       skill = max(skill, P_UNSKILLED);        /* restricted same as unskilled */
+       turnsleft = spellknow(idx);
+       *outbuf = '\0'; /* lint suppression */
+
+       if (turnsleft < 1L) {
+           /* spell has expired; hero can't successfully cast it anymore */
+           Strcpy(outbuf, "(gone)");
+       } else if (turnsleft >= (long)KEEN) {
+           /* full retention, first turn or immediately after reading book */
+           Strcpy(outbuf, "100%");
+       } else {
+           /*
+            * Retention is displayed as a range of percentages of
+            * amount of time left until memory of the spell expires;
+            * the precision of the range depends upon hero's skill
+            * in this spell.
+            *    expert:  2% intervals; 1-2,   3-4,  ...,   99-100;
+            *   skilled:  5% intervals; 1-5,   6-10, ...,   95-100;
+            *     basic: 10% intervals; 1-10, 11-20, ...,   91-100;
+            * unskilled: 25% intervals; 1-25, 26-50, 51-75, 76-100.
+            *
+            * At the low end of each range, a value of N% really means
+            * (N-1)%+1 through N%; so 1% is "greater than 0, at most 200".
+            * KEEN is a multiple of 100; KEEN/100 loses no precision.
+            */
+           percent = (turnsleft - 1L) / ((long)KEEN / 100L) + 1L;
+           accuracy = (skill == P_EXPERT) ? 2L : (skill == P_SKILLED) ? 5L :
+                       (skill == P_BASIC) ? 10L : 25L;
+           /* round up to the high end of this range */
+           percent = accuracy * ((percent - 1L) / accuracy + 1L);
+           Sprintf(outbuf, "%ld%%-%ld%%", percent - accuracy + 1L, percent);
+       }
+       return outbuf;
+}
 
 /* Learn a spell during creation of the initial inventory */
 void
 initialspell(obj)
 struct obj *obj;
 {
-       int i;
+       int i, otyp = obj->otyp;
 
-       for (i = 0; i < MAXSPELL; i++) {
-           if (spellid(i) == obj->otyp) {
-                pline("Error: Spell %s already known.",
-                               OBJ_NAME(objects[obj->otyp]));
-                return;
-           }
-           if (spellid(i) == NO_SPELL)  {
-               spl_book[i].sp_id = obj->otyp;
-               spl_book[i].sp_lev = objects[obj->otyp].oc_level;
-               incrnknow(i);
-               return;
-           }
+       for (i = 0; i < MAXSPELL; i++)
+           if (spellid(i) == NO_SPELL || spellid(i) == otyp) break;
+
+       if (i == MAXSPELL) {
+           impossible("Too many spells memorized!");
+       } else if (spellid(i) != NO_SPELL) {
+           /* initial inventory shouldn't contain duplicate spellbooks */
+           impossible("Spell %s already known.", OBJ_NAME(objects[otyp]));
+       } else {
+           spl_book[i].sp_id = otyp;
+           spl_book[i].sp_lev = objects[otyp].oc_level;
+           incrnknow(i, 0);
        }
-       impossible("Too many spells memorized!");
        return;
 }